Source: utils.js

const tf = require('@tensorflow/tfjs');

/**
 * Converts some TypedArray into 1d tfjs array.
 * @param {Array} data data to be converted to 1d tfjs array
 */
function t1d(data){
    if(data instanceof tf.Tensor){
        if(data.shape.length == 1){
            return data
        }
        if(data.shape.length == 2){
            return tf.reshape(data, [data.shape[0]])
        }
    }
    return tf.tensor1d(data)
}

module.exports.t1d = t1d

/**
 * Converts some TypedArray into 2d tfjs array.
 * @param {Array} data data to be converted to 2d tfjs array
 */
function t2d(data){
    if(data instanceof tf.Tensor){
        if(data.shape.length == 2){
            return data
        }
    }
    return tf.tensor2d(data)
}

module.exports.t2d = t2d

/**
 * Checks if the estimator is fitted by verifying the presence of 
 * “all_or_any” of the passed attributes and raises an Error
 *  with the given message.
 * @param {Object} estimator estimator to check to be fitted.
 * @param {Array} attributes list of attributes to check presence of.
 * @param {String} all_or_any Specify whether all or any of the 
 * given attributes must exist.
 */
function check_is_fitted(estimator, attributes, all_or_any='all'){
    var check = []
    for(var k of attributes){
        check.push(k in estimator.state)
    }
    var is_fitted = all_or_any == 'all'
    for(var c of check){
        if(all_or_any == 'all'){
            is_fitted = is_fitted && c
        }else{
            is_fitted = is_fitted || c
        }
    }

    if(!is_fitted){
        throw Error(
            "Estimator " + estimator.constructor.name + " is not fitted. Please " +
            "call .fit(X, y) method, with proper inputs X and outputs y."
        )
    }

    return true
}

module.exports.check_is_fitted = check_is_fitted

/**
 * Sets default parameter values for the model classes.
 * @param {Dictionary} params Parameters supplied by user
 * @param {Dictionary} defaults Default values of parameters
 */
function set_defaults(params=null, defaults=null){
    var result = {}
    defaults = defaults || {}
    for(var key in defaults){
        result[key] = defaults[key]
    }
    params = params || {}
    for(var key in params){
        result[key] = params[key]
    }
    return result
}

module.exports.set_defaults = set_defaults

/**
 * Check dimensions of the array.
 * @param {Array} array Array to check, and optionally convert to tf tensor.
 * @param {Number} min_nd minimum number of the dimensions in the array.
 * @param {Number} max_nd maximum number of dimensions.
 * @param {Array} min_shape array of maximal sizes for dimensions of the tensor.
 * @param {Array} max_shape maximal sizes of dimensions in array.
 * @param {Boolean} make_tf whether to convert the input array to tf tensor.
 */
function check_array(array, min_nd=2, max_nd=2, min_shape=[1, 1], max_shape=null, make_tf=true){
    var shape = null
    if(array.constructor.name === "Tensor"){
        shape = array.shape
    }else{
        // recursively get the size of array
        // ToDo: check the consistent size of all feature vectors
        var ap = array
        var shape = []
        while(ap instanceof Array){
            shape.push(ap.length)
            ap = ap[0]
        }
    }

    var n_dim = shape.length

    if(max_nd !== null && n_dim > max_nd){
        throw Error(n_dim + ' is too many dimensions in the input data: ' + array)
    }

    if(min_nd !== null && n_dim < min_nd){
        throw Error(n_dim + ' is too little dimensions in the input data: ' + array)
    }
    
    for(var i=0; i<n_dim; i++){
        if(min_shape !== null && i<min_shape.length && shape[i] < min_shape[i]){
            throw Error(shape[i] + ' is too small size of dimension ' + i + ' of input ' + array)
        }
        if(max_shape !== null && i<max_shape.length && shape[i] > max_shape[i]){
            throw Error(shape[i] + ' is too large size of dimension ' + i + ' of input ' + array)
        }
    }

    if(make_tf){
        return tf.tensor(array)
    }
    return array
}

module.exports.check_array = check_array

/**
 * Check if inputs X are of proper format.
 * @param {Array} X Input samples.
 * @param {Boolean} make_tf Whether to convert input samples to tf.tensor
 */
function check_2dX(X, make_tf=true){
    return check_array(X, 2, 2, [1, 1], null, make_tf)
}

module.exports.check_2dX = check_2dX

/**
 * Check if outputs y are of proper format.
 * @param {Array} y Outputs.
 * @param {Boolean} make_tf Whether to convert y to tf.tensor
 */
function check_1dy(y, make_tf=true){
    return check_array(y, 1, 1, [1], null, make_tf)
}

/**
 * Check whether a dataset is consistent, and whether inputs
 * are a matrix and outputs are a vector.
 * @param {Array} X Array of input observations.
 * @param {Array} y Outputs.
 * @param {Boolean} make_tf Whether to convert y to tf.tensor
 */
function check_2dXy(X, y, make_tf=true){
    var Nx = null
    var Ny = null
    // check consistent size of arrays
    if(X.constructor.name === 'Tensor'){
        Nx = X.shape[0]
    }else{
        Nx = X.length
    }
    if(y.constructor.name === 'Tensor'){
        Ny = y.shape[0]
    }else{
        Ny = y.length
    }

    if(Nx != Ny){
        throw Error('Inconsistent size of samples provided')
    }
    
    X = check_2dX(X, make_tf)
    y = check_1dy(y, make_tf)
    
    return [X, y]
}

module.exports.check_2dXy = check_2dXy

/**
 * Shuffles array in place.
 * @param {Array} a items An array containing the items.
 */
function shuffle(a) {
    var j, x, i;
    for (i = a.length - 1; i > 0; i--) {
        j = Math.floor(Math.random() * (i + 1));
        x = a[i];
        a[i] = a[j];
        a[j] = x;
    }
    return a;
}

/**
 * Generate index set for partitioning of array of size N into
 * a number of partitions. The size of partitions can be specified
 * with `partitions` argument. The function ensures that the minimal
 * size of every partition is at least one. This is important for
 * small sized data partitioning, for example, or in case of leave one
 * out cross - validation.
 * @param {Integer} N Size of array to be partitioned
 * @param {Array} weights Weights that are assigned to partitions. 
 * With higher weight, the partition is more likely to receive elements.
 * @returns {Array} Every element in the output array is a set of indicies
 * of the elements that belong to the i-th partition.
 */
function random_partitioning(N, weights){
    if(N < weights.length){
        throw new Error('Cannot split the array in desired manner. N is smaller than partitions.')
    }

    // make up random indicies
    var I = []
    for(var i=0; i<N; i++){
        I.push(i)
    }
    shuffle(I)

    var ix = 0
    // make up random partitions, necessary non - empty
    var P = []
    for(var w of weights){
        P.push([I[ix++]])
    }

    // divide whats left according to the weights provided
    N_left = N - weights.length

    // ensure that everything sums to 1.0
    var psum = weights.reduce((s, c)=>{return s+c}, 0.0)
    weights = weights.map((v)=>{return v / psum})

    // distribute elements that are left among all of the partitions
    for(var i=0; i<weights.length; i++){
        var p = P[i]
        var N_part = Math.round(weights[i] * N_left)
        
        if(i === weights.length-1){
            N_part = N  // use up all the elements in the last section
        }

        for(var j=0; j<N_part; j++){
            if(ix >= I.length){
                break
            }
            p.push(I[ix++])
        }
    }

    return P
}

module.exports.random_partitioning = random_partitioning