@bdelab/jscat
Version:
A library to support IRT-based computer adaptive testing in JavaScript
184 lines (183 loc) • 7.4 kB
JavaScript
"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;