UNPKG

nrn-agents

Version:

A library for creating and deploying gaming agents at scale

1,470 lines (1,448 loc) 69.4 kB
var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // lib/index.ts var index_exports = {}; __export(index_exports, { AgentFactory: () => agent_factory_default, DataFactory: () => data_factory_default, FeatureEngineering: () => feature_engineering_default, Registry: () => registry_default }); module.exports = __toCommonJS(index_exports); // lib/feature-engineering/angles.ts var getAngleBetweenPoints = (point1, point2) => { if (point2.x === point1.x && point2.y === point1.y) { return 0; } const upperBound = Math.atan(Infinity); const slope = -(point2.y - point1.y) / (point2.x - point1.x); var newAngle = Math.abs(Math.atan(slope) - upperBound) / upperBound * 90; if (point2.x < point1.x) { newAngle += 180; } return newAngle; }; // lib/feature-engineering/clusters.ts var getCenterPosition = (entities) => { const center = { x: 0, y: 0 }; entities.forEach((entity) => { center.x += entity.x; center.y += entity.y; }); center.x /= entities.length; center.y /= entities.length; return center; }; // lib/feature-engineering/raycast.ts var degreeToRadians = (angle) => { return angle * (Math.PI / 180); }; var calculateDistance = (point1, point2) => { return Math.hypot(point2.x - point1.x, point2.y - point1.y); }; var rayIntersectsRect = (origin, direction, rect) => { let { x: cx, y: cy, width, height } = rect; let { x: ox, y: oy } = origin; let { x: dx, y: dy } = direction; let rx = cx - width / 2; let ry = cy - height / 2; let t1 = (rx - ox) / dx; let t2 = (rx + width - ox) / dx; let t3 = (ry - oy) / dy; let t4 = (ry + height - oy) / dy; let tmin = Math.max(Math.min(t1, t2), Math.min(t3, t4)); let tmax = Math.min(Math.max(t1, t2), Math.max(t3, t4)); if (tmax < 0) return null; if (tmin > tmax) return null; return { x: ox + tmin * dx, y: oy + tmin * dy }; }; var castRay = (origin, colliders, angle, invertY = false) => { const rayDir = { x: Math.cos(degreeToRadians(angle)), y: (invertY ? -1 : 1) * Math.sin(degreeToRadians(angle)) }; let closestIntersection = null; colliders.forEach((collider) => { const intersection = rayIntersectsRect(origin, rayDir, collider); if (intersection) { if (!closestIntersection || Math.hypot(intersection.x - origin.x, intersection.y - origin.y) < Math.hypot(closestIntersection.x - origin.x, closestIntersection.y - origin.y)) { closestIntersection = intersection; } } }); return closestIntersection !== null ? { ...closestIntersection, angle, distance: calculateDistance(origin, closestIntersection), intersects: true } : { angle, intersects: false }; }; var raycast2d = (origin, colliders, angles, invertY = false) => { return angles.map((angle) => { return castRay(origin, colliders, angle, invertY); }); }; var getRayAngles = (numRays = 8) => { const incrementalAngle = 360 / numRays; return [...Array(numRays).keys()].map((i) => incrementalAngle * i); }; // lib/modules/feature-engineering.ts var FeatureEngineering = class _FeatureEngineering { static numFeatures; // The number of features defined in the state config. static stateConfig = []; // Static array to store the state configuration for features. /** Mapping of feature types to their respective processing functions. */ static conversionFunctions = { cosineSimilarity: this.getCosineSimilarity, raycast: this.getRaycasts, angle: this.getAngle, relativePosition: this.getRelativePosition, relativePositionToCluster: this.getRelativePositionToCluster, onehot: this.getOneHotEncoding, binary: this.getBinary, rescale: this.getRescaledValue, normalize: this.getNormalizedValue }; /** Number of features returned from feature engineering functions (-1 for dynamic size). */ static featureSizes = { cosineSimilarity: 1, raycast: -1, angle: 2, relativePosition: 3, relativePositionToCluster: 3, onehot: -1, binary: 1, rescale: 1, normalize: 1 }; /** Required data keys and setup for each feature type. */ static requiredData = { cosineSimilarity: { keys: { vector1: true, vector2: true } }, raycast: { keys: { origin: true, colliders: true, maxDistance: true }, setup: { numRays: false } }, angle: { keys: { entity1: true, entity2: true } }, relativePosition: { keys: { entity1: true, entity2: true, maxDistance: true } }, relativePositionToCluster: { keys: { origin: true, clusterEntities: true, maxDistance: true } }, onehot: { keys: { value: true }, setup: { options: true } }, binary: { keys: { value: true }, setup: { operator: true, comparison: true } }, rescale: { keys: { value: true, scaleFactor: true } }, normalize: { keys: { value: true }, setup: { mean: true, stdev: true } } }; /** * Sets the state configuration for feature extraction. * @param config - The array of feature configurations. */ static setStateConfig(config = []) { this.numFeatures = this._validateStateConfig(config); _FeatureEngineering.stateConfig = config; } /** * Validate that the state configuration is correct. * @param config - The array of feature configurations. * @returns The number of features in the state config */ static _validateStateConfig(config) { let numFeatures = 0; config.forEach((featureConfig) => { const configData = this.requiredData[featureConfig.type]; if (configData === void 0) { throw Error(`'${featureConfig.type}' is not a valid feature type`); } if (featureConfig.keys === void 0) { throw Error("'keys' is missing from state configuration"); } Object.keys(featureConfig.keys).forEach((key) => { if (configData.keys[key] === void 0) { throw Error(`'${key}' is not a valid key`); } }); Object.keys(configData.keys).forEach((key) => { if (configData.keys[key] && featureConfig.keys[key] === void 0) { throw Error(`'${key}' is missing from 'keys', but it is required`); } }); if (configData.setup) { Object.keys(configData.setup).forEach((key) => { if (configData.setup[key] && featureConfig.setup?.[key] === void 0) { throw Error(`'${key}' is missing from 'setup', but it is required`); } }); } if (this.featureSizes[featureConfig.type] !== -1) { numFeatures += this.featureSizes[featureConfig.type]; } else if (featureConfig.type === "raycast") { numFeatures += featureConfig.setup?.numRays ?? 8; } else if (featureConfig.type === "onehot") { numFeatures += featureConfig.setup.options.length; } }); return numFeatures; } /** * Validate that the key exists in the game world. * @param value - Value extracted from the world. * @param key The key used to extract a value. */ static _validateKeyInWorld(value, key) { if (value === void 0) { throw Error(`'${key}' does not exist in the world object`); } } /** * Extracts a value from the game world. * @param world - The world object containing the data for feature extraction. * @param key - Key to extract feature object. * @returns Object that will be used in feature engineering */ static _parseWorldWithKey(world, key) { const keySplit = key.split(/\.|\[|\]/).filter(Boolean); if (keySplit.length > 1) { return keySplit.reduce((feature, k) => { if (feature && feature[k] !== void 0) { return feature[k]; } throw Error(`Invalid key: ${key}`); }, world); } return world[key]; } /** * Extracts the state features from the world object based on the current state configuration. * @param world - The world object containing the data for feature extraction. * @returns An array of feature values. */ static getState(world) { const state = []; this.stateConfig.forEach((featureConfig) => { const featureObject = {}; Object.keys(featureConfig.keys).forEach((featureKey) => { featureObject[featureKey] = this._parseWorldWithKey(world, featureConfig.keys[featureKey]); this._validateKeyInWorld(featureObject[featureKey], featureConfig.keys[featureKey]); }); const featureConfigSetup = featureConfig.setup; if (featureConfigSetup !== void 0) { Object.keys(featureConfigSetup).forEach((setupKey) => { featureObject[setupKey] = featureConfigSetup[setupKey]; }); } const feature = _FeatureEngineering.conversionFunctions[featureConfig.type]( featureObject ); state.push(...feature); }); return state; } static _dotProduct(A, B) { if (A.length !== B.length) { throw new Error("Vectors are not the same dimensions"); } return A.reduce((sum, value, i) => sum + value * B[i], 0); } static _normL2(vector) { return Math.sqrt(_FeatureEngineering._dotProduct(vector, vector)); } /** * Processes the cosine similarity of two vectors. * @param params - Parameters required for cosine similarity. * @returns The cosine similarity. */ static getCosineSimilarity({ vector1, vector2 }) { const denom = _FeatureEngineering._normL2(vector1) * _FeatureEngineering._normL2(vector2); return [_FeatureEngineering._dotProduct(vector1, vector2) / denom]; } /** * Processes raycast features based on the provided parameters. * @param params - Parameters required for raycasting. * @returns An array of raycast results. */ static getRaycasts({ origin, colliders, maxDistance, numRays = 8 }) { const angles = getRayAngles(numRays); const rays = raycast2d(origin, colliders, angles); return rays.map((ray) => { if (ray.intersects) { return 1 - Math.min(ray.distance / maxDistance, 1); } else { return -1; } }); } /** * Processes the angle between an origin and another entity. * @param params - Parameters for angle calculation. * @returns An array containing the sine and cosine of the angle between the entities. */ static getAngle({ entity1, entity2 }) { const angleDegrees = getAngleBetweenPoints(entity1, entity2); const angleRadians = angleDegrees / 360 * Math.PI * 2; return [Math.sin(angleRadians), Math.cos(angleRadians)]; } /** * Processes the relative position between an origin and another entity. * @param params - Parameters for relative position calculation. * @returns An array containing the distance and directional components. */ static getRelativePosition({ entity1, entity2, maxDistance }) { const squaredDistance = (entity2.x - entity1.x) ** 2 + (entity2.y - entity1.y) ** 2; const distance = Math.sqrt(squaredDistance) / maxDistance; const [angleSin, angleCos] = _FeatureEngineering.getAngle({ entity1, entity2 }); return [distance, angleSin, angleCos]; } /** * Processes relative position features between an origin and a cluster of objects. * @param params - Parameters for relative position calculation. * @returns An array containing the distance and directional components. */ static getRelativePositionToCluster({ origin, clusterEntities, maxDistance }) { const clusterCenter = getCenterPosition(clusterEntities); return _FeatureEngineering.getRelativePosition({ entity1: origin, entity2: clusterCenter, maxDistance }); } /** * Processes one-hot encoding for a given value and set of options. * @param params - Parameters for one-hot encoding. * @returns An array representing the one-hot encoded value. */ static getOneHotEncoding({ value, options }) { return options.map((x) => x === value ? 1 : 0); } /** * Processes binary features based on a comparison operation. * @param params - Parameters for the binary operation. * @returns An array with the result of the comparison (1 or 0). */ static getBinary({ value, operator, comparison }) { let conditionMet = false; if (operator === "=") { conditionMet = value === comparison; } else if (operator === ">") { conditionMet = value > comparison; } else if (operator === "<") { conditionMet = value < comparison; } else if (operator === "!=") { conditionMet = value !== comparison; } return [conditionMet ? 1 : 0]; } /** * Processes rescaled values. * @param params - Parameters for rescaling. * @returns An array with the rescaled value. */ static getRescaledValue({ value, scaleFactor }) { return [value / scaleFactor]; } /** * Processes normalized values. * @param params - Parameters for normalization. * @returns An array with the normalized value. */ static getNormalizedValue({ value, mean, stdev }) { return [(value - mean) / stdev]; } }; var feature_engineering_default = FeatureEngineering; // lib/agent-wrappers/agent-wrapper-core.ts var AgentWrapperCore = class { numAgents; model; frameDelay; forcedHold; pressActions; holdActions; forceHoldActions; continuousActions; actionToHead; inputs; inputCooldown; previousHoldActions; lockedAction; constructor(model, config = {}, numAgents = 1) { this.numAgents = numAgents; this.model = model; this.frameDelay = config.frameDelay ?? 15; this.forcedHold = config.forcedHold ?? 4; this.pressActions = []; this.holdActions = config.holdActions ?? []; this.forceHoldActions = config.forceHoldActions ?? []; this.forceHoldActions.forEach((x) => { if (!this.holdActions.includes(x)) { this.holdActions.push(x); } }); this.actionToHead = {}; this.continuousActions = {}; Object.keys(model.config.actionMetadata).forEach((actionGroup) => { model.config.actionMetadata[actionGroup].order.forEach((action) => { if (model.config.actionMetadata[actionGroup].actionType === "continuous") { this.continuousActions[action] = true; } this.actionToHead[action] = actionGroup; if (!this.holdActions.includes(action)) { this.pressActions.push(action); } }); }); this.reset(); } reset() { this.inputs = Array.from({ length: this.numAgents }, () => ({})); this.inputCooldown = Array.from({ length: this.numAgents }, () => ({})); this.previousHoldActions = Array.from({ length: this.numAgents }, () => ({})); this.lockedAction = Array.from({ length: this.numAgents }, () => ({ name: "", head: "", cooldown: 0 })); for (let i = 0; i < this.numAgents; i++) { Object.keys(this.model.config.actionMetadata).forEach((actionGroup) => { this.model.config.actionMetadata[actionGroup].order.forEach((action) => { this.inputs[i][action] = false; this.inputCooldown[i][action] = 0; if (this.holdActions.includes(action)) { this.previousHoldActions[i][action] = false; } }); }); } } applyPressCooldown(inputs, action, agentIdx = 0) { if (this.continuousActions[action]) return; if (this.inputCooldown[agentIdx][action] > 0) { inputs[action] = false; this.inputCooldown[agentIdx][action] -= 1; } else if (inputs[action]) { this.inputCooldown[agentIdx][action] = this.frameDelay; } } applyHoldCooldown(inputs, action, agentIdx = 0) { if (this.continuousActions[action]) return; const cooldownBool = this.inputCooldown[agentIdx][action] > 0; if (cooldownBool) { inputs[action] = false; this.inputCooldown[agentIdx][action] -= 1; } if (!this.previousHoldActions[agentIdx][action]) { if (!cooldownBool && inputs[action] && this.forceHoldActions.includes(action)) { this.lockedAction[agentIdx] = { name: action, head: this.actionToHead[action], cooldown: this.forcedHold }; } } else if (!inputs[action]) { this.inputCooldown[agentIdx][action] = this.frameDelay; } } checkLockedAction(action, agentIdx = 0) { if (this.lockedAction[agentIdx].cooldown > 0 && this.actionToHead[action] === this.lockedAction[agentIdx].head) { if (action === this.lockedAction[agentIdx].name) { this.lockedAction[agentIdx].cooldown -= 1; if (this.lockedAction[agentIdx].cooldown === 0) { this.lockedAction[agentIdx] = { name: "", head: "", cooldown: 0 }; } } return true; } return false; } copyPrevHoldActions(inputs, agentIdx = 0) { this.model.config.actionMetadata[this.lockedAction[agentIdx].head].order.forEach((action) => { inputs[action] = this.previousHoldActions[agentIdx][action]; }); } applyFrameDelay(inputs, agentIdx = 0) { this.pressActions.forEach((action) => { this.applyPressCooldown(inputs, action, agentIdx); }); this.holdActions.forEach((action) => { const isLocked = this.checkLockedAction(action, agentIdx); if (!isLocked) { this.applyHoldCooldown(inputs, action, agentIdx); } else if (action === this.lockedAction[agentIdx].name) { this.copyPrevHoldActions(inputs, agentIdx); } }); } trackPreviousHoldInputs(inputs, agentIdx = 0) { this.holdActions.forEach((action) => { this.previousHoldActions[agentIdx][action] = inputs[action]; }); } isActionLocked(agentIdx = 0) { return this.lockedAction[agentIdx].cooldown > 0; } _selectAction(selectionInputs, selectionFunction, agentIdx = 0) { this.trackPreviousHoldInputs(this.inputs[agentIdx], agentIdx); let inputs; if (selectionFunction !== void 0) { inputs = selectionFunction.call(this, selectionInputs); } else { inputs = this.model.selectAction(selectionInputs); } this.applyFrameDelay(inputs, agentIdx); this.inputs[agentIdx] = inputs; return inputs; } }; // lib/agent-wrappers/probabilistic-agent-wrapper.ts var JOINT_ACTION_DELIMITER = "@"; var ProbabilisticAgentWrapper = class _ProbabilisticAgentWrapper extends AgentWrapperCore { numSamples; framesRemaining; previousPolicy; currentAction; actionSubkeys; policySimilarityThreshold; constructor(model, config = {}, numAgents = 1) { super(model, config, numAgents); this.numSamples = {}; this.framesRemaining = {}; this.previousPolicy = {}; this.currentAction = {}; this.actionSubkeys = {}; model.outputGroups.forEach((actionGroup) => { const rawNumSamples = config.numSamples?.[actionGroup] ?? 1; const metadata = model.config.actionMetadata[actionGroup]; if (rawNumSamples > 1) { if (metadata.policyMapping !== "probabilisticSampling") { throw Error( `policyMapping for '${actionGroup}' must be 'probabilisticSampling' to use numSamples > 1` ); } else if (metadata.actionType !== "discrete") { throw Error(`Cannot use numSamples > 1 for '${metadata.actionType}' action spaces`); } else if (metadata.order.filter((x) => this.holdActions.includes(x)).length === 0) { throw Error( `Some actions in the '${actionGroup}' action head must be included in 'holdActions' to use numSamples > 1` ); } else if (metadata.order.filter((x) => this.forceHoldActions.includes(x)).length === metadata.order.length) { throw Error( `All actions in the '${actionGroup}' action head are currently included in 'forceActions' - this is incompatible with numSamples > 1` ); } } this.numSamples[actionGroup] = rawNumSamples; this.framesRemaining[actionGroup] = Array.from({ length: numAgents }, () => 0); this.previousPolicy[actionGroup] = Array.from({ length: numAgents }, () => null); this.currentAction[actionGroup] = Array.from({ length: numAgents }, () => []); this.actionSubkeys[actionGroup] = []; }); this.policySimilarityThreshold = config.policySimilarityThreshold ?? 0.8; } static policySimilarity(A, B) { return 1 - A.reduce((sum, value, i) => sum + Math.abs(value - B[i]), 0); } static getMostCommonAction(array) { if (array.length === 0) return null; let modeMap = {}; let maxEl = array[0]; let maxCount = 1; for (let i = 0; i < array.length; i++) { let el = array[i]; if (modeMap[el] === void 0) { modeMap[el] = 0; } modeMap[el]++; if (modeMap[el] > maxCount) { maxEl = el; maxCount = modeMap[el]; } } return { action: maxEl, count: maxCount }; } monteCarloSampling(probabilities, actionKey, row = 0) { const actionArray = []; for (let i = 0; i < this.numSamples[actionKey]; i++) { actionArray.push(this.model.selectActionOneHead(probabilities, actionKey, row)); } return actionArray; } convertInputToString(input, actionKey) { if (this.actionSubkeys[actionKey].length === 0) { this.actionSubkeys[actionKey] = Object.keys(input).map((x) => x); } const actionArray = this.actionSubkeys[actionKey].filter((dir) => input[dir]); return actionArray.length > 0 ? actionArray.sort().join(JOINT_ACTION_DELIMITER) : void 0; } assignSampledInput(actionKey, agentIdx = 0) { const inputs = {}; this.actionSubkeys[actionKey].forEach((inputKey) => { inputs[inputKey] = this.currentAction[actionKey][agentIdx].includes(inputKey); }); return inputs; } sampleAction(probabilities, actionKey, agentIdx = 0, row = 0) { if (this.numSamples[actionKey] > 1) { let policyChange = false; if (this.previousPolicy[actionKey][agentIdx] !== null) { const similarity = _ProbabilisticAgentWrapper.policySimilarity( probabilities[actionKey][row], this.previousPolicy[actionKey][agentIdx] ); policyChange = similarity < this.policySimilarityThreshold; } if (this.framesRemaining[actionKey][agentIdx] === 0 || policyChange) { const actionArray = this.monteCarloSampling(probabilities, actionKey, row); const { action, count } = _ProbabilisticAgentWrapper.getMostCommonAction( actionArray.map((input) => { return this.convertInputToString(input, actionKey); }) ); this.currentAction[actionKey][agentIdx] = action ? action.split(JOINT_ACTION_DELIMITER) : []; this.framesRemaining[actionKey][agentIdx] = count; this.previousPolicy[actionKey][agentIdx] = probabilities[actionKey][row]; } this.framesRemaining[actionKey][agentIdx] -= 1; return this.assignSampledInput(actionKey, agentIdx); } else { return this.model.selectActionOneHead(probabilities, actionKey, row); } } forceNoAction(actionGroup) { const inputs = {}; this.model.config.actionMetadata[actionGroup].order.forEach((actionKey) => { inputs[actionKey] = false; }); return inputs; } // Should I write an override to check above? But we need the state... // isActionLocked() { // } selectionFunction({ probabilities, agentIdx = 0, row = 0 }) { let selection = {}; this.model.outputGroups.forEach((actionGroup) => { if (this.lockedAction[agentIdx].cooldown - 1 > 0) { if (this.lockedAction[agentIdx].cooldown === this.forcedHold) { const lockedDelta = this.framesRemaining[actionGroup][agentIdx] - this.lockedAction[agentIdx].cooldown; this.framesRemaining[actionGroup][agentIdx] = Math.max(lockedDelta + 1, 0); } } else { selection = { ...selection, ...this.sampleAction(probabilities, actionGroup, agentIdx, row) }; } }); return selection; } }; // lib/agent-wrappers/single-agent-wrapper.ts var SingleAgentWrapper = class extends ProbabilisticAgentWrapper { constructor(model, config = {}) { super(model, config); } selectAction(input) { const probabilities = this.model.getProbabilities(input); return this._selectAction({ probabilities, agentIdx: 0 }, this.selectionFunction); } }; // lib/modules/agents/general-agent-core.ts var import_nrn_ml = require("nrn-ml"); // lib/modules/helpers/state-validator.ts var StateValidator = class { modelTypes = []; expectedInputDims = []; addInputDimValidation(configs) { for (let idx = 0; idx < configs.length; idx++) { this.expectedInputDims[idx] = configs[idx].inputDim || configs[idx].nFeatures || configs[idx].numDiscreteStates; if (this.expectedInputDims[idx] === void 0) { throw Error("Expected input dimensionality for state validation is 'undefined'"); } } } validateState(inputs, idx = 0) { if (idx > this.modelTypes.length) { throw Error("'idx' out of range for 'modelTypes'"); } if (idx > this.expectedInputDims.length) { throw Error("'idx' out of range for 'expectedInputDims'"); } if (this.modelTypes[idx] === "neural-network") { const inputs1D = inputs; if (this._isArray1D(inputs1D)) { if (inputs1D.length !== this.expectedInputDims[idx]) { throw Error(`Input dimensionality mismatch: Received (${inputs1D.length}) vs Expected (${this.expectedInputDims[idx]})`); } return [inputs1D]; } else if (this._isArray2D(inputs)) { const inputs2D = inputs; inputs2D.forEach((inputRow) => { if (inputRow.length !== this.expectedInputDims[idx]) { throw Error(`Input dimensionality mismatch: Received (${inputRow.length}) vs Expected (${this.expectedInputDims[idx]})`); } }); } else { throw Error("'neural-network' model state must be a 1D array or Matrix"); } } else if (this.modelTypes[idx] === "simple") { const inputsNumber = inputs; if (!Number.isInteger(inputsNumber)) { throw Error("'simple' model state must be an integer"); } else if (inputsNumber < 0 || inputsNumber >= this.expectedInputDims[idx]) { throw Error(`Cell ${inputsNumber} is incompatable with model with cells between 0-${this.expectedInputDims[idx] - 1}`); } } return inputs; } getEmptyState(idx = 0) { if (this.modelTypes[idx] === "simple") { return -1; } else if (this.modelTypes[idx] === "neural-network") { return new Array(this.expectedInputDims[idx]).fill(0); } } _isArray1D(x) { const isArray = Array.isArray(x); const is1D = isArray && !Array.isArray(x[0]); return isArray && is1D; } _isArray2D(x) { const isArray = Array.isArray(x); const is2D = isArray && Array.isArray(x[0]) && !Array.isArray(x[0][0]); return isArray && is2D; } }; var state_validator_default = StateValidator; // lib/modules/agents/general-agent-core.ts var modelsMapping = { "simple": import_nrn_ml.TabularModel, "neural-network": import_nrn_ml.NeuralNetworkMultihead // "hierarchical": HierarchicalNeuralNetwork, }; var DEFAULT_AGENT_WRAPPER = { useAgentWrapper: false }; var GeneralAgentCore = class extends state_validator_default { numAgents; initializedBool; agentConfigs; dataCollectors; initialModelConfigs; actionConfig; constructor(numAgents = [1]) { super(); if (!Array.isArray(numAgents)) { throw Error("'numAgents' must be an array"); } numAgents.forEach((num) => { if (num < 1 || !Number.isInteger(num)) { throw Error("Elements in 'numAgents' must be a positive integer"); } }); this.numAgents = numAgents; this.initializedBool = false; this.initializeDataCollectors(); this.agentConfigs = Array.from({ length: numAgents.length }, () => DEFAULT_AGENT_WRAPPER); this.initialModelConfigs = new Array(numAgents.length); this.modelTypes = new Array(numAgents.length); this.actionConfig = { heads: [], metadata: {} }; } _setAgentConfig(agentConfig, idx = 0) { this.agentConfigs[idx] = agentConfig; } initializeDataCollectors() { this.dataCollectors = []; for (let i = 0; i < this.numAgents.length; i++) { this.dataCollectors.push([]); for (let j = 0; j < this.numAgents[i]; j++) { this.dataCollectors[i].push(new import_nrn_ml.DataCollector()); } } } validateModelType(modelConfig, idx = 0) { this.initialModelConfigs[idx] = modelConfig; if (modelsMapping[modelConfig.modelType] === void 0) { throw Error("Invalid model type"); } this.modelTypes[idx] = modelConfig.modelType; } addValidation(configs) { if (configs.length !== this.numAgents.length) { throw Error(`Incorrect number of 'configs'. Expecting ${this.numAgents.length}`); } this.addInputDimValidation(configs); for (let idx = 0; idx < this.numAgents.length; idx++) { for (let j = 0; j < this.numAgents[idx]; j++) { this.dataCollectors[idx][j].addValidationParams( configs[idx], this.modelTypes[idx] ); } } if (this.actionConfig.heads.length === 0) { this.actionConfig = { heads: configs[0].actionHeads, metadata: configs[0].actionMetadata }; } } setCollectionInterval(interval) { this.dataCollectors.forEach((collectorGroup) => { collectorGroup.forEach((collector) => { collector.setFrameInterval(interval); }); }); } setCollectionRewardThreshold(threshold) { this.dataCollectors.forEach((collectorGroup) => { collectorGroup.forEach((collector) => { collector.setRewardThreshold(threshold); }); }); } collect(dataInstance, groupIdx = 0, agentIdx = 0) { dataInstance.action = this.convertActionsForCollect(dataInstance.action); return this.dataCollectors[groupIdx][agentIdx].collect(dataInstance); } getTrainingData(groupIdx = 0, agentIdx = 0) { return this.dataCollectors[groupIdx][agentIdx].trainingData; } clearTrainingData() { this.dataCollectors.forEach((collectorGroup) => { collectorGroup.forEach((collector) => { collector.reset(); }); }); } createContinuousActionArray(rawAction, actionGroup) { return this.actionConfig.metadata[actionGroup].order.map((name) => rawAction[name]); } convertActionToOneHot(rawAction, actionGroup) { return [this.actionConfig.metadata[actionGroup].order.map((name) => { return rawAction[name] ? 1 : 0; })]; } convertActionsForCollect(rawAction) { if (!Array.isArray(rawAction)) { const validTypes = { discrete: "boolean", continuous: "number" }; const action = {}; this.actionConfig.heads.forEach((actionGroup) => { const { actionType, order } = this.actionConfig.metadata[actionGroup]; const sampleAction = rawAction[order[0]]; if (typeof sampleAction === validTypes[actionType]) { action[actionGroup] = actionType === "discrete" ? this.convertActionToOneHot(rawAction, actionGroup) : this.createContinuousActionArray(rawAction, actionGroup); } }); return action; } return rawAction; } convertToAnalog(action, allowInsideCircle = false) { const { x, y } = action; if (x === void 0 || y === void 0) { throw Error("'x' and 'y' must be defined in the action"); } const magnitude = Math.sqrt(x ** 2 + y ** 2); let scalingFactor = 1 / magnitude; if (allowInsideCircle) { scalingFactor = Math.min(1, scalingFactor); } return { x: x * scalingFactor, y: y * scalingFactor }; } _getNewModel(initialModelConfig, idx = 0) { if (initialModelConfig === void 0) { throw Error("Model config is not defined"); } return new modelsMapping[this.modelTypes[idx]]({ config: initialModelConfig }); } _getNewAgent(initialModelConfig, idx = 0) { const model = this._getNewModel(initialModelConfig); const agent = new SingleAgentWrapper(model, this.agentConfigs[idx]); return [model, agent]; } _createModel(modelData, idx = 0) { this.validateModelType(modelData.config, idx); if (modelData.parameters === void 0) { const newModel = this._getNewModel(modelData.config, idx); return { createdNewModel: true, model: newModel }; } else { return { createdNewModel: false, model: new modelsMapping[this.modelTypes[idx]](modelData) }; } } _createAgent(modelData, agentConfig, idx = 0) { this.agentConfigs[idx] = agentConfig; const { createdNewModel, model } = this._createModel(modelData, idx); return { createdNewModel, model, agent: new SingleAgentWrapper(model, agentConfig) }; } }; // lib/modules/agents/agent-core.ts var AgentCore = class extends GeneralAgentCore { model; trainedModel; agent; trainedAgent; constructor(agentConfig = { useAgentWrapper: false }) { super(); this.agentConfigs[0] = agentConfig; } setAgentConfig(agentConfig, policyMapping = {}) { if (this.model) { this._setAgentConfig(agentConfig); Object.keys(policyMapping).forEach((actionHead) => { const headMetadata = this.actionConfig.metadata[actionHead]; if (headMetadata) { headMetadata.policyMapping = policyMapping[actionHead]; } }); this.agent = new SingleAgentWrapper(this.model, agentConfig); } } createAgent(modelData) { const { createdNewModel, model, agent } = this._createAgent(modelData, this.agentConfigs[0]); this.model = model; this.agent = agent; this.addValidation([this.model.config]); this.initializedBool = true; return createdNewModel; } getProbabilities(inputs, postTrainingBool = false) { inputs = this.validateState(inputs); const model = postTrainingBool && this.trainedModel ? this.trainedModel : this.model; return model.getProbabilities(inputs); } selectAction(inputs, postTrainingBool = false) { inputs = this.validateState(inputs); if (postTrainingBool && this.trainedModel) { if (this.agentConfigs[0]?.useAgentWrapper && this.trainedAgent) { return this.trainedAgent.selectAction(inputs); } else { return this.trainedModel.selectAction(inputs); } } if (this.agentConfigs[0]?.useAgentWrapper) { return this.agent.selectAction(inputs); } else { return this.model.selectAction(inputs); } } clearInputTracker() { this.agent.reset(); } }; var agent_core_default = AgentCore; // lib/modules/helpers/api-client.ts var import_axios = __toESM(require("axios"), 1); // lib/constants.ts var API_BASE_URL = "https://api.nrnagents.ai"; var API_VERSION = "v1"; var API_URL = `${API_BASE_URL}/${API_VERSION}`; var SDK_NAME = "nrn-agents"; var SDK_LANGUAGE = "javascript"; var SDK_VERSION = "0.2.3"; // lib/utils/error-handler.ts var colors = { red: "\x1B[31m", yellow: "\x1B[33m", white: "\x1B[37m", gray: "\x1B[90m", blue: "\x1B[34m", reset: "\x1B[0m" }; var SDKError = class extends Error { constructor(code, message, status = null, details = null) { super(message); this.name = this.constructor.name; this.code = code; this.status = status; this.details = details; Error.captureStackTrace(this, this.constructor); } toString() { const output = []; output.push(`${colors.red} === ${this.name} ===${colors.reset}`); output.push(`${colors.yellow}Code: ${this.code}${colors.reset}`); output.push(`${colors.white}Message: ${this.message}${colors.reset}`); if (this.status) { output.push(`${colors.gray}Status: ${this.status}${colors.reset}`); } if (this.details) { output.push(`${colors.gray}Details:${colors.reset}`); output.push(`${colors.gray}${JSON.stringify(this.details, null, 2)}${colors.reset}`); } if (this.stack) { output.push(`${colors.blue} Stack Trace:${colors.reset}`); const stackLines = this.stack.split("\n").slice(1); output.push(`${colors.gray}${stackLines.join("\n")}${colors.reset}`); } output.push(`${colors.red}${"=".repeat(20)}${colors.reset}`); return output.join("\n"); } toJSON() { return { name: this.name, code: this.code, message: this.message, status: this.status, details: this.details, stack: this.stack }; } }; var APIError = class extends SDKError { constructor(response) { const { status } = response; const { code, message, details, validationErrors } = response.data.error || {}; if (status === 400 && validationErrors) { super("VALIDATION_ERROR", "Validation failed", status, { validationErrors, details }); } else { super(code || "API_ERROR", message || "Unknown API Error", status, details); } } }; var NetworkError = class extends SDKError { constructor(message, details = null) { super("NETWORK_ERROR", message, null, { ...details, timestamp: (/* @__PURE__ */ new Date()).toISOString() }); } }; var ConfigurationError = class extends SDKError { constructor(message, details = null) { super("CONFIGURATION_ERROR", message, null, { ...details, timestamp: (/* @__PURE__ */ new Date()).toISOString() }); } }; var TimeoutError = class extends SDKError { constructor(message = "Request timed out", details = null) { super("TIMEOUT_ERROR", message, null, { ...details, timestamp: (/* @__PURE__ */ new Date()).toISOString() }); } }; // lib/modules/helpers/api-client.ts var APIClient = class _APIClient { static instance = null; apiKey; session; isValidSession; useCookieAuth; gameId; game; isValidGame; chunkingThreshold; backend; client; constructor() { if (_APIClient.instance) { return _APIClient.instance; } this.apiKey = ""; this.session = {}; this.isValidSession = false; this.useCookieAuth = false; this.gameId = ""; this.game = {}; this.isValidGame = false; this.chunkingThreshold = 1e4; this._createClient(API_URL); _APIClient.instance = this; } static getInstance() { if (!_APIClient.instance) { _APIClient.instance = new _APIClient(); } return _APIClient.instance; } _createClient(backend) { this.backend = backend; this.client = import_axios.default.create({ baseURL: this.backend, timeout: 3e4, withCredentials: true, headers: { "Content-Type": "application/json" } }); this._setupInterceptors(); } _setupInterceptors() { this.client.interceptors.request.use( (config) => { if (!this.apiKey && !this.useCookieAuth) { throw new ConfigurationError("API key not set"); } config.headers = { ...config.headers, ...!this.useCookieAuth && { "x-api-key": this.apiKey }, "x-sdk-name": SDK_NAME, "x-sdk-language": SDK_LANGUAGE, "x-sdk-version": SDK_VERSION }; return config; }, (error) => Promise.reject(error) ); this.client.interceptors.response.use( (response) => response, (error) => this._handleError(error, true) ); } _handleError(error, throwError = false) { let sdkError; if (error.response?.data?.error) { sdkError = new APIError(error.response); } else if (error.code === "ERR_NETWORK") { sdkError = new NetworkError( "Unable to connect to the server. Please check your internet connection.", { originalError: error.message } ); } else if (error.code === "ECONNABORTED") { sdkError = new TimeoutError("The request timed out. Please try again.", { originalError: error.message }); } else { sdkError = error; } if (throwError) { throw sdkError; } return sdkError; } async setApiKey(apiKey) { this.apiKey = apiKey; return await this.validateSession(); } async setGameId(gameId) { this.gameId = gameId; return await this.validateGame(); } async overrideBackendUrl(newBackend) { this._createClient(newBackend); return await this.validateSession(); } async setUseCookieAuth(useCookieAuth) { this.useCookieAuth = useCookieAuth; this._setupInterceptors; return await this.validateSession(); } setChunkingThreshold(threshold) { this.chunkingThreshold = threshold; } async validateGame() { this.isValidGame = false; this.game = {}; if (!this.gameId) { return false; } try { const response = await this.get(`/games/${this.gameId}`); this.game = response.data; this.isValidGame = true; return true; } catch { return false; } } async validateSession() { this.isValidSession = false; this.session = {}; if (!this.apiKey) { return false; } try { const response = await this.get("/auth/me"); const data = response.data; if (typeof data.id !== "string" || !data.id.startsWith("usr_") || typeof data.name !== "string" || typeof data.username !== "string" || typeof data.email !== "string" || typeof data.isRegistered !== "boolean" || typeof data.isAdmin !== "boolean") { return false; } this.session = data; this.isValidSession = true; return true; } catch { return false; } } async get(path, config = {}) { return this.client.get(path, config); } async post(path, data = {}, config = {}) { return this.client.post(path, data, config); } async put(path, data = {}, config = {}) { return this.client.put(path, data, config); } async delete(path, config = {}) { return this.client.delete(path, config); } async patch(path, data = {}, config = {}) { return this.client.patch(path, data, config); } async chunkedUpload(endpoint, body, dataToChunk, options = {}) { const { onProgress, retryAttempts = 3, retryDelay = 1e3 } = options; const totalChunks = Math.ceil(dataToChunk.length / this.chunkingThreshold) || 1; let chunkId; let response; for (let chunkNumber = 0; chunkNumber < totalChunks; chunkNumber++) { const currentChunk = dataToChunk.slice( chunkNumber * this.chunkingThreshold, (chunkNumber + 1) * this.chunkingThreshold ); const chunkData = { ...body, data: currentChunk, totalChunks: totalChunks > 1 ? totalChunks : void 0, chunkNumber: totalChunks > 1 ? chunkNumber : void 0, chunkId }; let attempts = 0; while (attempts < retryAttempts) { try { response = await this.post(endpoint, chunkData); if (chunkNumber === 0) { chunkId = response.data.id; } if (onProgress) { onProgress({ chunk: chunkNumber + 1, totalChunks, progress: (chunkNumber + 1) / totalChunks * 100 }); } break; } catch (error) { attempts++; if (attempts === retryAttempts) { throw error; } await new Promise( (resolve) => setTimeout(resolve, retryDelay * Math.pow(2, attempts - 1)) ); } } } return response.data; } }; var api_client_default = APIClient; // lib/modules/agents/base-agent.ts var BaseAgent = class extends agent_core_default { api; id; name; architecture; owner; agentType; modelData; constructor(agentData, options = { owner: void 0, agentConfig: void 0, delayInit: true }) { super(options.agentConfig); this.api = api_client_default.getInstance(); this.id = agentData.id; this.name = agentData.name; this.architecture = agentData.architecture; this.owner = options.owner || { type: "user", id: this.api.session.id }; if (!options.delayInit) { this.initialize(); } this.agentType = "base"; } setName(newName) { this.name = newName; } async initialize() { try { const validBool = await this._getModelData(); if (validBool) { const createdNewModel = this.createAgent(this.modelData); delete this.modelData; if (createdNewModel) { console.log("CREATED NEW MODEL - SHOULD WE SAVE?"); } } else { throw Error("Could not fetch model"); } } catch { throw Error("Failed to initialize model"); } } _getAgentUrl() { return `/games/${this.api.gameId}/agents/${this.id}`; } _getDataCreateUrl() { return `/games/${this.api.gameId}/datasets`; } async _getModelData() { try { const modelDataResponse = await this.api.get(this._getAgentUrl()); this.name = modelDataResponse.data.name; this.architecture = modelDataResponse.data.architecture; if (this.architecture.config.stateSpaceConfig) { feature_engineering_default.setStateConfig(this.architecture.config.stateSpaceConfig.config); } this.modelData = {}; if (modelDataResponse.data.modelData) { if (modelDataResponse.data.modelData.parameters) { this.modelData.parameters = modelDataResponse.data.modelData.parameters; } else if (modelDataResponse.data.modelData.frequencies) { this.modelData.frequencies = modelDataResponse.data.modelData.frequencies; } this.modelData.config = { ...modelDataResponse.data.modelData.config, modelType: this.architecture.modelType }; } return true; } catch (err) { if (err.response?.data) console.log(err.response?.data); return false; } } async uploadData(contributionMapId = "") { try { await this.api.chunkedUpload( `${this._getDataCreateUrl()}/${contributionMapId}`, { agentId: this.id, agentType: this.agentType }, this.getTrainingData() ); return true; } catch (err) { console.log(err); console.log(err.response?.data); return false; } } async save(newModelBool = false) { throw Error("Still in development"); } async delete() { await this.api.delete(this._getAgentUrl()); } }; var base_agent_default = BaseAgent; // lib/modules/agents/imitation-learning-agent.ts var ImitationLearningAgent = class extends base_agent_default { constructor(agentData, options) { super(agentData, options); this.agentType = "imitation"; } async train(trainingData, config) { try { const configMapping = { "simple": { updatableCells: [], multiplier: 1 }, "neural-network": { epochs: 2500, batchSize: 32 } }; return true; } catch (err) { console.log(err.response?.data); return false; } } async save(newModelBool = false) { await super.save(newModelBool); if (!newModelBool) { this.discardTraining(true); } } discardTraining(discardData = false) { this.trainedModel = null; this.trainedAgent = null; if (discardData) { this.clearTrainingData(); } } }; var imitation_learning_agent_default = ImitationLearningAgent; // lib/modules/agents/reinforcement-learning-agent.ts var ReinforcementLearningAgent = class extends base_agent_default { constructor(agentData, options) { super(agentData, options); this.agentType = "reinforcement"; } }; var reinforcement_learning_agent_default = ReinforcementLearningAgent; // lib/modules/agents/demo-agent.ts var DemoAgent = class extends agent_core_default { constructor(modelData, agentConfig) { super(agentConfig); this.createAgent(modelData); } async downloadParameters() { throw Error("Ability to download parameters is not implemented yet"); } }; var demo_agent_default = DemoAgent; // lib/modules/agents/trainable-demo-agent.ts var TrainableDemoAgent = class extends demo_agent_default { api; constructor(modelData, agentConfig) { super(modelData, agentConfig); this.api = api_client_default.getInstance(); } async train(config) { const configMapping = { simple: { updatableCells: [], multiplier: 1 }, "neural-network": { epochs: 200, batchSize: 32 } }; try { const modelData = { config: { ...this.model.config, modelType: this.modelTypes[0] } }; if (this.modelTypes[0] === "neural-network") { modelData.parameters = this.model.parameters; } else if (this.modelTypes[0] === "simple") { modelData.frequencies = this.model.frequencies; } const trainingResponse = await this.api.post("imitation/train-demo", { modelData, trainingData: this.getTrainingData(),