UNPKG

@bdelab/jscat

Version:

A library to support IRT-based computer adaptive testing in JavaScript

184 lines (183 loc) 7.4 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.StopIfSEMeasurementBelowThreshold = exports.StopAfterNItems = exports.StopOnSEMeasurementPlateau = exports.EarlyStopping = void 0; const uniq_1 = __importDefault(require("lodash/uniq")); /** * Abstract class for early stopping strategies. */ class EarlyStopping { constructor({ logicalOperation = 'or' }) { this._seMeasurements = {}; this._nItems = {}; this._earlyStop = false; if (!['and', 'or', 'only'].includes(logicalOperation.toLowerCase())) { throw new Error(`Invalid logical operation. Expected "and", "or", or "only". Received "${logicalOperation}"`); } this._logicalOperation = logicalOperation.toLowerCase(); } get earlyStop() { return this._earlyStop; } get nItems() { return this._nItems; } get seMeasurements() { return this._seMeasurements; } get logicalOperation() { return this._logicalOperation; } /** * Update the internal state of the early stopping strategy based on the provided cats. * @param {CatMap<Cat>}cats - A map of cats to update. */ _updateCats(cats) { var _a, _b; for (const catName in cats) { const cat = cats[catName]; const nItems = cat.nItems; const seMeasurement = cat.seMeasurement; if (nItems > ((_a = this._nItems[catName]) !== null && _a !== void 0 ? _a : 0)) { this._nItems[catName] = nItems; this._seMeasurements[catName] = [...((_b = this._seMeasurements[catName]) !== null && _b !== void 0 ? _b : []), seMeasurement]; } } } /** * Abstract method to be implemented by subclasses to update the early stopping strategy. * @param {CatMap<Cat>} cats - A map of cats to update. */ update(cats, catToSelect) { this._updateCats(cats); // This updates internal state with current cat data // Collect the stopping conditions for all cats const conditions = this.evaluationCats.map((catName) => this._evaluateStoppingCondition(catName)); // Evaluate the stopping condition based on the logical operation if (this._logicalOperation === 'and') { this._earlyStop = conditions.every(Boolean); // All conditions must be true for 'and' } else if (this._logicalOperation === 'or') { this._earlyStop = conditions.some(Boolean); // Any condition can be true for 'or' } else if (this._logicalOperation === 'only') { if (catToSelect === undefined) { throw new Error('Must provide a cat to select for "only" stopping condition'); } // Evaluate the stopping condition for the selected cat if (this.evaluationCats.includes(catToSelect)) { this._earlyStop = this._evaluateStoppingCondition(catToSelect); } else { this._earlyStop = false; // Default to false if the selected cat is not in evaluationCats } } } } exports.EarlyStopping = EarlyStopping; /** * Class implementing early stopping based on a plateau in standard error of measurement. */ class StopOnSEMeasurementPlateau extends EarlyStopping { constructor(input) { var _a; super(input); this._patience = input.patience; this._tolerance = (_a = input.tolerance) !== null && _a !== void 0 ? _a : {}; } get evaluationCats() { return (0, uniq_1.default)([...Object.keys(this._patience), ...Object.keys(this._tolerance)]); } get patience() { return this._patience; } get tolerance() { return this._tolerance; } _evaluateStoppingCondition(catToEvaluate) { const seMeasurements = this._seMeasurements[catToEvaluate]; // Use MAX_SAFE_INTEGER and MAX_VALUE to prevent early stopping if the `catToEvaluate` is missing from the cats map. const patience = this._patience[catToEvaluate]; const tolerance = this._tolerance[catToEvaluate]; let earlyStop = false; if ((seMeasurements === null || seMeasurements === void 0 ? void 0 : seMeasurements.length) >= patience) { const mean = seMeasurements.slice(-patience).reduce((sum, se) => sum + se, 0) / patience; const withinTolerance = seMeasurements.slice(-patience).every((se) => Math.abs(se - mean) <= tolerance); if (withinTolerance) { earlyStop = true; } } return earlyStop; } } exports.StopOnSEMeasurementPlateau = StopOnSEMeasurementPlateau; /** * Class implementing early stopping after a certain number of items. */ class StopAfterNItems extends EarlyStopping { constructor(input) { super(input); this._requiredItems = input.requiredItems; } get requiredItems() { return this._requiredItems; } get evaluationCats() { return Object.keys(this._requiredItems); } _evaluateStoppingCondition(catToEvaluate) { const requiredItems = this._requiredItems[catToEvaluate]; const nItems = this._nItems[catToEvaluate]; let earlyStop = false; if (nItems >= requiredItems) { earlyStop = true; } return earlyStop; } } exports.StopAfterNItems = StopAfterNItems; /** * Class implementing early stopping if the standard error of measurement drops below a certain threshold. */ class StopIfSEMeasurementBelowThreshold extends EarlyStopping { constructor(input) { var _a, _b; super(input); this._seMeasurementThreshold = input.seMeasurementThreshold; this._patience = (_a = input.patience) !== null && _a !== void 0 ? _a : {}; this._tolerance = (_b = input.tolerance) !== null && _b !== void 0 ? _b : {}; } get patience() { return this._patience; } get tolerance() { return this._tolerance; } get seMeasurementThreshold() { return this._seMeasurementThreshold; } get evaluationCats() { return (0, uniq_1.default)([ ...Object.keys(this._patience), ...Object.keys(this._tolerance), ...Object.keys(this._seMeasurementThreshold), ]); } _evaluateStoppingCondition(catToEvaluate) { var _a, _b, _c, _d; const seMeasurements = (_a = this._seMeasurements[catToEvaluate]) !== null && _a !== void 0 ? _a : []; const seThreshold = (_b = this._seMeasurementThreshold[catToEvaluate]) !== null && _b !== void 0 ? _b : 0; const patience = (_c = this._patience[catToEvaluate]) !== null && _c !== void 0 ? _c : 1; const tolerance = (_d = this._tolerance[catToEvaluate]) !== null && _d !== void 0 ? _d : 0; let earlyStop = false; if (seMeasurements.length >= patience) { const withinTolerance = seMeasurements.slice(-patience).every((se) => se - seThreshold <= tolerance); if (withinTolerance) { earlyStop = true; } } return earlyStop; } } exports.StopIfSEMeasurementBelowThreshold = StopIfSEMeasurementBelowThreshold;