UNPKG

@bdelab/jscat

Version:

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

165 lines (135 loc) 6.25 kB
import { itemResponseFunction, fisherInformation, findClosest, normal, uniform } from '../utils'; describe('itemResponseFunction', () => { it('correctly calculates the probability', () => { expect(itemResponseFunction(0, { a: 1, b: -0.3, c: 0.35, d: 1 })).toBeCloseTo(0.7234, 2); expect(itemResponseFunction(0, { a: 1, b: 0, c: 0, d: 1 })).toBeCloseTo(0.5, 2); expect(itemResponseFunction(0, { a: 0.5, b: 0, c: 0.25, d: 1 })).toBeCloseTo(0.625, 2); }); }); describe('fisherInformation', () => { it('correctly calculates the information', () => { expect(fisherInformation(0, { a: 1.53, b: -0.5, c: 0.5, d: 1 })).toBeCloseTo(0.206, 2); expect(fisherInformation(2.35, { a: 1, b: 2, c: 0.3, d: 1 })).toBeCloseTo(0.1401, 2); }); }); describe('findClosest', () => { const stimuli = [ { difficulty: 1, discrimination: 1, guessing: 0.25, slipping: 0.75 }, { difficulty: 4, discrimination: 1, guessing: 0.25, slipping: 0.75 }, { difficulty: 10, discrimination: 1, guessing: 0.25, slipping: 0.75 }, { difficulty: 11, discrimination: 1, guessing: 0.25, slipping: 0.75 }, ]; it('correctly selects the first item if appropriate', () => { expect(findClosest(stimuli, 0)).toBe(0); }); it('correctly selects the last item if appropriate', () => { expect(findClosest(stimuli, 1000)).toBe(3); }); it('correctly selects a middle item if it equals exactly', () => { expect(findClosest(stimuli, 10)).toBe(2); }); it('correctly selects the one item closest to the target if less than', () => { const stimuliWithDecimal = [ { difficulty: 1.1, discrimination: 1, guessing: 0.25, slipping: 0.75 }, { difficulty: 4.2, discrimination: 1, guessing: 0.25, slipping: 0.75 }, { difficulty: 10.3, discrimination: 1, guessing: 0.25, slipping: 0.75 }, { difficulty: 11.4, discrimination: 1, guessing: 0.25, slipping: 0.75 }, ]; expect(findClosest(stimuliWithDecimal, 5.1)).toBe(1); }); it('correctly selects the one item closest to the target if greater than', () => { const stimuliWithDecimal = [ { difficulty: 1.1, discrimination: 1, guessing: 0.25, slipping: 0.75 }, { difficulty: 4.2, discrimination: 1, guessing: 0.25, slipping: 0.75 }, { difficulty: 10.3, discrimination: 1, guessing: 0.25, slipping: 0.75 }, { difficulty: 11.4, discrimination: 1, guessing: 0.25, slipping: 0.75 }, ]; expect(findClosest(stimuliWithDecimal, 9.1)).toBe(2); }); }); describe('normal', () => { it('should create a normal distribution with default parameters', () => { const dist = normal(); expect(dist.length).toBeGreaterThan(0); expect(dist[0].length).toBe(2); // Each point should have x and y coordinates // Find the peak of the distribution const maxY = Math.max(...dist.map((p) => p[1])); const peakPoint = dist.find((p) => p[1] === maxY); // With default parameters (mean=0), the peak should be at x=0 expect(peakPoint && peakPoint[0]).toBeCloseTo(0, 1); // Default range should be [-4, 4] expect(dist[0][0]).toBeCloseTo(-4, 1); expect(dist[dist.length - 1][0]).toBeCloseTo(4, 1); }); it('should create a normal distribution with custom mean and standard deviation', () => { const mean = 2; const stdDev = 0.5; const dist = normal(mean, stdDev); // Find the peak of the distribution const maxY = Math.max(...dist.map((p) => p[1])); const peakPoint = dist.find((p) => p[1] === maxY); // Peak should be at x = mean expect(peakPoint && peakPoint[0]).toBeCloseTo(mean, 1); // With smaller stdDev, peak should be higher than default expect(maxY).toBeGreaterThan(0.4); // Normal peak with stdDev=1 is ~0.4 }); it('should respect custom min and max range', () => { const min = -2; const max = 2; const dist = normal(0, 1, min, max); // Check range bounds expect(dist[0][0]).toBeCloseTo(min, 1); expect(dist[dist.length - 1][0]).toBeCloseTo(max, 1); }); it('should use custom step size', () => { const stepSize = 0.5; const dist = normal(0, 1, -4, 4, stepSize); // Check step size between consecutive points for (let i = 1; i < dist.length; i++) { expect(dist[i][0] - dist[i - 1][0]).toBeCloseTo(stepSize, 3); } }); it('should create a symmetric distribution around mean', () => { const mean = 1; const dist = normal(mean, 1, -3, 5); // Asymmetric range but should still be symmetric around mean // Find points equidistant from mean and compare their y-values const tolerance = 0.001; dist.forEach((point) => { const oppositePoint = dist.find((p) => Math.abs(p[0] - (mean + (mean - point[0]))) < tolerance); if (oppositePoint) { expect(point[1]).toBeCloseTo(oppositePoint[1], 5); } }); }); }); describe('uniform', () => { it('outputs correct probabilities and boundaries', () => { const result = uniform(-2, 2, 0.5, -3, 3); const probs = result.map(([, p]: [number, number]) => p); const xs = result.map(([x]: [number, number]) => x); // Probabilities sum to 1 expect(probs.reduce((a: number, b: number) => a + b, 0)).toBeCloseTo(1, 6); // Boundaries have nonzero probability expect(probs[xs.indexOf(-2)]).toBeGreaterThan(0); expect(probs[xs.indexOf(2)]).toBeGreaterThan(0); // Outside bounds are zero expect(probs[xs.indexOf(-3)]).toBeCloseTo(0, 6); expect(probs[xs.indexOf(3)]).toBeCloseTo(0, 6); }); it('probabilities are uniform within support', () => { const result = uniform(-1, 1, 0.5, -2, 2); const probsInSupport = result.filter(([x]) => x >= -1 && x <= 1).map(([, p]) => p); // All probabilities in support should be equal const firstProb = probsInSupport[0]; probsInSupport.forEach((p) => { expect(p).toBeCloseTo(firstProb, 6); }); }); it(`it should use the first two values as the fullmin and fullmax if they are not provided`, () => { const result = uniform(-1, 1, 0.5); const probs = result.map(([, p]: [number, number]) => p); const xs = result.map(([x]: [number, number]) => x); expect(probs.reduce((a: number, b: number) => a + b, 0)).toBeCloseTo(1, 6); expect(xs[0]).toBeCloseTo(-1, 6); }); });