/** Small, deterministic teaching models. No recorded neural or participant data. */ export class NeuronModel { constructor(){this.reset()} reset(){this.t=0;this.v=-65;this.refractory=0;this.spikes=[];this.count=0;this.trace=[{t:0,v:-65,spike:false}];} pulse(amount=8,{threshold=-50,refractory=4}={}){ if(this.refractory>0)return false; this.v+=amount;if(this.v>=threshold){this.count++;this.spikes.push(this.t);this.v=-65;this.refractory=refractory;this.trace.push({t:this.t,v:20,spike:true});} return true; } step(duration,{excitation=18,inhibition=0,threshold=-50,tau=20,refractory=4,pattern='steady'}={}){ if(!Number.isFinite(duration)||duration<0||duration>10000)throw Error('Choose a simulation interval between 0 and 10,000 ms.'); if(!(tau>0)||refractory<0||threshold<=-65)throw Error('Invalid neuron settings.'); const end=this.t+duration; while(this.t0){this.refractory=Math.max(0,this.refractory-dt);this.v=-65;} else {const input=(pattern==='steady'||this.t%100<40?excitation:0)-inhibition,target=-65+input; this.v=target+(this.v-target)*Math.exp(-dt/tau); if(this.v>=threshold){spike=true;this.count++;this.spikes.push(this.t+dt);this.v=-65;this.refractory=refractory;} } this.t+=dt;this.trace.push({t:this.t,v:spike?20:this.v,spike}); } this.trace=this.trace.filter(row=>row.t>=this.t-1000);this.spikes=this.spikes.filter(t=>t>=this.t-1000); return this; } rate(){const window=Math.min(500,this.t);return window>0?this.spikes.filter(t=>t>this.t-window).length*1000/window:0;} } export class AssociationModel { constructor(){this.reset()} reset(){this.weights={A:0,B:0};this.history=[];} trial(cues=['A'],outcome=1,rate=.2){ if(!Array.isArray(cues)||!cues.length||new Set(cues).size!==cues.length||cues.some(c=>!['A','B'].includes(c)))throw Error('Choose cue A, B, or A + B.'); if(![0,1].includes(outcome)||!(rate>0&&rate<=.5))throw Error('Invalid learning settings.'); if(this.history.length>=5000)throw Error('This run has reached 5,000 trials. Reset learning to start another.'); const prediction=cues.reduce((sum,cue)=>sum+this.weights[cue],0),error=outcome-prediction; for(const cue of cues)this.weights[cue]+=rate*error; const row={trial:this.history.length+1,cues:cues.join('+'),outcome,prediction,error,A:this.weights.A,B:this.weights.B,rate};this.history.push(row);return row; } }