# Copyright (c) 2019 Arpit Bhatia and contributors                               #src
#                                                                                #src
# Permission is hereby granted, free of charge, to any person obtaining a copy   #src
# of this software and associated documentation files (the "Software"), to deal  #src
# in the Software without restriction, including without limitation the rights   #src
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell      #src
# copies of the Software, and to permit persons to whom the Software is          #src
# furnished to do so, subject to the following conditions:                       #src
#                                                                                #src
# The above copyright notice and this permission notice shall be included in all #src
# copies or substantial portions of the Software.                                #src
#                                                                                #src
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR     #src
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,       #src
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE    #src
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER         #src
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,  #src
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE  #src
# SOFTWARE.                                                                      #src

# # Getting started with Julia

# Since JuMP is embedded in Julia, knowing some basic Julia is important
# for learning JuMP. This tutorial is designed to provide a minimalist
# crash course in the basics of Julia. You can find resources that provide
# a more comprehensive introduction to Julia [here](https://julialang.org/learning/).

# ## Installing Julia

# To install Julia, [download the latest stable release](https://julialang.org/downloads/),
# then follow the [platform specific install instructions](https://julialang.org/downloads/platform/).

# !!! tip
#     Unless you know otherwise, you probably want the 64-bit version.

# Next, you need an IDE to develop in. VS Code is a popular choice, so follow
# [these install instructions](https://www.julia-vscode.org/docs/stable/gettingstarted/).

# ## Numbers and arithmetic

# Since we want to solve optimization problems, we're going to be using a lot of
# math. Luckily, Julia is great for math, with all the usual operators:

@show 1 + 1
@show 1 - 2
@show 2 * 2
@show 4 / 5
@show 3^2
nothing #hide

# !!! info
#     The `@` in front of something indicates that it is a macro, which is just
#     a special type of function. In this case, `@show` prints the expression as
#     typed (e.g., `1 - 2`), as well as the evaluation of the expression (`-1`).

# Did you notice how Julia didn't print `.0` after some of the numbers? Julia is
# a dynamic language, which means you never have to explictly declare the type
# of a variable. However, in the background, Julia is giving each variable a
# type. Check the type of something using the `typeof` function:

@show typeof(1)
@show typeof(1.0)
nothing #hide

# Here `1` is an `Int64`, which is an integer with 64 bits of precision, and
# `1.0` is a `Float64`, which is a floating point number with 64-bits of
# precision.

# !!! tip
#     If you aren't familiar with floating point numbers, make sure to read
#     the [Floating point numbers](@ref) section.

# We create complex numbers using `im`:

x = 2 + 1im
@show real(x)
@show imag(x)
@show typeof(x)
@show x * (1 - 2im)
nothing #hide

# !!! info
#     The curly brackets surround what we call the _parameters_ of a type. You
#     can read `Complex{Int64}`  as "a complex number, where the real and
#     imaginary parts are represented by `Int64`." If we call
#     `typeof(1.0 + 2.0im)` it will be `Complex{Float64}`, which a complex
#     number with the parts represented by `Float64`.

# There are also some cool things like an irrational representation of π.

π

# !!! tip
#     To make π (and most other greek letters), type `\pi` and then press
#     `[TAB]`.

typeof(π)

# Athough if we do math with irrational numbers, they get converted to
# `Float64`:

typeof(2π / 3)

# ### Floating point numbers

# !!! warning
#     If you aren't familiar with floating point numbers, make sure to read this
#     section carefully.

# A `Float64` is a [floating point](https://en.wikipedia.org/wiki/Floating-point_arithmetic)
# approximation of a real number using 64-bits of information.

# Because it is an approximation, things we know hold true in mathematics don't
# hold true in a computer! For example:

0.1 * 3 == 0.3

#- A more complicated example is:

sin(2π / 3) == √3 / 2

# !!! tip
#     Get `√` by typing `\sqrt` then press `[TAB]`.

# Let's see what the differences are:

0.1 * 3 - 0.3

#-

sin(2π / 3) - √3 / 2

# They are small, but not zero!

# One way of explaining this difference is to consider how we would write
# `1 / 3` and `2 / 3` using only four digits after the decimal point. We would
# write `1 / 3` as `0.3333`, and `2 / 3` as `0.6667`. So, depiste the fact that
# `2 * (1 / 3) == 2 / 3`, `2 * 0.3333 == 0.6666 != 0.6667`.

# Let's try that again using ≈ (`\approx + [TAB]`) instead of `==`:

0.1 * 3 ≈ 0.3

#-

sin(2π / 3) ≈ √3 / 2

# `≈` is just a clever way of calling the `isapprox` function:

isapprox(sin(2π / 3), √3 / 2; atol = 1e-8)

# !!! warning
#     Floating point is the reason solvers use tolerances when they solve
#     optimization models. A common mistake you're likely to make is checking
#     whether a binary variable is 0 using `value(z) == 0`. Always remember to
#     use something like `isapprox` when comparing floating point numbers.
#
#     Gurobi has a [good series of articles](https://www.gurobi.com/documentation/9.0/refman/num_grb_guidelines_for_num.html)
#     on the implications of floating point in optimization if you want to read
#     more.

# If you aren't careful, floating point arithmetic can throw up all manner of
# issues. For example:

1 + 1e-16 == 1

# It even turns out that floating point numbers aren't associative!

(1 + 1e-16) - 1e-16 == 1 + (1e-16 - 1e-16)

# It's important to note that this issue isn't Julia-specific. It happens in
# every programming language (try it out in Python).

# ## Vectors, matrices and arrays

# Similar to Matlab, Julia has native support for vectors, matrices and tensors;
# all of which are represented by arrays of different dimensions. Vectors are
# constructed by comma-separated elements surrounded by square brackets:

b = [5, 6]

# !!! info
#     `Array{Int64, 1}` means that this is an `Array`, with `Int64` elements,
#     and it has `1` dimension.

# Matrices can by constructed with spaces separating the columns, and semicolons
# separating the rows:

A = [1.0 2.0; 3.0 4.0]

# Note how this time the type is `Array{Float64, 2}`; the elements are `Float64`
# and there are `2` dimenions.

# We can do linear algebra:

x = A \ b

# !!! info
#     Here is floating point at work again! `x` is approximately `[-4, 4.5]`.

#-

A * x

#-

A * x ≈ b

# Note that when multiplying vectors and matrices, dimensions matter. For
# example, you can't multiply a vector by a vector:

try  #hide
b * b
catch err; showerror(stderr, err); end  #hide

# But multiplying transposes works:

b' * b

#-

b * b'

# ## Other common types

# ### Strings

# Double quotes are used for strings:

typeof("This is Julia")

# Unicode is fine in strings:

typeof("π is about 3.1415")

# Use [`println`](https://docs.julialang.org/en/v1/base/io-network/#Base.println)
# to print a string:

println("Hello, World!")

# We can use `$()` to interpolate values into a string:

x = 123
println("The value of x is: $(x)")

# ### Symbols

# Julia `Symbol`s provide a way to make human readable unique identifiers:

:my_id

#-

typeof(:my_id)

# You can think of a `Symbol` as a `String` that takes up less memory, and that
# can't be modified.

# ### Tuples

# Julia makes extensive use of a simple data structure called Tuples. Tuples are
# immutable collections of values. For example:

t = ("hello", 1.2, :foo)

#-

typeof(t)

# Tuples can be accessed by index, similar to arrays:

t[2]

# And they be "unpacked" like so:

a, b, c = t
b

# The values can also be given names, which is a convenient way of making
# light-weight data structures.

t = (word = "hello", num = 1.2, sym = :foo)

# Values can be accessed using dot syntax:

t.word

# ## Dictionaries

# Similar to Python, Julia has native support for dictionaries. Dictionaries
# provide a very generic way of mapping keys to values.  For example, a map of
# integers to strings:

d1 = Dict(1 => "A", 2 => "B", 4 => "D")

# !!! info
#     Type-stuff again: `Dict{Int64,String}` is a dictionary with `Int64` keys
#     and `String` values.

# Looking up a values uses the bracket syntax:

d1[2]

# Dictionaries support non-integer keys and can mix data types:

Dict("A" => 1, "B" => 2.5, "D" => 2 - 3im)

# !!! info
#     Julia types form a hierarchy. Here the value type of the dictionary is
#     `Number`, which is a generalization of `Int64`, `Float64`, and `Complex{Int}`.
#     In general, having variables with "Abstract" types like `Number` can lead
#     to slower code, so you should try to make sure every element in a
#     dictionary or vector is the same type. For example, in this case we could
#     represent every element as a `Complex{Float64}`:

Dict("A" => 1.0 + 0.0im, "B" => 2.5 + 0.0im, "D" => 2.0 - 3.0im)

# Dictionaries can be nested:

d2 = Dict("A" => 1, "B" => 2, "D" => Dict(:foo => 3, :bar => 4))

#-

d2["B"]

#-

d2["D"][:foo]

# ## Loops

# Julia has native support for for-each style loops with the syntax
# `for <value> in <collection> end`:

for i in 1:5
    println(i)
end

# !!! info
#     Ranges are constructed as `start:stop`, or `start:step:stop`.

#-

for i in [1.2, 2.3, 3.4, 4.5, 5.6]
    println(i)
end

# This for-each loop also works with dictionaries:

for (key, value) in Dict("A" => 1, "B" => 2.5, "D" => 2 - 3im)
    println("$(key): $(value)")
end

# Note that in contrast to vector languages like Matlab and R, loops do not
# result in a significant performance degradation in Julia.

# ## Control Flow

# Julia control flow is similar to Matlab, using the keywords
# `if-elseif-else-end`, and the logical operators `||` and `&&` for **or** and
# **and** respectively:

for i in 0:3:15
    if i < 5
        println("$(i) is less than 5")
    elseif i < 10
        println("$(i) is less than 10")
    else
        if i == 10
            println("the value is 10")
        else
            println("$(i) is bigger than 10")
        end
    end
end

# ## Comprehensions

# Similar to languages like Haskell and Python, Julia supports the use of simple
# loops in the construction of arrays and dictionaries, called comprehenions.
#
# A list of increasing integers:

[i for i in 1:5]

# Matrices can be built by including multiple indices:

[i * j for i in 1:5, j in 5:10]

# Conditional statements can be used to filter out some values:

[i for i in 1:10 if i % 2 == 1]

# A similar syntax can be used for building dictionaries:

Dict("$(i)" => i for i in 1:10 if i % 2 == 1)

# ## Functions

# A simple function is defined as follows:

function print_hello()
    println("hello")
end
print_hello()

# Arguments can be added to a function:

function print_it(x)
    println(x)
end
print_it("hello")
print_it(1.234)
print_it(:my_id)

# Optional keyword arguments are also possible:

function print_it(x; prefix = "value:")
    println("$(prefix) $(x)")
end
print_it(1.234)
print_it(1.234, prefix = "val:")

# The keyword `return` is used to specify the return values of a function:

function mult(x; y = 2.0)
    return x * y
end

mult(4.0)

#-

mult(4.0, y = 5.0)

# ### Anonymous functions

# The syntax `input -> output` creates an anonymous function. These are most
# useful when passed to other functions. For example:

f = x -> x^2
f(2)

#-

map(x -> x^2, 1:4)

# ### Type parameters

# We can constrain the inputs to a function using type parameters, which are
# `::` followed by the type of the input we want. For example:

function foo(x::Int)
    return x^2
end

function foo(x::Float64)
    return exp(x)
end

function foo(x::Number)
    return x + 1
end

@show foo(2)
@show foo(2.0)
@show foo(1 + 1im)
nothing #hide

# But what happens if we call `foo` with something we haven't defined it for?

try  #hide
foo([1, 2, 3])
catch err; showerror(stdout, err) end  #hide

# We get a dreaded `MethodError`! A `MethodError` means that you passed a
# function something that didn't match the type that it was expecting. In this
# case, the error message says that it doesn't know how to handle an
# `Array{Int64, 1}`, but it does know how to handle `Float64`, `Int64`, and
# `Number`.
#
# !!! tip
#     Read the "Closest candidates" part of the error message carefully to get a
#     hint as to what was expected.

# ### Broadcasting

# In the example above, we didn't define what to do if `f` was passed an
# `Array`. Luckily, Julia provides a convienient syntax for mapping `f`
# element-wise over arrays! Just add a `.` between the name of the function and
# the opening `(`. This works for _any_ function, including functions with
# multiple arguments. For example:

f.([1, 2, 3])

# !!! tip
#     Get a `MethodError` when calling a function that takes an `Array`? Try
#     broadcasting it!

# ## Mutable vs immutable objects

# Some types in Julia are *mutable*, which means you can change the values
# inside them. A good example is an array. You can modify the contents of an
# array without having to make a new array.

# In contrast, types like `Float64` are *immutable*. You can't modify the
# contents of a `Float64`.

# This is something to be aware of when passing types into functions. For
# example:

function mutability_example(mutable_type::Vector{Int}, immutable_type::Int)
    mutable_type[1] += 1
    immutable_type += 1
    return
end

mutable_type = [1, 2, 3]
immutable_type = 1

mutability_example(mutable_type, immutable_type)

println("mutable_type: $(mutable_type)")
println("immutable_type: $(immutable_type)")

# Because `Vector{Int}` is a mutable type, modifying the variable inside the
# function changed the value outside of the function. In constrast, the change
# to `immutable_type` didn't modify the value outside the function.

# You can check mutability with the `isimmutable` function:

isimmutable([1, 2, 3])

#-

isimmutable(1)

# ## The package manager

# ### Installing packages

# No matter how wonderful Julia's base language is, at some point you will want
# to use an extension package.  Some of these are built-in, for example random
# number generation is available in the `Random` package in the standard
# library. These packages are loaded with the commands `using` and `import`.

using Random  # The equivalent of Python's `from Random import *`
import Random  # The equivalent of Python's `import Random`

Random.seed!(33)

[rand() for i in 1:10]

# The Package Manager is used to install packages that are not part of Julia's
# standard library.

# For example the following can be used to install JuMP,
# ```julia
# using Pkg
# Pkg.add("JuMP")
# ```

# For a complete list of registed Julia packages see the package listing at
# [JuliaHub](https://juliahub.com).

# From time to you may wish to use a Julia package that is not registered.  In
# this case a git repository URL can be used to install the package.
# ```julia
# using Pkg
# Pkg.add("https://github.com/user-name/MyPackage.jl.git")
# ```

# ### Package environments

# By default, `Pkg.add` will add packages to Julia's global environment.
# However, Julia also has built-in support for virtual environments.

# Activate a virtual environment with:
# ```julia
# import Pkg; Pkg.activate("/path/to/environment")
# ```

# You can see what packages are installed in the current environment with
# `Pkg.status()`.

# !!! tip
#     We _strongly_ recommend you create a Pkg environment for each project
#     that you create in Julia, and add only the packages that you need, instead
#     of adding lots of packages to the global environment. The [Pkg manager documentation](https://julialang.github.io/Pkg.jl/v1/environments/)
#     has more information on this topic.


# Uncomment to debug:
# macro dbgutil(x); esc(:(println(_dbg($x)))); end
macro dbgutil(x); end

### @primitive and @zerograd macros:

# I would like to make these type signatures as specific as possible.
# The following are not allowed yet, see https://github.com/JuliaLang/julia/issues/3766
# f{T<:Number,A<:AbstractArray{T}}(x::Value{A})
# f{T<:Number,A<:AbstractArray}(x::Value{A{T}})

"""

`@primitive fx g1 g2...` can be used to define a new primitive
and (optionally) its gradients.

Julia supports multiple dispatch, i.e. a single function can have
multiple methods with different arg types.  AutoGrad supports
multiple dispatch for primitives and gradients.  Thus fx is a
typed method declaration such as:

* @primitive sin(x::Number)
* @primitive hypot(x1::Array,x2::Array),dy,y

The second example specifies variable names for the output gradient
`dy` and the output `y` after the method declaration which can be used
in gradient expressions.  Untyped, ellipsis and keyword arguments are
ok as in `f(a::Int,b,c...;d=1)`.  Parametric methods such as
`f{T<:Number}(x::T)` cannot be used.

The @primitive macro turns the first example into:

    local sin_r = recorder(sin)
    sin{T<:Number}(x::Value{T}) = sin_r(x)

This will cause any call to `sin` with a Value{T<:Number} argument
to be recorded.  With multiple arguments things are a bit more
complicated.  Here is what happens with the second example:

    local hypot_r = recorder(hypot)
    hypot{T<:Array,S<:Array}(x1::Value{T},x2::Value{S})=hypot_r(x1,x2)
    hypot{T<:Array,S<:Array}(x1::Value{T},x2::S)=hypot_r(x1,x2)
    hypot{T<:Array,S<:Array}(x1::T,x2::Value{S})=hypot_r(x1,x2)

We want the recorder version to be called if any one of the arguments
is a boxed Value.  There is no easy way to specify this in Julia, so
the macro generates all 2^N-1 boxed/unboxed argument combinations.

The method declaration can optionally be followed by gradient
expressions.  Here are the same examples with gradients:

* @primitive sin(x::Number),dy (dy*cos(x))
* @primitive hypot(x1::Array,x2::Array),dy,y  `(dy.*x1./y)`  `(dy.*x2./y)`

Note that the parameters, the return variable and the output gradient
of the original function can be used in the gradient expressions.

In AutoGrad, gradients are defined using gradient methods that have
the following signature:

    f(Grad{i},dy,y,x...) => dx[i]

For the first example here is the generated gradient method:

`sin{T<:Number}(::Type{Grad{1}}, dy, y, x::Value{T})=(dy*cos(x))`

For the second example a different gradient method is generated for
each argument:

`hypot{T<:Array,S<:Array}(::Type{Grad{1}},dy,y,x1::Value{T},x2::Value{S})=(dy.*x1./y)`
`hypot{T<:Array,S<:Array}(::Type{Grad{2}},dy,y,x1::Value{T},x2::Value{S})=(dy.*x2./y)`

In fact @primitive generates four more definitions for the other
boxed/unboxed argument combinations.

Non-differentiable functions such as `sign`, and non-numeric functions
such as `size` should be defined using the @zerograd macro instead.

"""
macro primitive(f,g...)
    isa(f,Expr) || error("'$f' not a method signature")
    if f.head == :tuple # Using f(x),dy,y to indicate return variable for gradients
        if length(f.args) == 3
            (f,dy,y) = f.args
        elseif length(f.args) == 2
            (f,dy) = f.args; y = gensym()
        else
            error("The first arg '$f' should have the format f(x),dy,y")
        end
    else
        dy = gensym(); y = gensym()
    end
    f.head == :call || error("'$f' not a method signature")
    isa(dy,Symbol) || error("Output gradient '$dy' not a symbol")
    isa(y,Symbol) || error("Return variable '$y' not a symbol")
    b = Expr(:block)
    r = gensym()
    push!(b.args, esc(:(local $r = recorder($(fname(f))))))
    rx = rcall(r,f)
    for fx in fsigs(f)
        push!(b.args, esc(:($fx = $rx)))
        for i=1:length(g)
            gx = gsig(fx,dy,y,i)
            push!(b.args, esc(:($gx = $(g[i]))))
        end
    end
    return b
end

"""
`@zerograd f(args...; kwargs...)` allows f to handle its Value inputs
by unboxing them like @primitive, but unlike @primitive it does not
record its actions or return a Value result.  Some functions, like
sign(), have zero gradient.  Others, like length() have discrete or
constant outputs.  These need to handle Value inputs, but do not need
to record anything and can return regular values.  Their output can be
treated like a constant in the program.  Use the @zerograd macro for
those.  Note that kwargs are NOT unboxed.
"""
macro zerograd(f)
    b = Expr(:block)
    f.head == :(::) && (f=f.args[1])
    for fx in fsigs(f)
        zx = zcall(fx)
        push!(b.args, esc(:($fx = $zx)))
    end
    return b
end

function zcall(f)
    z = copy(f)
    z1 = z.args[1]
    isa(z1,Expr) && z1.head==:curly && (z.args[1]=z1.args[1])
    for i=2:length(z.args)
        zi = z.args[i]
        if isa(zi,Symbol)
            # all done
        elseif !isa(zi,Expr)
            error("Unrecognized argtype '$zi'")
        elseif zi.head==:(::)
            (v,t) = zi.args
            if t==:Value || (isa(t,Expr) && t.head==:curly && t.args[1]==:Value)
                z.args[i] = :($v.value)
            else
                z.args[i] = v
            end
        elseif zi.head==:(...)  # done
        elseif zi.head==:parameters # done
        else
            error("Unrecognized argtype '$zi'")
        end
    end
    return z
end

# get name out of function declaration
function fname(f)
    n = f.args[1]
    isa(n,Expr) && n.head==:curly && error("parametric methods not currently supported")
    if isa(n,Symbol)
        return n
    else
        error("$n not a symbol")
    end
end

# create call to r using typeless argument of f
function rcall(r,f)
    rx = notypes(f)
    rx.args[1]=r
    # Need to fix kwargs
    r2 = rx.args[2]
    if isa(r2,Expr) && r2.head == :parameters
        for i in 1:length(r2.args)
            k = r2.args[i]
            if !isa(k,Expr); error("Bad kwarg '$k'")
            elseif k.head == :(...); continue
            elseif k.head != :kw; error("Bad kwarg '$k'")
            elseif !isa(k.args[1],Symbol); error("Bad kwarg '$k'")
            else; k.args[2]=k.args[1]; end
        end
    end
    return rx
end

# eliminate type declarations from a function call
function notypes(ex)
    if isa(ex, Expr)
        if (ex.head == :(::) || ex.head == :curly)
            return notypes(ex.args[1])
        else
            return Expr(ex.head, map(notypes, ex.args)...)
        end
    else
        return ex
    end
end

# create type signatures for f where one or more args are Nodes.
function fsigs(f)
    f1 = copy(f)
    a1 = f1.args[1] = Expr(:curly,fname(f1))
    nargs = 0
    for i=2:length(f1.args)
        ai = f1.args[i]
        if isa(ai,Symbol)
            nargs+=1
            ti = gensym()
            push!(a1.args, Expr(:<:, ti, Any))
            f1.args[i] = Expr(:(::),ai,ti)
        elseif !isa(ai,Expr)
            error("Neither Symbol nor Expr: $ai")
        elseif in(ai.head, (:parameters, :(...)))
            continue
        elseif ai.head == :(::)
            nargs+=1
            ti = gensym()
            push!(a1.args, Expr(:<:,ti,ai.args[2]))
            ai.args[2] = ti
        else
            error("Argtype not supported: '$ai'")
        end
    end
    flist = []
    for nodes=0:(1<<nargs-2)
        fn = copy(f1)
        iargs = 0
        for i=2:length(fn.args)
            ai = fn.args[i]
            in(ai.head, (:parameters, :(...))) && continue
            ai.head == :(::) || error("Bad arg '$ai'")
            if nodes & (1<<iargs) == 0
                ai.args[2] = Expr(:curly,:Value,ai.args[2])
            end
            iargs += 1
        end
        push!(flist, fn)
    end
    return flist
end

function gsig(f,dy,y,i)
    g = copy(f)
    if g.args[2].head == :parameters; a = 3; else; a = 2; end
    insert!(g.args, a, :(::Type{Grad{$i}}))
    insert!(g.args, a+1, dy)
    insert!(g.args, a+2, y)
    return g
end

### Testing Utilities:

if !isdefined(:runtests)
let tests=[]
    global addtest,runtests,alltests
    alltests()=tests
    addtest(t...)=push!(tests,t)
    function runtests(a=tests)
        for fx in a
            try
                tx = fixtest(fx...)
                check_grads(tx...; fname=fx[1])
            catch e
                warn((fx...,"$e"))
            end
        end
    end
end
end

function fixtest(f, x...)
    f = eval(f)
    y = f(x...)
    # detect and prevent testing of zero / undefined grads
    plist = Any[]               # define fnew(plist)
    alist = Any[x...]           # to return f(alist)
    fargs = Any[]               # call fnew(fargs...)
    gargs = (Value(y), Value(y), map(Value,x)...)
    for i=1:length(alist)
        g = nothing
        try
            g = f(Grad{i},gargs...)
        catch e
            if isa(e,MethodError) && e.f === f && e.args[1] === Grad{i}
                continue        # warn("No grad $i for $f: $e")
            else
                error("Error during $f$((Grad{i},gargs...)): $e")
            end
        end
        g == nothing && continue # zero grads
        push!(fargs, alist[i])
        alist[i] = Symbol("x$i")
        push!(plist, alist[i])
    end
    isempty(fargs) && error("$f has no differentiable arguments.")
    f1=f; f = eval(Expr(:->, Expr(:tuple, plist...), Expr(:call, f1, alist...)))
    # if f has non-scalar output, sum it
    isbits(y) || (f2=f; f=(x...)->toscalar(f2(x...)))
    return (f,fargs...)
end

function randin(range, dims...; eps=EPS)
    if isa(range, UnitRange{Int64})
        rand(range, dims...)
    elseif range==(-Inf,Inf)
        randn(dims...)
    elseif range==(0,Inf)
        eps-log(rand(dims...))
    elseif range==(1,Inf)
        eps+1-log(rand(dims...))
    elseif range==(-1,Inf)
        eps-1-log(rand(dims...))
    elseif range==(-1,1)
        (1-eps)*(2rand(dims...)-1)
    elseif range==(0,1)
        eps+(1-2eps)*rand(dims...)
    elseif range==(0,2)
        eps+2*(1-eps)*rand(dims...)
    elseif range==(-Inf,-1,1,Inf)
        x = sec(randn(dims...))
        sign(x)*eps + x
    else
        error("Unknown range $range")
    end
end

function addtest1(f,r)          # unary
    addtest(f,randin(r))
    addtest(f,randin(r,2))
end

function addtest2(f,r1,r2=r1)   # binary
    addtest(f,randin(r1),randin(r2))
    addtest(f,randin(r1),randin(r2,2))
    addtest(f,randin(r1,2),randin(r2))
    addtest(f,randin(r1,2),randin(r2,2))
end

function addtest3(f,r1,r2=r1)   # broadcasting
    addtest2(f,r1,r2)
    addtest(f,randin(r1,2),randin(r2,2,2))
    addtest(f,randin(r1,2,2),randin(r2,2))
    addtest(f,randin(r1,1,2),randin(r2,2,2))
    addtest(f,randin(r1,2,2),randin(r2,1,2))
end

# EPS, RTOL, ATOL = 1e-4, 1e-4, 1e-6
EPS, RTOL, ATOL = 1e-4, 1e-2, 1e-4

# TODO: do sampling or random direction for large args
"""
check_grads(fun, args...) checks the computed gradients for fun(args)
comparing them with numeric approximations.
"""
function check_grads(fun, args...; eps=EPS, rtol=RTOL, atol=ATOL, fname=fun)
    @dbgutil((:check_grads,fname,:args,args...))
    isempty(args) && error("No args given")
    exact = ntuple(i->grad(fun,i)(args...), length(args))
    numeric = nd(fun, args...; eps=eps)
    @dbgutil((:check_grads,fname,:exact,exact,:numeric,numeric))
    same = isequivalent(exact, numeric; rtol=rtol, atol=atol)
    same || warn((:check_grads,fname,:args,args,:exact,exact,:numeric,numeric))
    return same
end

function nd(f, args...; eps=EPS)
    @dbgutil((:nd,f,args..., :eps, eps))
    unary_f = x->f(x...)
    unary_nd(unary_f, args, eps)
end

unary_nd(f, x::Tuple, eps)         = ntuple(i->unary_nd(indexed_function(f, x, i), x[i], eps), length(x))
unary_nd(f, x::Associative, eps)   = (a=similar(x); for(k,v) in x; a[k] = unary_nd(indexed_function(f, x, k), v, eps); end; a)
unary_nd(f, x::AbstractArray, eps) = reshape(eltype(x)[unary_nd(indexed_function(f, x, i), v, eps) for (i,v) in enumerate(x)], size(x))
unary_nd(f, x::Complex, eps)       = ((f(x + eps/2) - f(x - eps/2)) / eps - im*(f(x + im*eps/2) - f(x - im*eps/2)) / eps)
unary_nd(f, x::Real, eps)          = ((f(x + eps/2) - f(x - eps/2)) / eps)

function indexed_function(fun, arg, index)
    function partial_function(x)
        if isa(arg, Tuple)
            local_arg = (arg[1:index-1]..., x, arg[index+1:end]...)
        else
            local_arg = copy(arg); local_arg[index] = x
        end
        return fun(local_arg)
    end
    return partial_function
end

# isequivalent uses isapprox for Number and AbstractArray{T<:Number}
isequivalent(x::Number,y::Number; o...)=isapprox(x,y;o...)
isequivalent{T<:Number,S<:Number}(x::AbstractArray{T},y::AbstractArray{S}; o...)=isapprox(x,y;o...)

# isequivalent extends to Tuple, Associative, and other Arrays, comparing elementwise
isequivalent(x::Tuple, y::Tuple; o...)=(length(x)==length(y) && all(i->isequivalent(x[i],y[i];o...), 1:length(x)))
isequivalent(x::AbstractArray, y::AbstractArray; o...)=(length(x)==length(y) && all(i->isequivalent(x[i],y[i];o...), 1:length(x)))
isequivalent(x::Associative, y::Associative; o...)=all(k->isequivalent(get(x,k,nothing),get(y,k,nothing);o...), unique([keys(x)...,keys(y)...]))

# isequivalent treats `nothing` as equivalent to zero or zero array.
isequivalent(x::Number,z::Void; o...)=isequivalent(z,x;o...)
isequivalent{T<:Number}(x::AbstractArray{T},z::Void; o...)=isequivalent(z,x;o...)
isequivalent(z::Void,x::Number; o...)=isapprox(zero(x),x;o...)
isequivalent{T<:Number}(z::Void,x::AbstractArray{T}; rtol::Real=Base.rtoldefault(T), atol::Real=0, norm::Function=vecnorm) = (norm(x) <= atol/(1-rtol)) # Modified from: linalg/generic.jl:522

# The way broadcasting works in Julia:
# y = f(x...) where f is a broadcasting operation.
# size(y) = broadcast_shape(x...)
# ndims(y) = max ndims(x)
# size(y,i) = max size(x,i)
# size(x,i) = 1 or size(y,i) for all x and i<=ndims(x)
# if ndims(x) < ndims(y) the extra dimensions of x are treated as 1

function unbroadcast(x, dx)
    if size(x)==size(dx)
        return dx
    elseif isa(getval(x),Number)
        return sum(dx)
    else
        d = []
        for i=1:ndims(dx)
            size(x,i) == size(dx,i) && continue
            size(x,i) != 1 && throw(DimensionMismatch())
            push!(d,i)
        end
        length(d)==1 && (d=d[1])
        return sum(dx, d)
    end
end

function toscalar(xv; rng=MersenneTwister())
    x = getval(xv)
    isa(x,Number) && return xv
    isa(x,OneHot) && (x = full(x))
    idx = isa(x,Tuple) ? (1:length(x)) : eachindex(x)
    s = 0
    for i in idx
        s += xv[i] * rand(rng)
    end
    return s
end

# sumvalues sums values of dictionaries, otherwise acts like sum:

sumvalues(x)=sum(x)
sumvalues(x::Associative)=sum(values(x))
@primitive sumvalues(x::Associative),ds fillvalues(ds,x)
fillvalues(v,x)=(y=similar(x);for k in keys(x); y[k]=v; end; y)
@primitive fillvalues(v,x),dxv sumvalues(dxv) nothing
addtest(sumvalues, Dict(1=>1.,2=>2.))
addtest(fillvalues, 0., Dict(1=>1.,2=>2.,3=>3.))

# This needs more work:
# @primitive values(x),dy Dict(map((a,b)->(a=>b), keys(x), dy))

# It gets tiresome to write `Type{Grad{1}}` after a while, here are
# some convenient aliases:

typealias D1 Type{Grad{1}}
typealias D2 Type{Grad{2}}
if !isdefined(:Dn)
typealias Dn{N} Type{Grad{N}}
end

# Pretty print for debugging:
_dbg(x)=x # extend to define short printable representations
_dbg(x::Tuple)=map(_dbg,x)
_dbg(x::Node)="N$(id2(x))_$(id2(x.value))"
_dbg(x::Value)="V$(id2(x))_$(_dbg(x.value))"
_dbg(x::Tape)="T$(join([id2(x),map(id2,x)...],'_'))"
_dbg(x::AbstractArray)="A$(join([id2(x),size(x)...],'_'))"
id2(x)=Int(object_id(x)%1000)

Base.show(io::IO, n::Value) = print(io, _dbg(n))
Base.show(io::IO, n::Node) = print(io, _dbg(n))
Base.show(io::IO, n::Tape) = print(io, _dbg(n))


# read_samples

"""

Read sample output files created by StanSample.jl.

This method is added to StanRun's read_sample function.

### Method
```julia
read_samples(model::SampleModel; start=1)
```

### Required arguments
```julia
* `model`                    : SampleModel
```

### Optional arguments
```julia
* `start=1`                  : First draw saved in MCMCHains.Chains object
```

"""
function read_samples(model::SampleModel;  start=1)

  local a3d, monitors, index, idx, indvec, ftype, noofsamples

  output_base = model.output_base
  name_base ="_chain"
  n_samples = model.method. num_samples
  n_chains = StanBase.get_n_chains(model)

  # Handle save_warmup
  start = model.method.save_warmup ? model.method.num_warmup+1 : start

  # a3d will contain the samples such that a3d[s, i, c] where

  #   s: num_samples
  #   i: variables (from cmdstan .csv file)
  #   c: n_chains

  # Read .csv files created by each chain

  for i in 1:n_chains
    if isfile(output_base*name_base*"_$(i).csv")
      #noofsamples = 0
      instream = open(output_base*name_base*"_$(i).csv")
      #
      # Skip initial set of commented lines, e.g. containing cmdstan version info, etc.
      #
      skipchars(isspace, instream, linecomment='#')
      #
      # First non-comment line contains names of variables
      #
      line = Unicode.normalize(readline(instream), newline2lf=true)
      idx = split(strip(line), ",")
      index = [idx[k] for k in 1:length(idx)]
      indvec = 1:length(index)

      if i == 1
        a3d = fill(0.0, n_samples, length(indvec), n_chains)
      end

      #println(size(a3d))
      skipchars(isspace, instream, linecomment='#')
      for j in 1:n_samples
        skipchars(isspace, instream, linecomment='#')
        line = Unicode.normalize(readline(instream), newline2lf=true)
        if eof(instream) && length(line) < 2
          close(instream)
          break
        else
          flds = parse.(Float64, split(strip(line), ","))
          flds = reshape(flds[indvec], 1, length(indvec))
          a3d[j,:,i] = flds
        end
      end   # read in samples
    end   # read in next file
  end   # read in file for each chain

  cnames = convert.(String, idx[indvec])
  chns = convert_a3d(a3d, cnames, Val(:mcmcchains); start=start)

end   # end of read_samples

function read_samples(model::StanModel; chain=1)
  read_samples(default_output_base(model)*"_chain_$(chain).csv")
end

"""

# convert_a3d

Convert the output file created by cmdstan to the shape of choice.

### Method
```julia
convert_a3d(a3d_array, cnames, ::Val{Symbol}; start=1)
```
### Required arguments
```julia
* `a3d_array::Array{Float64, 3},`      : Read in from output files created by cmdstan
* `cnames::Vector{AbstractString}`     : Monitored variable names
```

### Optional arguments
```julia
* `::Val{Symbol}`                      : Output format
* `::start=1`                          : First draw for MCMCChains.Chains
```
Method called is based on the output_format defined in the stanmodel, e.g.:

   stanmodel = Stanmodel(`num_samples`=1200, thin=2, name="bernoulli",
     model=bernoullimodel, `output_format`=:mcmcchains);

Current formats supported are:

1. :array (a3d_array format, the default for CmdStan)
2. :namedarray (NamedArrays object)
3. :dataframe (DataFrames object)
4. :mambachains (Mamba.Chains object)
5. :mcmcchains (TuringLang/MCMCChains.Chains object)

Options 3 through 5 are respectively provided by the packages StanDataFrames,
StanMamba, StanMCMCChains and StanMCMCChains.
```

### Return values
```julia
* `res`                       : Draws converted to the specified format.
```
"""
convert_a3d(a3d_array, cnames, ::Val{:array}; start=1) = a3d_array

convert_a3d(a3d_array, cnames, ::Val{:namedarray}; start=1) =
  [NamedArray(a3d_array[:,:,i], (collect(1:size(a3d_array, 1)), Symbol.(cnames)))
    for i in 1:size(a3d_array, 3)]

function convert_a3d(a3d_array, cnames, ::Val{:mcmcchains}; start=1)
  pi = filter(p -> length(p) > 2 && p[end-1:end] == "__", cnames)
  p = filter(p -> !(p in  pi), cnames)

  MCMCChains.Chains(a3d_array,
    cnames,
    Dict(
      :parameters => p,
      :internals => pi
    );
    start=start
  )
end


### A Pluto.jl notebook ###
# v0.17.3

using Markdown
using InteractiveUtils

# ╔═╡ c730d000-5ed6-11ec-36b6-39741ccad6c7
begin
	using Pkg; Pkg.activate(@__DIR__); Pkg.instantiate()
	Pkg.precompile()

	using PlutoUI
	using BenchmarkTools
	using DataFrames
	using DelimitedFiles
	using CSV
	using XLSX
	using Downloads
	using JLD, NPZ, MAT, RData
	using RCall
end;

# ╔═╡ 62c5d4f2-9776-43ae-8125-0d429d6cde42
PlutoUI.TableOfContents(aside=true, indent=true, depth=3)

# ╔═╡ 34f92fb1-6c1e-4db6-b795-8a526007ea92
html"""

"""

# ╔═╡ 13a10dac-8c7e-4e72-9fa9-d58ca8dd0202
md"""
# 💾 Data

Being able to easily load and process data is a crucial task that can make any data science more pleasant. In this notebook, we will cover most common types often encountered in data science tasks, and we will be using this data throughout the rest of this tutorial.

[YouTube link](https://www.youtube.com/watch?v=iG1dZBaxS-U)
"""

# ╔═╡ 5100df3f-c5d3-4346-bcd9-8afc92bc559c
md"""
## Get some data

In Julia, it's pretty easy to dowload a file from the web using the `download` function. But also, you can use your favorite command line commad to download files by easily switching from Julia via the `;` key. Let's try both.

**Note:** `download` depends on external tools such as curl, wget or fetch. So you must have one of these.
"""

# ╔═╡ abe62085-b630-4816-af11-b39133892849
data₁ = Downloads.download("https://raw.githubusercontent.com/nassarhuda/easy_data/master/programming_languages.csv", "programminglanguages.csv")

# ╔═╡ 51c909b1-1d37-458f-acd6-68f4041bc92f
md"""
## Read your data from text files

The key question here is to load data from files such as `csv` files, `xlsx` files, or just raw text files. We will go over some Julia packages that will allow us to read such files very easily.
"""

# ╔═╡ 6483858c-2e58-4930-b4e9-76b47032eac4
md"""
### DelimitedFiles.jl

Let's start with the package `DelimitedFiles` which is in the standard library. When loaded, the file is stored as a `Matrix`.

> **readdlm**(source,
> delim::AbstractChar,
> T::Type,
> eol::AbstractChar;
> header=false,
> skipstart=0,
> skipblanks=true,
> use_mmap,
> quotes=true,
> dims,
> comments=false,
> comment_char='#')
"""

# ╔═╡ 89a5a316-5058-4ff2-8d71-fc832a84c58d
begin
	data₂, header = readdlm("programming_languages.csv",',';header=true)
	header
end

# ╔═╡ 853a4e90-ac39-4d73-9953-c7b70e8f78ef
data₂

# ╔═╡ 3a22c018-2ca7-41df-9ee4-91dc8b64d940
md"""
> **Note:** the output table is a Matrix (2D Array).

We can use the function `writedlm`, from `DelimitedFiles.jl` package, to write to a text file...
"""

# ╔═╡ b4fcdcf9-793f-4d4a-a5fb-950108cb8188
writedlm("output/programminglanguages_dlm.txt", data₂, '-')

# ╔═╡ cfa37c7c-810c-4095-8887-262dee4a9d04
md"""
### CSV.jl

A more powerful package to use here is the `CSV.jl` package. By default, this package imports the data to a `DataFrame`, which can have several advantages as we will see below.

_In general, `CSV.jl` is the recommended way to load CSVs in Julia_. Only use `DelimitedFiles.jl` when you have a more complicated file where you want to specify several things.
"""

# ╔═╡ 4b651b5a-e562-46ef-a3b5-8ee85b5a61ed
df = CSV.read("programming_languages.csv", DataFrame)

# ╔═╡ 11471bbf-b7b4-4b6e-90cb-9e9ff47bbe4a
md"""
We can use the `typeof` function to make sure that the output is a DataFrame.
"""

# ╔═╡ 2652dc01-d80d-46d9-ae7a-9afa534e88cb
typeof(df)

# ╔═╡ b2f35e6b-8c19-4729-87a3-292858bfbcb5
md"""
We can use the macro `@btime`, from `BenchMarkTools.jl`, to prove that `CSV.jl` is faster than `DelimitedFiles.jl`...

**Note:** Check Julia REPL to see the processing time.
"""

# ╔═╡ e1210609-a216-4d54-be63-04f10c5c15bc
@btime df₁, h₁ = readdlm("programming_languages.csv",',';header=true);

# ╔═╡ 008b635f-c4e5-475a-83af-4bc3fc722344
@btime df₂ = CSV.read("programming_languages.csv", DataFrame);

# ╔═╡ 934c8549-c86c-45e6-a9f5-56297c73513d
md"""
We can use the function `write`, from `CSV.jl` package, to write to a text file...

**Note:** if we want to write a Matrix, we could first convert it into a DataFrame (using the `DataFrame` function) and, then, write it using the `write` function.
"""

# ╔═╡ 92708ece-3620-4cf5-b09d-098002044623
CSV.write("output/programminglanguages_CSV.csv", df)

# ╔═╡ cac4f1b5-f9e3-469e-9576-553a1cf1b363
md"""
### Some DataFrame Operations

There are different ways to slice a DataFrame. Suppose that we want just the first 10 rows of a DataFrame...
"""

# ╔═╡ 3d378ec4-659f-4032-a5b7-6d6b6d402c2f
df[1:10,:]

# ╔═╡ e50ae64c-ff64-4597-bf75-d7c921d23225
md"""
Suppose that we want all the instances of a given feature (column)...
"""

# ╔═╡ d9ea3574-2ffb-4fdb-9ced-db1dc669ecd9
df[!,"year"]

# ╔═╡ 09ff6d8b-15f2-4d8e-a775-dee357a417e4
df.year

# ╔═╡ 843a668f-7b08-4bce-b02a-3153d0eb7d0d
df[!,"year"] == df.year

# ╔═╡ 159a35f4-09fd-47bd-9935-88668a7e7f27
typeof(df.year)

# ╔═╡ 494ebede-80d0-430e-91aa-0a9050a1bb29
md"""
We can use the `names` function to get an array of all column names...
"""

# ╔═╡ 00d12274-3057-4a52-8400-0c8c3c358e6b
names(df)

# ╔═╡ fede3ce2-da03-4ff9-a13e-7ce55ec05bf6
md"""
We can use the `describe` function to check some data stats...
"""

# ╔═╡ 74783bbb-3c9d-4e15-bc54-b3d8e7c2d230
describe(df)

# ╔═╡ e4b3a444-c487-4fe8-8048-873b8a3affb7
md"""
Let's create two DataFrames...
"""

# ╔═╡ 0becba29-d5ff-4595-86ab-6b0575674047
begin
	foods = ["Apple","Cucumber","Tomato","Banana"]
	calories = [105,47,22,105]

	df_cal = DataFrame(Item=foods, Calories=calories)
end

# ╔═╡ 9d522bdc-d066-40ac-84c4-47287d9e07e0
begin
	prices = [0.85,1.6,0.8,0.6]

	df_price = DataFrame(Item=foods, Price=prices)
end

# ╔═╡ 79de9741-c913-4c00-8833-4f1b68f80ea5
md"""
Now, we can use the `innerjoin` function to join both DataFrames on `Item` column...
"""

# ╔═╡ fd724d83-41bb-422c-860e-2bbeff17e0dc
df_comb = innerjoin(df_cal, df_price, on=:Item)

# ╔═╡ 51301eeb-709a-4ea9-a1a5-d7967f8dd55d
md"""
### XLSX.jl

Another type of files that we may often need to read is XLSX files. Let's try to read this type of file using the `XLSX.jl` package...

> **readdata**(filepath, sheetname, cellrange)
"""

# ╔═╡ 64e7e90b-cd7d-4569-a715-4f27f1787e40
xlsx₁ = XLSX.readdata("data/zillow_data_download_april2020.xlsx",
    			  	  "Sale_counts_city",
    			  	  "A1:F9")

# ╔═╡ e5f2c69b-fbd8-4048-aaed-792a64a4d66c
md"""
If you don't want to specify cell ranges, you can use the `readtable` function. However, this will take a little longer...

> **readtable**(filepath, sheetname)
"""

# ╔═╡ 26047637-1b77-4203-b705-e34f74137295
xlsx₂ = XLSX.readtable("data/zillow_data_download_april2020.xlsx",
					   "Sale_counts_city")

# ╔═╡ 0f3d505c-a575-419f-8155-fd071f510714
typeof(xlsx₂)

# ╔═╡ c2f1eab2-862a-4eb8-b233-71567cdc9963
md"""
Here, `xlsx₂` is a tuple of two items. The first item is an vector of vectors where each vector corresponds to a column in the excel file...
"""

# ╔═╡ 3a924b3e-3dbe-49b6-9e5e-5a48b0ffd385
xlsx₂[1]

# ╔═╡ 58bad348-0586-4d0f-8d54-7c3e1a7a2c40
md"""
We can access the first column by typing...
"""

# ╔═╡ 036de982-36ad-431a-8445-517178626911
xlsx₂[1][1]

# ╔═╡ 4ead9039-f4c2-4f82-bf1e-2146718014a3
md"""
We can access the third column first 10 elements...
"""

# ╔═╡ bb04dc6c-e62e-4b2c-a247-ebe4836d8013
xlsx₂[1][3][begin:10]

# ╔═╡ d9550872-fa7c-4ebd-9a4d-828182c8e49d
md"""
On the other hand, the second is the header with the column names...
"""

# ╔═╡ e01a65ab-394a-4718-b9ae-ca2bb30e20b5
xlsx₂[2]

# ╔═╡ 1eb26b3c-8a5a-4648-bb73-f8291b656edc
md"""
We could check the names of the last 10 columns...
"""

# ╔═╡ cb027055-2cf6-4f74-b265-9d01c23c2967
xlsx₂[2][end-9:end]

# ╔═╡ 12dccfc3-e976-46e6-81b0-dc51b48309f4
md"""
And we can easily store this data in a `DataFrame`. We can use the "splat" operator to unwrap these arrays and pass them to the DataFrame constructor...
"""

# ╔═╡ 403ac783-9c02-4fd1-8506-afe0af36a14b
df_xlsx = DataFrame(xlsx₂...)
# df_xlsx = DataFrame(xlsx₂[1], xlsx₂[2])

# ╔═╡ 36d8898e-980c-4f54-bbff-277ded2229ea
md"""
**Note:** the first argument is the actual data and the second one comprises the column names.

We can also easily write data to an XLSX file (takes several amount of time)...
"""

# ╔═╡ 8d769d53-1902-4a09-8228-ce60174b22bb
# XLSX.writetable("output/writefile_using_XLSX.xlsx",xlsx₂[1],xlsx₂[2])

# ╔═╡ bc29d2b5-e6c2-4b45-8b79-cbb012659be1
md"""
## Importing your data

Often, the data you want to import is not stored in plain text, and you might want to import different kinds of types. Here we will go over importing `jld`, `npz`, `rda`, and `mat` files. Hopefully, these four will capture the types from four common programming languages used in Data Science (Julia, Python, R and Matlab).

We will use a toy example here of a very small matrix. But the same syntax will hold for bigger files.
"""

# ╔═╡ 7c2fef9c-4e9a-4637-bc8e-6636af5f98ae
md"""
### Julia Data (JLD)

JLD, for which files conventionally have the extension `.jld`, is a widely-used format for data storage with the Julia programming language.

JLD is a specific "dialect" of HDF5, a cross-platform, multi-language data storage format most frequently used for scientific data. By comparison with "plain" HDF5, JLD files automatically add attributes and naming conventions to preserve type information for each object.
"""

# ╔═╡ 8f51aeb5-3e75-4e5a-8c8f-ed0201eb8ce0
# load JLD file
jld_data = JLD.load("data/mytempdata.jld")

# ╔═╡ 879ac394-8598-4399-957f-1280ecd99478
# see the data
jld_data["tempdata"]

# ╔═╡ 0d27e3b3-79e3-424d-bc6b-94252900f7fb
# save JLD file
save("output/mywrite.jld", "A", jld_data)

# ╔═╡ 13053d54-8680-41e2-b416-8a7bc50f6c92
# JLD type
typeof(jld_data)

# ╔═╡ e0f49c77-0c0d-4740-9227-5619d14748b6
md"""
### Numpy Data (NPZ)

Several arrays into a single file in uncompressed Numpy `.npz` format.
"""

# ╔═╡ f4ddf1bf-0cbf-47c5-bb5f-f8abf0fb8ac2
# load NPZ file
npz_data = npzread("data/mytempdata.npz")

# ╔═╡ a979107e-f2cf-42da-aefa-5cb249bc7073
# save NPZ file
npzwrite("output/mywrite.npz", npz_data)

# ╔═╡ edd2093e-f84a-4563-acb7-219f2bd07399
# NPZ type
typeof(npz_data)

# ╔═╡ be91882d-fe73-40d0-8e75-30c2e8ddd564
md"""
### R Data (RDA)

The RData format (usually with extension `.rdata` or `.rda`) is a format designed for use with R, a system for statistical computation and related graphics, for storing a complete R workspace or selected "objects" from a workspace in a form that can be loaded back by R.
"""

# ╔═╡ 1f3092e8-3f3a-4d29-a9f5-e64ef96c13a8
# load RDA file
R_data = RData.load("data/mytempdata.rda")

# ╔═╡ cf9a1b00-79dd-4305-93ad-69f229319ce2
# see the data
R_data["tempdata"]

# ╔═╡ 1b49419d-18cd-4617-9442-15080e3ef588
begin
	# save RDA file
	@rput R_data
	R"save(R_data, file=\"output/mywrite.rda\")"
end

# ╔═╡ c3bce6db-d503-4a68-b83f-278c6d603f07
md"""
**Note:** we must use the `RCall.jl` package to save `.rda` files.
"""

# ╔═╡ 7131ac1c-984e-4ad9-a1cd-b1d46a4f240e
# RDA type
typeof(R_data)

# ╔═╡ 4f70b5df-0026-43cd-b769-d4ca50145fbd
md"""
### MATLAB® Data (MAT)

MAT-files are binary MATLAB® `.mat` files that store workspace variables.
"""

# ╔═╡ 0c1201b9-6acb-4206-a806-e09e04c6a65e
# load MAT file
matlab_data = matread("data/mytempdata.mat")

# ╔═╡ cc555ffc-0eea-4e35-b0d1-36d3a734f9f2
# see the data
matlab_data["tempdata"]

# ╔═╡ 5516dd98-3a87-4801-a62e-e91a95e4923a
# save MAT file
matwrite("output/mywrite.mat", matlab_data)

# ╔═╡ 53be8986-1f06-45a5-9303-5018a3ddf06f
# MAT type
typeof(matlab_data)

# ╔═╡ ecefa934-49a8-4abf-880d-d91d3f9f9d05
md"""
All files, when loaded, are `Dict`. The only exception is NPZ data, which is loaded as an `Matrix`.
"""

# ╔═╡ 01be19ba-2aaa-4746-af5d-8ce7f1cf3a55
md"""
## Processing different types of data

We will mainly cover `Vector` (`Matrix` included), `DataFrame`, and `Dict`. Let's bring back our programming languages dataset `data₂` and start playing it the matrix it's stored in.
"""

# ╔═╡ 3a6f9dc0-2186-440a-9d78-5da6acabcdae
md"""
### Matrix
"""

# ╔═╡ 4f9dca90-9f1a-4c0e-a510-fa640d109b0d
data₂

# ╔═╡ 57b8265f-be5c-4f05-91c6-6c4fec4f0a6b
md"""
Here are some quick questions we might want to ask about this simple data.

> **Q1:** Which year was was a given language invented?
"""

# ╔═╡ 157e9e3d-2e10-418e-997e-0178c14d7b3d
function year_created_mtx(data, language::String)
    loc = findfirst(data[:,2] .== language)
    !isnothing(loc) && return data[loc,1]
	error("Error: Language not found!")
end

# ╔═╡ e7bd695e-c7d6-4b38-8520-7d915e740015
md"""
**Note:** this function return the year just when `loc` local variable is not `nothing`. Otherwise, it returns an error message.
"""

# ╔═╡ c9ead4e0-aa01-43bc-8107-72d8ec4c073b
begin
	julia_year1 = year_created_mtx(data₂, "Julia")

	"Julia was created in $julia_year1."
end

# ╔═╡ 92b4a2d9-b271-42b3-97d1-647300a9439a
begin
	cobol_year1 = year_created_mtx(data₂, "COBOL")

	"COBOL was created in $cobol_year1."
end

# ╔═╡ 01a73335-69b9-4297-9910-a441848fe2c5
md"""
> **Q2:** How many languages were created in a given year?
"""

# ╔═╡ fbf69873-af41-428c-9bba-d63cdb20149b
function langs_per_year_mtx(data, year::Int64)
	count = length(findall(data[:,1] .== year))

	return count
end

# ╔═╡ 1ea19d7e-5748-4559-b2d2-fd4f4d449516
begin
	langs_1988₁ = langs_per_year_mtx(data₂, 1988)

	"In 1988, $langs_1988₁ language(s) was/were created."
end

# ╔═╡ 2592f7cd-95ec-493c-93e1-bbedf433367f
begin
	langs_2006₁ = langs_per_year_mtx(data₂, 2006)

	"In 2006, $langs_2006₁ language(s) was/were created."
end

# ╔═╡ 12121f45-8a7b-4709-bc85-b2fde7313a21
md"""
### DataFrame

Now let's try to store this data in a DataFrame...
"""

# ╔═╡ d455f80e-c5b5-4b1a-9667-86fade5ee3d3
# anonymous column names
data₃ = DataFrame(data₂, :auto)

# ╔═╡ 42c72fde-8331-4e40-b761-c50d0e2e8486
# specifying column names
data₄ = DataFrame(Year = data₂[:,1], Language = data₂[:,2])

# ╔═╡ 201a8534-91ee-4ac9-a120-51b29f196c23
# specifying both column names and data types
data₅ = DataFrame(Year = Int.(data₂[:,1]), Language = string.(data₂[:,2]))

# ╔═╡ 0b14e9fc-62e6-4ad7-ab67-eb36e340749c
md"""
> **Q1:** Which year was was a given language invented?
"""

# ╔═╡ 679b2b83-07f9-40bd-bdd1-89e7233bfade
function year_created_df(df,language::String)
    loc = findfirst(df.Language .== language)
	!isnothing(loc) && return df.Year[loc]
	error("Error: Language not found!")
end

# ╔═╡ f4738476-8b05-4dbc-a15e-6b3d59c90c50
md"""
**Note:** this function return the year just when `loc` local variable is not `nothing`. Otherwise, it returns an error message.
"""

# ╔═╡ c6b170be-6576-480e-b2ba-65d3b853871a
begin
	julia_year2 = year_created_df(data₅, "Julia")

	"Julia was created in $julia_year2."
end

# ╔═╡ 9977fdaa-db30-43a5-bd58-14087c2163e4
begin
	cobol_year2 = year_created_df(data₅, "COBOL")

	"COBOL was created in $cobol_year2."
end

# ╔═╡ a137ebd7-9db4-4c29-a694-b1acad5214a3
md"""
> **Q2:** How many languages were created in a given year?
"""

# ╔═╡ 2cc2938b-58d5-4c27-bdab-dddf22231bc8
function langs_per_year_df(df, year::Int64)
    count = length(findall(df.Year.==year))
    return count
end

# ╔═╡ ae7a1714-2245-4d5a-89bb-10b3adff787a
begin
	langs_1988₂ = langs_per_year_mtx(data₅, 1988)

	"In 1988, $langs_1988₂ language(s) was/were created."
end

# ╔═╡ ce4ac86d-5e06-4c3e-93d6-ebdee46439bd
begin
	langs_2006₂ = langs_per_year_mtx(data₅, 2006)

	"In 2006, $langs_2006₂ language(s) was/were created."
end

# ╔═╡ 515b7479-97d8-464b-8245-1034f3cd8bdc
md"""
### Dictionary

Next, we'll use dictionaries. A quick way to create a dictionary is with the `Dict()` command. But this creates a dictionary without types. Here, we will specify the types of this dictionary.
"""

# ╔═╡ 3b08a2c8-546b-4a6c-8ae0-2a1e64578af8
# dict from a list of tuples
Dict([("A", 1), ("B", 2)])

# ╔═╡ 21551a38-57fc-42c2-81a2-b314bb06250b
md"""
**Note:** the key type is `String`, while the value type is `Int64`.
"""

# ╔═╡ 6da1f5a7-56a2-4a7a-abfc-25ec3f44f892
# dictionary from a list of tuples
Dict([("A", 1), ("B", 2), ("C", [1,2,3])])

# ╔═╡ 43ad8297-d94c-45a0-941f-ab0d5bfd2ddb
md"""
**Note:** the key type is `String`, while the value type is `Any`, since we have integers and arrays as values.
"""

# ╔═╡ 9390d98f-94eb-48bb-ba9c-e6b3f67f762f
# empty dict
Dict()

# ╔═╡ 30718df7-5085-4d0d-8a86-7b4a08ff20b2
# empty dict, specifying that keys are Integers and values are Vectors of Strings
dict₁ = Dict{Integer, Vector{String}}()

# ╔═╡ f77a5189-19b4-4f99-b394-72190253912f
# appending a key-value pair to dict
dict₁[2012] = ["Julia", "Programming", "Language"]

# ╔═╡ 50ba8f3b-5104-4d4f-85ea-831d4afe9164
# updated dict
dict₁

# ╔═╡ bb814af7-62e3-4744-955f-31b557540057
md"""
Now, let's populate the dictionary with years as keys and vectors that hold all the programming languages created in each year as their values. Even though this looks like more work, we often need to do it just once.
"""

# ╔═╡ 9193df28-7ed1-4853-a97b-aa5a1232e07b
begin
	lang_dict = Dict{Integer, Vector{String}}()

	for i in 1:size(data₂,1)
		year, lang = data₂[i,:]
		if year ∈ keys(lang_dict)
			lang_dict[year] = push!(lang_dict[year], lang)
		else
			lang_dict[year] = [lang]
		end
	end
end

# ╔═╡ c5aca233-9a26-4800-9b8d-7f0f03852626
lang_dict

# ╔═╡ 7b2d5919-9629-4ca9-8195-f26183734044
md"""
> **Note:** there is an `enumerate()` method in Julia which is useful when you need not only the values `x` over which you are iterating, but also the number of iterations (index) so far.
**Syntax:**
```julia
for (index, value) in enumerate(df.column)
```
"""

# ╔═╡ d8054919-1554-4c73-a4a2-09383ee09224
md"""
We can check the size of the dictionary...
"""

# ╔═╡ 7d57f6cc-ab44-4658-a73e-92c873111155
length(keys(lang_dict))

# ╔═╡ a322a0e7-3984-4c00-95a5-7b23fc6ec06f
md"""
Or we can check the size of the vector made up by unique `Year` values...
"""

# ╔═╡ e0a28251-a7d6-4c17-b49e-84ffc91c60b6
length(unique(data₂[:,1]))

# ╔═╡ 9073e71f-b922-44a4-a470-5cb081588fcd
md"""
> **Q1:** Which year was was a given language invented?
"""

# ╔═╡ f9325e7b-8df8-448a-98b2-9ea8add93da6
function year_created_dict(dict,language::String)
    keys_vec = collect(keys(dict))
    lookup = map(keyid -> findfirst(dict[keyid] .== language), keys_vec)

    return keys_vec[findfirst((!isnothing).(lookup))]
end

# ╔═╡ c33df74d-dd81-4efa-81d6-a5deb4685f1d
begin
	julia_year3 = year_created_dict(lang_dict, "Julia")

	"Julia was created in $julia_year3."
end

# ╔═╡ a08ad2fd-5d3c-4e76-a059-0608de3e36aa
md"""
> **Q2:** How many languages were created in a given year?
"""

# ╔═╡ fb6df54d-0765-4493-b889-17707ff0cf14
langs_per_year_dict(dict, year::Int64) = length(dict[year])

# ╔═╡ d5e60574-5afd-498c-aedd-1f5ffc4c6bd7
begin
	langs_1988₃ = langs_per_year_dict(lang_dict, 1988)

	"In 1988, $langs_1988₃ language(s) was/were created."
end

# ╔═╡ ee8bcf97-9efa-47f0-a82e-cfaa2efd68e6
md"""
## Missing data

Let's remove the first year value from our data (Matrix) and, after that, create a DataFrame...
"""

# ╔═╡ b3927e97-7516-4a80-bcc1-49614cf67166
begin
	data₂[1,1] = missing

	data₆ = DataFrame(Year=data₂[:,1], Language=data₂[:,2])
end

# ╔═╡ dc7ed277-b044-464e-8ad3-998a83b00d1b
md"""
We can now use the `dropmissing!` function to remove (inplace) the line with missing value...
"""

# ╔═╡ e2b6b920-2e05-4168-af16-07e9611d895d
dropmissing!(data₆)

# ╔═╡ Cell order:
# ╟─c730d000-5ed6-11ec-36b6-39741ccad6c7
# ╟─62c5d4f2-9776-43ae-8125-0d429d6cde42
# ╟─34f92fb1-6c1e-4db6-b795-8a526007ea92
# ╟─13a10dac-8c7e-4e72-9fa9-d58ca8dd0202
# ╟─5100df3f-c5d3-4346-bcd9-8afc92bc559c
# ╠═abe62085-b630-4816-af11-b39133892849
# ╟─51c909b1-1d37-458f-acd6-68f4041bc92f
# ╟─6483858c-2e58-4930-b4e9-76b47032eac4
# ╠═89a5a316-5058-4ff2-8d71-fc832a84c58d
# ╠═853a4e90-ac39-4d73-9953-c7b70e8f78ef
# ╟─3a22c018-2ca7-41df-9ee4-91dc8b64d940
# ╠═b4fcdcf9-793f-4d4a-a5fb-950108cb8188
# ╟─cfa37c7c-810c-4095-8887-262dee4a9d04
# ╠═4b651b5a-e562-46ef-a3b5-8ee85b5a61ed
# ╟─11471bbf-b7b4-4b6e-90cb-9e9ff47bbe4a
# ╠═2652dc01-d80d-46d9-ae7a-9afa534e88cb
# ╟─b2f35e6b-8c19-4729-87a3-292858bfbcb5
# ╠═e1210609-a216-4d54-be63-04f10c5c15bc
# ╠═008b635f-c4e5-475a-83af-4bc3fc722344
# ╟─934c8549-c86c-45e6-a9f5-56297c73513d
# ╠═92708ece-3620-4cf5-b09d-098002044623
# ╟─cac4f1b5-f9e3-469e-9576-553a1cf1b363
# ╠═3d378ec4-659f-4032-a5b7-6d6b6d402c2f
# ╟─e50ae64c-ff64-4597-bf75-d7c921d23225
# ╠═d9ea3574-2ffb-4fdb-9ced-db1dc669ecd9
# ╠═09ff6d8b-15f2-4d8e-a775-dee357a417e4
# ╠═843a668f-7b08-4bce-b02a-3153d0eb7d0d
# ╠═159a35f4-09fd-47bd-9935-88668a7e7f27
# ╟─494ebede-80d0-430e-91aa-0a9050a1bb29
# ╠═00d12274-3057-4a52-8400-0c8c3c358e6b
# ╟─fede3ce2-da03-4ff9-a13e-7ce55ec05bf6
# ╠═74783bbb-3c9d-4e15-bc54-b3d8e7c2d230
# ╟─e4b3a444-c487-4fe8-8048-873b8a3affb7
# ╠═0becba29-d5ff-4595-86ab-6b0575674047
# ╠═9d522bdc-d066-40ac-84c4-47287d9e07e0
# ╟─79de9741-c913-4c00-8833-4f1b68f80ea5
# ╠═fd724d83-41bb-422c-860e-2bbeff17e0dc
# ╟─51301eeb-709a-4ea9-a1a5-d7967f8dd55d
# ╠═64e7e90b-cd7d-4569-a715-4f27f1787e40
# ╟─e5f2c69b-fbd8-4048-aaed-792a64a4d66c
# ╠═26047637-1b77-4203-b705-e34f74137295
# ╠═0f3d505c-a575-419f-8155-fd071f510714
# ╟─c2f1eab2-862a-4eb8-b233-71567cdc9963
# ╠═3a924b3e-3dbe-49b6-9e5e-5a48b0ffd385
# ╟─58bad348-0586-4d0f-8d54-7c3e1a7a2c40
# ╠═036de982-36ad-431a-8445-517178626911
# ╟─4ead9039-f4c2-4f82-bf1e-2146718014a3
# ╠═bb04dc6c-e62e-4b2c-a247-ebe4836d8013
# ╟─d9550872-fa7c-4ebd-9a4d-828182c8e49d
# ╠═e01a65ab-394a-4718-b9ae-ca2bb30e20b5
# ╟─1eb26b3c-8a5a-4648-bb73-f8291b656edc
# ╠═cb027055-2cf6-4f74-b265-9d01c23c2967
# ╟─12dccfc3-e976-46e6-81b0-dc51b48309f4
# ╠═403ac783-9c02-4fd1-8506-afe0af36a14b
# ╟─36d8898e-980c-4f54-bbff-277ded2229ea
# ╠═8d769d53-1902-4a09-8228-ce60174b22bb
# ╟─bc29d2b5-e6c2-4b45-8b79-cbb012659be1
# ╟─7c2fef9c-4e9a-4637-bc8e-6636af5f98ae
# ╠═8f51aeb5-3e75-4e5a-8c8f-ed0201eb8ce0
# ╠═879ac394-8598-4399-957f-1280ecd99478
# ╠═0d27e3b3-79e3-424d-bc6b-94252900f7fb
# ╠═13053d54-8680-41e2-b416-8a7bc50f6c92
# ╟─e0f49c77-0c0d-4740-9227-5619d14748b6
# ╠═f4ddf1bf-0cbf-47c5-bb5f-f8abf0fb8ac2
# ╠═a979107e-f2cf-42da-aefa-5cb249bc7073
# ╠═edd2093e-f84a-4563-acb7-219f2bd07399
# ╟─be91882d-fe73-40d0-8e75-30c2e8ddd564
# ╠═1f3092e8-3f3a-4d29-a9f5-e64ef96c13a8
# ╠═cf9a1b00-79dd-4305-93ad-69f229319ce2
# ╠═1b49419d-18cd-4617-9442-15080e3ef588
# ╟─c3bce6db-d503-4a68-b83f-278c6d603f07
# ╠═7131ac1c-984e-4ad9-a1cd-b1d46a4f240e
# ╟─4f70b5df-0026-43cd-b769-d4ca50145fbd
# ╠═0c1201b9-6acb-4206-a806-e09e04c6a65e
# ╠═cc555ffc-0eea-4e35-b0d1-36d3a734f9f2
# ╠═5516dd98-3a87-4801-a62e-e91a95e4923a
# ╠═53be8986-1f06-45a5-9303-5018a3ddf06f
# ╟─ecefa934-49a8-4abf-880d-d91d3f9f9d05
# ╟─01be19ba-2aaa-4746-af5d-8ce7f1cf3a55
# ╟─3a6f9dc0-2186-440a-9d78-5da6acabcdae
# ╠═4f9dca90-9f1a-4c0e-a510-fa640d109b0d
# ╟─57b8265f-be5c-4f05-91c6-6c4fec4f0a6b
# ╠═157e9e3d-2e10-418e-997e-0178c14d7b3d
# ╟─e7bd695e-c7d6-4b38-8520-7d915e740015
# ╠═c9ead4e0-aa01-43bc-8107-72d8ec4c073b
# ╠═92b4a2d9-b271-42b3-97d1-647300a9439a
# ╟─01a73335-69b9-4297-9910-a441848fe2c5
# ╠═fbf69873-af41-428c-9bba-d63cdb20149b
# ╠═1ea19d7e-5748-4559-b2d2-fd4f4d449516
# ╠═2592f7cd-95ec-493c-93e1-bbedf433367f
# ╟─12121f45-8a7b-4709-bc85-b2fde7313a21
# ╠═d455f80e-c5b5-4b1a-9667-86fade5ee3d3
# ╠═42c72fde-8331-4e40-b761-c50d0e2e8486
# ╠═201a8534-91ee-4ac9-a120-51b29f196c23
# ╟─0b14e9fc-62e6-4ad7-ab67-eb36e340749c
# ╠═679b2b83-07f9-40bd-bdd1-89e7233bfade
# ╟─f4738476-8b05-4dbc-a15e-6b3d59c90c50
# ╠═c6b170be-6576-480e-b2ba-65d3b853871a
# ╠═9977fdaa-db30-43a5-bd58-14087c2163e4
# ╟─a137ebd7-9db4-4c29-a694-b1acad5214a3
# ╠═2cc2938b-58d5-4c27-bdab-dddf22231bc8
# ╠═ae7a1714-2245-4d5a-89bb-10b3adff787a
# ╠═ce4ac86d-5e06-4c3e-93d6-ebdee46439bd
# ╟─515b7479-97d8-464b-8245-1034f3cd8bdc
# ╠═3b08a2c8-546b-4a6c-8ae0-2a1e64578af8
# ╠═21551a38-57fc-42c2-81a2-b314bb06250b
# ╠═6da1f5a7-56a2-4a7a-abfc-25ec3f44f892
# ╟─43ad8297-d94c-45a0-941f-ab0d5bfd2ddb
# ╠═9390d98f-94eb-48bb-ba9c-e6b3f67f762f
# ╠═30718df7-5085-4d0d-8a86-7b4a08ff20b2
# ╠═f77a5189-19b4-4f99-b394-72190253912f
# ╠═50ba8f3b-5104-4d4f-85ea-831d4afe9164
# ╟─bb814af7-62e3-4744-955f-31b557540057
# ╠═9193df28-7ed1-4853-a97b-aa5a1232e07b
# ╠═c5aca233-9a26-4800-9b8d-7f0f03852626
# ╟─7b2d5919-9629-4ca9-8195-f26183734044
# ╟─d8054919-1554-4c73-a4a2-09383ee09224
# ╠═7d57f6cc-ab44-4658-a73e-92c873111155
# ╟─a322a0e7-3984-4c00-95a5-7b23fc6ec06f
# ╠═e0a28251-a7d6-4c17-b49e-84ffc91c60b6
# ╟─9073e71f-b922-44a4-a470-5cb081588fcd
# ╠═f9325e7b-8df8-448a-98b2-9ea8add93da6
# ╠═c33df74d-dd81-4efa-81d6-a5deb4685f1d
# ╟─a08ad2fd-5d3c-4e76-a059-0608de3e36aa
# ╠═fb6df54d-0765-4493-b889-17707ff0cf14
# ╠═d5e60574-5afd-498c-aedd-1f5ffc4c6bd7
# ╟─ee8bcf97-9efa-47f0-a82e-cfaa2efd68e6
# ╠═b3927e97-7516-4a80-bcc1-49614cf67166
# ╟─dc7ed277-b044-464e-8ad3-998a83b00d1b
# ╠═e2b6b920-2e05-4168-af16-07e9611d895d


# # Stuff I Like About The Julia Programming Language

# `git_club 2020-06-19` - Hannes

# [View this notebook online](https://nbviewer.jupyter.org/github/Hasnep/stuff-i-like-about-julia/blob/master/stuff-i-like-about-julia.ipynb)

# > Julia's unofficial tagline is "Looks like Python, feels like Lisp, runs like Fortran."
# > I've been learning Julia for slightly less than a year now, and I'd like to share some of the things I've enjoyed about it.
# > I'll give an overview of the language's main features with code examples and discuss whether it really is the future scientific computing.

# Technically:
# > Julia is a high level, JIT compiled, dynamic language designed with multiple dispatch, automatic differentiation and metaprogramming.

# In practice:
# > Julia finds a balance between being fast and easy to use with lots of features you'll miss when you use another language.

# ## History

# - Julia was started in 2009 by Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and Alan Edelman
# - Announced in 2012
# - Version 1.0 was released in 2018
# - Now on version 1.4, heading for 1.5 soon

# ## Julia is easy to use

# Let's write a function from scratch to sum an array of numbers, first in Python:
# ```python
# def my_sum(array):
#     """
#     Sum a list.
#     """
#     total = 0
#     for x in array:
#         total += x
#     return total
# ```

# Then in Julia:

"""
Sum an array.
"""
function my_sum(array)
    total = 0
    for x in array
        total += x
    end
    return total
end

# This syntax will be familliar if you've used Python.

my_sum(1:10)

# Benchmarks for this function in different languages:
# - C: ~10ms
# - Python: ~500ms
# - Julia: ~10ms
# > Source: [An Introduction to Julia (Beginner Level) | SciPy 2018 Tutorial | Jane Herriman, Sacha Verweij](https://www.youtube.com/watch?v=b5xvVyzUnXI)

# ## It's fast

# Julia was designed to solve the "two language problem", where researchers use a slower, high-level language for research and then have to use a slower, low-level language once they hit a bottleneck or for production.
# Julia is both languages at the same time!

# Some benchmarks (from the Julia website) of different languages relative to C.
# ![Benchmarks of different languages relative to C](https://julialang.org/assets/benchmarks/benchmarks.svg)
# > Source: https://julialang.org/benchmarks/

# These benchmarks try to compare implementations of the same algorithm.
# Julia is comparable to compiled languages like Rust, Go, Fortran, etc. and is sometimes faster than C.
# Python is sometimes 100x slower than C.
# R is a bit slower than that.

# How is it so fast?

# Generally languages fall into two categories:

# - Compiled languages like C or Rust compile all the code before you run
# - Interpreted languages like Python or R don't compile

# Julia uses a _Just-In-Time_ (JIT) compiler which compiles a function the first time it is called.

# In my opinion, speed doesn't matter all that much

# - JIT compilation takes a short while
# - My time is more valuable than the computer's time

# Now, back to some code!

# ## Dynamically typed

# Let's write a function to calculate the $n$th Fibonacci number.
# We want the input `n` to only allow integers and we want to specify the output should only be integers.

"""
Calculate the nth Fibonacci number.
"""
function fib(n::Integer)::Integer
    if n < 2
        return n
    else
        return fib(n - 1) + fib(n - 2)
    end
end

# If we try to run the function with a non-integer input

# ```julia
# julia> fib(0.5)
# ```

# Julia will throw an error:

# ```
# ERROR: MethodError: no method matching fib(::Float64)
# Closest candidates are:
#   fib(::Integer)
# ```

fib(20)

# There is also a one-line function notation that is useful for short functions

short_fib(n::Integer)::Integer = n < 2 ? n : short_fib(n - 1) + short_fib(n - 2)

short_fib(20)

# ## It's free!

# Julia is MIT licenced.

# ## Broadcasting

# In Python, most functions accept one element.
# Running this code:
# ```python
# >>> import math
# >>> math.sin([1, 2, 3])
# ```
# will give this error:
# ```
# TypeError: must be real number, not list
# ```
# Pythonistas would probably use a list comprehension:
# ```python
# >>> [math.sin(x) for x in [1, 2, 3]]
# ```
# or a map:
# ```python
# >>> map(math.sin, [1, 2, 3])
# ```
# ```
# [0.8414709848078965, 0.9092974268256817, 0.1411200080598672]
# ```

# In R, most functions are vectorised
# ```R
# > sin(c(1, 2, 3))
# ```
# ```
# [1] 0.8414710 0.9092974 0.1411200
# ```

# In Julia, functions are not vectorised, for example, this line:
# ```julia
# julia> sin([1, 2, 3])
# ```
# gives this error:
# ```
# ERROR: MethodError: no method matching sin(::Array{Int64,1})
# ```

# Using the broadcast operator, the funciton is applied elementwise!

sin.([1, 2, 3])

# Broadcasting even works for user functions

fib.(1:10)

# And for operators:

## What are the first 10 square numbers?
(1:10).^2

# This is powerful, but sometimes tricky syntax.
# For example, adding a dot makes the length function broadcast over the array:

length(split("How many words are in this sentence?"))
# +
length.(split("How many characters are each of these words?"))

# ## Useful Unicode characters

# Julia lets you type lots of symbols using LaTeX-y abbreviations, e.g. type `\alpha` and press tab for α or `\sqrt` and tab for √.

α = 37

# The square root symbol is an abbreviation for the `sqrt()` function:

√α

# Some constants like π for pi and ℯ for Euler's constant are predefined:

## Use the approximate equals sign ≈ because this calculation is not exact
ℯ^(im * π) ≈ -1

# You can define your own operators using either built in functions:

const ⊂ = issubset
[2, 5] ⊂ [1, 2, 3, 4, 5]

# or user functions:

const ∑ = my_sum
∑(1:10)

# Because Julia supports almost any symbol you can type as a variable name, that includes emojis!

🔥 = 10
🐶 = 20
🌭 = 30
🔥 + 🐶 == 🌭

# ## Julia is (mostly) written in Julia

# If you can read Julia code, you can also read Julia's source code to understand what it does.
# I looked at the most recent PR as an example:

tensor(A::AbstractArray, B::AbstractArray) = [a * b for a in A, b in B]
const ⊗ = tensor
#-
[1, 2] ⊗ [3, 4, 5]

# Python's numpy is fast because it's mostly written in C/C++ (51.4%), but if you want to do something that numpy can't do, you need to either use C++ or write slower Python code.

# This bridges the gap between a Julia user and a Julia developer.
# For every user of Julia, there's another possible contributer.

# ## Multiple dispatch

# As an example, let's build a small Julia package based on [Measurements.jl](https://github.com/JuliaPhysics/Measurements.jl/).

"""
A number with some error.
"""
struct Uncertain <: Real
    val::Real
    err::Real
end

# Define the standard gravity on earth.

g = Uncertain(9.8, 0.1)

# Wouldn't it be nicer to show an uncertain number with the plus/minus symbol?
# Let's write a show function that dispatches on the Uncertain type:

Base.show(io::IO, x::Uncertain) = print(io, "$(x.val) ∓ $(x.err)")

g

# Let's define the plus/minus operator to create `Uncertain` numbers:

∓(a::Real, b::Real) = Uncertain(a, b)
my_height = 190 ∓ 1

# How do you add two uncertain measurements?

my_brothers_height = 175 ∓ 2

# ```julia
# julia> my_height + my_brothers_height
# ```
# ```
# ERROR: + not defined for Uncertain
# ```

# $$
# Q = a + b \\
# {\delta Q} = \sqrt{(\delta a)^2 + (\delta b)^2}
# $$

Base.:+(a::Uncertain, b::Uncertain) = (a.val + b.val) ∓ sqrt(a.err^2 + b.err^2)
my_height + my_brothers_height

# Similar for subtraction.

Base.:-(a::Uncertain, b::Uncertain) = (a.val - b.val) ∓ sqrt(a.err^2 + b.err^2)
my_height - my_brothers_height

# Slightly more complicated for multiplcation and division.

function Base.:*(a::Uncertain, b::Uncertain)
    total_value = a.val * b.val
    total_error = total_value * sqrt((a.err / a.val)^2 + (b.err / b.val)^2)
    return Uncertain(total_value, total_error)
end

function Base.:/(a::Uncertain, b::Uncertain)
    total_value = a.val / b.val
    total_error = total_value * sqrt((a.err / a.val)^2 + (b.err / b.val)^2)
    return Uncertain(total_value, total_error)
end
#-
my_height * my_brothers_height
#-
my_height / my_brothers_height

# Finally powers, again the exact formula is not important.

Base.:^(a::Uncertain, b::Real) = (a.val^b) ∓ (abs(b) * a.val^(b - 1) * a.err)
my_height^2.0

# Finally, we need to tell Julia what to do if we give it an `Uncertain` number and some other number in the same operation.

# ```julia
# julia> 1 + g
# ```
# ```
# ERROR: promotion of types Int64 and Uncertain failed to change any arguments
# ```

# We want to convert both numbers to our `Uncertain` type:

Base.promote_rule(::Type{Uncertain}, ::Type{T}) where T <: Real = Uncertain

# Coverting a real number to our Uncertain type just means we add an error of 0:

Base.convert(::Type{Uncertain}, x::Real) = Uncertain(x, 0)

# When Julia wants to convert an Uncertain number it doesn't need to do anything :

Base.convert(::Type{Uncertain}, x::Uncertain) = x
#-
1 + g

# Now we can solve a simple problem, how much time would it take if I dropped a ball from my height and my brother caught it at his height?

# Solve for t:
# $$
# t = \frac{\sqrt{2 a s + u^2} - u}{a} = \frac{\sqrt{2 g (h_1 - h_2)}}{g}
# $$

t = ((2 * g * (my_height - my_brothers_height))^0.5) / g

# Let's use the actual Measurements.jl package

using Measurements: Measurement, ±

# Let's solve the same problem as before and make sure we get the same result:

g = 9.8 ± 0.1
my_height = 190 ± 1
my_brothers_height = 175 ± 2

t = (2 * g * (my_height - my_brothers_height))^0.5 / g

# This next part is inspired by a JuliaCon talk called [_The Unreasonable Effectiveness of Multiple Dispatch_](https://www.youtube.com/watch?v=kc9HwsxE1OY) by one of the Julia co-founders, Stefan Karpinski.

# All the "methods" are outside the class definition, that can include being in a completely different package.

# Let's solve the same problem using differential equations!

# Calculate the position of the ball between time $t = 0$ and $t = 3$
# $$
# v = \frac{ds}{dt}
# $$
# $$
# f(t) = -g t = - (9.8 \pm 0.1) t
# $$
# With initial conditions $s_0 = 190 \pm 1$

using DifferentialEquations
#-
f(s, p, t) = -g * t
s₀ = my_height
time_span = (0.0, 3.0)
problem = ODEProblem(f, s₀, time_span)
#-
solution = solve(problem, Tsit5(),  saveat=0.1)
#-
using Plots
#-
plot(
    solution.t,
    solution.u,
    title="Solution to the ODE",
    xaxis="Time (t) in seconds",
    yaxis="Displacement s(t) in metres"
)

# ## Interoperability

# We can import any python module using the PyCall package:

using PyCall
#-
math = pyimport("math")
#-
math.sqrt(100)

# We can mix Python functions and variables with Julia code:

math.sin.([π,  math.pi, 2π,    2 * math.pi])

# We can define Python functions that call Julia funcitons

py"""
def pyfib(n, fun):
    if n < 2:
        return n
    else:
        return fun(n - 1, pyfib) + fun(n - 2, pyfib)
"""
#-
function jlfib(n, fun)
    if n < 2
        return n
    else
        return fun(n - 1, jlfib) + fun(n - 2, jlfib)
    end
end
#-
jlfib(20, py"pyfib")

# If you have some code written in another language (like Python), there can be a smooth transition to using Julia.

# # Summary

# - Julia looks a bit like Python
# - It's fast (with some caveats)
# - It's free!
# - It's written in Julia
# - It has powerful features like:
#     - Broadcasting syntax
#     - Unicode variables
#     - Multiple dispatch
#     - Good interoperability

# Other stuff I haven't mentioned:
# - Automatic differentiation (Swift also has this now)
# - Metaprogramming
# - Data science stuff (The first two letters of Jupyter Notebooks are named after Julia!)


# Copyright (c) 2019 Arpit Bhatia and contributors                               #src
#                                                                                #src
# Permission is hereby granted, free of charge, to any person obtaining a copy   #src
# of this software and associated documentation files (the "Software"), to deal  #src
# in the Software without restriction, including without limitation the rights   #src
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell      #src
# copies of the Software, and to permit persons to whom the Software is          #src
# furnished to do so, subject to the following conditions:                       #src
#                                                                                #src
# The above copyright notice and this permission notice shall be included in all #src
# copies or substantial portions of the Software.                                #src
#                                                                                #src
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR     #src
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,       #src
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE    #src
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER         #src
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,  #src
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE  #src
# SOFTWARE.                                                                      #src

# # Solvers and Solutions

# **Originally Contributed by**: Arpit Bhatia

# The purpose of this part of the tutorial is to introduce you to solvers and
# how to use them with JuMP. We'll also learn what to do with a problem after
# the solver has finished optimizing it.

# ## What is a Solver?

# A solver is a software package that incorporates algorithms for finding
# solutions to one or more classes of problem. For example, GLPK, which we used
# in the previous tutorials is a solver for linear programming (LP) and mixed
# integer programming (MIP) problems. It incorporates algorithms such as the
# simplex method, interior-point method etc. JuMP currently supports a number of
# open-source and commercial solvers which can be viewed in the [Supported-solvers](@ref)
# table.

# ## What is MathOptInterface?

# Each mathematical optimization solver API has its own concepts and data
# structures for representing optimization models and obtaining results.
# However, it is often desirable to represent an instance of an optimization
# problem at a higher level so that it is easy to try using different solvers.

# MathOptInterface (MOI) is an abstraction layer designed to provide an
# interface to mathematical optimization solvers so that users do not need to
# understand multiple solver-specific APIs. MOI can be used directly, or through
# a higher-level modeling interface like JuMP.

# Note that JuMP re-exports MathOptInterface and you can use the shortcut `MOI`
# to refer to MathOptInterface in your code.

# ## Constructing a model

# JuMP models can be created in three different modes: `AUTOMATIC`, `MANUAL` and
# `DIRECT`. We'll use the following LP to illustrate them.

# ```math
# \begin{aligned}
# & \max_{x,y} & x + 2y \\
# & \;\;\text{s.t.} & x + y &\leq 1 \\
# & & 0\leq x, y &\leq 1 \\
# \end{aligned}
# ```

using JuMP
using GLPK

# ### `AUTOMATIC` Mode

# #### With Optimizer

# This is the easiest method to use a solver in JuMP. In order to do so, we
# simply set the solver inside the Model constructor.

model_auto = Model(GLPK.Optimizer)
@variable(model_auto, 0 <= x <= 1)
@variable(model_auto, 0 <= y <= 1)
@constraint(model_auto, x + y <= 1)
@objective(model_auto, Max, x + 2y)
optimize!(model_auto)
objective_value(model_auto)

# #### No Optimizer (at first)

# It is also possible to create a JuMP model with no optimizer attached. After
# the model object is initialized empty and all its variables, constraints and
# objective are set, then we can attach the solver at `optimize!` time.

model_auto_no = Model()
@variable(model_auto_no, 0 <= x <= 1)
@variable(model_auto_no, 0 <= y <= 1)
@constraint(model_auto_no, x + y <= 1)
@objective(model_auto_no, Max, x + 2y)
set_optimizer(model_auto_no, GLPK.Optimizer)
optimize!(model_auto_no)
objective_value(model_auto_no)

# Note that we can also enforce the automatic mode by passing
# `caching_mode = MOIU.AUTOMATIC` in the Model function call.

# ### `MANUAL` Mode

# This mode is similar to the `AUTOMATIC` mode, but there are less protections
# from the user getting errors from the solver API. On the other side, nothing
# happens silently, which might give the user more control. It requires
# attaching the solver before the solve step using the `MOIU.attach_optimizer()`
# function.

model_manual = Model(GLPK.Optimizer, caching_mode = MOIU.MANUAL)
@variable(model_manual, 0 <= x <= 1)
@variable(model_manual, 0 <= y <= 1)
@constraint(model_manual, x + y <= 1)
@objective(model_manual, Max, x + 2y)
MOIU.attach_optimizer(model_manual)
optimize!(model_manual)
objective_value(model_manual)

# ### `DIRECT` Mode

# Some solvers are able to handle the problem data directly. This is common for
# LP/MIP solver but not very common for open-source conic solvers. In this case
# we do not set a optimizer, we set a backend which is more generic and is able
# to hold data and not only solving a model.

model_direct = direct_model(GLPK.Optimizer())
@variable(model_direct, 0 <= x <= 1)
@variable(model_direct, 0 <= y <= 1)
@constraint(model_direct, x + y <= 1)
@objective(model_direct, Max, x + 2y)
optimize!(model_direct)
objective_value(model_direct)

# ### Solver Options

# Many of the solvers also allow options to be passed in. However, these options
# are solver-specific. To find out the various options available, please check
# out the individual solver packages. Some examples for the GLPK solver are
# given below.

using GLPK

# To turn off printing (i.e. silence the solver),

model = Model(optimizer_with_attributes(GLPK.Optimizer, "msg_lev" => 0));

# To increase the maximum number of simplex iterations:

model = Model(optimizer_with_attributes(GLPK.Optimizer, "it_lim" => 10_000));

# To set the solution timeout limit (in milliseconds):

model = Model(optimizer_with_attributes(GLPK.Optimizer, "tm_lim" => 5_000));

# ## How to querying the solution

# So far we have seen all the elements and constructs related to writing a JuMP
# optimization model. In this section we reach the point of what to do with a
# solved problem. JuMP follows closely the concepts defined in MathOptInterface
# to answer user questions about a finished call to `optimize!(model)`. The
# three main steps in querying a solution are given below. We'll use the model
# we created in `AUTOMATIC` mode with an optimizer attached in this section.

# ### The termination status

# Termination statuses are meant to explain the reason why the optimizer stopped
# executing in the most recent call to `optimize!`.

termination_status(model_auto)

# You can view the different termination status codes by referring to the docs
# or though checking the possible types using the below command.

display(typeof(MOI.OPTIMAL))

# ### The primal and dual status

# These statuses indicate what kind of result is available to be queried from
# the model. It's possible that no result is available to be queried. We shall
# discuss more on the dual status and solutions in the Duality tutorial.

primal_status(model_auto)

#-

dual_status(model_auto)

# As we saw before, the result (solution) status codes can be viewed directly
# from Julia.

display(typeof(MOI.FEASIBLE_POINT))

# ### Getting the primal solution

# Provided the primal status is not `MOI.NO_SOLUTION`, we can inspect the
# solution values and optimal cost.

value(x)

#-

value(y)

#-

objective_value(model_auto)

# Since it is possible that no solution is available to be queried from the
# model, calls to [`value`](@ref) may throw errors. Hence, it is recommended to
# check for the presence of solutions.

model_no_solution = Model(GLPK.Optimizer)
@variable(model_no_solution, 0 <= x <= 1)
@variable(model_no_solution, 0 <= y <= 1)
@constraint(model_no_solution, x + y >= 3)
@objective(model_no_solution, Max, x + 2y)

optimize!(model_no_solution)

try #hide
if termination_status(model_no_solution) == MOI.OPTIMAL
    optimal_solution = value(x)
    optimal_objective = objective_value(model_no_solution)
elseif termination_status(model_no_solution) == MOI.TIME_LIMIT && has_values(model_no_solution)
    suboptimal_solution = value(x)
    suboptimal_objective = objective_value(model_no_solution)
else
    error("The model was not solved correctly.")
end
catch err; showerror(stderr, err); end  #hide


list_test_str = "light red bags contain 1 bright white bag, 2 muted yellow bags.
dark orange bags contain 3 bright white bags, 4 muted yellow bags.
bright white bags contain 1 shiny gold bag.
muted yellow bags contain 2 shiny gold bags, 9 faded blue bags.
shiny gold bags contain 1 dark olive bag, 2 vibrant plum bags.
dark olive bags contain 3 faded blue bags, 4 dotted black bags.
vibrant plum bags contain 5 faded blue bags, 6 dotted black bags.
faded blue bags contain no other bags.
dotted black bags contain no other bags."

list_test = split(list_test_str, '\n')

list = readlines("day7_input.txt")

bag = "shiny gold"

#regxrule = r"( bags contain | bag\, | bags\, | bag\.| bags\.)"

regxrule = r"( bags contain | bags?, | bags?.)"

regxrule = r" bags contain | bags?[,.] ?"

regxrule = r" bags contain | bags?(, |\.)"

function get_outer_bag_rules(list)
    outer = Dict()
    for line in list
        parts = split(line, regxrule, keepempty=false)
            for part in parts[2:end]
                if part ≠ "no other"
                    num, bag = split(part, ' ', limit=2)
                    if bag in keys(outer)
                        push!(outer[bag], parts[1])
                    else
                        push!(outer, bag => Set([parts[1]]))
                    end
                end
            end
    end
    return outer
end

function get_outer_bags(bag, outer_bag_rules, outer_set = Set())
    if bag in keys(outer_bag_rules)
        outermost_bags = outer_bag_rules[bag]
        if outermost_bags ∉ outer_set
            union!(outer_set,outermost_bags)
            for outer_bag ∈ outermost_bags
                union!(outer_set, get_outer_bags(outer_bag, outer_bag_rules, outer_set))
            end
        end
    end
    return outer_set
end

get_number_outer_bags(bag, list) = length(get_outer_bags(bag, get_outer_bag_rules(list)))

println("Number of outer bags containing `$bag` bag in test list is $(get_number_outer_bags(bag, list_test))\n")
println("Number of outer bags containing `$bag` bag in problem list is $(get_number_outer_bags(bag, list))\n")

#=
```julia
julia> @btime get_number_outer_bags("shiny gold", list)
  5.744 ms (99019 allocations: 3.22 MiB)
185

julia> @btime get_outer_bag_rules(list);
  1.970 ms (17606 allocations: 1.25 MiB)

julia> outer_bag_rules = get_outer_bag_rules(list);

julia> @btime length(get_outer_bags(bag, outer_bag_rules));
  3.742 ms (81414 allocations: 1.97 MiB)
```
=#

# Outra versão com mais regex, mas só um pouco mais rápida.

# A versão regex_web foi testada em https://regexr.com/5hutn
regexrule_web = r"(.+)(?: bags contain )((\d+) ([^,\n]*) (bag,|bags,) )?((\d+) ([^,\n]*) (bag,|bags,) )?((\d+) ([^,\n]*) (bag,|bags,) )?((\d+) ([^,\n]*) (bag,|bags,))?((\d+|no) ([^,\n]*) ((bag\.|bags\.)))\n?"
regexrule = r"(.+)(?: bags contain )((\d+) ([^,]*) (bag,|bags,) )?((\d+) ([^,]*) (bag,|bags,) )?((\d+) ([^,]*) (bag,|bags,) )?((\d+) ([^,]*) (bag,|bags,))?((\d+|no) ([^,]*) ((bag\.|bags\.)))"
#regexrule = r"(.+)(?: bags contain )((\d+) ([^,]*) (bag,|bags,) )?{4}((\d+|no) ([^,]*) ((bag\.|bags\.)))"

function get_outer_bag_rules2(list)
    outer = Dict()
    for line in list
        m = match(regexrule, line)
            for j in 3:4:length(m.captures)
                if m[j] ∉ ("", "no") && !(m[j+1] === nothing)
                    num = m[j]
                    bag = m[j+1]
                    if bag in keys(outer)
                        push!(outer[bag], m[1])
                    else
                        push!(outer, bag => Set([m[1]]))
                    end
                end
            end
    end
    return outer
end

# part 2

list_test2_str = "shiny gold bags contain 2 dark red bags.
dark red bags contain 2 dark orange bags.
dark orange bags contain 2 dark yellow bags.
dark yellow bags contain 2 dark green bags.
dark green bags contain 2 dark blue bags.
dark blue bags contain 2 dark violet bags.
dark violet bags contain no other bags."

list_test2 = split(list_test2_str, '\n')

function get_inner_bag_rules(list)
    inner = Dict()
    for line in list
        parts = split(line, regxrule, keepempty=false)
            for part in parts[2:end]
                if part ≠ "no other"
                    num, bag = split(part, ' ', limit=2)
                    if parts[1] in keys(inner)
                        push!(inner[parts[1]], (bag, parse(Int, num)))
                    else
                        push!(inner, parts[1] => [(bag, parse(Int, num))])
                    end
                end
            end
    end
    return inner
end

function get_next_inner_bags(bag, inner_bag_rules)
    if bag in keys(inner_bag_rules)
        innermost_stuff = inner_bag_rules[bag]
        inner_sum = 1 # start with containing bag
        for (innermost_bag, innermost_num) in innermost_stuff
            # recursively count inner bags
            inner_sum += innermost_num * get_next_inner_bags(innermost_bag, inner_bag_rules)
        end
        return inner_sum
    else
        return 1
    end
end

get_number_inner_bags(bag, list) = get_next_inner_bags(bag, get_inner_bag_rules(list)) - 1 # discount starting bag

println("Number of bags contained in `$bag` bag in test list is $(get_number_inner_bags(bag, list_test))\n")
println("Number of bags contained in `$bag` bag in second test list is $(get_number_inner_bags(bag, list_test2))\n")
println("Number of bags contained in `$bag` bag in problem list is $(get_number_inner_bags(bag, list))\n")

#=
# In this case, most of the time is taken extracting the rules.
# We don't need to keep growing with the set of color bags, just the number
```julia
julia> @btime get_number_inner_bags("shiny gold", list)
  1.970 ms (17252 allocations: 1.10 MiB)
89084

julia> inner_bag_rules = get_inner_bag_rules(list);

julia> @btime get_inner_bag_rules(list);
  1.906 ms (16404 allocations: 1.07 MiB)

julia> @btime get_next_inner_bags(bag, inner_bag_rules) - 1;
  61.513 μs (848 allocations: 33.66 KiB)

```
=#