@hcikit/workflow
Version:
A workflow manager for running experiments.
408 lines (407 loc) • 16.5 kB
JavaScript
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;