core/utils/globalUtils.js

/**
 * @namespace globalUtils
 */

/**
 * @method DAG
 * @memberof globalUtils
 * @description Directed Acyclic Graph implementation for solving promised-based functions
 * adopted from https://github.com/daanmichiels/promiseDAG
 * @param {Array} functions - functions required to run during a simulation as promised using the order [func1, func2...]
 * @param {Array} dag - dependency array listing the sequential executions for each funciton as [[0], [1], [0,1]...]
 * @param {Object} args - argument list used to run a specific function. This will change already on the engine
 * @param {String} type - running either a function DAG or a step DAG
 * @returns {Promise} fulfills the resolution for the DAG or returns error with the execution context.
 */

export const DAG = ({ functions, dag, args, type } = {}) => {
  dag = dag || [];
  return new Promise((resolve, reject) => {
    const N = functions.length;
    //A DAG can be both stepwise or functionwise. The next definition
    //asserts that there is a dag ready for each step linearly
    //e.g. step0->step1->step2...
    dag =
      type === "steps"
        ? (() => {
            let x = Array.from({ length: N }, (_, i) => [i - 1]);
            x[0] = [];
            return x;
          })()
        : dag;
    const counts = dag.map((x) => x.length);
    let stopped = false,
    remaining = N,
    values = [];

    const handleResolution = (promise, i, value) => {
      //Assumming that the worker is giving back a Float32Array. This slicing might be done some other way. Keep on mind!
      values[i] = value;
      if (stopped) {
        return;
      }
      remaining -= 1;
      if (remaining == 0) {
        resolve(values);
      }
      for (let j = 0; j < N; ++j) {
        if (counts[j] < 1) {
          continue;
        }
        if (dag[j].indexOf(i) >= 0) {
          counts[j] -= 1;
          if (counts[j] == 0) {
            let _args = [];
            for (let k = 0; k < dag[j].length; k++) {
              //Goes on a stepwise execution manner.
              _args =
                type === "steps"
                  ? new Float32Array(values[dag[j][k]][values[dag[j][k]].length - 1].slice())
                  //This needs correction
                  : values[dag[j][k]];
            }
            args[j].data = _args;
            var promise = null
            if (type === "steps"){
              promise = functions[j](j, _args)
            }
            else {
              promise = functions[j](args[j]);
            }
            promise.then(
              (value) => {
                handleResolution(promise, j, value);
              },
              (error) => {
                handleRejection(promise, j, error);
              }
            );
          }
        }
      }
    };

    const handleRejection = (promise, i, error) => {
      stopped = true;
      console.error('There was an error executing a function. More details: ', error)
      reject(error);
      return;
    };

    for (let i = 0; i < N; ++i) {
      if (counts[i] > 0) {
        continue;
      }

      var promise = null;
      
      if (type === "steps") {
      promise = functions[i](i);
    } else {
      promise = functions[i](args[i]);
    }
      promise.then(
        (value) => {
          handleResolution(promise, i, value);
        },
        (error) => {
          handleRejection(promise, i, error);
        }
      );
    }
  });
};

/**
 * @method dataCloner
 * @memberof globalUtils
 * @description Deep clones either objects or arrays
 * @param {Object} data - array or object
 * @returns {Object} deep cloned object or array to be used
 * @example
 * const arr = [1, 2, 3, [4, 5]];
 * const clonedArr = dataCloner(arr);
 */

export const dataCloner = (data) => {
  //Deep copy of array data using recursion
  const arrayCloner = (arr) => {
    if (arr instanceof Float32Array) {
      return arr;
    }
    if (Array.isArray(arr[0])) {
      const clonedArr = new Array(arr.length);
      for (let i = 0; i < arr.length; i++) {
        clonedArr[i] = new Float32Array(arrayCloner(arr[i]));
      }
      return clonedArr;
    } else {
      return new Float32Array(arr);
    }
  };

  const objectCloner = (inObject) => {
    let tempOb = {};

    for (let [key, value] of Object.entries(inObject)) {
      if (Array.isArray(value)) {
        tempOb[key] = arrayCloner(value);
      } else {
        if (typeof value === "object") {
          tempOb[key] = objectCloner(value);
        } else {
          tempOb[key] = value;
        }
      }
    }
    return tempOb;
  };

  try {
    if (Array.isArray(data)) {
      let stgD = arrayCloner(data);
      return flattenFloat32Array(stgD);
    } else {
      return objectCloner(data);
    }
  } catch (error) {
    return console.error(
      `There was an error cloning the data. More info: `,
      error
    );
  }
};

/**
 * @method arrayChanger
 * @memberof globalUtils
 * @description switches the rows/columns of an array
 *
 */

export const arrayChanger = (arr, width) =>
  arr.reduce(
    (rows, key, index) =>
      (index % width == 0
        ? rows.push([key])
        : rows[rows.length - 1].push(key)) && rows,
    []
  );

/**
 * Helper function for concatenating arrays.
 * @method concatArrays
 * @memberof globalUtils
 * @param {Array} arrays - collection of arrays to concatenate
 * @returns {Array} array - concatenated array
 */

export const concatArrays = (arrays) => {
  if (arrays.length === 1) {
    return arrays[0];
  }

  const totalLength = arrays.reduce((sum, array) => sum + array.length, 0);
  const finalArray = new Float32Array(totalLength);

  let offset = 0;
  for (let i = 0; i < arrays.length; i++) {
    finalArray.set(arrays[i], offset);
    offset += arrays[i].length;
  }

  return finalArray;
};

/**
 * Helper function for flatennizing a 2D array
 * @method flattenFloat32Array
 * @memberof globalUtils
 * @param {Array} arr  - N-D array to be flattened
 * @returns {Array} flattenized ND array
 */
export const flattenFloat32Array = (arr) => {
  if (typeof arr[0] !== "object") return arr;
  let flatArray = [];
  arr.forEach(function (subArray) {
    flatArray = flatArray.concat(Array.from(subArray));
  });
  return new Float32Array(flatArray);
};

/**
 * Retrieves the performance measures for function execution and worker execution.
 * @method getPerformanceMeasures
 * @memberof globalUtils
 * @returns {object} - The performance measures.
 * @property {number} funcExec - The duration of function execution.
 * @property {number} workerExec - The duration of worker execution.
 * @returns {Object} object containing execution times for both script and function exeuctions.
 */
export const getPerformanceMeasures = () => {
  return {
    funcExec: performance.measure(
      "measure-execution",
      "start-function",
      "end-function"
    ).duration,
    workerExec: performance.measure(
      "measure-execution",
      "start-script",
      "end-script"
    ).duration,
  };
};

/**
 * Imports JSON data from a file.
 * @method importJSONdata
 * @memberof globalUtils
 * @param {string} jsonFile - The path or URL to the JSON file.
 * @param {string} [dataFieldName="data"] - The name of the field containing the data in the JSON file.
 * @returns {Promise<*>} - A promise that resolves to the imported data.
 */
export const importJSONdata = async (jsonFile, dataFieldName = "data") => {
  const response = await fetch(`${jsonFile}.json`);
  const json = await response.json();
  const data = json[dataFieldName];
  return data;
};