function problem_generator_mw(
                            nprob::Int64,
                            n::Int64,
                            m::Int64,
                            rsp::Bool;
                            unconstrained::Bool=false,
                            x::Vector{Float64}=( NaN * ones(Float64, n) )
                            )

    @assert 0 ≤ nprob ≤ 22 "The number of the problem must satisfy 0 ≤ nprob ≤ 22."
    @assert length(x) == n "The dimension of vetor x must be n."

    # Initializes some useful variables and constants
    c_lower = - 1.0e20
    c_upper = 1.0e20

    if isnan( x[1] )

        usual_init_point = true

    end

    if nprob == 1

        # Linear function - Full Rank.

        @assert m ≥ n "The dimensions of the problem must satisfy m ≥ n."

        if usual_init_point

            @. x = 1.0

        end

        if unconstrained

            l = c_lower * ones(Float64, n)
            u = c_upper * ones(Float64, n)

        else

            l = 0.75 * ones(Float64, n)
            u = 15.0 * ones(Float64, n)

        end

        fmin = Array{Function}(undef, m)
        c = 2.0 / m

        for i = 1:n

            fmin[i] = x -> ( x[i] - c * sum(x) - 1.0 ) ^ 2.0

        end

        for i = (n+1):m

            fmin[i] = x -> ( - c * sum(x) - 1.0 ) ^ 2.0

        end

    elseif nprob == 2

        # Linear function - Rank 1.

        @assert m ≥ n "The dimensions of the problem must satisfy m ≥ n."

        if usual_init_point

            @. x = 1.0

        end

        if unconstrained

            l = c_lower * ones(Float64, n)
            u = c_upper * ones(Float64, n)

        else

            l = - ones(Float64, n)
            u = 10.0 * ones(Float64, n)

        end

        fmin = Array{Function}(undef, m)

        function f2(x)

            s = 0.0

            for i=1:length(x)

                s += i * x[i]

            end

            return s

        end

        for i=1:m

            fmin[i] = x -> ( i * f2(x) - 1.0 ) ^ 2.0

        end

    elseif nprob == 3

        # Linear function - Rank 1 with zero columns and rows.

        @assert m ≥ n "The dimensions of the problem must satisfy m ≥ n."

        if usual_init_point

            @. x = 1.0

        end

        if unconstrained

            l = c_lower * ones(Float64, n)
            u = c_upper * ones(Float64, n)

        else

            l = 2.0 * ones(Float64, n)
            u = 7.0 * ones(Float64, n)

        end

        fmin = Array{Function}(undef, m)

        function f3(x)

            s = 0.0

            for i=2:(length(x)-1)

                s += i * x[i]

            end

            return s

        end

        fmin[1] = x -> 1.0
        fmin[m] = x -> 1.0
        for i = 2:(m-1)

            fmin[i] = x -> ( ( i - 1.0 ) * f3(x) - 1.0 ) ^ 2.0

        end

    elseif nprob == 4

        # Rosenbrock function.

        @assert ( n == 2 ) && ( m == 2 ) "The dimensions of the problem must satisfy n = m = 2."

        if usual_init_point

            @. x = [-1.2, 1.0]

        end

        if unconstrained

            l = c_lower * ones(Float64, n)
            u = c_upper * ones(Float64, n)

        else

            l = - 5.0 * ones(Float64, n)
            u = zeros(Float64, n)

        end

        fmin = Array{Function}(undef, m)
        fmin[1] = x -> ( 10.0 * ( x[2] - x[1] ^ 2.0 ) ) ^ 2.0
        fmin[2] = x -> ( 1.0 - x[1] ) ^ 2.0

    elseif nprob == 5

        # Helical Valley function

        @assert ( n == 3 ) && ( m == 3 ) "The dimensions of the problem must satisfy n = m = 3."

        if usual_init_point

            @. x = [-1.0, 0.0, 0.0]

        end

        if unconstrained

            l = c_lower * ones(Float64, n)
            u = c_upper * ones(Float64, n)

        else

            l = - 10.0 * ones(Float64, n)
            u = - ones(Float64, n)

        end

        fmin = Array{Function}(undef, m)

        function f5(x)

            if x[1] > 0.0

                return atan( x[2] / x[1] ) / ( 2.0 * pi )

            elseif x[1] < 0.0

                return atan( x[2] / x[1] ) / ( 2.0 * pi ) + 0.5

            else

                return 0.25

            end

        end

        fmin[1] = x -> ( 10.0 * ( x[3] - 10.0 * f5(x) ) ) ^ 2.0
        fmin[2] = x -> ( 10.0 * ( sqrt( x[1] ^ 2.0 + x[2] ^ 2.0 ) - 1.0 ) ) ^ 2.0
        fmin[3] = x -> ( x[3] ) ^ 2.0

    elseif nprob == 6

        # Powell Singular function

        @assert ( n == 4 ) && ( m == 4 ) "The dimensions of the problem must satisfy n = m = 4."

        if usual_init_point

            @. x = [3.0, -1.0, 0.0, 1.0]

        end

        if unconstrained

            l = c_lower * ones(Float64, n)
            u = c_upper * ones(Float64, n)

        else

            l = - ones(Float64, n)
            u = 10.0 * ones(Float64, n)

        end

        fmin = Array{Function}(undef, m)
        fmin[1] = x -> ( x[1] + 10.0 * x[2] ) ^ 2.0
        fmin[2] = x -> 5.0 * ( x[3] - x[4] ) ^ 2.0
        fmin[3] = x -> ( x[2] - 2.0 * x[3] ) ^ 4.0
        fmin[4] = x -> 10.0 * ( x[1] - x[4] ) ^ 4.0

    elseif nprob == 7

        # Freudenstein and Roth function

        @assert ( n == 2 ) && ( m == 2 ) "The dimensions of the problem must satisfy n = m = 2."

        if usual_init_point

            @. x = [0.5, -2.0]

        end

        if unconstrained

            l = c_lower * ones(Float64, n)
            u = c_upper * ones(Float64, n)

        else

            l = - 3.0 * ones(Float64, n)
            u = 3.0 * ones(Float64, n)

        end

        fmin = Array{Function}(undef, m)
        fmin[1] = x -> ( - 13.0 + x[1] + ( ( 5.0 - x[2] ) * x[2] - 2.0 ) * x[2] ) ^ 2.0
        fmin[2] = x -> ( - 29.0 + x[1] + ( ( x[2] + 1.0 ) * x[2] - 14.0 ) * x[2] ) ^ 2.0

    elseif nprob == 8

        # Bard function

        @assert ( n == 3 ) && ( m == 15 ) "The dimensions of the problem must satisfy n = 3 e m = 15."

        if usual_init_point

            @. x = 1.0

        end

        if unconstrained

            l = c_lower * ones(Float64, n)
            u = c_upper * ones(Float64, n)

        else

            l = 0.5 * ones(Float64, n)
            u = 10.0 * ones(Float64, n)

        end

        fmin = Array{Function}(undef, m)

        y = [
                0.14, 0.18, 0.22, 0.25, 0.29,
                0.32, 0.35, 0.39, 0.37, 0.58,
                0.73, 0.96, 1.34, 2.10, 4.39
                ]

        for i=1:m

            v = 16.0 - i
            w = min( i, v )

            fmin[i] = x -> ( y[i] - ( x[1] + ( i ) / ( v * x[2] + w * x[3] ) ) ) ^ 2.0

        end

    elseif nprob == 9

        # Kowalik and Osborne function

        @assert ( n == 4 ) && ( m == 11 ) "The dimensions of the problem must satisfy n = 4 e m = 11."

        if usual_init_point

            @. x = [0.25, 0.39, 0.415, 0.39]

        end

        if unconstrained

            l = c_lower * ones(Float64, n)
            u = c_upper * ones(Float64, n)

        else

            l = [0.0, 0.0, 0.415, 0.0]
            u = [0.275, 0.4, 1.0, 0.4]

        end

        fmin = Array{Function}(undef, m)
        y1 = [ 0.1957, 0.1947, 0.1735, 0.16, 0.0844, 0.0627, 0.0456, 0.0342, 0.0323, 0.0235, 0.0246 ]
        y2 = [ 4.0, 2.0, 1.0, 0.5, 0.25, 0.167, 0.125, 0.1, 0.0833, 0.0714, 0.0625 ]

        for i=1:m

            fmin[i] = x -> ( y1[i] - ( x[1] * ( y2[i] ^ 2.0 + y2[i] * x[2] ) ) / ( y2[i] ^ 2.0 + y2[i] * x[3] + x[4] ) ) ^ 2.0

        end

    elseif nprob == 10

        # Meyer function

        @assert ( n == 3 ) && ( m == 16 ) "The dimensions of the problem must satisfy n = 3 e m = 16."

        if usual_init_point

            @. x = [0.02, 4000.0, 250.0]

        end

        if unconstrained

            l = c_lower * ones(Float64, n)
            u = c_upper * ones(Float64, n)

        else

            l = - 100.0 * ones(Float64, n)
            u = [100.0, 4000.0, 1000.0]

        end

        fmin = Array{Function}(undef, m)
        y = [
                34780.0, 28610.0, 23650.0, 19630.0, 16370.0, 13720.0, 11540.0, 9744.0,
                8261.0, 7030.0, 6005.0, 5147.0, 4427.0, 3820.0, 3307.0, 2872.0 ]

        for i=1:m

            fmin[i] = x -> ( x[1] * exp( x[2] / ( 45.0 + 5.0 * i + x[3] ) ) - y[i] ) ^ 2.0

        end

    elseif nprob == 11

        # Watson function

        @assert ( 2 ≤ n ≤ 31 ) && ( m == 31 ) "The dimensions of the problem must satisfy 2 ≤ n ≤ 31 e m = 31."

        if usual_init_point

            @. x = 0.0

        end

        if unconstrained

            l = c_lower * ones(Float64, n)
            u = c_upper * ones(Float64, n)

        else

            l = ones(Float64, n)
            u = 10.0 * ones(Float64, n)

        end

        fmin = Array{Function}(undef, m)

        function f11a(x, ti)

            s = 0.0

            for j = 2:length(x)

                s += ( j - 1.0 ) * x[j] * ti ^ ( j - 2.0 )

            end

            return s

        end

        function f11b(x, ti)

            s = 0.0

            for j = 1:length(x)

                s += x[j] * ti ^ ( j - 1.0 )

            end

            return s

        end

        for i = 1:29

            ti = i / 29.0

            fmin[i] = x -> ( f11a(x, ti) - ( f11b(x, ti) ) ^ 2.0 - 1.0 ) ^ 2.0

        end

        fmin[30] = x -> ( x[1] ) ^ 2.0
        fmin[31] = x -> ( x[2] - x[1] ^ 2.0 - 1.0 ) ^ 2.0

    elseif nprob == 12

        # Box three-dimensional function

        @assert ( n == 3 ) && ( m ≥ n ) "The dimensions of the problem must satisfy n = 3 e m ≥ n."

        if usual_init_point

            @. x = [0.0, 10.0, 20.0]

        end

        if unconstrained

            l = c_lower * ones(Float64, n)
            u = c_upper * ones(Float64, n)

        else

            l = [-1.0, -50.0, 15.0]
            u = [50.0, 10.0, 50.0]

        end

        fmin = Array{Function}(undef, m)

        for i=1:m

            ti = 0.1 * i

            fmin[i] = x -> ( exp( - ti * x[1] ) - exp( - ti * x[2] ) - x[3] * ( exp( - ti ) - exp( - 10.0 * ti ) ) ) ^ 2.0

        end

    elseif nprob == 13

        # Jennrich and Sampson function

        @assert ( n == 2 ) && ( m ≥ n ) "The dimensions of the problem must satisfy n = 2 e m ≥ n."

        if usual_init_point

            @. x = [0.3, 0.4]

        end

        if unconstrained

            l = c_lower * ones(Float64, n)
            u = c_upper * ones(Float64, n)

        else

            l = [-5.0, 0.0]
            u = [0.3, 10.0]

        end

        fmin = Array{Function}(undef, m)

        for i=1:m

            fmin[i] = x -> ( 2.0 + 2.0 * i - ( exp( i * x[1] ) + exp( i * x[2] ) ) ) ^ 2.0

        end

    elseif nprob == 14

        # Brown and Dennis function

        @assert ( n == 4 ) && ( m ≥ n ) "The dimensions of the problem must satisfy n = 4 e m ≥ n."

        if usual_init_point

            @. x = [25.0, 5.0, - 5.0, - 1.0]

        end

        if unconstrained

            l = c_lower * ones(Float64, n)
            u = c_upper * ones(Float64, n)

        else

            l = [0.0, 0.0, -10.0, -10.0]
            u = [35.0, 10.0, 10.0, 0.0]

        end

        fmin = Array{Function}(undef, m)

        for i = 1:m

            ti = i / 5.0

            fmin[i] = x -> ( ( x[1] + ti * x[2] - exp( ti ) ) ^ 2.0 + ( x[3] + x[4] * sin( ti ) - cos( ti ) ) ^ 2.0 ) ^ 2.0

        end

    elseif nprob == 15

        # Chebyquad function

        @assert ( n ≥ 1 ) && ( m ≥ n ) "The dimensions of the problem must satisfy n ≥ 1 e m ≥ n."

        if usual_init_point

            c = n + 1.0

            for i=1:n

                x[i] = i / c

            end

        end

        if unconstrained

            l = c_lower * ones(Float64, n)
            u = c_upper * ones(Float64, n)

        else

            l = zeros(Float64, n)
            l[n] = 0.75
            u = ones(Float64, n)

        end

        fmin = Array{Function}(undef, m)

        function f15a(x, i)

            y = 2.0 * x - 1.0

            if y > 1.0

                return cosh( i * acosh( y ) )

            elseif y < - 1.0

                return ( - 1.0 ) ^ i * cosh( i * acosh( - y ) )

            else

                return cos( i * acos( y ) )

            end

        end

        function f15b(x, i)

            n = length(x)

            s = 0.0

            for j=1:n

                a = f15a( x[j], i )
                s += a

            end

            s *= ( 1.0 / n )

            if iseven( i )

                s += 1.0 / ( i ^ 2.0 - 1.0 )

            end

            return s

        end

        for i=1:m

            fmin[i] = x -> ( f15b(x, i) ) ^ 2.0

        end

    elseif nprob == 16

        # Brown almost-linear function

        @assert ( n ≥ 1 ) && ( m == n ) "The dimensions of the problem must satisfy n ≥ 1 e m = n."

        if usual_init_point

            @. x = 0.5

        end

        if unconstrained

            l = c_lower * ones(Float64, n)
            u = c_upper * ones(Float64, n)

        else

            l = - ones(Float64, n)
            u = 0.75 * ones(Float64, n)

        end

        fmin = Array{Function}(undef, m)

        for i = 1:(n-1)

            fmin[i] = x -> ( x[i] + sum(x) - ( n + 1.0 ) ) ^ 2.0

        end

        fmin[n] = x -> ( prod(x) - 1.0 ) ^ 2.0

    elseif nprob == 17

        # Osborne 1 function

        @assert ( n == 5 ) && ( m == 33 ) "The dimensions of the problem must satisfy n = 5 e m = 33."

        if usual_init_point

            @. x = [0.5, 1.5, -1.0, 0.01, 0.02]

        end

        if unconstrained

            l = c_lower * ones(Float64, n)
            u = c_upper * ones(Float64, n)

        else

            l = - ones(Float64, n)
            u = 2.0 * ones(Float64, n)

        end

        fmin = Array{Function}(undef, m)
        y = [
                0.844, 0.908, 0.932, 0.936, 0.925, 0.908, 0.881, 0.850, 0.818, 0.784, 0.751,
                0.718, 0.685, 0.658, 0.628, 0.603, 0.580, 0.558, 0.538, 0.522, 0.506, 0.490,
                0.478, 0.467, 0.457, 0.448, 0.438, 0.431, 0.424, 0.420, 0.414, 0.411, 0.406
                ]

        for i = 1:m

            ti = 10.0 * (i - 1.0)
            fmin[i] = x -> ( y[i] - ( x[1] + x[2] * exp( - ti * x[4] ) + x[3] * exp( - ti * x[5] ) ) ) ^ 2.0

        end

    elseif nprob == 18

        # Osborne 2 function

        @assert ( n == 11 ) && ( m == 65 ) "The dimensions of the problem must satisfy n = 11 e m = 65."

        if usual_init_point

            @. x = [1.3, 0.65, 0.65, 0.7, 0.6, 3.0, 5.0, 7.0, 2.0, 4.5, 5.5]

        end

        if unconstrained

            l = c_lower * ones(Float64, n)
            u = c_upper * ones(Float64, n)

        else

            l = zeros(Float64, n)
            l[n] = 4.0
            u = 50.0 * ones(Float64, n)
            u[1] = 14.0
            u[2] = 5.0
            u[3] = 5.0
            u[4] = 5.0

        end

        fmin = Array{Function}(undef, m)
        y = [
                1.366, 1.191, 1.112, 1.013, 0.991, 0.885, 0.831, 0.847, 0.786, 0.725, 0.746, 0.679, 0.608,
                0.655, 0.616, 0.606, 0.602, 0.626, 0.651, 0.724, 0.649, 0.649, 0.694, 0.644, 0.624, 0.661,
                0.612, 0.558, 0.533, 0.495, 0.500, 0.423, 0.395, 0.375, 0.372, 0.391, 0.396, 0.405, 0.428,
                0.429, 0.523, 0.562, 0.607, 0.653, 0.672, 0.708, 0.633, 0.668, 0.645, 0.632, 0.591, 0.559,
                0.597, 0.625, 0.739, 0.710, 0.729, 0.720, 0.636, 0.581, 0.428, 0.292, 0.162, 0.098, 0.054
                ]

        for i=1:m

            ti = (i - 1.0) / 10.0
            fmin[i] = x -> ( y[i] - ( x[1] * exp( - ti * x[5] ) + x[2] * exp( - ( ti - x[9] ) ^ 2.0 * x[6] ) + x[3] * exp( - ( ti - x[10] ) ^ 2.0 * x[7] ) + x[4] * exp( - ( ti - x[11] ) ^ 2.0 * x[8] ) ) ) ^ 2.0

        end

    elseif nprob == 19

        # BDQRTIC function

        @assert ( n ≥ 5 ) && ( m == 2 * ( n - 4 ) ) "The dimensions of the problem must satisfy n ≥ 5 e m = 2 * ( n - 4 )."

        if usual_init_point

            @. x = 1.0

        end

        if unconstrained

            l = c_lower * ones(Float64, n)
            u = c_upper * ones(Float64, n)

        else

            l = - 5.0 * ones(Float64, n)
            l[1] = 0.8
            u = 10.0 * ones(Float64, n)

        end

        fmin = Array{Function}(undef, m)

        for i=1:(n - 4)

            fmin[i] = x -> ( - 4.0 * x[i] + 3.0 ) ^ 2.0
            fmin[n - 4 + i] = x -> ( x[i] ^ 2.0 + 2.0 * x[i + 1] ^ 2.0 + 3.0 * x[i + 2] ^ 2.0 + 4.0 * x[i + 3] ^ 2.0 + 5.0 * x[n] ^ 2.0 ) ^ 2.0

        end

    elseif nprob == 20

        # Cube function

        @assert ( n ≥ 2 ) && ( m == n ) "The dimensions of the problem must satisfy n ≥ 2 e m = n."

        if usual_init_point

            @. x = 0.5

        end

        if unconstrained

            l = c_lower * ones(Float64, n)
            u = c_upper * ones(Float64, n)

        else

            l = - 5.0 * ones(Float64, n)
            u = 10.0 * ones(Float64, n)
            u[1] = 0.8

        end

        fmin = Array{Function}(undef, m)

        fmin[1] = x -> ( x[1] - 1.0 ) ^ 2.0

        for i=2:m

            fmin[i] = x -> ( 10.0 * ( x[i] - x[i - 1] ^ 3.0 ) ) ^ 2.0

        end

    elseif nprob == 21

        # Mancino function

        @assert ( n ≥ 2 ) && ( m == n ) "The dimensions of the problem must satisfy n ≥ 2 e m = n."

        if usual_init_point

            for i = 1:n

                s = 0.0

                for j = 1:n

                    ti = i / j

                    s += sqrt( ti ) * ( ( sin( log( sqrt( ti ) ) ) ) ^ 5.0 + ( cos( log( sqrt( ti ) ) ) ) ^ 5.0  )

                end

                x[i] = - 8.710996 - 4.0 * ( ( i - 50.0 ) ^ 3.0 + s )

            end

        end

        if unconstrained

            l = c_lower * ones(Float64, n)
            u = c_upper * ones(Float64, n)

        else

            l = 2.0e5 * ones(Float64, n)
            l[n-1] = 2.5e5
            u = 1.0e7 * ones(Float64, n)
            u[n] = 3.0e5

        end

        fmin = Array{Function}(undef, m)

        function f21(x, i)

            s = 0.0

            for j = 1:length(x)

                v = sqrt( x[i] ^ 2.0 + i / j )
                s += v * ( ( sin( log( v ) ) ) ^ 5.0 + ( cos( log( v ) ) ) ^ 5.0 )

            end

            return s

        end

        for i = 1:m

            fmin[i] = x -> ( 1400.0 * x[i] + ( i - 50.0 ) ^ 3.0 + f21(x, i) ) ^ 2.0

        end

    elseif nprob == 22

        # HEART8LS function

        @assert ( n == 8 ) && ( m == 8 ) "The dimensions of the problem must satisfy n = m = 8."

        if usual_init_point

            @. x = [ - 0.3, - 0.39, 0.3, - 0.344, - 1.2, 2.69, 1.59, - 1.5 ]

        end

        if unconstrained

            l = c_lower * ones(Float64, n)
            u = c_upper * ones(Float64, n)

        else

            l = - 30.0 * ones(Float64, n)
            u = 30.0 * ones(Float64, n)
            u[3] = 3.5
            u[n] = -1.0

        end

        fmin = Array{Function}(undef, m)
        fmin[1] = x -> ( x[1] + x[2] + 0.69 ) ^ 2.0
        fmin[2] = x -> ( x[3] + x[4] + 0.044 ) ^ 2.0
        fmin[3] = x -> ( x[5] * x[1] + x[6] * x[2] - x[7] * x[3] - x[8] * x[4] + 1.57 ) ^ 2.0
        fmin[4] = x -> ( x[7] * x[1] + x[8] * x[2] + x[5] * x[3] + x[6] * x[4] + 1.31 ) ^ 2.0
        fmin[5] = x -> ( x[1] * ( x[5] ^ 2.0 - x[7] ^ 2.0 ) - 2.0 * x[3] * x[5] * x[7] + x[2] * ( x[6] ^ 2.0 - x[8] ^ 2.0 ) - 2.0 * x[4] * x[6] * x[8] + 2.65 ) ^ 2.0
        fmin[6] = x -> ( x[3] * ( x[5] ^ 2.0 - x[7] ^ 2.0 ) + 2.0 * x[1] * x[5] * x[7] + x[4] * ( x[6] ^ 2.0 - x[8] ^ 2.0 ) + 2.0 * x[2] * x[6] * x[8] - 2.0 ) ^ 2.0
        fmin[7] = x -> ( x[1] * x[5] * ( x[5] ^ 2.0 - 3.0 * x[7] ^ 2.0 ) + x[3] * x[7] * ( x[7] ^ 2.0 - 3.0 * x[5] ^ 2.0 ) + x[2] * x[6] * ( x[6] ^ 2.0 - 3.0 * x[8] ^ 2.0 ) + x[4] * x[8] * ( x[8] ^ 2.0 - 3.0 * x[6] ^ 2.0 ) + 12.6 ) ^ 2.0
        fmin[8] = x -> ( x[3] * x[5] * ( x[5] ^ 2.0 - 3.0 * x[7] ^ 2.0 ) - x[1] * x[7] * ( x[7] ^ 2.0 - 3.0 * x[5] ^ 2.0 ) + x[4] * x[6] * ( x[6] ^ 2.0 - 3.0 * x[8] ^ 2.0 ) - x[2] * x[8] * ( x[8] ^ 2.0 - 3.0 * x[6] ^ 2.0 ) - 9.48 ) ^ 2.0

    end

    if rsp

        @. x *= 10.0

    end

    return x, l, u, fmin

end

function problem_generator_hs(
                                nprob::Int64
                                )

    @assert 1 ≤ nprob ≤ 100 "The number of the problem must satisfy 1 ≤ nprob ≤ 100."

    # Defines the objective functions based on Hock-Schittkowski test-set.

    function hs1(x)

        return 100.0 * ( x[2] - x[1] ^ 2.0 ) ^ 2.0 + ( 1.0 - x[1] ) ^ 2.0

    end

    function hs3(x)

        return x[2] + 1.0e-5 * ( x[2] - x[1] ) ^ 2.0

    end

    function hs4(x)

        return ( x[1]  + 1.0 ) ^ 3.0 / 3.0 + x[2]

    end

    function hs5(x)

        return sin( x[1] + x[2] ) + ( x[1] - x[2] ) ^ 2.0 - 1.5 * x[1] + 2.5 * x[2] + 1.0

    end

    function hs25(x)

        s = 0.0

        for i = 1:99

            ui = 25.0 + ( - 50.0 * log( 0.01 * i ) ) ^ ( 2.0 / 3.0)

            s += ( - 0.01 * i + exp( - ( 1.0 / x[1] ) * ( ui - x[2] ) ^ ( x[3] ) ) ) ^ 2.0

        end

        return s

    end

    function hs38(x)

        return 100.0 * ( x[2] - x[1] ^ 2.0 ) ^ 2.0 + ( 1.0 - x[1] ) ^ 2.0 + 90.0 * ( x[4] - x[3] ^ 2.0 ) ^ 2.0 + ( 1.0 - x[3] ) ^ 2.0 + 10.1 * ( ( x[2] - 1.0 ) ^ 2.0 + ( x[4] - 1.0 ) ^ 2.0 ) + 19.8 * ( x[2] - 1.0 ) * ( x[4] - 1.0 )

    end

    function hs45(x)

        return 2.0 - x[1] * x[2] * x[3] * x[4] * x[5] / 120.0

    end

    function hs110(x)

        s1 = 0.0
        s2 = 1.0

        for i = 1:10

            s1 += ( log( x[i] - 2.0 ) ) ^ 2.0 + ( log( 10.0 - x[i] ) ) ^ 2.0
            s2 *= x[i]

        end

        return s1 - s2 ^ ( 0.2 )

    end

    # Initializes some useful variables and constants
    c_lower = - 1.0e20
    c_upper = 1.0e20

    if nprob == 1

        n = 2
        x = [ -2.0, 1.0 ]
        l = [ c_lower, 0.0 ]
        u = c_upper * ones(Float64, n)
        fmin = [ hs1, hs3 ]

    elseif nprob == 2

        n = 2
        x = [ 1.125, 0.125 ]
        l = [ 1.0, 0.0 ]
        u = c_upper * ones(Float64, n)
        fmin = [ hs1, hs4 ]

    elseif nprob == 3

        n = 2
        x = zeros(Float64, n)
        l = [ - 1.5, - 1.5 ]
        u = [ 4.0, 3.0 ]
        fmin = [ hs1, hs5 ]

    elseif nprob == 4

        n = 3
        x = [ 100.0, 12.5, 3.0]
        l = [ 0.1, 0.0, 0.0 ]
        u = [ 100.0, 25.6, 5.0 ]
        fmin = [ hs1, hs25 ]

    elseif nprob == 5

        n = 4
        x = [ - 3.0, - 1.0, - 3.0, - 1.0 ]
        l = [ - 10.0, - 1.5, - 10.0, - 10.0 ]
        u = 10.0 * ones(Float64, n)
        fmin = [ hs1, hs38 ]

    elseif nprob == 6

        n = 5
        x = 2.0 * ones(Float64, n)
        l = zeros(Float64, n)
        u = [ 1.0, 2.0, 3.0, 4.0, 5.0 ]
        fmin = [ hs1, hs45 ]

    elseif nprob == 7

        n = 10
        x = 9.0 * ones(Float64, n)
        l = 2.001 * ones(Float64, n)
        u = 9.999 * ones(Float64, n)
        fmin = [ hs1, hs110 ]

    elseif nprob == 8

        n = 2
        x = [ 1.125, 0.125 ]
        l = [ 1.0, 0.0 ]
        u = c_upper * ones(Float64, n)
        fmin = [ hs3, hs4 ]

    elseif nprob == 9

        n = 2
        x = zeros(Float64, n)
        l = [ - 1.5, 0.0 ]
        u = [ 4.0, 3.0 ]
        fmin = [ hs3, hs5 ]

    elseif nprob == 10

        n = 3
        x = [ 100.0, 12.5, 3.0]
        l = [ 0.1, 0.0, 0.0 ]
        u = [ 100.0, 25.6, 5.0 ]
        fmin = [ hs3, hs25 ]

    elseif  nprob == 11

        n = 4
        x = [ - 3.0, - 1.0, - 3.0, - 1.0 ]
        l = [ - 10.0, 0.0, - 10.0, - 10.0 ]
        u = 10.0 * ones(Float64, 4)
        fmin = [ hs3, hs38 ]

    elseif nprob == 12

        n = 5
        x = 2.0 * ones(Float64, n)
        l = zeros(Float64, n)
        u = [ 1.0, 2.0, 3.0, 4.0, 5.0 ]
        fmin = [ hs3, hs45 ]

    elseif nprob == 13

        n = 10
        x = 9.0 * ones(Float64, n)
        l = 2.001 * ones(Float64, n)
        u = 9.999 * ones(Float64, n)
        fmin = [ hs3, hs110 ]

    elseif nprob == 14

        n = 2
        x = [ 1.125, 0.125 ]
        l = [ 1.0, 0.0 ]
        u = [ 4.0, 3.0 ]
        fmin = [ hs4, hs5 ]

    elseif nprob == 15

        n = 3
        x = [ 100.0, 12.5, 3.0]
        l = [ 1.0, 0.0, 0.0 ]
        u = [ 100.0, 25.6, 5.0 ]
        fmin = [ hs4, hs25 ]

    elseif nprob == 16

        n = 4
        x = [ 3.0, 1.0, - 3.0, - 1.0 ]
        l = [ 1.0 , 0.0, - 10.0, - 10.0 ]
        u = 10.0 * ones(Float64, n)
        fmin = [ hs4, hs38 ]

    elseif nprob == 17

        n = 5
        x = 2.0 * ones(Float64, n)
        l = zeros(Float64, n)
        l[1] = 1.0
        u = [ 1.0, 2.0, 3.0, 4.0, 5.0 ]
        fmin = [ hs4, hs45 ]

    elseif nprob == 18

        n = 10
        x = 9.0 * ones(Float64, n)
        l = 2.001 * ones(Float64, n)
        u = 9.999 * ones(Float64, n)
        fmin = [ hs4, hs110 ]

    elseif nprob == 19

        n = 4
        x = [ -3.0, -1.0, -3.0, -1.0 ]
        l = [ -1.5, -3.0, -10.0, -10.0 ]
        u = [ 4.0, 3.0, 10.0, 10.0 ]
        fmin = [ hs5, hs38 ]

    elseif nprob == 20

        n = 5
        x = 2.0 * ones(Float64, n)
        l = zeros(Float64, n)
        u = [ 1.0, 2.0, 3.0, 4.0, 5.0 ]
        fmin = [ hs5, hs45 ]

    elseif nprob == 21

        n = 4
        x = [ -3.0, -1.0, -3.0, -1.0 ]
        l = [ 0.1, 0.0, 0.0, -10.0 ]
        u = [ 10.0, 10.0, 5.0, 10.0 ]
        fmin = [ hs25, hs38 ]

    elseif nprob == 22

        n = 5
        x = 2.0 * ones(Float64, n)
        l = zeros(Float64, n)
        l[1] = 0.1
        u = [ 1.0, 2.0, 3.0, 4.0, 5.0 ]
        fmin = [ hs25, hs45 ]

    elseif nprob == 23

        n = 5
        x = 2.0 * ones(Float64, n)
        l = zeros(Float64, n)
        u = [ 1.0, 2.0, 3.0, 4.0, 5.0 ]
        fmin = [ hs38, hs45 ]

    elseif nprob == 24

        n = 10
        x = 9.0 * ones(Float64, n)
        l = 2.001 * ones(Float64, n)
        u = 9.999 * ones(Float64, n)
        fmin = [ hs38, hs110 ]

    elseif nprob == 25

        n = 2
        x = [10.0, 1.0]
        l = [1.0, 0.0]
        u = c_upper * ones(Float64, n)
        fmin = [ hs1, hs3, hs4 ]

    elseif nprob == 26

        n = 2
        x = [10.0, 1.0]
        l = [- 1.5, 0.0]
        u = [4.0, 3.0]
        fmin = [ hs1, hs3, hs5 ]

    elseif nprob == 27

        n = 3
        x = [ 100.0, 12.5, 3.0]
        l = [ 0.1, 0.0, 0.0 ]
        u = [ 100.0, 25.6, 5.0 ]
        fmin = [ hs1, hs3, hs25 ]

    elseif  nprob == 28

        n = 4
        x = [ - 3.0, - 1.0, - 3.0, - 1.0 ]
        l = [ - 10.0, 0.0, - 10.0, - 10.0 ]
        u = 10.0 * ones(Float64, 4)
        fmin = [ hs1, hs3, hs38 ]

    elseif nprob == 29

        n = 5
        x = 2.0 * ones(Float64, n)
        l = zeros(Float64, n)
        u = [ 1.0, 2.0, 3.0, 4.0, 5.0 ]
        fmin = [ hs1, hs3, hs45 ]

    elseif nprob == 30

        n = 10
        x = 9.0 * ones(Float64, n)
        l = 2.001 * ones(Float64, n)
        u = 9.999 * ones(Float64, n)
        fmin = [ hs1, hs3, hs110 ]

    elseif nprob == 31

        n = 2
        x = [ 1.125, 0.125 ]
        l = [ 1.0, 0.0 ]
        u = [ 4.0, 3.0 ]
        fmin = [ hs1, hs4, hs5 ]

    elseif nprob == 32

        n = 3
        x = [ 100.0, 12.5, 3.0]
        l = [ 1.0, 0.0, 0.0 ]
        u = [ 100.0, 25.6, 5.0 ]
        fmin = [ hs1, hs4, hs25 ]

    elseif nprob == 33

        n = 4
        x = [ 3.0, 1.0, - 3.0, - 1.0 ]
        l = [ 1.0 , 0.0, - 10.0, - 10.0 ]
        u = 10.0 * ones(Float64, n)
        fmin = [ hs1, hs4, hs38 ]

    elseif nprob == 34

        n = 5
        x = 2.0 * ones(Float64, n)
        l = zeros(Float64, n)
        l[1] = 1.0
        u = [ 1.0, 2.0, 3.0, 4.0, 5.0 ]
        fmin = [ hs1, hs4, hs45 ]

    elseif nprob == 35

        n = 10
        x = 9.0 * ones(Float64, n)
        l = 2.001 * ones(Float64, n)
        u = 9.999 * ones(Float64, n)
        fmin = [ hs1, hs4, hs110 ]

    elseif nprob == 36

        n = 4
        x = [ -3.0, -1.0, -3.0, -1.0 ]
        l = [ -1.5, 0.0, -10.0, -10.0 ]
        u = [ 4.0, 3.0, 10.0, 10.0 ]
        fmin = [ hs1, hs5, hs38 ]

    elseif nprob == 37

        n = 5
        x = 2.0 * ones(Float64, n)
        l = zeros(Float64, n)
        u = [ 1.0, 2.0, 3.0, 4.0, 5.0 ]
        fmin = [ hs1, hs5, hs45 ]

    elseif nprob == 38

        n = 4
        x = [ -3.0, -1.0, -3.0, -1.0 ]
        l = [ 0.1, 0.0, 0.0, -10.0 ]
        u = [ 10.0, 10.0, 5.0, 10.0 ]
        fmin = [ hs1, hs25, hs38 ]

    elseif nprob == 39

        n = 5
        x = 2.0 * ones(Float64, n)
        l = zeros(Float64, n)
        l[1] = 0.1
        u = [ 1.0, 2.0, 3.0, 4.0, 5.0 ]
        fmin = [ hs1, hs25, hs45 ]

    elseif nprob == 40

        n = 5
        x = 2.0 * ones(Float64, n)
        l = zeros(Float64, n)
        u = [ 1.0, 2.0, 3.0, 4.0, 5.0 ]
        fmin = [ hs1, hs38, hs45 ]

    elseif nprob == 41

        n = 10
        x = 9.0 * ones(Float64, n)
        l = 2.001 * ones(Float64, n)
        u = 9.999 * ones(Float64, n)
        fmin = [ hs1, hs38, hs110 ]

    elseif nprob == 42

        n = 2
        x = [ 1.125, 0.125 ]
        l = [ 1.0, 0.0 ]
        u = [ 4.0, 3.0 ]
        fmin = [ hs3, hs4, hs5 ]

    elseif nprob == 43

        n = 3
        x = [ 100.0, 12.5, 3.0]
        l = [ 1.0, 0.0, 0.0 ]
        u = [ 100.0, 25.6, 5.0 ]
        fmin = [ hs3, hs4, hs25 ]

    elseif nprob == 44

        n = 4
        x = [ 3.0, 1.0, - 3.0, - 1.0 ]
        l = [ 1.0 , 0.0, - 10.0, - 10.0 ]
        u = 10.0 * ones(Float64, n)
        fmin = [ hs3, hs4, hs38 ]

    elseif nprob == 45

        n = 5
        x = 2.0 * ones(Float64, n)
        l = zeros(Float64, n)
        l[1] = 1.0
        u = [ 1.0, 2.0, 3.0, 4.0, 5.0 ]
        fmin = [ hs3, hs4, hs45 ]

    elseif nprob == 46

        n = 10
        x = 9.0 * ones(Float64, n)
        l = 2.001 * ones(Float64, n)
        u = 9.999 * ones(Float64, n)
        fmin = [ hs3, hs4, hs110 ]

    elseif nprob == 47

        n = 4
        x = [ -3.0, -1.0, -3.0, -1.0 ]
        l = [ -1.5, 0.0, -10.0, -10.0 ]
        u = [ 4.0, 3.0, 10.0, 10.0 ]
        fmin = [ hs3, hs5, hs38 ]

    elseif nprob == 48

        n = 5
        x = 2.0 * ones(Float64, n)
        l = zeros(Float64, n)
        u = [ 1.0, 2.0, 3.0, 4.0, 5.0 ]
        fmin = [ hs3, hs5, hs45 ]

    elseif nprob == 49

        n = 4
        x = [ -3.0, -1.0, -3.0, -1.0 ]
        l = [ 0.1, 0.0, 0.0, -10.0 ]
        u = [ 10.0, 10.0, 5.0, 10.0 ]
        fmin = [ hs3, hs25, hs38 ]

    elseif nprob == 50

        n = 5
        x = 2.0 * ones(Float64, n)
        l = zeros(Float64, n)
        l[1] = 0.1
        u = [ 1.0, 2.0, 3.0, 4.0, 5.0 ]
        fmin = [ hs3, hs25, hs45 ]

    elseif nprob == 51

        n = 5
        x = 2.0 * ones(Float64, n)
        l = zeros(Float64, n)
        u = [ 1.0, 2.0, 3.0, 4.0, 5.0 ]
        fmin = [ hs3, hs38, hs45 ]

    elseif nprob == 52

        n = 10
        x = 9.0 * ones(Float64, n)
        l = 2.001 * ones(Float64, n)
        u = 9.999 * ones(Float64, n)
        fmin = [ hs3, hs38, hs110 ]

    elseif nprob == 53

        n = 3
        x = [ 100.0, 12.5, 3.0]
        l = [ 1.0, 0.0, 0.0 ]
        u = [ 4.0, 3.0, 5.0 ]
        fmin = [ hs4, hs5, hs25 ]

    elseif nprob == 54

        n = 4
        x = [ -3.0, -1.0, -3.0, -1.0 ]
        l = [ 1.0, 0.0, -10.0, -10.0 ]
        u = [ 4.0, 3.0, 10.0, 10.0 ]
        fmin = [ hs4, hs5, hs38 ]

    elseif nprob == 55

        n = 5
        x = 2.0 * ones(Float64, n)
        l = zeros(Float64, n)
        u = [ 1.0, 2.0, 3.0, 4.0, 5.0 ]
        fmin = [ hs4, hs5, hs45 ]

    elseif nprob == 56

        n = 4
        x = [ -3.0, -1.0, -3.0, -1.0 ]
        l = [ 1.0, 0.0, 0.0, -10.0 ]
        u = [ 10.0, 10.0, 5.0, 10.0 ]
        fmin = [ hs4, hs25, hs38 ]

    elseif nprob == 57

        n = 5
        x = 2.0 * ones(Float64, n)
        l = zeros(Float64, n)
        l[1] = 0.1
        u = [ 1.0, 2.0, 3.0, 4.0, 5.0 ]
        fmin = [ hs4, hs25, hs45 ]

    elseif nprob == 58

        n = 5
        x = 2.0 * ones(Float64, n)
        l = zeros(Float64, n)
        l[1] = 1.0
        u = [ 1.0, 2.0, 3.0, 4.0, 5.0 ]
        fmin = [ hs4, hs38, hs45 ]

    elseif nprob == 59

        n = 10
        x = 9.0 * ones(Float64, n)
        l = 2.001 * ones(Float64, n)
        u = 9.999 * ones(Float64, n)
        fmin = [ hs4, hs38, hs110 ]

    elseif nprob == 60

        n = 4
        x = [ -3.0, -1.0, -3.0, -1.0 ]
        l = [ 0.1, 0.0, 0.0, -10.0 ]
        u = [ 4.0, 3.0, 5.0, 10.0 ]
        fmin = [ hs5, hs25, hs38 ]

    elseif nprob == 61

        n = 5
        x = 2.0 * ones(Float64, n)
        l = zeros(Float64, n)
        l[1] = 0.1
        u = [ 1.0, 2.0, 3.0, 4.0, 5.0 ]
        fmin = [ hs5, hs25, hs45 ]

    elseif nprob == 62

        n = 5
        x = 2.0 * ones(Float64, n)
        l = zeros(Float64, n)
        u = [ 1.0, 2.0, 3.0, 4.0, 5.0 ]
        fmin = [ hs5, hs38, hs45 ]

    elseif nprob == 63

        n = 5
        x = 2.0 * ones(Float64, n)
        l = zeros(Float64, n)
        l[1] = 0.1
        u = [ 1.0, 2.0, 3.0, 4.0, 5.0 ]
        fmin = [ hs25, hs38, hs45 ]

    elseif nprob == 64

        n = 2
        x = [ 1.125, 0.125 ]
        l = [ 1.0, 0.0 ]
        u = [ 4.0, 3.0 ]
        fmin = [ hs1, hs3, hs4, hs5 ]

    elseif nprob == 65

        n = 3
        x = [ 100.0, 12.5, 3.0]
        l = [ 1.0, 0.0, 0.0 ]
        u = [ 100.0, 25.6, 5.0 ]
        fmin = [ hs1, hs3, hs4, hs25 ]

    elseif nprob == 66

        n = 4
        x = [ 3.0, 1.0, - 3.0, - 1.0 ]
        l = [ 1.0 , 0.0, - 10.0, - 10.0 ]
        u = 10.0 * ones(Float64, n)
        fmin = [ hs1, hs3, hs4, hs38 ]

    elseif nprob == 67

        n = 5
        x = 2.0 * ones(Float64, n)
        l = zeros(Float64, n)
        l[1] = 1.0
        u = [ 1.0, 2.0, 3.0, 4.0, 5.0 ]
        fmin = [ hs1, hs3, hs4, hs45 ]

    elseif nprob == 68

        n = 10
        x = 9.0 * ones(Float64, n)
        l = 2.001 * ones(Float64, n)
        u = 9.999 * ones(Float64, n)
        fmin = [ hs1, hs3, hs4, hs110 ]

    elseif nprob == 69

        n = 3
        x = [ 100.0, 12.5, 3.0]
        l = [ 0.1, 0.0, 0.0 ]
        u = [ 4.0, 3.0, 5.0 ]
        fmin = [ hs1, hs3, hs5, hs25 ]

    elseif nprob == 70

        n = 4
        x = [ -3.0, -1.0, -3.0, -1.0 ]
        l = [ -1.5, 0.0, -10.0, -10.0 ]
        u = [ 4.0, 3.0, 10.0, 10.0 ]
        fmin = [ hs1, hs3, hs5, hs38 ]

    elseif nprob == 71

        n = 5
        x = 2.0 * ones(Float64, n)
        l = zeros(Float64, n)
        u = [ 1.0, 2.0, 3.0, 4.0, 5.0 ]
        fmin = [ hs1, hs3, hs5, hs45 ]

    elseif nprob == 72

        n = 4
        x = [ -3.0, -1.0, -3.0, -1.0 ]
        l = [ 0.1, 0.0, 0.0, -10.0 ]
        u = [ 10.0, 10.0, 5.0, 10.0 ]
        fmin = [ hs1, hs3, hs25, hs38 ]

    elseif nprob == 73

        n = 5
        x = 2.0 * ones(Float64, n)
        l = zeros(Float64, n)
        l[1] = 0.1
        u = [ 1.0, 2.0, 3.0, 4.0, 5.0 ]
        fmin = [ hs1, hs3, hs25, hs45 ]

    elseif nprob == 74

        n = 5
        x = 2.0 * ones(Float64, n)
        l = zeros(Float64, n)
        u = [ 1.0, 2.0, 3.0, 4.0, 5.0 ]
        fmin = [ hs1, hs3, hs38, hs45 ]

    elseif nprob == 75

        n = 10
        x = 9.0 * ones(Float64, n)
        l = 2.001 * ones(Float64, n)
        u = 9.999 * ones(Float64, n)
        fmin = [ hs1, hs3, hs38, hs110 ]

    elseif nprob == 76

        n = 3
        x = [ 100.0, 12.5, 3.0]
        l = [ 1.0, 0.0, 0.0 ]
        u = [ 4.0, 3.0, 5.0 ]
        fmin = [ hs1, hs4, hs5, hs25 ]

    elseif nprob == 77

        n = 4
        x = [ -3.0, -1.0, -3.0, -1.0 ]
        l = [ 1.0, 0.0, -10.0, -10.0 ]
        u = [ 4.0, 3.0, 10.0, 10.0 ]
        fmin = [ hs1, hs4, hs5, hs38 ]

    elseif nprob == 78

        n = 5
        x = 2.0 * ones(Float64, n)
        l = zeros(Float64, n)
        l[1] = 1.0
        u = [ 1.0, 2.0, 3.0, 4.0, 5.0 ]
        fmin = [ hs1, hs4, hs5, hs45 ]

    elseif nprob == 79

        n = 4
        x = [ -3.0, -1.0, -3.0, -1.0 ]
        l = [ 1.0, 0.0, 0.0, -10.0 ]
        u = [ 10.0, 10.0, 5.0, 10.0 ]
        fmin = [ hs1, hs4, hs25, hs38 ]

    elseif nprob == 80

        n = 5
        x = 2.0 * ones(Float64, n)
        l = zeros(Float64, n)
        l[1] = 1.0
        u = [ 1.0, 2.0, 3.0, 4.0, 5.0 ]
        fmin = [ hs1, hs4, hs25, hs45 ]

    elseif nprob == 81

        n = 5
        x = 2.0 * ones(Float64, n)
        l = zeros(Float64, n)
        l[1] = 1.0
        u = [ 1.0, 2.0, 3.0, 4.0, 5.0 ]
        fmin = [ hs1, hs4, hs38, hs45 ]

    elseif nprob == 82

        n = 10
        x = 9.0 * ones(Float64, n)
        l = 2.001 * ones(Float64, n)
        u = 9.999 * ones(Float64, n)
        fmin = [ hs1, hs4, hs38, hs110 ]

    elseif nprob == 83

        n = 4
        x = [ -3.0, -1.0, -3.0, -1.0 ]
        l = [ 0.1, 0.0, 0.0, -10.0 ]
        u = [ 4.0, 3.0, 5.0, 10.0 ]
        fmin = [ hs1, hs5, hs25, hs38 ]

    elseif nprob == 84

        n = 5
        x = 2.0 * ones(Float64, n)
        l = zeros(Float64, n)
        l[1] = 0.1
        u = [ 1.0, 2.0, 3.0, 4.0, 5.0 ]
        fmin = [ hs1, hs5, hs25, hs45 ]

    elseif nprob == 85

        n = 5
        x = 2.0 * ones(Float64, n)
        l = zeros(Float64, n)
        u = [ 1.0, 2.0, 3.0, 4.0, 5.0 ]
        fmin = [ hs1, hs5, hs38, hs45 ]

    elseif nprob == 86

        n = 5
        x = 2.0 * ones(Float64, n)
        l = zeros(Float64, n)
        l[1] = 0.1
        u = [ 1.0, 2.0, 3.0, 4.0, 5.0 ]
        fmin = [ hs1, hs25, hs38, hs45 ]

    elseif nprob == 87

        n = 3
        x = [ 100.0, 12.5, 3.0]
        l = [ 1.0, 0.0, 0.0 ]
        u = [ 4.0, 3.0, 5.0 ]
        fmin = [ hs3, hs4, hs5, hs25 ]

    elseif nprob == 88

        n = 4
        x = [ -3.0, -1.0, -3.0, -1.0 ]
        l = [ 1.0, 0.0, -10.0, -10.0 ]
        u = [ 4.0, 3.0, 10.0, 10.0 ]
        fmin = [ hs3, hs4, hs5, hs38 ]

    elseif nprob == 89

        n = 5
        x = 2.0 * ones(Float64, n)
        l = zeros(Float64, n)
        l[1] = 1.0
        u = [ 1.0, 2.0, 3.0, 4.0, 5.0 ]
        fmin = [ hs3, hs4, hs5, hs45 ]

    elseif nprob == 90

        n = 4
        x = [ -3.0, -1.0, -3.0, -1.0 ]
        l = [ 1.0, 0.0, 0.0, -10.0 ]
        u = [ 10.0, 10.0, 5.0, 10.0 ]
        fmin = [ hs3, hs4, hs25, hs38 ]

    elseif nprob == 91

        n = 5
        x = 2.0 * ones(Float64, n)
        l = zeros(Float64, n)
        l[1] = 1.0
        u = [ 1.0, 2.0, 3.0, 4.0, 5.0 ]
        fmin = [ hs3, hs4, hs25, hs45 ]

    elseif nprob == 92

        n = 4
        x = [ -3.0, -1.0, -3.0, -1.0 ]
        l = [ 0.1, 0.0, 0.0, -10.0 ]
        u = [ 4.0, 3.0, 5.0, 10.0 ]
        fmin = [ hs3, hs5, hs25, hs38 ]

    elseif nprob == 93

        n = 5
        x = 2.0 * ones(Float64, n)
        l = zeros(Float64, n)
        l[1] = 0.1
        u = [ 1.0, 2.0, 3.0, 4.0, 5.0 ]
        fmin = [ hs3, hs5, hs25, hs45 ]

    elseif nprob == 94

        n = 5
        x = 2.0 * ones(Float64, n)
        l = zeros(Float64, n)
        u = [ 1.0, 2.0, 3.0, 4.0, 5.0 ]
        fmin = [ hs3, hs5, hs38, hs45 ]

    elseif nprob == 95

        n = 5
        x = 2.0 * ones(Float64, n)
        l = zeros(Float64, n)
        l[1] = 0.1
        u = [ 1.0, 2.0, 3.0, 4.0, 5.0 ]
        fmin = [ hs3, hs25, hs38, hs45 ]

    elseif nprob == 96

        n = 4
        x = [ -3.0, -1.0, -3.0, -1.0 ]
        l = [ 1.0, 0.0, 0.0, -10.0 ]
        u = [ 4.0, 3.0, 5.0, 10.0 ]
        fmin = [ hs4, hs5, hs25, hs38 ]

    elseif nprob == 97

        n = 5
        x = 2.0 * ones(Float64, n)
        l = zeros(Float64, n)
        l[1] = 1.0
        u = [ 1.0, 2.0, 3.0, 4.0, 5.0 ]
        fmin = [ hs4, hs5, hs25, hs45 ]

    elseif nprob == 98

        n = 5
        x = 2.0 * ones(Float64, n)
        l = zeros(Float64, n)
        l[1] = 1.0
        u = [ 1.0, 2.0, 3.0, 4.0, 5.0 ]
        fmin = [ hs4, hs5, hs38, hs45 ]

    elseif nprob == 99

        n = 5
        x = 2.0 * ones(Float64, n)
        l = zeros(Float64, n)
        l[1] = 1.0
        u = [ 1.0, 2.0, 3.0, 4.0, 5.0 ]
        fmin = [ hs4, hs25, hs38, hs45 ]

    elseif nprob == 100

        n = 5
        x = 2.0 * ones(Float64, n)
        l = zeros(Float64, n)
        l[1] = 0.1
        u = [ 1.0, 2.0, 3.0, 4.0, 5.0 ]
        fmin = [ hs5, hs25, hs38, hs45 ]

    end

    return x, l, u, fmin

end


function ConjugateGradients(d,operators,parameters;Niter=10,mu=0,tol=1.0e-15)
    # Conjugate Gradients following Algorithm 2 from Scales, 1987.
    # The user provides an array of linear operators. Ensure linear operator(s) pass the dot product.

    cost = Float64[]
    r = copy(d)
    g = LinearOperator(r,operators,parameters,adj=true)
    m = zeros(g)
    s = copy(g)
    gamma = InnerProduct(g,g)
    gamma00 = gamma
    cost0 = InnerProduct(r,r)
    push!(cost,1.0)
    for iter = 1 : Niter
	t = LinearOperator(s,operators,parameters,adj=false)
	delta = InnerProduct(t,t) + mu*InnerProduct(s,s)
	if delta <= tol
#	    println("delta reached tolerance, ending at iteration ",iter)
	    break;
	end
	alpha = gamma/delta
	m = m + alpha*s
	r = r - alpha*t
	g = LinearOperator(r,operators,parameters,adj=true)
	g = g - mu*m
	gamma0 = copy(gamma)
	gamma = InnerProduct(g,g)
        cost1 = InnerProduct(r,r) + mu*InnerProduct(m,m)
        push!(cost,cost1/cost0)
	beta = gamma/gamma0
	s = beta*s + g
	if (sqrt(gamma) <= sqrt(gamma00) * tol)
	    println("tolerance reached, ending at iteration ",iter)
	    break;
	end
    end

    return m, cost
end

function ConjugateGradients(m::String,d::String,operators,parameters,cost_file::String;Niter=10,mu=0,tol=1.0e-15)
    # Conjugate Gradients following Algorithm 2 from Scales, 1987.
    # The user provides an array of linear operators. Ensure linear operator(s) pass the dot product.

    cost = Float64[]
    rand_string = string(round(Int,rand()*100000))
    g = join(["tmp_CG_g_",rand_string])
    s = join(["tmp_CG_s_",rand_string])
    r = join(["tmp_CG_r_",rand_string])
    t = join(["tmp_CG_t_",rand_string])
    SeisCopy(d,r)
    fp = open(cost_file,"w")
    @compat write(fp,join(["began execution at: ",Libc.strftime(time()),"\n"]))
    close(fp)
    LinearOperator(g,r,operators,parameters,adj=true)
    SeisCopy(g,s)
    SeisCopy(g,m)
    CGStep(m,g,a=0.0,b=0.0)
    gamma = InnerProduct(g,g)
    gamma00 = gamma
    cost0 = InnerProduct(r,r)
    push!(cost,1.0)
    fp = open(cost_file,"a")
    write(fp,join([string(cost[1]),"\n"]))
    close(fp)
    for iter = 1 : Niter
	LinearOperator(s,t,operators,parameters,adj=false)
	delta = InnerProduct(t,t) + mu*InnerProduct(s,s)
	alpha = gamma/delta
	CGStep(m,s,a=1.0,b=alpha)
	CGStep(r,t,a=1.0,b=-alpha)
	LinearOperator(g,r,operators,parameters,adj=true)
	CGStep(g,m,a=1.0,b=-mu)
	gamma0 = copy(gamma)
	gamma = InnerProduct(g,g)
        cost1 = InnerProduct(r,r) + mu*InnerProduct(m,m)
        push!(cost,cost1/cost0)
	fp = open(cost_file,"a")
	write(fp,join([string(cost[iter+1]),"\n"]))
	close(fp)
	beta = gamma/gamma0
	CGStep(s,g,a=beta,b=1.0)
	if (sqrt(gamma) <= sqrt(gamma00) * tol)
	    println("tolerance reached, ending at iteration ",iter)
	    break;
	end
    end
    SeisRemove(g)
    SeisRemove(s)
    SeisRemove(r)
    SeisRemove(t)

end

function ConjugateGradients(m::Array{String,1},d::Array{String,1},operators,parameters,cost_file::String;Niter=10,mu=[0.0;0.0],tol=1.0e-15)
    # Conjugate Gradients following Algorithm 2 from Scales, 1987.
    # The user provides an array of linear operators. Ensure linear operator(s) pass the dot product.

    cost = Float64[]
    rand_string = string(round(Int,rand()*100000))
    g = [join(["tmp_CG_g1_",rand_string]);join(["tmp_CG_g2_",rand_string])]
    s = [join(["tmp_CG_s1_",rand_string]);join(["tmp_CG_s2_",rand_string])]
    r = [join(["tmp_CG_r1_",rand_string]);join(["tmp_CG_r2_",rand_string])]
    t = [join(["tmp_CG_t1_",rand_string]);join(["tmp_CG_t2_",rand_string])]
    SeisCopy(d,r)
    fp = open(cost_file,"w")
    @compat write(fp,join(["began execution at: ",Libc.strftime(time()),"\n"]))
    close(fp)
    LinearOperator(g,r,operators,parameters,adj=true)
    SeisCopy(g,s)
    SeisCopy(g,m)
    CGStep(m,g,a=[0.0;0.0],b=[0.0;0.0])
    gamma = InnerProduct(g,g)
    gamma00 = gamma
    cost0 = InnerProduct(r,r)
    push!(cost,1.0)
    fp = open(cost_file,"a")
    write(fp,join([string(cost[1]),"\n"]))
    close(fp)
    for iter = 1 : Niter
	LinearOperator(s,t,operators,parameters,adj=false)
	delta = InnerProduct(t,t) + mu[1]*InnerProduct(s[1],s[1]) + mu[2]*InnerProduct(s[2],s[2])
	alpha = gamma/delta
	CGStep(m,s,a=[1.0;1.0],b=[alpha;alpha])
	CGStep(r,t,a=[1.0;1.0],b=[-alpha;-alpha])
	LinearOperator(g,r,operators,parameters,adj=true)
	CGStep(g,m,a=[1.0;1.0],b=[-mu[1];-mu[2]])
	gamma0 = copy(gamma)
	gamma = InnerProduct(g,g)
        cost1 = InnerProduct(r,r) + mu[1]*InnerProduct(m[1],m[1]) + mu[2]*InnerProduct(m[2],m[2])
        push!(cost,cost1/cost0)
	fp = open(cost_file,"a")
	write(fp,join([string(cost[iter+1]),"\n"]))
	close(fp)
	beta = gamma/gamma0
	CGStep(s,g,a=[beta;beta],b=[1.0;1.0])
	if (sqrt(gamma) <= sqrt(gamma00) * tol)
	    println("tolerance reached, ending at iteration ",iter)
	    break;
	end
    end
    SeisRemove(g);
    SeisRemove(s);
    SeisRemove(r);
    SeisRemove(t);

end


board = [[" ", " ", " "],
         [" ", " ", " "],
         [" ", " ", " "]]

player1 = "X"
player2 = "O"

player1_win = -1
tie = 0
player2_win = 1

function check_win(board)

    # Rows
    for i in 1:3
        if (board[i][1] == board[i][2] == board[i][3]) && (board[i][1] in [player1, player2])
            if (board[i][1] == player1)
                return player1_win
            end
            return player2_win
        end
    end

    # Column
    for i in 1:3
        if (board[1][i] == board[2][i] == board[3][i]) && (board[1][i] in [player1, player2])
            if (board[1][i] == player1)
                return player1_win
            end
            return player2_win
        end
    end

    # Principal diagonal
    if (board[1][1] == board[2][2] == board[3][3]) && (board[1][1] in [player1, player2])
        if (board[1][1] == player1)
            return player1_win
        end
        return player2_win
    end

    # Other diag
    if (board[1][3] == board[2][2] == board[3][1]) && (board[1][3] in [player1, player2])
        if (board[1][3] == player1)
            return player1_win
        end
        return player2_win
    end

    # Tie
    if length(check_available(board)) == 0
        return tie
    end

    # No one wins, game still goes on
    return nothing
end

function minimax(board, depth, is_maximising)
    result = check_win(board)

    if result !== nothing
        return result
    elseif is_maximising

        best_score = -Inf

        for i in 1:3, j in 1:3
            if board[i][j] == " "

                board[i][j] = player2

                score = minimax(board, depth + 1, false)

                board[i][j] = " "

                best_score = max(score, best_score)
            end
        end

        return best_score
    else
        best_score = Inf

        for i in 1:3, j in 1:3
            if board[i][j] == " "

                board[i][j] = player1

                score = minimax(board, depth + 1, true)

                board[i][j] = " "

                best_score = min(score, best_score)
            end
        end

        return best_score
    end

end

function best_move(board)
    best_score = -Inf

    best_pos = (0, 0)

    for i in 1:3, j in 1:3
        if board[i][j] == " "

            board[i][j] = player2

            score = minimax(board, 0, false)

            board[i][j] = " "

            if score > best_score
                best_score = score
                best_pos = (i, j)
            end
        end
    end
    return best_pos
end

function print_board(board)
    println("    1   2   3")
    println(" 1  ", join(board[1], " | "))
    println("   ———┼———┼———")
    println(" 2  ", join(board[2], " | "))
    println("   ———┼———┼———")
    println(" 3  ", join(board[3], " | "))
end

function make_move!(board, player, (x, y))
    board[x][y] = player
end

function check_available(board)
    available_cells = []
    for i in 1:3, j in 1:3
        if board[i][j] == " "
            push!(available_cells, (i, j))
        end
    end
    return available_cells
end

function ai_move!(board)
    make_move!(board, player2, best_move(board))
end

print_board(board)

while true
    print_board(board)

    print("Enter: ")
    txt = split(readline())
    make_move!(board, player1, (parse(Int, txt[1]), parse(Int, txt[2])))

    if check_win(board) == player1_win
        println(1)
        break
    elseif check_win(board) == player2_win
        println(2)
        break
    elseif check_win(board) == tie
        println("Tie!")
        break
    end

    ai_move!(board)
    print_board(board)

    if check_win(board) == player1_win
        println(1)
        break
    elseif check_win(board) == player2_win
        println(2)
        break
    elseif check_win(board) == tie
        println("Tie!")
        break
    end
end


# This routine was made by Leandro Martinez https://github.com/leandromartinez98

using DelimitedFiles
include("./align.jl")

#a = readdlm("./1.xyz")
#b = readdlm("./2.xyz")

function rmsd(x,y)
	# Align x to y (same size, correspondence given by sequence)

	z = align(x,y)

	# Compute rmsd before and after alignment

	rmsd_before = 0.
	rmsd_after = 0.
	n = size(x,1)
	for i in size(x,1)
#	  global rmsd_before, rmsd_after, n
	  rmsd_before = rmsd_before + (x[i,1] - y[i,1])^2 + (x[i,2] - y[i,2])^2 + (x[i,3] - y[i,3])^2
	  rmsd_after = rmsd_after + (z[i,1] - y[i,1])^2 + (z[i,2] - y[i,2])^2 + (z[i,3] - y[i,3])^2
	end
	rmsd_before = sqrt(rmsd_before/n)
	rmsd_after = sqrt(rmsd_after/n)
#	println(" rmsd before = ", rmsd_before)
#	println(" rmsd after = ", rmsd_after)
	return [rmsd_before,rmsd_after]
end


#----------------------------------------------------------------------------------------------------------------------------------------
# Implements solution to the diffusion equation in a N-layered finite cylinder as given in [1].
# Solutions are given in the spatial, frequency, and time domains for a point source located in the middle of the cylinder top.
#
# [1] André Liemert and Alwin Kienle, "Light diffusion in a turbid cylinder. II. Layered case," Opt. Express 18, 9266-9279 (2010)
#----------------------------------------------------------------------------------------------------------------------------------------

@with_kw struct Nlayer_cylinder{T <: Real} <: DiffusionParameters
    μsp::Vector{T} = [10.0, 10.0, 10.0, 10.0]               # reduced scattering coefficient (1/cm)
    μa::Vector{T} = [0.1, 0.1, 0.1, 0.1]                    # absorption coefficient (1/cm)
    n_ext::T = 1.0                                          # surrounding index of refraction
    n_med::Vector{T} = [1.0, 1.0, 1.0, 1.0]                 # layers index of refraction

    l::Vector{T} = [0.5, 0.8, 1.0, 5.0]                     # length of cylinder layers (cm)
    ρ::Union{T, AbstractVector{T}} = 1.0                    # source-detector separation (cm)
    a::T = 5.0                                              # radius of cylinder (cm)
    z::T = 0.0                                              # detector depth (cm)

    ω::T = 0.0                                              # modulation frequency
    bessels::AbstractVector{T} = besselroots[1:1000]        # roots of J0
end

#-------------------------------------------
# Steady-State Fluence
#-------------------------------------------
"""
    fluence_DA_Nlay_cylinder_CW(ρ, μa, μsp, n_ext, n_med, l, a, z, besselroots)

Compute the steady-state fluence in an N-layered cylinder. Source is assumed to be located on the top middle of the cylinder.
No checks on arguments are made except enforcing that the source is located in the top layer.
ρ, μa, and z should be >= 0.0.
μsp, n_ext, n_med, a should be > 0.0.
Each layer in l should be > 0.0.
In general, ρ should always be < a.
All arguments should have the same types and the length of μsp, μa, n_med, l should be equal.
It is advised to use the Nlayer_cylinder() structure format for inputs.

# Arguments
- `ρ`: source-detector separation in cylindrical coordinates (distance from middle z-axis of cylinder) (cm⁻¹)
- `μa`: absorption coefficient (cm⁻¹)
- `μsp`: reduced scattering coefficient (cm⁻¹)
- `n_ext`: the boundary's index of refraction (air or detector)
- `n_med`: the sample medium's index of refraction
- `l`: layer thicknesses (cm)
- `a`: cylinder radius (cm)
- `z`: source depth within cylinder
- `besselroots`: roots of bessel function of first kind zero order J0(x) = 0

# Examples
julia> `fluence_DA_Nlay_cylinder_CW(1.0, [0.1, 0.1], [10.0, 10.0], 1.0, [1.0, 1.0], [4.5, 4.5], 10.0, 0.0, besselroots)`
"""
function fluence_DA_Nlay_cylinder_CW(ρ, μa, μsp, n_ext, n_med, l, a, z, besselroots)
    D = D_coeff.(μsp)
    N = length(D)
    A = A_coeff.(n_med / n_ext)
    z0 = z0_coeff(μsp[1])
    zb = zb_coeff.(A, D)
    @assert z0 < l[1]

    if z < l[1]
        return _kernel_fluence_DA_Nlay_cylinder(ρ, D, μa, a, zb, z, z0, l, n_med, besselroots, _green_Nlaycylin_top, N) / (π * (a + zb[1])^2)
    elseif z > sum(l[1:end - 1])
        return _kernel_fluence_DA_Nlay_cylinder(ρ, D, μa, a, zb, z, z0, l, n_med, besselroots, _green_Nlaycylin_bottom, N) / (π * (a + zb[1])^2)
    end
end
"""
    fluence_DA_Nlay_cylinder_CW(data)

Wrapper to fluence_DA_Nlay_cylinder_CW(ρ, μa, μsp, n_ext, n_med, l, a, z, besselroots) with inputs given as a structure (data).

# Examples
julia> data = Nlayer_cylinder(a = 10.0, l = [1.0, 1.0, 1.0, 2.0], z = 5.0)
julia> `fluence_DA_Nlay_cylinder_CW(data)`
"""
function fluence_DA_Nlay_cylinder_CW(data)
    return fluence_DA_Nlay_cylinder_CW(data.ρ, data.μa, data.μsp, data.n_ext, data.n_med, data.l, data.a, data.z, data.bessels)
end

#-------------------------------------------
# Steady-State Flux
#-------------------------------------------
"""
    flux_DA_Nlay_cylinder_CW(ρ, μa, μsp, n_ext, n_med, l, a, z, besselroots)

Compute the steady-state flux using Fick's law D[1]*∂ϕ(ρ)/∂z for z = 0 (reflectance) and -D[end]*∂ϕ(ρ)/∂z for z = sum(l) (transmittance) in an N-layered cylinder.
Source is assumed to be located on the top middle of the cylinder. z must be equal to 0 or the total length sum(l) of cylinder.

# Arguments
- `ρ`: source-detector separation in cylindrical coordinates (distance from middle z-axis of cylinder) (cm⁻¹)
- `μa`: absorption coefficient (cm⁻¹)
- `μsp`: reduced scattering coefficient (cm⁻¹)
- `n_ext`: the boundary's index of refraction (air or detector)
- `n_med`: the sample medium's index of refraction
- `l`: layer thicknesses (cm)
- `a`: cylinder radius (cm)
- `z`: source depth within cylinder: must be equal to 0 or sum(l)
- `besselroots`: roots of bessel function of first kind zero order J0(x) = 0

# Examples
julia> `flux_DA_Nlay_cylinder_CW(1.0, [0.1, 0.1], [10.0, 10.0], 1.0, [1.0, 1.0], [4.5, 4.5], 10.0, 0.0, besselroots)`
"""
function flux_DA_Nlay_cylinder_CW(ρ, μa, μsp, n_ext, n_med, l, a, z, besselroots)
    @assert z == zero(eltype(z)) || z == sum(l)
    D = D_coeff.(μsp)
    if z == zero(eltype(z))
        return D[1] * ForwardDiff.derivative(dz -> fluence_DA_Nlay_cylinder_CW(ρ, μa, μsp, n_ext, n_med, l, a, dz, besselroots), z)
    elseif z == sum(l)
        return -D[end] * ForwardDiff.derivative(dz -> fluence_DA_Nlay_cylinder_CW(ρ, μa, μsp, n_ext, n_med, l, a, dz, besselroots), z)
    end
end
"""
    flux_DA_Nlay_cylinder_CW(data, besselroots)

Wrapper to flux_DA_Nlay_cylinder_CW(ρ, μa, μsp, n_ext, n_med, l, a, z, besselroots) with inputs given as a structure (data).

# Examples
julia> data = Nlayer_cylinder(a = 10.0, l = [1.0, 1.0, 1.0, 2.0], z = 5.0)
julia> `flux_DA_Nlay_cylinder_CW(data, besselroots)`
"""
function flux_DA_Nlay_cylinder_CW(data)
    return flux_DA_Nlay_cylinder_CW(data.ρ, data.μa, data.μsp, data.n_ext, data.n_med, data.l, data.a, data.z, data.bessels)
end

#-------------------------------------------
# Time-Domain Fluence
#-------------------------------------------
"""
    fluence_DA_Nlay_cylinder_TD(t, ρ, μa, μsp, n_ext, n_med, l, a, z, besselroots; N = 24, ILT = hyper_fixed)

Compute the time-domain fluence in an N-layered cylinder. Source is assumed to be located on the top middle of the cylinder.
See notes on fluence_DA_Nlay_cylinder_CW for input checks with the initial constraint that all values of t should be > 0.0 and ordered least to greatest.
Do not have any value of t be zero or negative.

It is best to use `hyper_fixed` as the inverse laplace transform if t consists of many time points. Utilize `hyperbola` for a single time point.
The value of N should be proportional to the dynamic range of the time-domain signal needed. For later times you will need a larger N.
The lowest fluence value you can compute will be no less than the machine precision you utilize. Float64 values are limited to fluence > ~2.2-16.

# Arguments
- `t`: time point or vector (ns)
- `ρ`: source-detector separation in cylindrical coordinates (distance from middle z-axis of cylinder) (cm⁻¹)
- `μa`: absorption coefficient (cm⁻¹)
- `μsp`: reduced scattering coefficient (cm⁻¹)
- `n_ext`: the boundary's index of refraction (air or detector)
- `n_med`: the sample medium's index of refraction
- `l`: layer thicknesses (cm)
- `a`: cylinder radius (cm)
- `z`: source depth within cylinder
- `besselroots`: roots of bessel function of first kind zero order J0(x) = 0
- `N`: number of Hankel-Laplace calculations
- `ILT`: inverse laplace transform function

# Examples
julia> `fluence_DA_Nlay_cylinder_TD(0.1:0.1:2.0, [0.1, 0.1], [10.0, 10.0], 1.0, [1.0, 1.0], [4.5, 4.5], 10.0, 0.0, besselroots)`
"""
function fluence_DA_Nlay_cylinder_TD(t::AbstractFloat, ρ, μa, μsp, n_ext, n_med, l, a, z, besselroots; N = 18, ILT = hyperbola)
    return ILT(s -> _fluence_DA_Nlay_cylinder_Laplace(ρ, μa, μsp, n_ext, n_med, l, a, z, s, besselroots), t, N = N)
end
function fluence_DA_Nlay_cylinder_TD(t::AbstractArray, ρ, μa, μsp, n_ext, n_med, l, a, z, besselroots; N = 24, ILT = hyper_fixed)
    T = promote_type(eltype(ρ), eltype(μa), eltype(μsp), eltype(z), eltype(l))
    return ILT(s -> _fluence_DA_Nlay_cylinder_Laplace(ρ, μa, μsp, n_ext, n_med, l, a, z, s, besselroots), t, N = N, T = T)
end
"""
    fluence_DA_Nlay_cylinder_TD(t, data; N = 24)

Wrapper to fluence_DA_Nlay_cylinder_TD(t, ρ, μa, μsp, n_ext, n_med, l, a, z, besselroots; N = 24, ILT = hyper_fixed) with inputs given as a structure (data).

# Examples
julia> data = Nlayer_cylinder(a = 10.0, l = [1.0, 1.0, 1.0, 2.0], z = 5.0)
julia> `fluence_DA_Nlay_cylinder_TD(0.5:1.0:2.5, data)`
"""
function fluence_DA_Nlay_cylinder_TD(t, data; N = 24)
    return fluence_DA_Nlay_cylinder_TD(t, data.ρ, data.μa, data.μsp, data.n_ext, data.n_med, data.l, data.a, data.z, data.bessels, N = N)
end

#-------------------------------------------
# Time-Domain Flux
#-------------------------------------------
"""
    flux_DA_Nlay_cylinder_TD(t, ρ, μa, μsp, n_ext, n_med, l, a, z, besselroots; N = 24)

Compute the time-domain flux using Fick's law D[1]*∂ϕ(ρ, t)/∂z for z = 0 (reflectance) and -D[end]*∂ϕ(ρ, t)/∂z for z = sum(l) (transmittance) in an N-layered cylinder.
Source is assumed to be located on the top middle of the cylinder. z must be equal to 0 or the total length sum(l) of cylinder.
Uses the hyperbola contour if t is an AbstractFloat and the fixed hyperbola contour if t is an AbstractVector.
∂ϕ(ρ, t)/∂z is calculated using forward mode auto-differentiation with ForwardDiff.jl

# Arguments
- `t`: time point or vector (ns)
- `ρ`: source-detector separation in cylindrical coordinates (distance from middle z-axis of cylinder) (cm⁻¹)
- `μa`: absorption coefficient (cm⁻¹)
- `μsp`: reduced scattering coefficient (cm⁻¹)
- `n_ext`: the boundary's index of refraction (air or detector)
- `n_med`: the sample medium's index of refraction
- `l`: layer thicknesses (cm)
- `a`: cylinder radius (cm)
- `z`: source depth within cylinder
- `besselroots`: roots of bessel function of first kind zero order J0(x) = 0
- `N`: number of Hankel-Laplace calculations

# Examples
julia> `fluence_DA_Nlay_cylinder_TD(0.1:0.1:2.0, [0.1, 0.1], [10.0, 10.0], 1.0, [1.0, 1.0], [4.5, 4.5], 10.0, 0.0, besselroots)`
"""
function flux_DA_Nlay_cylinder_TD(t::AbstractVector, ρ, μa, μsp, n_ext, n_med, l, a, z, besselroots; N = 24)
    @assert z == zero(eltype(z)) || z == sum(l)
    D = D_coeff.(μsp)

    ## need to know the type of z to preallocate vectors coorectly in hyper_fixed for autodiff
    function _ILT(z::T) where {T}
        return hyper_fixed(s -> _fluence_DA_Nlay_cylinder_Laplace(ρ, μa, μsp, n_ext, n_med, l, a, z, s, besselroots), t, N = N, T = T)
    end
    if z == zero(eltype(z))
        return D[1]*ForwardDiff.derivative(_ILT, z)
    elseif z == sum(l)
        return -D[end]*ForwardDiff.derivative(_ILT, z)
    end
end
function flux_DA_Nlay_cylinder_TD(t::AbstractFloat, ρ, μa, μsp, n_ext, n_med, l, a, z, besselroots; N = 24)
    @assert z == zero(eltype(z)) || z == sum(l)
    D = D_coeff.(μsp)
    if z == zero(eltype(z))
        return D[1] * ForwardDiff.derivative(dz -> hyperbola(s -> _fluence_DA_Nlay_cylinder_Laplace(ρ, μa, μsp, n_ext, n_med, l, a, dz, s, besselroots), t, N = N), 0.0)
    elseif z == sum(l)
        return -D[end] * ForwardDiff.derivative(dz -> hyperbola(s -> _fluence_DA_Nlay_cylinder_Laplace(ρ, μa, μsp, n_ext, n_med, l, a, dz, s, besselroots), t, N = N), sum(l))
    end
end

"""
    flux_DA_Nlay_cylinder_TD(t, data; N = 24)

Wrapper to flux_DA_Nlay_cylinder_TD(t, ρ, μa, μsp, n_ext, n_med, l, a, z, besselroots; N = 24) with inputs given as a structure (data).

# Examples
julia> data = Nlayer_cylinder(a = 10.0, l = [1.0, 1.0, 1.0, 2.0], z = 5.0)
julia> `flux_DA_Nlay_cylinder_TD(0.5:1.0:2.5, data)`
"""
function flux_DA_Nlay_cylinder_TD(t, data;  N = 24)
    return flux_DA_Nlay_cylinder_TD(t, data.ρ, data.μa, data.μsp, data.n_ext, data.n_med, data.l, data.a, data.z, data.bessels, N = N)
end

#-------------------------------------------
# Frequency-Domain Fluence
#-------------------------------------------
"""
    fluence_DA_Nlay_cylinder_FD(ρ, μa, μsp, n_ext, n_med, l, a, z, ω, besselroots)

Compute the frequency modulated fluence in an N-layered cylinder. Source is assumed to be located on the top middle of the cylinder.

# Arguments
- `ρ`: source-detector separation in cylindrical coordinates (distance from middle z-axis of cylinder) (cm⁻¹)
- `μa`: absorption coefficient (cm⁻¹)
- `μsp`: reduced scattering coefficient (cm⁻¹)
- `n_ext`: the boundary's index of refraction (air or detector)
- `n_med`: the sample medium's index of refraction
- `l`: layer thicknesses (cm)
- `a`: cylinder radius (cm)
- `z`: source depth within cylinder
- `ω`: source modulation frequency (1/ns)
- `besselroots`: roots of bessel function of first kind zero order J0(x) = 0

# Examples
julia> `fluence_DA_Nlay_cylinder_CW(1.0, [0.1, 0.1], [10.0, 10.0], 1.0, [1.0, 1.0], [4.5, 4.5], 10.0, 0.0, 1.0, besselroots)`
"""
function fluence_DA_Nlay_cylinder_FD(ρ, μa, μsp, n_ext, n_med, l, a, z, ω, besselroots)
    ν = ν_coeff.(n_med)
    μa_complex = μa .+ ω * im ./ ν

    return fluence_DA_Nlay_cylinder_CW(ρ, μa_complex, μsp, n_ext, n_med, l, a, z, besselroots)
end
"""
    fluence_DA_Nlay_cylinder_FD(data)

Wrapper to fluence_DA_Nlay_cylinder_FD(ρ, μa, μsp, n_ext, n_med, l, a, z, ω, besselroots) with inputs given as a structure (data).

# Examples
julia> data = Nlayer_cylinder(a = 10.0, l = [1.0, 1.0, 1.0, 2.0], z = 5.0, ω = 1.0)
julia> `fluence_DA_Nlay_cylinder_FD(data)`
"""
function fluence_DA_Nlay_cylinder_FD(data)
    return fluence_DA_Nlay_cylinder_FD(data.ρ, data.μa, data.μsp, data.n_ext, data.n_med, data.l, data.a, data.z, data.ω, data.bessels)
end

#-------------------------------------------------------------------------------
# this function is the base for the laplace transform in the time-domain
#-------------------------------------------------------------------------------
function _fluence_DA_Nlay_cylinder_Laplace(ρ, μa, μsp, n_ext, n_med, l, a, z, s, besselroots)
    ν = ν_coeff.(n_med)
    μa_complex = μa .+ s ./ ν

    return fluence_DA_Nlay_cylinder_CW(ρ, μa_complex, μsp, n_ext, n_med, l, a, z, besselroots)
end

#-------------------------------------------------------------------------------
# _kernel_fluence_DA_Nlay_cylinder provides the main kernel of the program.
# The inputs are similar to fluence_DA_Nlay_cylinder_CW.
# D is the diffusion coefficient and N is the number of layers.
# green is the Green's function for either the first or bottom layer (below).
#-------------------------------------------------------------------------------
function _kernel_fluence_DA_Nlay_cylinder(ρ::AbstractFloat, D, μa, a, zb, z, z0, l, n_med, besselroots, green, N)
    ϕ = zero(eltype(μa))
    ϕ_tmp = zero(eltype(μa))
    α = zeros(eltype(μa), N)

    for ind in eachindex(besselroots)
        ϕ_tmp = green(α, besselroots[ind] / (a + zb[1]), μa, D, z, z0, zb, l, n_med, N)
        ϕ_tmp *= besselj0(besselroots[ind] / (a + zb[1]) * ρ)
        ϕ_tmp /= (besselj1(besselroots[ind]))^2
        ϕ += ϕ_tmp
    end

    return ϕ
end

function _kernel_fluence_DA_Nlay_cylinder(ρ::AbstractVector, D, μa, a, zb, z, z0, l, n_med, besselroots, green, N)
    ϕ = zeros(eltype(μa), length(ρ))
    ϕ_tmp = zero(eltype(μa))
    α = zeros(eltype(μa), N)

    for ind in eachindex(besselroots)
        tmp = besselroots[ind] / (a + zb[1])
        ϕ_tmp = green(α, tmp, μa, D, z, z0, zb, l, n_med, N)
        ϕ_tmp /= (besselj1(besselroots[ind]))^2
        for ρ_ind in eachindex(ρ)
            ϕ[ρ_ind] += ϕ_tmp * besselj0(tmp * ρ[ρ_ind])
        end
    end

    return ϕ
end
#-------------------------------------------------------------------------------
# Calculates the Green's function in the first (top) and last (bottom) layer
# sinh and cosh have been expanded as exponentials.
# A common term has been factored out. This cancels for G1 but not GN (see _βγN_correction)
# For N = 2, 3, 4 coefficients are explicitly calculated.
# For N > 4, β and γ are calculated recursively using eqn. 17 & 18.
#-------------------------------------------------------------------------------
@inline function α_coeff!(α, μa, D, sn)
    @inbounds for ind in 1:length(μa)
        α[ind] = sqrt(μa[ind] / D[ind] + sn^2)
    end
    return α
end
@inline function _green_Nlaycylin_top(α, sn, μa, D, z, z0, zb, l, n, N)
    α = α_coeff!(α, μa, D, sn)

    if N == 4
        β, γ = _get_βγ4(α, D, n, zb, l)
    elseif N == 3
        β, γ = _get_βγ3(α, D, n, zb, l)
    elseif N == 2
        β, γ = _get_βγ2(α, D, n, zb, l)
    elseif N > 4
        β, γ = _get_βγk(α, D, n, zb, l)
    end

    tmp1 = D[1] * α[1] * n[1]^2 * β
    tmp2 = D[2] * α[2] * n[2]^2 * γ
    tmp3 = exp(-2 * α[1] * (l[1] + zb[1]))

    g  = (exp(-α[1] * abs(z - z0)) - exp(-α[1] * (z + z0 + 2 * zb[1])))
    g1 = exp(α[1] * (z + z0 - 2 * l[1])) * (1 - exp(-2 * α[1] * (z0 + zb[1]))) * (1 - exp(-2 * α[1] * (z + zb[1])))
    g1 *= (tmp1 - tmp2)
    g1 /= (tmp1 * (1 + tmp3) + tmp2 * (1 - tmp3))

    return (g + g1) / (2 * D[1] * α[1])
end
@inline function _green_Nlaycylin_bottom(α, sn, μa, D, z, z0, zb, l, n, N)
    α = α_coeff!(α, μa, D, sn)

    if N == 4
        β, γ = _get_βγ4(α, D, n, zb, l)
        βγ_correction = _βγ4_correction(α, zb, l)
    elseif N == 3
        β, γ = _get_βγ3(α, D, n, zb, l)
        βγ_correction = _βγ3_correction(α, zb, l)
    elseif N == 2
        β, γ = _get_βγ2(α, D, n, zb, l)
        βγ_correction = _βγ2_correction(α, zb, l)
    elseif N > 4
        β, γ = _get_βγk(α, D, n, zb, l)
        βγ_correction = _βγN_correction(α, zb, l)
    end

    tmp = 1.0
    for ind in (N - 1):-1:2
        tmp *= D[ind] * α[ind] * n[ind]^2
    end

    gN = n[end]^2 * tmp * 2^(N - 1) / 2
    gN *= exp(α[1] * (z0 - l[1]) + α[end] * (sum(l) + zb[end] - z) - βγ_correction)
    gN /= D[1] * α[1] * n[1]^2 * β * (1 + exp(-2 * α[1] * (l[1] + zb[1]))) + D[2] * α[2] * n[2]^2 * γ * (1 - exp(-2 * α[1] * (l[1] + zb[1])))
    gN *= (1 - exp(-2 * α[1] * (z0 + zb[1]))) * (1 - exp(-2 * α[end] * (sum(l) + zb[end] - z)))

    return gN
 end

#-------------------------------------------------------------------------------
# Calculate β and γ coefficients with eqn. 17 in [1].
# sinh and cosh have been expanded as exponentials.
# A common term has been factored out. This cancels for G1 but not GN (see _βγN_correction)
# For N = 2, 3, 4 coefficients are explicitly calculated.
# For N > 4, β and γ are calculated recursively using eqn. 17 & 18.
#-------------------------------------------------------------------------------
@inline function _get_βγ2(α, D, n, zb, l)
    tmp1 = exp(-2 * α[2] * (l[2] + zb[2]))

    β = (1 - tmp1)
    γ = (1 + tmp1)

    return β, γ
end
@inline function _get_βγ3(α, D, n, zb, l)
    tmp1 = D[2] * α[2] * n[2]^2
    tmp2 = D[3] * α[3] * n[3]^2
    tmp3 = exp(-2 * α[2] * l[2])
    tmp4 = exp(-2 * α[3] * (l[3] + zb[2]))

    β  = tmp1 * (1 + tmp3) * (1 - tmp4)
    β += tmp2 * (1 - tmp3) * (1 + tmp4)

    γ  = tmp1 * (1 - tmp3) * (1 - tmp4)
    γ += tmp2 * (1 + tmp3) * (1 + tmp4)

    return β, γ
end
@inline function _get_βγ4(α, D, n, zb, l)
    tmp5 = D[2] * α[2] * n[2]^2
    tmp1 = D[3] * α[3] * n[3]^2
    tmp4 = D[4] * α[4] * n[4]^2

    tmp6 = exp(-2 * α[2] * l[2])
    tmp2 = exp(-2 * α[3] * l[3])
    tmp3 = exp(-2 * α[4] * (l[4] + zb[2]))

    β_4  = tmp1 * (1 + tmp2) * (1 - tmp3)
    β_4 += tmp4 * (1 - tmp2) * (1 + tmp3)

    γ_4  = tmp1 * (1 - tmp2) * (1 - tmp3)
    γ_4 += tmp4 * (1 + tmp2) * (1 + tmp3)

    β = (tmp5 * β_4 * (1 + tmp6) + tmp1 * γ_4 * (1 - tmp6))
    γ = (tmp5 * β_4 * (1 - tmp6) + tmp1 * γ_4 * (1 + tmp6))

    return β, γ
end
@inline function _get_βγk(α, D, n, zb, l)
    βN, γN = _get_βγN(α, D, n, zb, l)

    for k in length(α):-1:4
        tmp1 = D[k - 2] * α[k - 2] * n[k - 2]^2 * βN
        tmp2 = D[k - 1] * α[k - 1] * n[k - 1]^2 * γN
        tmp3 = exp(-2 * α[k - 2] * l[k - 2])

        βN  =  tmp1 * (1 + tmp3)
        βN +=  tmp2 * (1 - tmp3)
        γN  =  tmp1 * (1 - tmp3)
        γN +=  tmp2 * (1 + tmp3)

    end

    return βN, γN
end
@inline function _get_βγN(α, D, n, zb, l)
    tmp1 = D[end - 1] * α[end - 1] * n[end - 1]^2
    tmp2 = D[end] * α[end] * n[end]^2
    tmp3 = exp(-2 * α[end - 1] * l[end - 1])
    tmp4 = exp(-2 * α[end] * (l[end] + zb[2]))

    βN  =   tmp1 * (1 + tmp3) *  (1 - tmp4)
    βN +=   tmp2 * (1 - tmp3) *  (1 + tmp4)

    γN =  tmp1 * (1 - tmp3) *  (1 - tmp4)
    γN +=  tmp2 * (1 + tmp3) *  (1 + tmp4)

    return βN, γN
end

#-------------------------------------------------------------------------------
# Calculate βγ correction for N layer green's function.
# This is done because a common term was factored out from general expressions
# They don't cancel for the N layer so have to divide them out.
# For N = 2, 3, 4 coefficients are explicitly calculated.
# For N > 4, β and γ are calculated recursively
#-------------------------------------------------------------------------------
@inline function _βγ2_correction(α, zb, l)
    return α[2] * (l[2] + zb[2])
end
function _βγ3_correction(α, zb, l)
    return α[2] * l[2] + α[3] * (l[3] + zb[2])
end
@inline function _βγ4_correction(α, zb, l)
    return α[2] * l[2] + α[3] * l[3] + α[4] * (l[4] + zb[2])
end
@inline function _βγ5_correction(α, zb, l)
    return α[2] * l[2] + α[3] * l[3] + α[4] * l[4] + α[5] * (l[5] + zb[2])
end
@inline function _βγN_correction(α, zb, l)
    out = α[end] * (l[end] + zb[2])
    for ind in (length(α) - 1):-1:2
        out += α[ind] * l[ind]
    end
    return out
end
#-------------------------------------------------------------------------------


import Base: zero, one, +, -, *, /, //, ^,==, inv, iszero, isone,convert, show,rand

struct NTL_INIT_zz_p end # type for init
const _init_zz_p=NTL_INIT_zz_p()
"""
struct zz_p{T} is the type of finite fields

    ## Examples:
    julia> zz_p{7} # The type of finite fields of cardinality 7
    julia> zz_p{3}(4) # Element 4 in zz_p{3} which equals 1 in zz_p{3}
    julia> zz_p{3}() # Element 0 in zz_p{3}
"""
mutable struct zz_p{T}
    _rep::Int
    function zz_p{T}() where {T}
        new(0)
    end
    function zz_p{T}(a::Int, ::NTL_INIT_zz_p) where {T}
        new(a)
    end
    function zz_p{T}(a::Int) where {T}
        new(mod(a, T))
    end
end

## info of type zz_p{T}
"""
Generate the type zz_p{T} of prime fields

    ## Examples:
    julia> zz_p(3)
"""
function zz_p(a::Int)
    return zz_p{a}
end
function modulus(::Type{zz_p{T}}) where {T}
    return T
end
function zero(::Type{zz_p{T}}) where {T}
    return zz_p{T}(0)
end
function one(::Type{zz_p{T}}) where {T}
    return zz_p{T}(1, _init_zz_p)
end
## convert
function convert(::Type{zz_p{T}}, a::Int) where {T}
    return zz_p{T}(a)
end
function convert(::Type{Int}, a::zz_p{T}) where {T}
    return a._rep
end
## representation
function modulus(::Type{zz_p{T}}) where {T}
    return T
end
@inline function swap!(x::zz_p{T}, y::zz_p{T}) where {T}
    z = x._rep;x._rep = y._rep; y._rep =z;
    nothing
end

## inline funcitons
#const NTL_BITS_PER_LONG=Sys.WORD_SIZE
# @inline function cast_unsigned(a::Int)
#     return Core.bitcast(UInt, a)
# end
# @inline function sp_SignMask(a::Int)
#    return -(cast_unsigned(a) >> (NTL_BITS_PER_LONG-1));
# end
@inline function sp_CorrectExcess(a::Int, n::Int)
   return a-n >= 0 ? a-n : a
end
@inline function sp_CorrectDeficit(a::Int, n::Int)
   return a >= 0 ? a : a+n
end

## add
@inline function AddMod0(a::Int,b::Int, n::Int)::Int
    return Int(mod(Int128(a)+Int128(b),n))
 end
@inline function AddMod(a::Int,b::Int, n::Int)::Int
    r = a+b;
    return sp_CorrectExcess(r, n)
 end
function add!(z::zz_p{T} , x::zz_p{T}, y::zz_p{T}) where {T}
    z._rep = AddMod(x._rep,y._rep, T)
    return z
end
function add(x::zz_p{T}, y::zz_p{T}) where {T}
    Z=AddMod(x._rep,y._rep, T)
    return zz_p{T}(Z, _init_zz_p)
end
+(x::zz_p{T}, y::zz_p{T}) where {T} = add(x,y)
add(x::zz_p{T}, Y::Int) where {T} = add(x,convert(zz_p{T},Y))
add(X::Int, y::zz_p{T}) where {T} = add(convert(zz_p{T},X),y)

## sub
@inline function SubMod0(a::Int,b::Int, n::Int)::Int
    return mod(a-b, n)
 end
@inline function SubMod(a::Int,b::Int, n::Int)::Int
    r = a-b;
    return sp_CorrectDeficit(r, n)
 end
function sub!(z::zz_p{T} , x::zz_p{T}, y::zz_p{T}) where {T}
    z._rep = SubMod(x._rep,y._rep, T)
    return z
end
function sub(x::zz_p{T}, y::zz_p{T}) where {T}
    Z=SubMod(x._rep,y._rep, T)
    return zz_p{T}(Z, _init_zz_p)
end
-(x::zz_p{T}, y::zz_p{T}) where {T} = sub(x,y)
-(x::zz_p{T}, Y::Int) where {T} = sub(x,convert(zz_p{T},Y))
-(X::Int, y::zz_p{T}) where {T} = sub(convert(zz_p{T},X),y)

## negate
# x = -a
@inline function NegateMod(a::Int, n::Int)::Int
    # mod((0- a) , n)
      return SubMod(0, a, n)
   end
function negate!(x::zz_p{T}, a::zz_p{T}) where {T}
    x._rep=NegateMod(a._rep,T)
    return x
end
function negate(a::zz_p{T}) where {T}
    Z = NegateMod(a._rep,T)
    return zz_p{T}(Z, _init_zz_p)
end
-(a::zz_p{T}) where {T} = negate(a)

## mul
@inline function MulMod(a::Int, b::Int, n::Int)::Int
    Int(mod(Int128(a)*Int128(b),n))
end
function mul!(z::zz_p{T} , x::zz_p{T}, y::zz_p{T}) where {T}
    z._rep = MulMod(x._rep,y._rep, T)
    return z
end
function mul(x::zz_p{T}, y::zz_p{T}) where {T}
    Z=MulMod(x._rep,y._rep, T)
    return zz_p{T}(Z, _init_zz_p)
end

*(x::zz_p{T}, y::zz_p{T}) where {T} = mul(x,y)
*(x::zz_p{T}, Y::Int) where {T} = mul(x, convert(zz_p{T},Y))
*(X::Int, y::zz_p{T}) where {T} = mul(convert(zz_p{T},X), y)

## inv
# 1/a mod n
function InvMod(a::Int, n::Int)::Int
    d,u,v = gcdx(a, n)
    if d!= 1
        error("InvMod: inverse undefined")
    elseif u< 0
        return u+n
    end
    return u
end
function inv!(z::zz_p{T}, x::zz_p{T}) where {T}
    z._rep=InvMod(x._rep, T)
    return z
end
function inv(x::zz_p{T}) where {T}
    Z = InvMod(x._rep, T)
    return zz_p{T}(Z, _init_zz_p)
end

##div
function div!(z::zz_p{T} , x::zz_p{T}, y::zz_p{T}) where {T}
    z._rep = MulMod(x._rep, InvMod(y._rep, T), T)
    return z
end
function div(x::zz_p{T}, y::zz_p{T}) where {T}
    Z=MulMod(x._rep, InvMod(y._rep, T), T)
    return zz_p{T}(Z, _init_zz_p)
end
/(x::zz_p{T}, y::zz_p{T}) where {T} = div(x,y)
/(x::zz_p{T}, Y::Int) where {T} = div(x, convert(zz_p{T},Y))
/(X::Int, y::zz_p{T}) where {T} = div(convert(zz_p{T},X), y)

## comparison
@inline function iszero(x::zz_p{T}) where {T}
    return x._rep == 0
end
@inline function isone(x::zz_p{T}) where {T}
    return x._rep == 1
end
@inline function ==(x::zz_p{T}, y::zz_p{T}) where {T}
    return x._rep == y._rep
end
@inline function ==(x::zz_p{T}, Y::Int) where {T}
    return x._rep == Y
end
@inline function ==(X::Int, y::zz_p{T}) where {T}
    return X == y._rep
end
## random numbers
function rand(::Type{zz_p{T}}) where {T}
    return zz_p{T}(rand(Int))
end

## power x = a^e
function PowerMod(a::Int, e::Int , n::Int)
   if e < 0
      return InvMod(PowerMod(a,-e,n),n)
   end
    x::Int = 1;
    y::Int = a;
    while (e != 0)
        if (e & 1) != 0
                x = MulMod(x, y, n)
        end
        y = MulMod(y, y, n);
        e = e >> 1;
    end
   return x;
end

## show
show(io::IO, x::zz_p{T}) where {T} = print(io, "zz_", T, "(", x._rep ,")" )

@inline function power!(z::zz_p{T}, x::zz_p{T}, e::Int) where {T}
    z._rep= PowerMod(x._rep, e, T)
    return z
end

@inline function power(x::zz_p{T}, e::Int) where {T}
    Z= PowerMod(x._rep, e, T)
    return zz_p{T}(Z, _init_zz_p)
end

^(x::zz_p{T}, e::Int) where {T} = power(x,e)


using MCMCChains
using Turing
using Random
using Test

@testset "ess tests" begin
    Random.seed!(20)

    dat = [0,1,0,0,0,0,0,0,0,1]

    @model bermodel(y) = begin
        theta ~ Beta(1,1)
        for n = 1:length(y)
            y[n] ~ Bernoulli(theta)
        end
    end

    # Make four chains.
    chn = mapreduce(x->sample(bermodel(dat),
        NUTS(2000, 1000, 0.65)),
        chainscat,
        1:4)

    e = ess(chn[1001:2000, :, :])

    @test all([z ≥ 1000 && z ≤ 2000 for z in e[:ess]])
    @test all(isapprox.(e[:r_hat], 1.0, atol=0.1))
end


@reexport module _Math

import Nemo: add!, sub!, mul!

export
    checked_add, checked_sub,
    setbits,
    @activate_mathematica,
    @activate_matlab

using Reexport: @reexport

checked_add(a::Number, b::Number) = checked_add(promote(a, b)...)
checked_add(a::T, b::T) where {T<:Number} = Base.checked_add(a, b)
checked_add(a::BigInt, b::BigInt) = a + b
checked_add(a::BigFloat, b::BigFloat) = a + b

checked_sub(a::Number, b::Number) = checked_sub(promote(a, b)...)
checked_sub(a::T, b::T) where {T<:Number} = Base.checked_sub(a, b)
checked_sub(a::BigInt, b::BigInt) = a - b
checked_sub(a::BigFloat, b::BigFloat) = a - b

setbits(n::Integer, idx) = begin
    m = copy(n)
    for i in idx
        m |= one(n) << i
    end
    m
end

macro activate_mathematica(); Expr(:using, :Mathematica); end
macro activate_matlab(); Expr(:using, :MATLAB); end

include("gmp.jl")
include("mpfr.jl")
include("Series/series.jl")
include("GroupTheory/grouptheory.jl")
include("NumberTheory/numbertheory.jl")
include("Combinatorics/combinatorics.jl")
include("Algebra/algebra.jl")
include("EllipticCurves/ellipticcurves.jl")

end


# Generate right hand coefficients
function mycoeffs(f, n, lam)
    F = Fun(f, Ultraspherical(lam))
    m = coefficients(F)
    length(m) < n ? (return [m; zeros(n-length(m))]) : (return m[1:n])
end


# addition of positive definite matrices

using PDMats

@testset "addition" begin
    for T in (Float64, Float32)
        printstyled("Testing addition with eltype = $T\n"; color=:blue)
        M = convert(Array{T,2}, [4.0 -2.0 -1.0; -2.0 5.0 -1.0; -1.0 -1.0 6.0])
        V = convert(Array{T,1}, [1.5, 2.5, 2.0])
        local X = convert(T, 2.0)

        pm1 = PDMat(M)
        pm2 = PDiagMat(V)
        pm3 = ScalMat(3, X)
        pm4 = X * I
        pm5 = PDSparseMat(sparse(M))

        pmats = Any[pm1, pm2, pm3] #, pm5]

        for p1 in pmats, p2 in pmats
            pr = p1 + p2
            @test size(pr) == size(p1)
            @test Matrix(pr) ≈ Matrix(p1) + Matrix(p2)

            pr = pdadd(p1, p2, convert(T, 1.5))
            @test size(pr) == size(p1)
            @test Matrix(pr) ≈ Matrix(p1) + Matrix(p2) * convert(T, 1.5)
        end

        for p1 in pmats
            pr = p1 + pm4
            @test size(pr) == size(p1)
            @test Matrix(pr) ≈ Matrix(p1) + pm4
        end
    end
end


# Core functions for hidden Markov models

# Copyright (c) 2016 Jeffrey W. Miller
# This software is released under the MIT License.
# If you use this software in your research, please cite:
#
#   Jeffrey W. Miller (2016). Lecture Notes on Advanced Stochastic Modeling. Duke University, Durham, NC.

# This code assumes the following have been defined:
#   log_q = function such that log_q(x_t,phi[z_t]) = log p(x_t | Z_t = z_t), where phi is a vector of parameters.
#   qrnd = function such that qrnd(phi[z_t]) generates a sample from p(x_t|z_t), where phi is a vector of parameters.
#   phi_init = function that creates an array of phi parameters.
#   phi_max! = function that finds the maximal phi parameters for the given data x and weights gamma.

# The functions below involve the following variables:
#   m = number of hidden states
#   n = length of observed sequence
#   log_pi = length m vector with log_pi[s] = log p(Z_1=s).
#   log_T = m-by-m matrix with log_T[r,s] = log p(Z_t=s | Z_{t-1}=r) = log of transition probability r->s.
#   phi = length m vector with phi[s] = parameters for the emission distribution p(x_t | z_t=s).

using Distributions

# Generate a sequence of n hidden states z and observations x.
# OUTPUT:
#    x,z = vectors x_{1:n} and z_{1:n} sampled from p(x_{1:n},z_{1:n}).
function generate(n,log_pi,log_T,phi)
    m = length(log_pi)
    z = zeros(Int,n)
    D = [Categorical(exp(vec(log_T[r,:]))) for r=1:m]
    z[1] = rand(Categorical(exp(log_pi)))
    for t = 2:n
        z[t] = rand(D[z[t-1]])
    end
    x = [qrnd(phi[z[t]]) for t=1:n]
    return x,z
end

# Find most probable sequence of hidden states z given observations x.
# OUTPUT: z = a vector z_{1:n} maximizing p(z_{1:n} | x_{1:n}).
function viterbi(x,log_pi,log_T,phi)
    m = length(log_pi)  # number of hidden states
    n = length(x)  # length of observed sequence
    F = zeros(n,m)  # maxima
    A = zeros(n,m)  # maximizers

    # Compute F and A
    for s = 1:m
        F[1,s] = log_pi[s] + log_q(x[1],phi[s])
    end
    for j = 2:n
        for s = 1:m
            maxval = -Inf
            maxind = 0
            for r = 1:m
                newval = F[j-1,r] + log_T[r,s]
                if newval > maxval
                    maxval = newval
                    maxind = r
                end
            end
            F[j,s] = maxval +  log_q(x[j],phi[s])
            A[j,s] = maxind
        end
    end

    # Recover optimal z
    z = zeros(Int,n)
    z[n] = indmax(F[n,:])
    for t = n-1:-1:1
        z[t] = A[t+1,z[t+1]]
    end
    return z
end

# Compute log(sum(exp(x))) in a way that avoids numerical underflow/overflow.
function logsumexp(x)
    mx = maximum(x)
    if mx==-Inf
        return -Inf
    elseif mx==Inf
        return Inf
    else
        s = 0.0
        for xi in x
            s += exp(xi-mx)
        end
        return log(s)+mx
    end
end

# Run the forward algorithm to compute log(p(x_{1:j},z_j) for each j, z_j.
# OUTPUT: G = n-by-m matrix with G[j,s] = log(p(x_{1:j},z_j=s)).
function forward(x,log_pi,log_T,phi)
    m = length(log_pi)  # number of hidden states
    n = length(x)  # length of observed sequence
    G = zeros(n,m)  # G[j,s] = log(p(x_{1:j},z_j=s))
    l = zeros(m)  # temporary vector

    for s = 1:m
        G[1,s] = log_pi[s] + log_q(x[1],phi[s])
    end
    for j = 2:n
        for s = 1:m
            for r = 1:m
                l[r] = G[j-1,r] + log_T[r,s] + log_q(x[j],phi[s])
            end
            G[j,s] = logsumexp(l)
        end
    end
    log_m = logsumexp(G[n,:])  # log(p(x_{1:n}))
    return G,log_m
end

# Run the backward algorithm to compute log(p(x_{j+1:n}|z_j)) for each j, z_j.
# OUTPUT: H = n-by-m matrix with H[j,s] = log(p(x_{j+1:n}|z_j=s)).
function backward(x,log_pi,log_T,phi)
    m = length(log_pi)  # number of hidden states
    n = length(x)  # length of observed sequence
    H = zeros(n,m)  # H[j,r] = log(p(x_{j+1:n}|z_j=r))
    l = zeros(m)  # temporary vector

    for r = 1:m
        H[n,r] = 0.0
    end
    for j = n-1:-1:1
        for r = 1:m
            for s = 1:m
                l[s] = H[j+1,s] + log_T[r,s] + log_q(x[j+1],phi[s])
            end
            H[j,r] = logsumexp(l)
        end
    end
    return H
end

# Estimate the HMM parameters using the Baum-Welch algorithm.
function estimate(x,m,tolerance)
    n = length(x)  # length of observed sequence

    # randomly initialize parameters
    log_pi = log(rand(Dirichlet(m,1)))
    log_T = zeros(m,m)
    for r = 1:m; log_T[r,:] = log(rand(Dirichlet(m,1))); end
    phi = phi_init(m,x)

    # initialize EM variables
    gamma = zeros(n,m)  # gamma[j,r] = p(z_j=r | x)
    delta = zeros(n-1,m,m)  # delta[j,r,s] = p(z_j=r, z_{j+1}=s | x)
    log_m = -Inf
    log_m_old = 0.0
    n_iterations = 0

    println("\nEstimating parameters using Baum-Welch...")
    while abs(log_m - log_m_old) > tolerance
        log_m_old = log_m

        # E-step: compute gamma and delta
        G,log_m = forward(x,log_pi,log_T,phi)
        H = backward(x,log_pi,log_T,phi)
        for j = 1:n
            for r = 1:m
                gamma[j,r] = exp(G[j,r] + H[j,r] - log_m)
            end
        end
        for j = 1:n-1
            for r = 1:m
                for s = 1:m
                    delta[j,r,s] = exp(G[j,r] + log_T[r,s] +
                        log_q(x[j+1],phi[s]) + H[j+1,s] - log_m)
                end
            end
        end

        # M-step: update log_pi, log_T, and phi
        log_pi = vec(log(gamma[1,:] ./ sum(gamma[1,:])))
        C = squeeze(sum(delta,1),1)
        log_T = log(C./sum(C,2))
        phi_max!(phi,x,gamma)

        # check change in log likelihood
        @assert(log_m > log_m_old)

        n_iterations += 1
        println(n_iterations, " ", log_m)
    end
    return log_pi,log_T,phi,log_m
end


export conv4, deconv4, mat, pool, unpool
using NNlib: conv, DenseConvDims, maxpool, meanpool, PoolDims, ∇conv_data, ∇conv_filter, ∇maxpool, ∇meanpool
using LinearAlgebra: lmul!
using AutoGrad: AutoGrad, @primitive1

"""
    conv4(w, x; kwargs...)

Execute convolutions or cross-correlations using filters specified with `w` over tensor `x`.

If `w` has dimensions `(W1,W2,...,Cx,Cy)` and `x` has dimensions `(X1,X2,...,Cx,N)`, the
result `y` will have dimensions `(Y1,Y2,...,Cy,N)` where `Cx` is the number of input channels,
`Cy` is the number of output channels, `N` is the number of instances, and `Wi,Xi,Yi` are
spatial dimensions with `Yi` determined by:

    Yi = 1 + floor((Xi + 2*padding[i] - ((Wi-1)*dilation[i] + 1)) / stride[i])

`padding`, `stride` and `dilation` are keyword arguments that can be specified as a single
number (in which case they apply to all dimensions), or an array/tuple with entries for each
spatial dimension.

# Keywords

* `padding=0`: the number of extra zeros implicitly concatenated at the start and end of each dimension.
* `stride=1`: the number of elements to slide to reach the next filtering window.
* `dilation=1`: dilation factor for each dimension.
* `mode=0`: 0 for convolution and 1 for cross-correlation (which flips the filter).
* `alpha=1`: can be used to scale the result.
* `group=1`: can be used to perform grouped convolutions.

"""
function conv4(w, x; padding=0, stride=1, dilation=1, mode=0, alpha=1, group=1)
    @assert group == 1 "Grouped convolutions not yet implemented in NNlib, see https://github.com/JuliaGPU/CuArrays.jl/pull/523"
    N = ndims(x)
    stride = expand(Val(N-2), stride)
    padding = expand(Val(N-2), padding)
    dilation = expand(Val(N-2), dilation)
    cdims = DenseConvDims(x, w; stride = stride, padding = padding, dilation = dilation, flipkernel = (mode!=0))
    y = conv(x, w, cdims)
    alpha == 1 ? y : lmul!(alpha, y)
end

function conv4w(w,x,dy; padding=0, stride=1, dilation=1, mode=0, alpha=1, group=1)
    @assert group == 1 "Grouped convolutions not yet implemented in NNlib, see https://github.com/JuliaGPU/CuArrays.jl/pull/523"
    N = ndims(x)
    stride = expand(Val(N-2), stride)
    padding = expand(Val(N-2), padding)
    dilation = expand(Val(N-2), dilation)
    cdims = DenseConvDims(x, w; stride = stride, padding = padding, dilation = dilation, flipkernel = (mode!=0))
    dw = ∇conv_filter(x, dy, cdims)
    alpha == 1 ? dw : lmul!(alpha, dw)
end

function conv4x(w,x,dy; padding=0, stride=1, dilation=1, mode=0, alpha=1, group=1)
    @assert group == 1 "Grouped convolutions not yet implemented in NNlib, see https://github.com/JuliaGPU/CuArrays.jl/pull/523"
    N = ndims(x)
    stride = expand(Val(N-2), stride)
    padding = expand(Val(N-2), padding)
    dilation = expand(Val(N-2), dilation)
    cdims = DenseConvDims(x, w; stride = stride, padding = padding, dilation = dilation, flipkernel = (mode!=0))
    dx = ∇conv_data(dy, w, cdims)
    alpha == 1 ? dx : lmul!(alpha, dx)
end

@primitive1 conv4(w,x; o...),dy,y       conv4w(w,x,dy;o...)   conv4x(w,x,dy;o...)
@primitive1 conv4w(w,x,dy;o...),ddw,dw  nothing               conv4x(ddw,x,dy;o...)  conv4(ddw,x;o...)
@primitive1 conv4x(w,x,dy;o...),ddx,dx  conv4w(w,ddx,dy;o...) nothing                conv4(w,ddx;o...)

"""
    deconv4(w, x; kwargs...)

Simulate 4-D deconvolution by using _transposed convolution_ operation. Its forward pass is
equivalent to backward pass of a convolution (gradients with respect to input
tensor). Likewise, its backward pass (gradients with respect to input tensor) is equivalent to
forward pass of a convolution. Since it swaps forward and backward passes of convolution
operation, padding and stride options belong to output tensor. See [this
report](https://arxiv.org/abs/1603.07285) for further explanation.

If `w` has dimensions `(W1,W2,...,Cy,Cx)` and `x` has dimensions `(X1,X2,...,Cx,N)`, the
result `y=deconv4(w,x)` will have dimensions `(Y1,Y2,...,Cy,N)` where

    Yi = (Xi - 1)*stride[i] + ((Wi-1)*dilation[i] + 1) - 2*padding[i]

Here Cx is the number of x channels, Cy is the number of y channels, N is the number of
instances, and Wi,Xi,Yi are spatial dimensions. Padding and stride are keyword arguments that
can be specified as a single number (in which case they apply to all dimensions), or an
array/tuple with entries for each spatial dimension.

# Keywords

* `padding=0`: the number of extra zeros implicitly concatenated at the start and at the end of each dimension.
* `stride=1`: the number of elements to slide to reach the next filtering window.
* `mode=0`: 0 for convolution and 1 for cross-correlation.
* `alpha=1`: can be used to scale the result.
* `handle`: handle to a previously created cuDNN context. Defaults to a Knet allocated handle.
* `group=1`: can be used to perform grouped convolutions.

"""
function deconv4(w,y; o...)
    x = similar(y,dcdims(w,y;o...))
    return conv4x(w,x,y;o...)
end

@primitive1 deconv4(w,x;o...),dy  conv4w(w,dy,x;o...)  conv4(w,dy;o...)

"""
    pool(x; kwargs...)

Compute pooling of input values (i.e., the maximum or average of several adjacent values) to
produce an output with smaller height and/or width.

If `x` has dimensions `(X1,X2,...,Cx,N)`, the result `y` will have dimensions
`(Y1,Y2,...,Cx,N)` where

    Yi=1+floor((Xi+2*padding[i]-window[i])/stride[i])

Here `Cx` is the number of input channels, `N` is the number of instances, and `Xi,Yi` are
spatial dimensions.  `window`, `padding` and `stride` are keyword arguments that can be
specified as a single number (in which case they apply to all dimensions), or an array/tuple
with entries for each spatial dimension.

# Keywords:

* `window=2`: the pooling window size for each dimension.
* `padding=0`: the number of extra zeros implicitly concatenated at the start and at the end of each dimension.
* `stride=window`: the number of elements to slide to reach the next pooling window.
* `mode=0`: 0 for max, 1 for average including padded values, 2 for average excluding padded values, 3 for deterministic max.
* `maxpoolingNanOpt=1`: Nan numbers are not propagated if 0, they are propagated if 1.
* `alpha=1`: can be used to scale the result.

"""
function pool(x; window=2, padding=0, stride=window, mode=0, maxpoolingNanOpt=1, alpha=1)
    mode, maxpoolingNanOpt = checkpoolopts(x, window, padding, stride, mode, maxpoolingNanOpt, alpha)
    N = ndims(x)
    window = expand(Val(N-2), window)
    stride = expand(Val(N-2), stride)
    padding = expand(Val(N-2), padding)
    pdims = PoolDims(x, window; padding = padding, stride = stride)
    y = (mode == 0 ? maxpool(x, pdims) :
         mode == 1 ? meanpool(x, pdims) :
         mode == 2 ? error("Pool mode=2 not yet implemented in NNlib. See https://github.com/FluxML/NNlib.jl/issues/218") :
         mode == 3 ? maxpool(x, pdims) :
         error("mode=$mode is not supported for CPU pool."))
    alpha == 1 ? y : lmul!(alpha, y)
end

function poolx(x,y,dy; window=2, padding=0, stride=window, mode=0, maxpoolingNanOpt=1, alpha=1)
    mode, maxpoolingNanOpt = checkpoolopts(x, window, padding, stride, mode, maxpoolingNanOpt, alpha)
    if alpha != 1
        y = y ./ eltype(y)(alpha)
    end
    N = ndims(x)
    window = expand(Val(N-2), window)
    stride = expand(Val(N-2), stride)
    padding = expand(Val(N-2), padding)
    pdims = PoolDims(x, window; padding = padding, stride = stride)
    dx = (mode == 0 ? ∇maxpool(dy, y, x, pdims) :
          mode == 1 ? ∇meanpool(dy, y, x, pdims) :
          mode == 2 ? error("Pool mode=2 not yet implemented in NNlib. See https://github.com/FluxML/NNlib.jl/issues/218") :
          mode == 3 ? ∇maxpool(dy, y, x, pdims) :
          error("mode=$mode is not supported for CPU pool."))
    alpha == 1 ? dx : lmul!(alpha, dx)
end

@primitive1 pool(x;o...),dy,y  poolx(x,y,dy;o...)
@primitive1 poolx(x,y,dy;o...),ddx,dx  nothing  nothing  pool(ddx;o...)

function checkpoolopts(x, window, padding, stride, mode, maxpoolingNanOpt, alpha)
    @assert mode ∈ 0:3 "Bad pooling mode=$mode"
    if mode == 2
        @warn "Pool mode=2 not yet implemented in NNlib, using 1 instead. See https://github.com/FluxML/NNlib.jl/issues/218" maxlog=1
        mode = 1
    end
    @assert maxpoolingNanOpt ∈ (0,1) "Bad pooling maxpoolingNanOpt=$maxpoolingNanOpt"
    if maxpoolingNanOpt == 0
        @warn "Pool maxpoolingNanOpt=0 not yet implemented in NNlib, using 1 instead. See https://github.com/FluxML/NNlib.jl/issues/218" maxlog=1
        maxpoolingNanOpt = 1
    end
    if padding != 0 && x isa Array
        @warn "Pool padding is buggy in NNlib, use with caution. See https://github.com/FluxML/NNlib.jl/issues/229" maxlog=1
    end
    return (mode, maxpoolingNanOpt)
end

"""
    unpool(x; o...)

Perform the reverse of pooling: `x == pool(unpool(x;o...); o...)`
"""
function unpool(x; window=2, alpha=1, o...) # padding=0, stride=window, mode=0, maxpoolingNanOpt=0
    w = prod(psize(window,x))
    y = similar(x,updims(x; window=window, o...))
    # Leave unpool as a non-primitive, it is just a poolx call
    poolx(y,x,x.*w; o..., window=window, mode=1, alpha=1/alpha)
end

"""
    mat(x; dims = ndims(x) - 1)

Reshape `x` into a two-dimensional matrix by joining the first dims dimensions, i.e.
`reshape(x, prod(size(x,i) for i in 1:dims), :)`

`dims=ndims(x)-1` (default) is typically used when turning the output of a 4-D convolution
result into a 2-D input for a fully connected layer.

`dims=1` is typically used when turning the 3-D output of an RNN layer into a 2-D input for
a fully connected layer.

`dims=0` will turn the input into a row vector, `dims=ndims(x)` will turn it into a column
vector.

"""
mat(x; dims::Int=ndims(x)-1)=reshape(x, (dims > 0 ? prod(size(x,i) for i in 1:dims) : 1), :)

## Dimension helpers:

# outputDim = 1 + ( inputDim + 2*pad - (((filterDim-1)*dilation)+1) )/convolutionStride;
# inputDim = (outputDim - 1) * convolutionStride + (((filterDim-1)*dilation)+1) - 2*pad
function dcdims(w,y; padding=0, stride=1, dilation=1, group=1, o...)
    N = ndims(y)
    @assert size(y,N-1) == size(w,N)
    ntuple(N) do i
        if i < N-1
            pi = (if isa(padding,Number); padding; else padding[i]; end)
            si = (if isa(stride,Number); stride; else stride[i]; end)
            di = (if isa(dilation,Number); dilation; else dilation[i]; end)
            si*(size(y,i)-1) + (((size(w,i)-1)*di)+1) - 2*pi
        elseif i == N-1
            size(w,N-1) * group
        else
            size(y,N)
        end
    end
end

# convert padding etc. size to an Int array of the right dimension
function psize(p, x)
    nd = ndims(x)-2
    if isa(p,Number)
        fill(Int(p),nd)
    elseif length(p)==nd
        collect(Int,p)
    else
        throw(DimensionMismatch("psize: $p $nd"))
    end
end

function updims(x; window=2, padding=0, stride=window, o...)
    window = psize(window,x)
    stride = psize(stride,x)
    padding = psize(padding,x)
    N = ndims(x)
    ntuple(N) do i
        if i < N-1
            (size(x,i)-1)*stride[i]+window[i]-2*padding[i]
        else
            size(x,i)
        end
    end
end

# convolution padding size that preserves the input size when filter size is odd and stride=1
padsize(w)=ntuple(i->div(size(w,i)-1,2), ndims(w)-2)

expand(N, i::Tuple) = i
expand(N, i::Integer) = ntuple(_ -> i, N)

# TODO: Grouped convolutions not yet implemented in NNlib, see https://github.com/JuliaGPU/CuArrays.jl/pull/523
# TODO: maxpoolingNanOpt not yet implemented in NNlib, see https://github.com/FluxML/NNlib.jl/issues/218
# TODO: Pool mode=2 not yet implemented in NNlib, see https://github.com/FluxML/NNlib.jl/issues/218
# TODO: unpool Does not work correctly for every window, padding, mode combination?


using Makie
using AbstractPlotting: textslider

function main()
    sa, _a = textslider(range(-1.,stop=1.,length=100),"a", start=1.)
    sb, _b = textslider(range(-1.,stop=1.,length=100),"b", start=0.)
    xs = -π:0.01:π
    N = length(xs)
    ab = lift(tuple,_a,_b)
    pos = lift(ab) do (a,b)
        map(LinRange(-2π, 2π, 100)) do x
            Point2f0(x,sin(a*x+b))
        end
    end

    p = lines(pos)

    RecordEvents(
        hbox(p, vbox(sa,sb), parent=Scene()),
        "output"
    )
end

main()


# output = wx + b
immutable Affine{T,P<:Params} <: Learnable
    nin::Int
    nout::Int
    input::SumNode{T,1}
    output::OutputNode{T,1}
    params::P
end

function Affine{T}(::Type{T}, nin::Int, nout::Int,
                    θ::AbstractVector = zeros(T, nout*(nin+1)),
                    ∇::AbstractVector = zeros(T, nout*(nin+1)))
    input = InputNode(T, nin)
    output = OutputNode(T, nout)
    params = Params(θ, ∇, ((nout,nin), (nout,)))
    w, b = params.views
    initialize_weights!(w)
    initialize_bias!(b)
    Affine(nin, nout, input, output, params)
end
Affine(nin::Int, nout::Int, args...) = Affine(Float64, nin, nout, args...)

function Base.show(io::IO, t::Affine)
    print(io, "Affine{$(t.nin)-->$(t.nout)}")
end

# compute output = wx + b
function transform!(t::Affine)
    w, b = t.params.views
    x = t.input.val
    y = t.output.val
    copy!(y, b)
    for o=1:t.nout
        y[o] += sum(w[o,i] * x[i] for i=1:t.nin)
    end
    y
end

# update the partial derivatives:
#   ∇x = ∂L/∂x
#   ∇w = ∂L/∂w
#   ∇b = ∂L/∂b
# use the chain rule, assuming that we've already updated ∇out = ∂L/∂y
function grad!(t::Affine)
    w, b = t.params.views
    ∇w, ∇b = t.params.∇_views
    x = t.input.val
    ∇x = t.input.∇
    ∇y = t.output.∇

    # ∇x, ∇w
    for i=1:t.nin
        ∇xᵢ = zero(eltype(∇x))
        for o=1:t.nout
            ∇xᵢ += w[o,i] * ∇y[o]
            ∇w[o,i] = x[i] * ∇y[o]
        end
        ∇x[i] = ∇xᵢ
    end

    # ∇b
    copy!(∇b, ∇y)
    return
end