/*
* 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());
}
}
}
}
}