Skip to content

Commit

Permalink
add randomseed
Browse files Browse the repository at this point in the history
  • Loading branch information
AnyaWMa committed Sep 1, 2022
1 parent a8ac130 commit 60571e6
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 51 deletions.
26 changes: 25 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"devDependencies": {
"@types/jest": "^28.1.6",
"@types/lodash": "^4.14.182",
"@types/seedrandom": "^3.0.2",
"@typescript-eslint/eslint-plugin": "^5.30.7",
"@typescript-eslint/parser": "^5.30.7",
"eslint": "^8.20.0",
Expand All @@ -47,6 +48,7 @@
},
"dependencies": {
"lodash": "^4.17.21",
"optimization-js": "^0.0.0"
"optimization-js": "^0.0.0",
"seedrandom": "^3.0.5"
}
}
69 changes: 45 additions & 24 deletions src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,35 @@ import { Cat } from '../index';
import { Stimulus } from '../type';

describe('Cat', () => {
const cat1 = new Cat('MLE', 'MFI');
it('constructs an adaptive test', () => {
let cat1: Cat, cat2: Cat, cat3: Cat, cat4: Cat;
beforeEach(() => {
cat1 = new Cat();
const theta = cat1.updateAbilityEstimate([
{ a: 2.225, b: -1.885, c: 0.21, d: 1 },
{ a: 1.174, b: -2.411, c: 0.212, d: 1 },
{ a: 2.104, b: -2.439, c: 0.192, d: 1 }
], [1, 0, 1]);
cat2 = new Cat( {nStartItems: 1, startSelect: "miDdle"});
cat3 = new Cat();
cat4 = new Cat({itemSelect:'RANDOM', randomSeed: "test"});
});

const s1: Stimulus = { difficulty: 0.5, word: 'looking' };
const s2: Stimulus = { difficulty: 3.5, word: 'opaque' };
const s3: Stimulus = { difficulty: 2, word: "right" };
const s4: Stimulus = { difficulty: -2.5, word: 'yes' };
const s5: Stimulus = { difficulty: -1.8, word: 'mom' };
const stimuli = [s1, s2, s3, s4, s5];

//const cat1 = new Cat('MLE', 'MFI');
it('constructs an adaptive test', () => {
expect(cat1.method).toBe('mle');
expect(cat1.itemSelect).toBe('mfi');
})

it('correctly updates ability estimate', () => {
const theta = cat1.updateAbilityEstimate([
{ a: 2.225, b: -1.885, c: 0.21, d: 1 },
{ a: 1.174, b: -2.411, c: 0.212, d: 1 },
{ a: 2.104, b: -2.439, c: 0.192, d: 1 }
], [1, 0, 1]);
expect(theta).toBeCloseTo(-1.642307, 1)
expect(cat1.theta).toBeCloseTo(-1.642307, 1)
})

const cat2 = new Cat('MLE', 'MFI');
it('correctly updates standard error of mean of ability estimate', () => {
const theta = cat2.updateAbilityEstimate([
{ a: 1, b: -0.4473004, c: 0.5, d: 1 },
Expand All @@ -31,30 +42,40 @@ describe('Cat', () => {
{ a: 1, b: 0.5293703, c: 0.5, d: 1 }
], [1, 1, 1, 1, 1, 0, 1]);
expect(cat2.seMeasurement).toBeCloseTo(1.455, 1)
})

it('correctly updates number of items', () => {
expect(cat2.nItems).toEqual(7);
})

const cat3 = new Cat('MLE', 'MFI');
const s1: Stimulus = { difficulty: 0.5, word: 'looking' };
const s2: Stimulus = { difficulty: 3.5, word: 'opaque' };
const s3: Stimulus = { difficulty: 2, word: "right" };
const s4: Stimulus = { difficulty: -2.5, word: 'yes' };
const s5: Stimulus = { difficulty: -1.8, word: 'mom' };
const stimuli = [s1, s2, s3, s4, s5];
it('correctly suggests the next item (closest method)', () => {
const expected = { nextStimulus: s5, remainingStimuli: [s4, s1, s3, s2] };
const received = cat1.findNextItem(stimuli, 'closest');
expect(received).toEqual(expected);
})

it('correctly suggests the next item (mfi method)', () => {
const expected = { nextStimulus: s1, remainingStimuli: [s4, s5, s3, s2] };
const received = cat3.findNextItem(stimuli, 'MFI', true);
const received = cat3.findNextItem(stimuli, 'MFI');
expect(received).toEqual(expected);
})

it('correctly suggests the next item (closest method)', () => {
const expected = { nextStimulus: s5, remainingStimuli: [s4, s1, s3, s2] };
const received = cat1.findNextItem(stimuli, 'closest', true);

it('correctly suggests the next item (middle method)', () => {
const expected = { nextStimulus: s1, remainingStimuli: [s4, s5, s3, s2] };
const received = cat2.findNextItem(stimuli);
expect(received).toEqual(expected);
})

it('correctly suggests the next item (random method)', () => {

let received;
received = cat4.findNextItem(stimuli);
expect(received.nextStimulus).toEqual(s1);
received = cat4.findNextItem(received.remainingStimuli);
expect(received.nextStimulus).toEqual(s3);
received = cat4.findNextItem(received.remainingStimuli);
expect(received.nextStimulus).toEqual(s5);
received = cat4.findNextItem(received.remainingStimuli);
expect(received.nextStimulus).toEqual(s2);

})

});
68 changes: 43 additions & 25 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,22 @@ import { minimize_Powell } from 'optimization-js';
import { cloneDeep } from 'lodash';
import {Stimulus, Zeta} from './type';
import {itemResponseFunction, fisherInformation, normal} from './utils';
import seedrandom from 'seedrandom';

export const abilityPrior = normal();

export interface CatInput {
method?: string;
itemSelect?: string;
nStartItems?: number;
startSelect?: string;
theta?: number;
minTheta?: number;
maxTheta?: number;
prior?: number[][];
randomSeed?: string | null
}

export class Cat {
public method: string;
public itemSelect: string;
Expand All @@ -18,26 +31,32 @@ export class Cat {
private _seMeasurement: number;
public nStartItems: number;
public startSelect: string;
private readonly _rng: Function;


/**
*
* @param method - ability estimator, e.g. MLE or EAP, default = 'MLE'
* @param itemSelect - the method of item selection, e.g. "MFI", "random", "closest", default method = 'MFI'
* @param theta - initial theta estimate
* @param minTheta - lower bound of theta
* @param maxTheta - higher bound of theta
* @param prior - the prior distribution
* @param nStartItems - first n trials to keep non-adaptive selection
* @param startSelect - rule to select first n trials
* Create a Cat object. This expects an single object parameter with the following keys
* @param {{method: string, itemSelect: string, nStartItems: number, startSelect:string, theta: number, minTheta: number, maxTheta: number, prior: number[][]}=} destructuredParam
* method: ability estimator, e.g. MLE or EAP, default = 'MLE'
* itemSelect: the method of item selection, e.g. "MFI", "random", "closest", default method = 'MFI'
* nStartItems: first n trials to keep non-adaptive selection
* startSelect: rule to select first n trials
* theta: initial theta estimate
* minTheta: lower bound of theta
* maxTheta: higher bound of theta
* prior: the prior distribution
* randomSeed: set a random seed to trace the simulation
*/
constructor(method: string = 'MLE',
itemSelect: string = 'MFI',
theta = 0,
minTheta = -4,

constructor({method = 'MLE',
itemSelect = 'MFI',
nStartItems = 0,
startSelect = 'middle',
theta = 0,
minTheta = -4,
maxTheta = 4,
prior = abilityPrior,
nStartItems = 0,
startSelect = 'middle') {
randomSeed = null}: CatInput = {}) {

this.method = Cat.validateMethod(method);

Expand All @@ -54,7 +73,7 @@ export class Cat {
this._nItems = 0;
this._seMeasurement = Infinity;
this.nStartItems = nStartItems;
this.startSelect = startSelect;
this._rng = (randomSeed === null) ? seedrandom() : seedrandom(randomSeed);
}

public get theta() {
Expand Down Expand Up @@ -183,7 +202,7 @@ export class Cat {
/**
* find the next available item from an input array of stimuli based on a selection method
* @param stimuli - an array of stimulus
* @param itemSelect -
* @param itemSelect - the item selection method
* @param deepCopy - default deepCopy = true
* @returns {nextStimulus: Stimulus,
remainingStimuli: Array<Stimulus>}
Expand All @@ -201,19 +220,18 @@ export class Cat {
selector = this.startSelect
}

if (selector === 'mfi') {
return this.selectorMFI(arr);
} else if (selector === 'middle') { // middle will only be used in startSelect
if (selector === 'middle') { // middle will only be used in startSelect
return this.selectorMiddle(arr);
} else if (selector === 'closest') {
return this.selectorClosest(arr);
} else if (selector === 'random') {
return Cat.selectorRandom(arr);
return this.selectorRandom(arr);
} else {
return this.selectorMFI(arr);
}
}

private selectorMFI(arr: Stimulus[]){
console.log(arr);
const stimuliAddFisher = arr.map((element: Stimulus) => ({
fisherInformation: fisherInformation(this._theta, {a: 1, b: element.difficulty, c: 0.5, d: 1}),
...element,
Expand All @@ -222,7 +240,6 @@ export class Cat {
stimuliAddFisher.forEach((stimulus: Stimulus) => {
delete stimulus['fisherInformation'];
});
console.log(1);
return {
nextStimulus: stimuliAddFisher[0],
remainingStimuli: stimuliAddFisher.slice(1),
Expand Down Expand Up @@ -255,8 +272,9 @@ export class Cat {
};
}

private static selectorRandom(arr: Stimulus[]){
const index = Math.floor(Math.random() * arr.length);
private selectorRandom(arr: Stimulus[]){

const index = Math.floor(this._rng() * arr.length);
const nextItem = arr.splice(index, 1)[0];
return {
nextStimulus: nextItem,
Expand Down

0 comments on commit 60571e6

Please sign in to comment.