FunctionNNLayer
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © RicardoSantos

//@version=5

// @description Generalized Neural Network Layer method.
library(title='FunctionNNLayer')

import RicardoSantos/MLActivationFunctions/1 as activation
import RicardoSantos/FunctionNNPerceptron/1 as perceptron

// reference: 
//      https://blog.primen.dk/basic-neural-network-code-example-java/

// @function helper method to generate random weights of (A * B) size.
// @param previous_size int, number of nodes on the left.
// @param next_size int, number of nodes on the right.
// @returns float array.
generate_random_weights (int previous_size, int next_size) => //{
    _weights = matrix.new<float>(next_size, previous_size)
    for _i = 0 to next_size-1
        for _j = 0 to previous_size-1
            matrix.set(_weights, _i, _j, math.random() * 2.0 - 1.0)
    _weights
//}

mprint (M, format='0.000') => //{
    _rows = matrix.rows(M)
    _cols = matrix.columns(M)
    _s = ''
    for _i = 0 to _rows-1
        _s += '[ '
        for _j = 0 to _cols-1
            _s += ((_j == _rows-1) ? '-> ': '') + str.tostring(matrix.get(M, _i, _j), format) + ' '
        _s += ']\n'
    _s
//}

to_column_matrix (float[] a) => //{
    _m = matrix.new<float>()
    matrix.add_col(_m, 0, a)
    _m
//}

// @function Generalized Layer.
// @param inputs float array, input values.
// @param weights float matrix, weight values.
// @param n_nodes int, number of nodes in layer.
// @param activation_function string, default='sigmoid', name of the activation function used.
// @param bias float, default=1.0, bias to pass into activation function.
// @param alpha float, default=na, if required to pass into activation function.
// @param scale float, default=na, if required to pass into activation function.
// @returns float
export function (float[] inputs, matrix<float> weights, string activation_function='sigmoid', float bias=1.0, float alpha=na, float scale=na) => //{
    int _size_i = array.size(inputs)
    int _Wrows = matrix.rows(weights)
    int _Wcols = matrix.columns(weights)
    //
    switch
        _Wrows < 1 => runtime.error('FunctionNNLayer -> layer(): "weights" must have atleast one row of values.')
        _size_i < 1 => runtime.error('FunctionNNLayer -> layer(): "inputs" array is empty.')
        _Wcols != _size_i => runtime.error(str.format('FunctionNNLayer -> layer(): number of elements in weights matrix do not match number of inputs/nodes, expecting {0}, found {1} .', _size_i, _Wcols))
    // run the perceptron over the inputs and weights:
    float[] _outputs = array.new_float(_Wrows)
    for _node_i = 0 to _Wrows-1
        float[] _weights = matrix.row(weights, _node_i)
        array.set(_outputs, _node_i, perceptron.function(inputs, _weights, activation_function, bias, alpha, scale))
    _outputs
//{ usage:
i = array.from(0.0, 1, 2)
var table T = table.new(position.bottom_right, 7, 3, bgcolor=#000000, frame_color=color.black, frame_width=2, border_width=2, border_color=color.gray)
if barstate.islastconfirmedhistory
    // run a simple NN forward cycle once:
    _wh1 = generate_random_weights(3, 4)
    _wh2 = generate_random_weights(4, 5)
    _wo = generate_random_weights(5, 3)
    h1 = function(i, _wh1) // 1st hidden layer with 4 nodes, from inputs
    h2 = function(h1, _wh2) // 2nd level hidden layer with 5 nodes , from first hidden
    o = function(h2, _wo) // output layer with 3 nodes 
    // label.new(bar_index, 0.0, str.tostring(matrix.mult(matrix.transpose(_wh1), i)))
    //
    table.cell(T, 0, 0, 'Input:', text_color=color.silver, text_halign=text.align_right)
    table.cell(T, 1, 0, 'Hidden 1:', text_color=color.silver, text_halign=text.align_right), 
    table.cell(T, 3, 0, 'Hidden 2:', text_color=color.silver, text_halign=text.align_right)
    table.cell(T, 5, 0, 'output:', text_color=color.silver, text_halign=text.align_right)
    table.cell(T, 0, 1, '', text_color=color.silver, text_halign=text.align_right)
    table.cell(T, 1, 1, 'weights:', text_color=color.silver, text_halign=text.align_right), 
    table.cell(T, 2, 1, 'node:', text_color=color.silver, text_halign=text.align_right), 
    table.cell(T, 3, 1, 'weights:', text_color=color.silver, text_halign=text.align_right)
    table.cell(T, 4, 1, 'node:', text_color=color.silver, text_halign=text.align_right), 
    table.cell(T, 5, 1, 'weights:', text_color=color.silver, text_halign=text.align_right)
    table.cell(T, 6, 1, 'node:', text_color=color.silver, text_halign=text.align_right), 
    table.merge_cells(T, 1, 0, 2, 0)
    table.merge_cells(T, 3, 0, 4, 0)
    table.merge_cells(T, 5, 0, 6, 0)
    table.cell(T, 0, 2, str.tostring(to_column_matrix(i), '0.000'), text_color=color.silver, text_halign=text.align_left)
    table.cell(T, 1, 2, str.tostring(_wh1, '0.000'), text_color=color.silver, text_halign=text.align_left)
    table.cell(T, 2, 2, str.tostring(to_column_matrix(h1), '0.000'), text_color=color.silver, text_halign=text.align_left)
    table.cell(T, 3, 2, str.tostring(_wh2, '0.000'), text_color=color.silver, text_halign=text.align_left)
    table.cell(T, 4, 2, str.tostring(to_column_matrix(h2), '0.000'), text_color=color.silver, text_halign=text.align_left)
    table.cell(T, 5, 2, str.tostring(_wo, '0.000'), text_color=color.silver, text_halign=text.align_left)
    table.cell(T, 6, 2, str.tostring(to_column_matrix(o), '0.000'), text_color=color.silver, text_halign=text.align_left)
//{ remarks:
//}}}
