import { kernels } from "./core/kernels.js";
import { splits } from "./core/utils/splits.js";
import { dataCloner, importJSONdata } from "./core/utils/globalUtils.js";
import engine from "./core/mainEngine.js";
import webrtc from "./webrtc/webrtc.js";
/**
* @description Main class for the compute modules. It creates instances of the different engines available to run concurrent or parallel runs.
* @class hydroCompute
* @param {...string} args - Optional argument to set the initial engine.
* @example
* const compute = new hydroCompute() // empty constructor - javascript engine
* const compute = new hydroCompute('wasm') // arguments - engine in arguments set.
*/
class hydroCompute {
constructor(...args) {
this.calledEngines = {};
this.currentEngine;
this.currentEngineName = null;
this.instanceRun = 0;
this.availableData = [];
this.engineResults = {};
/**
* @typedef {object} hydroCompute.utils
* @memberof hydroCompute
*/
this.utils = {
/**
* @description Generates random data.
* @memberof hydroCompute.utils
* @param {number} size - Size of each array element.
* @param {number} maxValue - Maximum value for random number generation.
* @param {number} length - Length of the generated array.
* @param {boolean} [save=false] - Whether to save the data or not.
* @returns {Array|void} - Generated random data array or void if saved.
*
*/
genRandomData: (size, maxValue, length, save = false) => {
let name = `${this.makeId(5)}`;
const data = Array.from({ length: length }, () =>
Array.from({ length: size }, () =>
Math.floor(Math.random() * maxValue)
)
);
if (save) {
this.data({ id: name, data: data });
{
return console.log(`Data has been saved with nametag ${name}`);
}
} else return data;
},
/**
* @description Cleans the array by removing Infinity, null, undefined, and NaN values.
* @memberof hydroCompute.utils
* @param {Array} array - The array to be cleaned.
* @returns {Array} - The cleaned array.
*/
cleanArray: (array) => {
return array.filter((value) => {
// Exclude Infinity, null, undefined, and NaN values
return (
value !== Infinity &&
value !== null &&
value !== undefined &&
!Number.isNaN(value)
);
});
},
};
//Initiate the module with the workers api. If required, the user can change to another backend product
args.length !== 0
? this.setEngine(args[0])
: (() => {
console.log("The javascript engine has been set as default.");
this.setEngine(args.currentEngine || "javascript");
})();
}
/**
* @description Verifies that an engine is set
* @memberof hydroCompute
*/
isEngineSet() {
typeof this.currentEngine === "undefined"
? () => {
console.error(
"Please set the required engine first before initializing!"
);
}
: null;
}
/**
* @description Sets the current engine based on the specified kernel.
* @memberof hydroCompute
* @param {string} kernel - The name of the kernel.
* @returns {Promise<void>} - A Promise that resolves once the engine is set.
*/
async setEngine(kernel) {
this.currentEngineName = kernel;
if (this.currentEngineName === "webgpu") {
try {
const adapter = await navigator.gpu.requestAdapter();
this.currentEngine = new engine(
this.currentEngineName,
kernels[this.currentEngineName]
);
} catch (error) {
console.error(
"WebGPU is not available in your browser. Returning to JavaScript engine."
);
this.currentEngineName = "javascript";
this.currentEngine = new engine(
this.currentEngineName,
kernels[this.currentEngineName]
);
}
} else {
this.currentEngineName === "webrtc"
? (this.currentEngine = new webrtc())
: (this.currentEngine = new engine(
this.currentEngineName,
kernels[this.currentEngineName]
));
}
if (Object.keys(this.calledEngines).includes(kernel)) {
this.calledEngines[kernel] += 1;
} else {
this.calledEngines[kernel] = 1;
}
}
/**
* @description Runs the specified functions with the given arguments using the current engine. The engine must be set previous to the run function to be called.
* @memberof hydroCompute
* @param {Object|string} args - The configuration object or the relative path of the script to run.
* @param {Array} args.dataIds - An array of data IDs.
* @param {Array} args.functions - An array of function names.
* @param {Array} [args.funcArgs=[]] - An array of function arguments.
* @param {Array} [args.dependencies=[]] - An array specifying the dependencies between functions.
* @param {Array} [args.scriptName=[]] - An array of script names.
* @param {Array} [args.dataSplits=[]] - An array specifying if data should be split for each function.
* @returns {Promise<void>} - A Promise that resolves once the functions are executed.
* @example
* //Case 1: Running a script in home folder with 'main' function steering the script and a single data instance saved on 'availableData'
* await compute.run('scriptName');
* //Case 2: Running a function from the ones available on each engine using a multiple data ids
* await compute.run({functions: ['f1', 'f2', 'f3'], dataIds: ['id1', 'id2', 'id3']})
* //Case 3: Linking steps and linking functions within steps
* await compute.run({functions: [['f1', 'f2'], ['f3']],, dependencies:[[[], [0]], []] dataIds: ['id1', 'id2', 'id3']})
*/
async run(
//CASE 1: functions running on "main" or "_mainFunction" saved on local dev and passing a string
args = {
dataIds: [[]],
functions: ["main"],
scriptName: [
this.currentEngineName == "javascript"
? "jsExample.js"
: this.currentEngineName == "webgpu"
? "webgpuExample.js"
: "wasmExample.js",
],
}
) {
//When having to run a script, the user can pass the relative path directly and the compute will do the rest
if (typeof args === "string") {
let stgScript = args.slice();
args = {
dataIds: [[]],
functions: ["main"],
scriptName: [stgScript],
dependencies: [],
};
args.dataSplits = Array.from(
{ length: args.dataIds.length },
(_, i) => false
);
}
//This will run in case there are no arguments or a configuration object has been passed
else {
args = args;
}
let {
//engine = this.currentEngine,
dataIds = [[]],
functions,
funcArgs = [],
dependencies = [],
scriptName = [],
dataSplits = Array.from({ length: dataIds.length }, (_, i) => false),
} = args;
//CHANGE: This just moved the mapping done before here but stil needs update!!
functions = Array.from({ length: dataIds.length }, (_, i) => functions);
if (dependencies === true) {
dependencies = [];
for (let i = 0; i < functions.length; i++) {
const innerLoop = [];
for (let j = 0; j < functions[i].length; j++) {
innerLoop.push(j > 0 ? [j - 1] : []);
}
dependencies.push(innerLoop);
}
} else if (dependencies.length > 0 && dataIds.length === 1) {
dependencies = Array.from({ length: functions[0].length }, () => []);
} else if (dependencies.length > 0 && dataIds.length > 1) {
dependencies = Array.from({ length: dataIds.length }, () => dependencies);
}
scriptName = Array.from({ length: dataIds.length }, (_, i) => scriptName);
for (let i = 0; i < dataIds.length; i++) {
if (typeof dataIds[i] === "number") {
dataIds[i] = JSON.stringify(dataIds[i]);
}
}
//Single data passed into the function.
//It is better if the split function does the legwork of data allocation per function instead.
let data = (() => {
let dataArray = [],
lengthArray = [];
try {
//Case there is only one dataset available within the framework
if (this.availableData.length === 1) {
dataArray.push(this.availableData[0].data.slice());
lengthArray.push(this.availableData[0].length);
} else {
for (let item of this.availableData) {
//if the user has passed multiple data into the framework
for (let id of dataIds) {
if (id === item.id) {
//create a copy that will be cloned down the execution
dataArray.push(item.data.slice());
//keep track of the length of items
lengthArray.push(item.length);
}
}
}
}
return [dataArray, lengthArray];
} catch (error) {
console.error(
`Data with nametag: "${id}" not found in the storage.`,
error
);
return null;
}
})();
if (
(data !== null && functions.length > 0) ||
(data === null && funcArgs.length > 0 && functions.length > 0) ||
(typeof data[0].length !== "undefined" && data[0].length !== 0)
) {
//Data passed in raw without splitting
try {
this.instanceRun += 1;
let flag = await this.currentEngine.run({
isSplit: dataSplits,
scriptName,
data: data !== null ? data[0] : [],
length: data !== null ? data[1] : 0,
functions,
funcArgs,
dependencies,
linked: args.linked || false,
});
//functions = Array.from({length: dataIds.length}, (_, i) => functions)
//Await for results from the engine to finish
if (flag) {
this.setResults(dataIds);
}
} catch (error) {
console.error(
"There was an error with the given run. More info: ",
error
);
return;
}
} else {
return console.error("There was an error pulling the data.", error);
}
}
/**
* Sets the results of the current engine and stores them in the `engineResults` object.
* @memberof hydroCompute
* @param {Array} names - An array of names corresponding to the data IDs.
* @returns {void}
*/
setResults(names) {
const stgOb = Object.fromEntries(
Object.entries({ ...this.currentEngine.results }).map(
([key, value], index) => [names[index], value]
)
);
this.engineResults[`Simulation_${this.instanceRun}`] = {
engineName: this.currentEngineName,
...stgOb,
};
console.log(`Simulation finished.`);
//setting results to be saved in main class
this.currentEngine.setEngine();
}
/**
* Calculates and sets the total function time and total script time for each result in the `engineResults` object.
* @memberof hydroCompute
* @returns {void}
*/
setTotalTime() {
Object.keys(this.engineResults).forEach((key) => {
let ft = 0,
st = 0;
let currentResult = this.engineResults[key];
for (let resName in currentResult) {
if (resName !== "engineName") {
ft = ft + currentResult[resName].funcEx;
st = st + currentResult[resName].scriptEx;
}
}
currentResult.totalFuncTime = ft;
currentResult.totalScrTime = st;
});
}
/**
* Returns the name of the current engine.
* @memberof hydroCompute
* @returns {string} The name of the current engine.
*/
currentEngine() {
return this.currentEngineName;
}
/**
* Saves the provided data into the available data storage.
* @memberof hydroCompute
* @param {Object|string} args - The data to be saved. It can be passed as an object or a string.
* @param {string} args.id - (Optional) The ID of the data container. If not provided, a random ID will be generated.
* @param {Array|number|string} args.data - The data to be saved. It can be an array, a number, or a string.
* @param {Object} args.splits - (Optional) The splitting configuration for the data.
*/
async data(args) {
try {
//Assuming the args is being passed as a string fetching a JSON object
if (typeof args === "string") {
let jsonData = await importJSONdata(args);
args = { data: jsonData };
} else {
//Assuming the user is passing an object with di, data, and splitting definition
args = args;
}
//Set container
let container = {
id: typeof args.id === "undefined" ? this.makeId(5) : args.id,
length: args.data[0] instanceof Array ? args.data.length : 1,
};
typeof args.data[0] === "string"
? (args.data = args.data.map(Number))
: null;
if (typeof args.splits === "undefined") {
container.data = dataCloner(args.data);
this.availableData.push(container);
} else {
let partition = splits.main(args.splits.function, {
...args.splits,
data: dataCloner(args.data),
});
container.data = partition;
this.availableData.push(container);
}
} catch (error) {
console.log("Data could not be saved. More info: \n", error);
return;
}
}
/**
* Retrieves the results for a specific simulation by name.
* @memberof hydroCompute
* @param {string} name - The name of the simulation.
* @returns {Array} - An array of objects containing the results and associated functions.
*/
results(name) {
if (typeof this.currentEngine === "undefined")
return console.error(
"Please set the required engine first before initializing!"
);
let stgViewer = [];
for (let resultName in this.engineResults[name]) {
let x = [],
y = [];
if (
resultName !== "engineName" &&
resultName !== "totalFuncTime" &&
resultName !== "totalScrTime"
) {
for (
let k = 0;
k < this.engineResults[name][resultName].results.length;
k++
) {
let stgRes = this.engineResults[name][resultName].results[k];
let stgFunc = this.engineResults[name][resultName].funcOrder[k];
//for (let result in this.engineResults[name][resultName][stgRes].results) {
if (stgRes.byteLength !== 0) {
x.push(Array.from(new Float32Array(stgRes)));
y.push(stgFunc);
//}
}
}
stgViewer.push({ name: resultName, results: x, functions: y });
}
}
return stgViewer;
}
/**
* Retrieves the available engines.
* @memberof hydroCompute
* @returns {Array} - An array containing the names of the available engines.
*/
availableEngines() {
return Object.keys(kernels);
}
/**
* Retrieves the available engine scripts.
* @memberof hydroCompute
* @returns {Promise<Map>} - A Promise that resolves to a Map object containing the available engine scripts.
*/
async engineScripts() {
let m = await this.currentEngine.availableScripts();
if (this.currentEngineName === "wasm") {
let _m = new Map();
let stgM = Object.keys(m);
for (var x of stgM) {
let stgKeys = m[x].keys();
for (var y of stgKeys) {
_m.set(`${x}-${y}`, m[x].get(y));
}
}
return _m;
} else if (this.currentEngineName === "webrtc")
console.log(
"No scripts available to run in the webrtc engine. Please checkout documentation"
);
else return m;
}
/**
* Searches the function splits available for data manipulation
* @memberof hydroCompute
* @returns {Object} map containing the available split functions in the engines
*/
availableSplits() {
let r = Object.keys(splits);
r = r.filter((ele) => (ele === undefined || ele === "main" ? null : ele));
return r;
}
/**
* Generates a random ID string.
* @memberof hydroCompute
* @param {number} length - The length of the ID string.
* @returns {string} - The generated ID string.
*/
makeId(length) {
let result = "",
characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
charactersLength = characters.length;
for (var i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
/**
* Retrieves the total function time and total script time for a specific result.
* @memberof hydroCompute
* @param {string} res - The name of the result.
* @returns {number[]} - An array containing the total function time and total script time.
*/
getResTimes(res) {
return [
this.engineResults[res].totalFuncTime,
this.engineResults[res].totalScrTime,
];
}
/**
* Calculates the total function time and total script time for all available results.
* @memberof hydroCompute
* @returns {number[]} - An array containing the total function time and total script time.
*/
getTotalTime() {
let fnTotal = 0,
scrTotal = 0;
for (let result of this.availableResults()) {
let stgRes = this.getResTimes(result);
(fnTotal += stgRes[0]), (scrTotal += stgRes[1]);
}
return [fnTotal, scrTotal];
}
/**
* Retrieves the available results stored in the `engineResults` object.
* @memberof hydroCompute
* @returns {string[]} - An array containing the names of the available results.
*/
availableResults() {
return Object.keys(this.engineResults);
}
}
typeof window !== "undefined" ? (window.hydroCompute = hydroCompute) : null;
export default hydroCompute;