/* * BestPEST.cs * Authors: August Zinsser, Adam Nabinger * Copyright (c) 2007-2008 Cornell University This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ using System; using System.Collections.Generic; using System.Globalization; using System.IO; using Util; namespace MaritimeDefender { /// /// 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 { #region Constants // Minimum stimulus value to test on the (assumed sigmoid) user's response curve private const double mMinL = -1; // Max stimulus level private const double mMaxL = 1; // Defines the interval between legal values to test for maximization of information gain by MLE private const double mStepSize = .001f; // The amount of steps between the minimum stimulus level and the maximum stimulus level private const int count = (int)((mMaxL - mMinL) / mStepSize) + 1; #endregion #region Fields // The path name of the file to load/save PEST data private readonly string filename; /// /// This array holds the current function approximation computed by the algorithm. /// This data is persisted between calls, to minimize the necessary computation to choose a value. /// It is also persisted to disk to store the state between executions. /// private readonly double[] workingSet = new double[count]; #endregion #region Creation /// /// Sets up the pest algorithm to return values between -1 and 1. /// /// The path name of the file to load/save best PEST data public BestPEST(string pFilename) { filename = GameScreen.GlobalRootStorageDirectory + UserProfileUtility.OutputDirectory + pFilename; } #endregion #region Initialization /// /// Initializes any needed objects at creation /// public void Initialize() { if (File.Exists(filename)) { try { load(); return; } catch (Exception e) { MainGame.LogError("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); } #endregion #region Management /// /// Preforms any last needed functions /// public void Dispose() { save(); } #endregion #region File I/O /// /// Load saved data from the file. /// private void load() { using (FileStream stream = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read)) using (TextReader reader = new StreamReader(stream)) { for (int i = 0; i < count; ++i) { workingSet[i] = double.Parse(reader.ReadLine(), CultureInfo.InvariantCulture); } } } /// /// Save all trial data to the file. /// private void save() { using (FileStream stream = new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.Read)) using (TextWriter writer = new StreamWriter(stream)) { for (int i = 0; i < count; ++i) { writer.WriteLine(workingSet[i].ToString("R", CultureInfo.InvariantCulture)); } } } #endregion #region PEST Trial /// /// 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, false otherwise public bool Record(double value, bool response) { double scale = Settings.mPsychophysicalSigmoidScale; if (value <= mMaxL && value >= mMinL) { for (int i = 0; i < count; ++i) { workingSet[i] += 1.0 / (1.0 + (Math.Exp((response ? 1 : -1) * scale * (value - (mMinL + (i * mStepSize)))))); } return true; } return false; } /// /// Uses Maximum Likelihood Estimation to return the value that will grant the maximum information gain on the next trial /// Find the value x that maximizes the equation: /// min(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 /// public double NextValue() { double curMinSum = double.PositiveInfinity; double curMinI = 0; for (int i = 0; i < count; ++i) { if (workingSet[i] < curMinSum) { curMinSum = workingSet[i]; curMinI = i; } } double curMinL = mMinL + (curMinI * mStepSize); // Make sure to never return exactly the min value since the game interprets that as a negative trial? if (curMinL == mMinL) { curMinL += mStepSize * .1f; } return curMinL; } #endregion } }