UNPKG

@hcikit/workflow

Version:

A workflow manager for running experiments.

408 lines (407 loc) 16.5 kB
var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (g && (g = 0, op[0] && (_ = 0)), _) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; var __values = (this && this.__values) || function(o) { var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; if (m) return m.call(o); if (o && typeof o.length === "number") return { next: function () { if (o && i >= o.length) o = void 0; return { value: o && o[i++], done: !o }; } }; throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); }; var __read = (this && this.__read) || function (o, n) { var m = typeof Symbol === "function" && o[Symbol.iterator]; if (!m) return o; var i = m.call(o), r, ar = [], e; try { while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); } catch (error) { e = { error: error }; } finally { try { if (r && !r.done && (m = i["return"])) m.call(i); } finally { if (e) throw e.error; } } return ar; }; var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); }; import { mergeWith, pickBy, isEqual, pick } from "lodash-es"; import { __INDEX__, } from "./types.js"; export { default as schema } from "./schemas.json"; export * from "./utils.js"; export * from "./types.js"; // TODO: rename to core or configuration rather than workflow? // ----- Whole configuration ------ /** * Given a configuration, determines whether the experiment is complete or not. * * @param {Configuration} configuration * @returns {boolean} indicates whether an experiment is complete or not. */ export function experimentComplete(configuration) { var _a; return ((_a = configuration[__INDEX__]) === null || _a === void 0 ? void 0 : _a.length) === 0; } export function getTotalTasks(configuration) { return Array.from(iterateConfiguration(configuration)).length; } // ------- PROPS ------- /** * Finds all of the props for a given task based on which ones start with a lowercase value * and then also the ones that match the task name (which must start with a capital letter). * * @param {Object} props * @param {string} task * @returns an object containing all of the props for a given task */ // eslint-disable-next-line @typescript-eslint/ban-types export function scopePropsForTask(props, task) { return merge(pickBy(props, function (_, k) { return k[0] === k[0].toLowerCase(); }), props[task]); } /** * This function reads the index of the configuration and outputs the current props for it. * If there is no index it assumes the experiment is just beginning. * * @param {Configuration} configuration * @returns the props for the current index of an experiment */ export function getCurrentProps(configuration) { return getPropsFor(configuration, getCurrentIndex(configuration)); } /** * Gets the props for an experiment with a given index. * * @param {ExperimentIndex} index * @param {Configuration} configuration * @param {boolean} deleteLogs whether to include the logs or not. * @returns the props for a given index on a configuration. */ export function getPropsFor(configuration, index, deleteLogs) { var e_1, _a; if (deleteLogs === void 0) { deleteLogs = true; } if (experimentComplete(configuration)) { return {}; } var props = {}; index = getLeafIndex(configuration, index); try { // Loop over every level of the index, collecting the props from that level at every step. // We work our way down from the top in order to let properties override each other, // More specific props override other ones. for (var index_1 = __values(index), index_1_1 = index_1.next(); !index_1_1.done; index_1_1 = index_1.next()) { var nextLevelIndex = index_1_1.value; var properties = Object.assign({}, configuration); // We want to ignore the children props. delete properties.children; if (!configuration.children) { break; } configuration = configuration.children[nextLevelIndex]; // This gives us a deep merge, which means changes don't propagate. props = merge(props, properties); } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (index_1_1 && !index_1_1.done && (_a = index_1.return)) _a.call(index_1); } finally { if (e_1) throw e_1.error; } } // Because the above for loop changes configuration, this configuration now talks about the bottom // level which we haven't looked at quite yet. props = merge(props, configuration); if (deleteLogs) { delete props.logs; } return props; } // ----- INDEX ----- /** * Traverses down the tree to the requested index and then returns the config at that index. * * @param {ExperimentIndex} index * @param {Configuration} initialConfiguration * @returns {Configuration} the requested level of config */ export function getConfigurationAtIndex(initialConfiguration, index) { return (index.reduce(function (config, value) { var _a; return (_a = config === null || config === void 0 ? void 0 : config.children) === null || _a === void 0 ? void 0 : _a[value]; }, initialConfiguration) || {}); } /** * Finds the nearest leaf index to a starting index by traversing down the tree and always choosing * the first child. * @param {ExperimentIndex} index * @param {Configuration} configuration * @returns {ExperimentIndex} the nearest leaf index. */ export function getLeafIndex(configuration, index) { if (index.length === 0) { return index; } index = __spreadArray([], __read(index), false); while ("children" in getConfigurationAtIndex(configuration, index)) { index.push(0); } return index; } /** * This function is a helper function to get the current index, or if the experiment hasn't started it returns [0] * * @param {Configuration} configuration * @returns {ExperimentIndex} the current experiment index, or [0] if there is none. */ export function getCurrentIndex(configuration) { var index = [0]; var myInd = configuration[__INDEX__]; if (myInd !== undefined) { index = __spreadArray([], __read(myInd), false); } return getLeafIndex(configuration, index); } /** * Takes an index and tells us what task number it is. This is useful for progress bars. * One trick to using this is we can truncate a config and just take one of the children, then * that gives us the progress within a certain section of an experiment. * @param {Configuration} configuration * @param {ExperimentIndex} index * @returns {number} */ export function indexToTaskNumber(configuration, index) { var e_2, _a; index = getLeafIndex(configuration, index); var i = 0; try { for (var _b = __values(iterateConfiguration(configuration)), _c = _b.next(); !_c.done; _c = _b.next()) { var currentIndex = _c.value; if (isEqual(index, currentIndex)) { return i; } i++; } } catch (e_2_1) { e_2 = { error: e_2_1 }; } finally { try { if (_c && !_c.done && (_a = _b.return)) _a.call(_b); } finally { if (e_2) throw e_2.error; } } throw new Error("Index not found!"); } /** * Takes a task number and returns an index. This is useful for dev tools because it can let us * move forward in the experiment. * @param {Configuration} configuration * @param {number} taskNumber * @returns {ExperimentIndex} */ export function taskNumberToIndex(configuration, taskNumber) { var e_3, _a; var i = 0; try { for (var _b = __values(iterateConfiguration(configuration)), _c = _b.next(); !_c.done; _c = _b.next()) { var currentIndex = _c.value; if (i === taskNumber) { return currentIndex; } i++; } } catch (e_3_1) { e_3 = { error: e_3_1 }; } finally { try { if (_c && !_c.done && (_a = _b.return)) _a.call(_b); } finally { if (e_3) throw e_3.error; } } throw new Error("Index not found!"); } // ------ Mutators ------ /** * Returns the next leaf index after this task is complete. The function is slightly misnamed because it doesn't actually change any indices. * * @param {Configuration} configuration * @returns {ExperimentIndex} */ export function advanceConfiguration(configuration, index) { var _a, _b; if (experimentComplete(configuration)) { return configuration; } if (index !== undefined) { return __assign(__assign({}, configuration), (_a = {}, _a[__INDEX__] = index, _a)); } index = getCurrentIndex(configuration); index = getLeafIndex(configuration, index); var newIndexValue; do { var nextIndexValue = index.pop(); if (nextIndexValue === undefined) { break; } newIndexValue = 1 + nextIndexValue; var currentChildren = getConfigurationAtIndex(configuration, index).children; if (!currentChildren) { throw new Error("Error advancing task, the current index path doesn't exist."); } if (newIndexValue < currentChildren.length) { index.push(newIndexValue); break; } } while (index.length); return __assign(__assign({}, configuration), (_b = {}, _b[__INDEX__] = getLeafIndex(configuration, index), _b)); } /** * This function actually places a new log object on the config at the current index, or if the index isn't a leaf it finds the leaf index. * * @param {Configuration} configuration * @param {Log | any} log * @returns */ // We use omit2 because Omit clobbers all of the stuff in it. export function logToConfiguration(configuration, log) { if (experimentComplete(configuration)) { throw new Error("Attempting to log when the experiment is complete."); } var index = getCurrentIndex(configuration); return modifyConfiguration(configuration, { logs: __spreadArray(__spreadArray([], __read((getConfigurationAtIndex(configuration, index).logs || [])), false), [ __assign(__assign({}, log), { timestamp: Date.now() }), ], false), }, index, false); } /** * Takes the config and actually modifies something about it according to the modified config. * modifiedConfig is an object and all of the properties in it overwrite the current properties at the current leaf index. * * @param {Configuration} configuration * @param {ExperimentIndex} index * @param {Object} modifiedConfiguration * @param {boolean} logResult */ export function modifyConfiguration(configuration, modifiedConfiguration, index, logResult) { if (logResult === void 0) { logResult = true; } if (experimentComplete(configuration)) { throw new Error("Attempted to modify an experiment that was already complete."); } if (!index) { index = getCurrentIndex(configuration); } var originalConfiguration = __assign({}, configuration); if (logResult) { var configToEdit = getConfigurationAtIndex(configuration, index); originalConfiguration = logToConfiguration(originalConfiguration, { from: pick(configToEdit, Object.keys(modifiedConfiguration)), to: modifiedConfiguration, index: index, type: "MODIFY_CONFIGURATION", }); } configuration = originalConfiguration; // Iterate downwards to find the part we want to modify. for (var i = 0; i < index.length; i++) { var nextLevelIndex = index[i]; if (!configuration.children) { throw new Error("There was an error modifying the config, the index did not exist."); } configuration.children = __spreadArray(__spreadArray(__spreadArray([], __read(configuration.children.slice(0, nextLevelIndex)), false), [ __assign({}, configuration.children[nextLevelIndex]) ], false), __read(configuration.children.slice(nextLevelIndex + 1)), false); configuration = configuration.children[nextLevelIndex]; } // Modify the parts we actually want to modify Object.assign(configuration, modifiedConfiguration); return originalConfiguration; } // ----- Iterating configs ----- export function iterateConfiguration(configuration) { var toSearch, searchingIndex, searchingConfiguration, i; return __generator(this, function (_a) { switch (_a.label) { case 0: toSearch = [[]]; _a.label = 1; case 1: if (!(toSearch.length > 0)) return [3 /*break*/, 5]; searchingIndex = toSearch.pop(); if (searchingIndex === undefined) { return [3 /*break*/, 5]; } searchingConfiguration = getConfigurationAtIndex(configuration, searchingIndex); if (!searchingConfiguration.children) return [3 /*break*/, 2]; for (i = searchingConfiguration.children.length - 1; i >= 0; i--) { toSearch.push(__spreadArray(__spreadArray([], __read(searchingIndex), false), [i], false)); } return [3 /*break*/, 4]; case 2: return [4 /*yield*/, searchingIndex]; case 3: _a.sent(); _a.label = 4; case 4: return [3 /*break*/, 1]; case 5: return [2 /*return*/]; } }); } export function flattenConfigurationWithProps(configuration) { return Array.from(iterateConfiguration(configuration)).map(function (index) { return getPropsFor(configuration, index, false); }); } export function mergeArraysSpecial(object, source) { return mergeWith(object, source, function (value, srcValue) { if (Array.isArray(value)) { return srcValue; } }); } var merge = mergeArraysSpecial;