/* * BestPEST.cs * Authors: August Zinsser, Adam Nabinger * * Copyright Matthew Belmonte 2007 */ using System; using System.Collections.Generic; namespace tAC_Engine { /// /// Stores a list of previous trial value-response pairs and calculates the next value that would yield the maximum information gained. /// Clients of this class must call Record(...) after each trial and then NextValue() to retrieve the next value to present /// public class BestPEST { /// /// Used to hold the value-response pairs. Response = -1 corresponds to a "No" response, and +1 corresponds to a "Yes" response /// NOTE: -1 does NOT necessarily mean the user answered incorrectly and similarly +1 does not necessarily mean the user answered correctly /// private class ValueResponsePair { private double mValue; private double mResponse; public double Value { set { mValue = value; } get { return mValue; } } public bool Response { set { mResponse = value ? 1 : -1; } get { return mResponse == 1; } } public double ResponseValue { get { return mResponse; } } /// /// Used to store each response and its corresponding value /// /// Level of stimulus presented /// Did the user detect it? True = 1, False = -1 public ValueResponsePair(double value, bool response) { mValue = value; mResponse = response ? 1 : -1; } internal string toDataString() { return mValue.ToString("R") + separator + mResponse.ToString("R"); } internal static ValueResponsePair fromDataString(string data) { string[] str = data.Split(separator); return new ValueResponsePair(double.Parse(str[0]), (double.Parse(str[1]) == 1)); } } private const string filename = @"LogFiles\PEST.dat"; private const char separator = ':'; private List mTrials = new List(); // Catalogues previous trial results private double mLastValue = 0; // Last non-zero coherence value private const double mMinL = -1; // Minimum stimulus value to test on the (assumed sigmoid) user's response curve private const double mMaxL = 1; // Max stimulus level private const double mStepSize = .001f; // Defines the interval between legal values to test for maximization of information gain by MLE public double LastNonZeroCoherence { get { return mLastValue; } } /// /// Sets up the pest algorithm to return values between -1 and 1. /// public BestPEST() { if (System.IO.File.Exists(filename)) { try { load(); return; } catch (Exception e) { Logger.LogCode(Logger.ExperimentalCode.DEBUG_Misc, "Error loading PEST data: " + e); } } // Place data points at the two endpoints to give the algorithm something to start with Record(-1, false); Record(1, true); } /// /// Records a trial's value along with its response /// /// Presented level of stimulus /// User response (true for "Present", false for Not "Present") referring to the presence of stimulus /// True if the value is valid and the trial was recorded, flase otherwise public bool Record(double value, bool response) { if (value > 0) mLastValue = value; if (value <= mMaxL && value >= mMinL) { ValueResponsePair trial = new ValueResponsePair(value, response); mTrials.Add(trial); append(trial); return true; } return false; } /// /// Uses Maximum Likelihood Estimation to return the value that will grant the maximum information gain on the next trial /// /// public double NextValue() { // Find the value x that maximizes the equation: // max(Product(from j=1 to j=n-1) (1 + e^(-r(j)*(m(j)-x)))^-1) // Return this value as the next level of stimulus to present double curMaxSum = double.NegativeInfinity; double curMaxL = 0; int n = mTrials.Count; double sum; // This maximization algorithm could be optimized if it becomes a bottleneck for (double x = mMinL; x <= mMaxL; x += mStepSize) { sum = 0; foreach (ValueResponsePair trial in mTrials) { sum -= Math.Log10(1.0 + 1.0 / (Math.Exp(trial.ResponseValue * (trial.Value - x)))); } if (sum > curMaxSum) { curMaxSum = sum; curMaxL = x; } } // Make sure to never return exactly the min value since the game interprets that as a negative trial if (curMaxL == mMinL) { curMaxL += mStepSize * .1f; } return curMaxL; } /// /// Load saved data from the file. /// private void load() { using (System.IO.TextReader reader = new System.IO.StreamReader(filename)) { string line; while ((line = reader.ReadLine()) != null) { mTrials.Add(ValueResponsePair.fromDataString(line)); } } } /// /// Append a single trial to the file. /// /// /// private void append(ValueResponsePair trial) { using (System.IO.TextWriter writer = new System.IO.StreamWriter(filename, true)) { writer.WriteLine(trial.toDataString()); } } /// /// Save all trials to the file. /// private void save() { using (System.IO.TextWriter writer = new System.IO.StreamWriter(filename)) { foreach (ValueResponsePair pair in mTrials) { writer.WriteLine(pair.toDataString()); } } } } }