// file: $isip_ifc/class/pr/RelevanceVectorMachine/rvm_07.cc
// version: $Id: rvm_07.cc 10424 2006-02-10 20:37:45Z raghavan $
//

// isip include files
//
#include "RelevanceVectorMachine.h"
#include <MatrixFloat.h>
#include <LinearAlgebra.h>
#include <SingleLinkedList.h>

// method: irlsTrain
//
// argument:
//  RVMTrainData& tdata: data stored for training the models
//
// return: boolean value indicating status
//
// this method completes one pass of IRLS training to update the weights
// given the currently assigne hyperparamters. In this step, we find
// those weights that maximize equation (24) of [1].  The iteratively
// reweighted least squares algorithm is used to find w_mp. It proceeds
// as follows:
//
//   a. initialize
//   b. loop until convergence
//      1. compute the B matrix
//      2. compute the Hessian and the gradient
//      3. update weights with formula
//          new weights = weights - inv(Hessian) * gradient
//      4. check convergence
//
boolean RelevanceVectorMachine::irlsTrain(RVMTrainData& tdata_a) { 

  
  // 1.a. initialize:
  //   curr_weights: holds the weights currently being considered for update
  //
  long start_index = 0;
  if (weights_d.length() < tdata_a.dimA_d) {
    tdata_a.curr_weights_d(0) = bias_d;
    start_index = 1;
  }
  tdata_a.curr_weights_d.move(weights_d, weights_d.length(), 0, start_index);

  // make sure there are some weights that need updating
  //
  if (tdata_a.dimA_d == 0) {
    tdata_a.curr_weights_d.setLength(0);
    tdata_a.old_irls_weights_d.setLength(0);
    tdata_a.gradient_d.setLength(0);
    tdata_a.hessian_d.setDimensions(0,0);
    tdata_a.covar_cholesky_d.setDimensions(0,0);
    tdata_a.sigma_d.assign(0.5);
    tdata_a.B_d.assign(0.25);
    return true;
  }
  
  // debugging information
  //
  if (debug_level_d > Integral::NONE) {
    Console::put(L"IRLS convergence loop");
    Console::increaseIndention();
  }
  
  // 1.b. loop until convergence:
  //   convergence is achieved once the weights do not change over an
  //   iteration. This corresponds to the gradient of the likelihood
  //   function with respect to the weights going to zero.
  //
  boolean irls_converged = false;
  boolean last_iteration = false;
  long num_irls_its = 0;
  float lambda = 1.0;
  boolean reduced = true; // initially true to skip the first iteration
  Float old_likelihood = 0;
  Float new_likelihood = old_likelihood;
  long size_B = tdata_a.B_d.getNumRows();
  VectorFloat tmp_stor;
  
  while (!irls_converged) {
    
    // debugging information
    //
    if (debug_level_d > Integral::NONE) {
      String its(L"IRLS iteration: ");
      its.concat(num_irls_its);
      Console::put(its);
    }
    
    if (debug_level_d > Integral::BRIEF) {
      tdata_a.curr_weights_d.debug(L"current weights");
    }
    
    // copy out the current best weights to the weight vector so they can
    // be used for evaluations
    //
    if (num_irls_its > 0) {
      if (start_index == 1) {
	bias_d = tdata_a.curr_weights_d(0);
      }
      else {
	bias_d = 0.0;
      }
      weights_d.move(tdata_a.curr_weights_d, weights_d.length(),
		     start_index, 0);
    }
    
    // 1.b.1. compute the B matrix and the sigma vector
    //
    computeSigma(tdata_a);
    for (long i = 0; i < size_B; i++) {
      tdata_a.B_d.setValue(i, i, tdata_a.sigma_d(i) * (1-tdata_a.sigma_d(i)));
    }
    
    // debugging information
    //
    if (debug_level_d > Integral::BRIEF) {
      tdata_a.sigma_d.debug(L"sigma");
      VectorFloat tmp_vec;
      tdata_a.B_d.getDiagonal(tmp_vec);
      tmp_vec.debug(L"B");
    }
    
    // if the likelihood decreases then the newton step was too large and needs
    // to be reduced
    //
    new_likelihood = computeLikelihood(tdata_a);
    
    if (debug_level_d > Integral::BRIEF) {
      new_likelihood.debug(L"new likelihood");
    }
    
    if (!reduced) {
      if ((new_likelihood < old_likelihood) || (num_irls_its < 2)) {
	
	// store the weights computed on the last IRLS iteration
	//
	old_likelihood = new_likelihood;
	if (lambda < 0.9) {
	  if (debug_level_d > Integral::BRIEF) {
	    Console::put(L"increasing step parameter");
	  }
	  lambda *= 2.0;
	}
	else {
	  lambda = 1.0;
	}
      }
      else {
	
	// revert to the previous weights
	//
	if (debug_level_d > Integral::BRIEF) {
	  Console::put(L"reducing step parameter");
	}
	tdata_a.curr_weights_d.assign(tdata_a.old_irls_weights_d);
	lambda *= 0.5;
	if (lambda < 0.0001) {
	  last_iteration = true;
	}
	reduced = true;
	continue;
      }
    }
    reduced = false;
    tdata_a.old_irls_weights_d.assign(tdata_a.curr_weights_d);
    
    if (debug_level_d > Integral::BRIEF) {
      tdata_a.curr_weights_d.debug(L"current weights");
    }
    
    if (debug_level_d > Integral::BRIEF) {
      VectorFloat error_signal;
      error_signal.sub(tdata_a.targets_d, tdata_a.sigma_d);
      error_signal.abs();
      Float out = error_signal.sum();
      out.debug(L"sum-squared error");
    }
    
    // 1.b.2. compute the gradient and Hessian
    //
    
    // gradient = phi * [targets_d - sigma] - A * weights
    // sigma is reused as temporary data space in the second segment
    // for computing A * weights.
    //
    boolean status = true;
    status &= tmp_stor.sub(tdata_a.targets_d, tdata_a.sigma_d);
    // REMOVE:
    //    tmp_stor.debug(L"tmp_stor");
    status &= tdata_a.phi_d.multv(tdata_a.gradient_d, tmp_stor);
    status &= tdata_a.A_d.multv(tmp_stor, tdata_a.curr_weights_d);
    status &= tdata_a.gradient_d.sub(tmp_stor);             

    // REMOVE: debug infor please delete after debugging
    //
    // tdata_a.targets_d.debug(L"targets");
//     tdata_a.phi_d.debug(L"phi");
//     tdata_a.sigma_d.debug(L"sigma");
//     tdata_a.A_d.debug(L"A");
//     tdata_a.curr_weights_d.debug(L"weights");
//     tdata_a.gradient_d.debug(L"gradient");
    
    if (!status) {
      tdata_a.gradient_d.debug(L"computed gradient");
      return Error::handle(name(), L"train:gradient computation", ERR,
			   __FILE__, __LINE__);
    }
    
    // debugging information
    //
    if (debug_level_d > Integral::DETAILED) {
      tdata_a.gradient_d.debug(L"gradient");
    }
    
    // Hessian = -(phi * B * transpose(phi) + A)
    // this is a quadratic form about B
    //
    status = true;
    status &= tdata_a.B_d.quadratic(tdata_a.hessian_d,
				  tdata_a.phi_d); // phi * B * phi'
    status &= -(tdata_a.hessian_d.add(tdata_a.A_d));   // phi * B * phi' + A

    // // REMOVE:
//     //
//     tdata_a.B_d.debug(L"B");
//     tdata_a.hessian_d.debug(L"hessian");
    
    if (!status) {
      tdata_a.hessian_d.debug(L"computed hessian");
      return Error::handle(name(), L"train:Hessian computation", ERR,
			   __FILE__, __LINE__);
    }
    
    // debugging information
    //
    if (debug_level_d > Integral::DETAILED) {
      tdata_a.hessian_d.debug(L"Hessian");
    }
    
    // with the cholesky decomposition, we can solve systems of equations
    // as well as determine the diagonal elements of the covariance. This
    // is all we need for the RVM training
    //
    if (!tdata_a.hessian_d.decompositionCholesky(tdata_a.covar_cholesky_d)) {
      tdata_a.sigma_d.debug(L"sigma");
      tdata_a.targets_d.debug(L"targets");
      tdata_a.phi_d.debug(L"phi");
      tdata_a.A_d.debug(L"A");
      tdata_a.gradient_d.debug(L"gradient");
      tdata_a.B_d.debug(L"B");
      tdata_a.hessian_d.debug(L"computed hessian");
      tdata_a.covar_cholesky_d.debug(L"partial cholesky");
      return Error::handle(name(), L"train:Cholesky decomposition", ERR,
			   __FILE__, __LINE__);
    }
    
    // debugging information
    //
    if (debug_level_d > Integral::BRIEF) {
      tdata_a.covar_cholesky_d.debug(L"cholesky decomposition");
    }
    
    // 1.b.3. update the weights:
    //   because the space is convex we take a full Newton step
    //   weights* = weights - inverse(Hessian) * gradient 
    //
    status = true;
    status &= tdata_a.covar_cholesky_d.choleskySolve(tmp_stor,
						   tdata_a.covar_cholesky_d,
						   tdata_a.gradient_d);

    if (!last_iteration) {
      tmp_stor.mult(lambda);
      status &= tdata_a.curr_weights_d.add(tdata_a.old_irls_weights_d,
					 tmp_stor);
      // 1.b.4. check convergence
      //
      if ((tdata_a.gradient_d.norm()/
	   (float)tdata_a.curr_weights_d.length()) < 1e-6) {
	last_iteration = true;
      }
    }
    else {
      irls_converged = true;
    }
    
    if (!status) {
      tdata_a.covar_cholesky_d.debug(L"computed cholesky");
      return Error::handle(name(), L"train:weight update", ERR,
			   __FILE__, __LINE__);
    }
    
    // next iteration
    //
    num_irls_its++;
  }

  if (debug_level_d > Integral::BRIEF) {
    new_likelihood.debug(L"new likelihood");
    String out(L"gradient = ");
    out.concat((tdata_a.gradient_d.norm()/(float)tdata_a.curr_weights_d.length()));
    out.concat(L" after ");
    out.concat(num_irls_its);
    out.concat(L" iterations.");
    Console::put(out);
  }

  // generate the true hessian (negative of the one we store)
  //
  tdata_a.hessian_d.neg();

  computeSigma(tdata_a);

  if (debug_level_d > Integral::DETAILED) {

    tdata_a.curr_weights_d.debug(L"current weights after IRLS");
    VectorFloat tmp_1;
    tmp_1.sub(tdata_a.targets_d, tdata_a.sigma_d);
    VectorFloat tmp_weights;

    MatrixFloat tmp_matrix(tdata_a.A_d);
    tmp_matrix.inverse();
    tmp_matrix.changeType(Integral::FULL);
    tmp_matrix.mult(tdata_a.phi_d);
    tmp_matrix.multv(tmp_weights, tmp_1);
    tmp_weights.debug(L"computed weights after IRLS");
  }

  // debugging information
  //
  if (debug_level_d > Integral::NONE) {
    Console::decreaseIndention();
    Console::put(L"IRLS convergence achieved");
  }
  
  // exit gracefully
  //
  return true;
}


// method: initFull
//
// argument:
//  RVMTrainData& tdata: data stored for training the models
//
// return: boolean value indicating status
//
// initializes the data structures in preparation for a full training
// pass
//
boolean RelevanceVectorMachine::initFull(RVMTrainData& tdata_a) {
  
  // number of samples stored
  //
  tdata_a.num_samples_d = vectors_d.length();
  
  // the weights are a column vector of length N. The bias is stored
  // separately
  //
  weights_d.setLength(tdata_a.num_samples_d);

  // initialize the active parameter list
  //
  tdata_a.active_params_d.setLength(tdata_a.num_samples_d + 1);
  Boolean t = true;
  tdata_a.active_params_d.assign(t);

  // the hyperparameters, alphas, are stored as a diagonal array of
  // dimension N+1 x N+1.
  //
  tdata_a.A_d.setDimensions(tdata_a.num_samples_d + 1, tdata_a.num_samples_d + 1,
			  false, Integral::DIAGONAL);
  
  // the phi matrix (pp. 214 of [1]) is the design matrix of kernel
  // evaluations. Note that here we use the transpose of the PHI matrix
  // in [1] since our matrices are assumed to be column matrices. Thus
  // phi is an N+1 x N matrix column matrix rather than a N x N+1 row
  // matrix as they do in [1].
  //
  tdata_a.phi_d.setDimensions(tdata_a.num_samples_d + 1, tdata_a.num_samples_d,
 			      false, Integral::FULL);
  
  // the sigma vector is the probability of each sample given the current
  // weights. the B matrix is a diagonal matrix of size N x N that is used
  // in the least squares optimization of the weights (pp. 219 of [1]).
  // B[i,i] = sigma(i) * (1 - sigma(i))
  //
  tdata_a.sigma_d.setLength(tdata_a.num_samples_d, false);
  tdata_a.B_d.setDimensions(tdata_a.num_samples_d, tdata_a.num_samples_d,
			  false, Integral::DIAGONAL);
  
  // temporary storage for the gradient vector, hessian matrix and the
  // covariance matrix (or at least the cholesky decomposition of the
  // hessian which can be used to find the covariance)
  //
  tdata_a.gradient_d.setLength(tdata_a.num_samples_d + 1, false);
  tdata_a.hessian_d.setDimensions(tdata_a.num_samples_d + 1,
				tdata_a.num_samples_d + 1, false,
				Integral::SYMMETRIC);
  tdata_a.covar_cholesky_d.setDimensions(tdata_a.num_samples_d+1,
				       tdata_a.num_samples_d+1, false,
				       Integral::LOWER_TRIANGULAR);
  
  // curr_weights are used during the optimization to hold the current set
  // of weights in a single vector. last_rvm_weights hold the last set of
  // weights that were converged to in the RVM training iteration. If these
  // weights match the weights found in the next iteration then we have
  // reached global convergence (subject to a few other rules).
  // old_irls_weights contains the last set of weights tried for the
  // IRLS convergence tests
  //
  tdata_a.curr_weights_d.setLength(tdata_a.num_samples_d + 1, false);
  tdata_a.last_rvm_weights_d.setLength(tdata_a.num_samples_d + 1, false);

  // if the weights stagnate then we can discontinue training even if the
  // hyperparameters are oscillating.
  //
  tdata_a.last_changed_d = 0;
  
  // compute the phi matrix:
  // The first element in each column is set to 1.0. Element i,j in the
  // matrix corresponds to the kernel function evaluated with
  // vectors_d(j) and vectors_d(i-1) as arguments
  //
  float kernel_value = 0.0;
  for (long j = 0; j < tdata_a.num_samples_d; j++) {
    tdata_a.phi_d.setValue(0, j, 1.0);
  }
  for (long i = 1; i <= tdata_a.num_samples_d; i++) {
    for (long j = 0; j < tdata_a.num_samples_d; j++) {
      kernel_d.compute(kernel_value, vectors_d(j), vectors_d(i-1));
      tdata_a.phi_d.setValue(i, j, kernel_value);
    }
  }

  // initialize the A matrix:
  //  A is initialized to the N+1 x N+1 indentity matrix
  //  then seed with equal entries of 1/N^2
  //
  tdata_a.A_d.makeIdentity(tdata_a.num_samples_d+1);
  tdata_a.A_d.mult(1.0 / tdata_a.num_samples_d / tdata_a.num_samples_d);
  
  // keep track of the number of hyperparameters:
  //  if dim_A is equal to the size of the weight_d vector then the bias
  //  has been pruned.
  //
  tdata_a.dimA_d = tdata_a.A_d.getNumRows();

  // initialize weights to zero - this gives every input vector probability
  // 1/2 of being an in-class example.
  // i.e. sigma(0.0) = 1 / (1 + exp(0)) = 1/2
  //
  weights_d.assign(0.0);
  bias_d = 0.0;

  // check that the data and targets are appropriate for training
  //
  if ((tdata_a.num_samples_d == 0) ||
      (targets_d.length() != tdata_a.num_samples_d)) {
    return Error::handle(name(), L"initFullTrain", ERR_INIT,
			 __FILE__, __LINE__);
  }

  tdata_a.targets_d.assign(targets_d);
  
  // exit gracefully
  //
  return true;
}


// method: initIncremental
//
// argument:
//  RVMTrainData& tdata: data stored for training the models
//
// return: boolean value indicating status
//
// initializes the data structures in preparation for an incremental training
// pass
//
boolean RelevanceVectorMachine::initIterativeRefinement(RVMTrainData& tdata_a) {

  // declare local variables
  //
  String tmpstr;
  if (debug_level_d > Integral::NONE) {
    tmpstr.assign(L"number of examples = ");
    tmpstr.concat(vectors_d.length());
    Console::put(tmpstr);
  }
  
  // number of samples stored
  //
  tdata_a.last_changed_d = 0;
  tdata_a.num_samples_d = vectors_d.length();
  
  // set the incremental training quantities
  //
  tdata_a.S_d.setLength(vectors_d.length() + 1);
  tdata_a.Q_d.setLength(vectors_d.length() + 1);
  tdata_a.hyperparams_d.setLength(vectors_d.length() + 1);
  tdata_a.active_params_d.setLength(vectors_d.length() + 1);
  tdata_a.last_hyperparams_d.setLength(vectors_d.length() + 1);
  tdata_a.twoback_hyperparams_d.setLength(vectors_d.length() + 1);
  tdata_a.hyperparams_d.assign(1.0 / vectors_d.length() / vectors_d.length());
  Boolean t = true;
  tdata_a.active_params_d.assign(t);
  tdata_a.last_hyperparams_d.assign(tdata_a.alpha_thresh_d * 10);
  tdata_a.twoback_hyperparams_d.assign(tdata_a.alpha_thresh_d * 10);
  tdata_a.weights_d.setLength(vectors_d.length() + 1);
  tdata_a.weights_d.assign(0.0);

  // check that the data and targets are appropriate for training
  //
  if ((vectors_d.length() == 0) ||
      (targets_d.length() != vectors_d.length())) {
    return Error::handle(name(), L"initIncrementalTrain", ERR_INIT,
			 __FILE__, __LINE__);
  }

  tdata_a.targets_d.assign(targets_d);

  // if we are to restart the training from some intermediate point
  // then do so. Otherwise initialize the training data.
  //
  if (tdata_a.load_restart_d) {
    readRestartData(tdata_a);
  }

  if (debug_level_d > Integral::NONE) {
    tmpstr.assign(L"number of weights: ");
    tmpstr.concat(tdata_a.curr_weights_d.length());
    Console::put(tmpstr);
  }

  // exit gracefully
  //
  return true;
}

// method: initializeRVSubsets
//
// arguments:
//  RVMTrainData& tdata: data stored for training the models
//
// return: boolean value indicating status
//
// create the next subsets of relevance vectors for training
//
boolean RelevanceVectorMachine::initializeRVSubsets(RVMTrainData& tdata_a) {

  // determine which parameters are active and which are not
  // ignore the bias since we are going to add it in manually to
  // every model.
  //
  String tmpstr;
  long N = 0;
  for (long i = 1; i < tdata_a.active_params_d.length(); i++) {
    if (tdata_a.active_params_d(i)) {
      N++;
    }
  }
  tdata_a.active_rvs_d.setLength(N);
  for (long j = 0, i = 1; i < tdata_a.active_params_d.length(); i++) {
    if (tdata_a.active_params_d(i)) {
      tdata_a.active_rvs_d(j++) = i;
    }
  }
    
  if (debug_level_d > Integral::BRIEF) {
    tmpstr.assign(L"N = ");
    tmpstr.concat(N);
    Console::put(tmpstr);
    tdata_a.active_rvs_d.debug(L"active_rvs_d");
  }
  
  if (debug_level_d > Integral::NONE) {
    tmpstr.assign(L"subset_size_d = ");
    tmpstr.concat(tdata_a.subset_size_d);
    Console::put(tmpstr);
  }
  
  // determine the number of subsets to use
  //
  tdata_a.num_subsets_d = 1;

  // create the subsets
  //
  tdata_a.subsets_d.setLength(tdata_a.num_subsets_d);
  
  // randomly permute the index set (any division will do, but maybe by
  // randomly choosing subsets, we get something out of it)
  //
  VectorLong indices(N);
  indices.randperm(N);
  
  // loop over each subset and create it
  //
  tdata_a.subsets_d(tdata_a.num_subsets_d - 1).setLength(0);  
  tdata_a.subsets_d(tdata_a.num_subsets_d - 1).move(indices, indices.length(),
						    0, 0);

  if (debug_level_d > Integral::BRIEF) {
    tdata_a.subsets_d.debug(L"tdata_a.subsets_d");
  }

  // exit gracefully
  //
  return true;
}

// method: updateSubsetRVM
//
// arguments:
//  RVMTrainData& tdata_global: global training data
//  RelevanceVectorMachine& rvm: RVM to train up
//  RVMTrainData& tdata_subset: training data for this subset
//
// return: boolean value indicating status
//
// determine which relevance vectors remain after a pass of training and
// update their values
//
boolean RelevanceVectorMachine::updateSubsetRVM(RVMTrainData& tdata_global_a,
						RelevanceVectorMachine& rvm_a,
						RVMTrainData& tdata_subset_a) {

  if (debug_level_d > Integral::BRIEF) {
    tdata_global_a.debug(L"global data before updating");
  }
  
  // get the active subset
  //
  VectorLong this_subset;
  if (tdata_global_a.subsets_d(0).length() >= tdata_global_a.subset_size_d) {
    this_subset.move(tdata_global_a.subsets_d(0), tdata_global_a.subset_size_d,
		     0, 0);
  }
  else {
    this_subset.assign(tdata_global_a.subsets_d(0));
  }

  long sub_len = this_subset.length();
  
  // figure out what was pruned (the bias is the 0th entry which we
  // ignore for now
  //
  long weight_num = 0;
  VectorFloat tmp_A;
  tdata_subset_a.A_d.getDiagonal(tmp_A);
  long hyperparam_num = 0;

  // see if bias was pruned
  //
  if (tmp_A.length() != rvm_a.weights_d.length()) {
    hyperparam_num = 1;
  }

  VectorLong tmp_vec(sub_len);
  long jjj = 0;

  for (long i = 1; i <= sub_len; i++) {

    // find the index of this entry
    //
    long k = this_subset(i - 1);
    long kk = tdata_global_a.active_rvs_d(k);
    
    // keep these rvs since they survived the previous iteration
    // update their stored weights and hyperparameters
    //
    if (tdata_subset_a.active_params_d(i) == true) {
      if (debug_level_d > Integral::NONE) {
	String tmpstr(L"keeping rv # ");
	tmpstr.concat(kk);
	Console::put(tmpstr);
      }
      tmp_vec(jjj++) = k;
      
      // update the weight and hyperparameter
      //
      tdata_global_a.weights_d(kk) = rvm_a.weights_d(weight_num);
      tdata_global_a.hyperparams_d(kk) = tmp_A(hyperparam_num);
      weight_num++;
      hyperparam_num++;
    }
    else {
      if (debug_level_d > Integral::BRIEF) {
	String tmpstr(L"removing rv # ");
	tmpstr.concat(kk);
	Console::put(tmpstr);
      }

      // update the weight and hyperparameter
      //
      tdata_global_a.weights_d(kk) = 0.0;
      tdata_global_a.hyperparams_d(kk) = tdata_global_a.alpha_thresh_d * 10;

      // deactivate the parameter
      //
      tdata_global_a.active_params_d(kk) = false;
    }
  }

  if (debug_level_d > Integral::BRIEF) {
    tmp_vec.debug(L"retained elements");
  }
  
  // add the retained data from the last iteration to the next
  // iteration. if the current subset is less than the maximum
  // subset length then we have already traversed all of the data
  //
  tmp_vec.setLength(jjj);
  tdata_global_a.subsets_d(0).move(tmp_vec, tmp_vec.length(), 0, 0);
  tdata_global_a.subsets_d(0).deleteRange(jjj, this_subset.length() - jjj);

  if (debug_level_d > Integral::BRIEF) {
    tdata_global_a.debug(L"global data after updating");
  }

  // exit gracefully
  //
  return true;
}

// method: initializeSubsetRVM
//
// arguments:
//  RelevanceVectorMachine& rvm: RVM to train up
//  RVMTrainData& tdata_subset: training data for this subset
//  RVMTrainData& tdata_global: global training data
//
// return: boolean value indicating status
//
// setup the RVM model and training data for a subset pass of training
//
boolean RelevanceVectorMachine::initializeSubsetRVM(
					    RelevanceVectorMachine& rvm_a,
					    RVMTrainData& tdata_subset_a,
					    RVMTrainData& tdata_global_a) {

  // local variables
  //
  String tmpstr;

  // set the rvm kernel
  //
  rvm_a.setKernel(kernel_d);
  
  // get the active subset
  //
  VectorLong next_subset;
  if (tdata_global_a.subsets_d(0).length() >= tdata_global_a.subset_size_d) {
    next_subset.move(tdata_global_a.subsets_d(0), tdata_global_a.subset_size_d,
		     0, 0);
  }
  else {
    next_subset.assign(tdata_global_a.subsets_d(0));
  }

  // debugging information
  //
  if (debug_level_d > Integral::NONE) {
    next_subset.debug(L"next subset");
  }
  
  // copy the basic data to the subset data object
  //
  tdata_subset_a.assignParameters(tdata_global_a);

  // setup the number of samples (potential relevance vectors)
  //
  tdata_subset_a.num_samples_d = next_subset.length();

  if (debug_level_d > Integral::BRIEF) {
    tmpstr.assign(L"subset num samples = ");
    tmpstr.concat(tdata_subset_a.num_samples_d);
    Console::put(tmpstr);
  }
  
  // setup the weight vector
  //
  rvm_a.weights_d.setLength(tdata_subset_a.num_samples_d);
  rvm_a.targets_d.setLength(tdata_subset_a.num_samples_d);
  rvm_a.vectors_d.setLength(tdata_subset_a.num_samples_d);

  // setup the hyperparameter matrix
  //
  tdata_subset_a.A_d.setDimensions(tdata_subset_a.num_samples_d + 1,
				   tdata_subset_a.num_samples_d + 1,
				   false, Integral::DIAGONAL);

  // setup the phi matrix 
  //
  tdata_subset_a.phi_d.setDimensions(tdata_subset_a.num_samples_d + 1,
				     tdata_global_a.num_samples_d,
				     false, Integral::FULL);
  
  // setup the sigma vector
  //
  tdata_subset_a.sigma_d.setLength(tdata_global_a.num_samples_d, false);
  tdata_subset_a.B_d.setDimensions(tdata_global_a.num_samples_d,
				   tdata_global_a.num_samples_d,
				   false, Integral::DIAGONAL);
  
  // setup temporary storage for the gradient vector, hessian matrix and the
  // covariance matrix (or at least the cholesky decomposition of the
  // hessian which can be used to find the covariance)
  //
  tdata_subset_a.gradient_d.setLength(tdata_subset_a.num_samples_d + 1, false);
  tdata_subset_a.hessian_d.setDimensions(tdata_subset_a.num_samples_d + 1,
					 tdata_subset_a.num_samples_d + 1,
					 false,
					 Integral::SYMMETRIC);
  tdata_subset_a.covar_cholesky_d.setDimensions(tdata_subset_a.num_samples_d + 1,
						tdata_subset_a.num_samples_d + 1,
						false,
						Integral::LOWER_TRIANGULAR);
  
  // setup temporary weight storage
  //
  tdata_subset_a.curr_weights_d.setLength(tdata_subset_a.num_samples_d + 1,
					  false);
  tdata_subset_a.last_rvm_weights_d.setLength(tdata_subset_a.num_samples_d + 1,
					      false);

  // compute the phi matrix:
  // The first element in each column is set to 1.0. Element i,j in the
  // matrix corresponds to the kernel function evaluated with
  // vectors_d(j) and vectors_d(i-1) as arguments. Notice that the j
  // here is slightly different since we are dealing with randomized
  // subsets. 
  //
  float kernel_value = 0.0;
  for (long j = 0; j < tdata_global_a.num_samples_d; j++) {
    tdata_subset_a.phi_d.setValue(0, j, 1.0);
  }
  
  for (long i = 1; i <= tdata_subset_a.num_samples_d; i++) {
    
    // get the subset index
    //
    long k = next_subset(i - 1);
    long kk = tdata_global_a.active_rvs_d(k);
    
    for (long j = 0; j < tdata_global_a.num_samples_d; j++) {
      kernel_d.compute(kernel_value, vectors_d(j), vectors_d(kk - 1));
      tdata_subset_a.phi_d.setValue(i, j, kernel_value);
    }
  }

  // initialize the A matrix:
  //  A is initialized to the N+1 x N+1 indentity matrix
  //  then seed with equal entries of 1/N^2
  //
  tdata_subset_a.A_d.makeIdentity(tdata_subset_a.num_samples_d + 1);
  tdata_subset_a.A_d.mult(1.0 / tdata_global_a.num_samples_d / tdata_global_a.num_samples_d);
  tdata_subset_a.dimA_d = tdata_subset_a.num_samples_d + 1;
    
  // set the weights, bias, targets and vectors
  // all weights will be reset to zero at this point
  //
  rvm_a.bias_d = 0;
  for (long i = 0; i < tdata_subset_a.num_samples_d; i++) {
    
    // get the subset index
    //
    long k = next_subset(i);
    long kk = tdata_global_a.active_rvs_d(k);
    rvm_a.weights_d(i) = 0.0;
    rvm_a.targets_d(i) = targets_d(kk - 1);
    rvm_a.vectors_d(i) = vectors_d(kk - 1);
  }
  tdata_subset_a.targets_d.assign(tdata_global_a.targets_d);

  // copy the current weights
  //
  tdata_subset_a.curr_weights_d(0) = rvm_a.bias_d;
  tdata_subset_a.curr_weights_d.move(rvm_a.weights_d,
				     tdata_subset_a.num_samples_d,
				     0, 1);
  
  tdata_subset_a.active_params_d.setLength(tdata_subset_a.num_samples_d + 1);
  Boolean t = true;
  tdata_subset_a.active_params_d.assign(t);

  // exit gracefully
  //
  return true;
}


// method: reinitialize
//
// arguments:
//  RVMTrainData& tdata: data stored for training the models
//
// return: boolean value indicating status
//
// prepare for a next pass of training. create the phi matrix and resize
// other matrices
//
boolean RelevanceVectorMachine::reinitialize(RVMTrainData& tdata_a) {
  
  // declare local variables
  //
  long num_weights = 0;
  boolean is_biased = (tdata_a.hyperparams_d(0) < tdata_a.alpha_thresh_d);
  for (long i = 1; i < tdata_a.hyperparams_d.length(); i++) {
    if (tdata_a.hyperparams_d(i) < tdata_a.alpha_thresh_d) {
      num_weights++;
    }
  }

  // set the new number of weights
  //
  weights_d.setLength(num_weights);

  if (is_biased) {
    num_weights++;
  }
  
  // the hyperparameters, alphas, are stored as a diagonal array
  // dimension M x M.
  //
  tdata_a.A_d.setDimensions(num_weights, num_weights, false, Integral::DIAGONAL);
  
  // the phi matrix (pp. 214 of [1]) is the full design matrix of kernel
  // evaluations. Note that here we use the transpose of the PHI matrix
  // in [1] since our matrices are assumed to be column matrices. Thus
  // phi is an M x N matrix column matrix rather than a N x M row
  // matrix as they do in [1].
  //
  tdata_a.phi_d.setDimensions(num_weights, vectors_d.length(), false,
			    Integral::FULL);
  
  // the sigma vector is the probability of each sample given the current
  // weights. the B matrix is a diagonal matrix of size N x N that is used
  // in the least squares optimization of the weights (pp. 219 of [1]).
  // B[i,i] = sigma(i) * (1 - sigma(i))
  //
  tdata_a.sigma_d.setLength(vectors_d.length(), false);
  tdata_a.B_d.setDimensions(vectors_d.length(), vectors_d.length(),
			  false, Integral::DIAGONAL);
  
  // temporary storage for the gradient vector, hessian matrix and the
  // covariance matrix (or at least the cholesky decomposition of the
  // hessian which can be used to find the covariance)
  //
  tdata_a.gradient_d.setLength(num_weights, false);
  tdata_a.hessian_d.setDimensions(num_weights, num_weights, false,
				Integral::SYMMETRIC);
  tdata_a.covar_cholesky_d.setDimensions(num_weights, num_weights, false,
				       Integral::LOWER_TRIANGULAR);
  
  // curr_weights are used during the optimization to hold the current set
  // of weights in a single vector. 
  //
  tdata_a.curr_weights_d.setLength(num_weights, false);

  // compute the phi matrix:
  // The first element in each column is set to 1.0 if the bias is
  // not pruned. Element i,j in the matrix corresponds to the kernel
  // function evaluated with vectors_d(j) and vectors_d(i-1) as arguments
  //
  VectorFloat tmp;
  long j = 0;
  long k = 0;
  if (tdata_a.hyperparams_d(0) < tdata_a.alpha_thresh_d) {
    tdata_a.A_d.setValue(j, j, tdata_a.hyperparams_d(0));
    getPhiRowFromCache(tmp, 0, tdata_a);
    tdata_a.phi_d.setRow(j, tmp);
    bias_d = tdata_a.weights_d(0);
    tdata_a.curr_weights_d(j) = bias_d;
    j++;
  }
  
  for (long i = 1; i < tdata_a.hyperparams_d.length(); i++) {
    if (tdata_a.hyperparams_d(i) < tdata_a.alpha_thresh_d) {
      tdata_a.A_d.setValue(j, j, tdata_a.hyperparams_d(i));
      getPhiRowFromCache(tmp, i, tdata_a);
      tdata_a.phi_d.setRow(j, tmp);
      weights_d(k) = tdata_a.weights_d(i);
      tdata_a.curr_weights_d(j) = weights_d(k);
      j++;
      k++;
    }
  }

  // keep track of the number of hyperparameters:
  //  if dim_A is equal to the size of the weight_d vector then the bias
  //  has been pruned.
  //
  tdata_a.dimA_d = tdata_a.A_d.getNumRows();

  // exit gracefully
  //
  if (num_weights == 0) {
    return false;
  }
  
  return true;
}

// method: pruneAndUpdate
//
// argument:
//  RVMTrainData& tdata: data stored for training the models
//
// return: boolean value indicating status
//
// prunes off vectors whose hyperparameters have gone to infinity and
// updates working data sets
//
boolean RelevanceVectorMachine::pruneAndUpdate(RVMTrainData& tdata_a) {
  
  // debugging information
  //
  if (debug_level_d > Integral::BRIEF) {
    tdata_a.A_d.debug(L"A");
  }
  
  // prune any weights that have gone to zero
  //
  pruneWeights(tdata_a);
  
  // update the number of hyperparameters after pruning
  //
  tdata_a.dimA_d = tdata_a.A_d.getNumRows();
  
  // reset the size of the vector set
  //
  tdata_a.num_samples_d = weights_d.length();
  
  // reset the sizes of all working data
  //
  tdata_a.hessian_d.setDimensions(tdata_a.dimA_d, tdata_a.dimA_d, false);
  tdata_a.hessian_d.setCapacity(tdata_a.dimA_d, tdata_a.dimA_d, false);
  
  tdata_a.covar_cholesky_d.setDimensions(tdata_a.dimA_d, tdata_a.dimA_d,
					 false);
  tdata_a.covar_cholesky_d.setCapacity(tdata_a.dimA_d, tdata_a.dimA_d,false);
  
  tdata_a.gradient_d.setLength(tdata_a.dimA_d, false);
  tdata_a.gradient_d.setCapacity(tdata_a.dimA_d, false);
  
  tdata_a.curr_weights_d.setLength(tdata_a.dimA_d);
  tdata_a.curr_weights_d.setCapacity(tdata_a.dimA_d);
  
  // exit gracefully
  //
  return true;
}


// method: updateHyperparameters
//
// argument:
//  boolean& updated: takes a 'true' value if the hyperparameter matrix was
//                    modified on this iteration - indicating that either
//                    a hyperparameter went to zero or 
//  RVMTrainData& tdata: data stored for training the models
//  ALGORITHM mode: training mode
//  boolean use_mackay: whether to use mackay pruning or aggressive pruning
//
// return: boolean value indicating status
//
// update the hyperparameter values
//
boolean RelevanceVectorMachine::updateHyperparameters(boolean& updated_a,
						      RVMTrainData& tdata_a,
						      ALGORITHM mode_a,
						      boolean use_mackay_a) {

  // initialize the output flag
  //
  updated_a = false;

  if (algorithm_d == FULL) {
    return updateHyperparametersFull(updated_a, tdata_a, use_mackay_a);
  }
  else {
    return updateHyperparametersIncremental(updated_a, tdata_a);
  }
}


// method: updateHyperparametersFull
//
// argument:
//  boolean& updated: takes a 'true' value if the hyperparameter matrix was
//                    modified on this iteration - indicating that either
//                    a hyperparameter went to zero or 
//  RVMTrainData& tdata: data stored for training the models
//  boolean use_mackay: use mackay style pruning or aggressive pruning?
//
// return: boolean value indicating status
//
// update the hyperparameter values
//
boolean RelevanceVectorMachine::updateHyperparametersFull(boolean& updated_a,
						  RVMTrainData& tdata_a,
						  boolean use_mackay_a) {

  // update the momentum components
  // only use momentum if no hyperparams have been pruned
  // recently.
  //
  tdata_a.twoback_hyperparams_d.assign(tdata_a.last_hyperparams_d);
  tdata_a.A_d.getDiagonal(tdata_a.last_hyperparams_d);
  boolean use_momentum = (tdata_a.twoback_hyperparams_d.length() ==
			  tdata_a.last_hyperparams_d.length());
  
  // loop over each hyperparameter and update - remember that A is
  // diagonal where A(i,i) = alpha_i. The update equation used is
  // equation (16) of [1]. We also test for convergence in this loop.
  // If no alphas have substantially changed then we assume convergence.
  // We must take some care here that NaN and Infinity values do not
  // creap into our calculations
  //
  Double gamma = 0.0;
  double tmp_alpha = 0.0;
  double old_alpha = 0.0;
  Double sum_gamma = 0.0;
  
  for (long i = 0; i < tdata_a.dimA_d; i++) {
    
    // equation (16): alpha = (1 - alpha(i) * covar(i,i)) / (weights^2)
    //
    old_alpha = tdata_a.A_d.getValue(i,i);

    // error checking
    //
    if (old_alpha < 0.0) {
      tdata_a.A_d.debug(L"A");
      return Error::handle(name(), L"train:invalid A", ERR,
			   __FILE__, __LINE__);        
    }
    
    if (tdata_a.covar_cholesky_d(i,i) < 0.0) {
      tdata_a.covar_cholesky_d.debug(L"covar_cholesky");
      return Error::handle(name(), L"train:invalid covar", ERR,
			   __FILE__, __LINE__);        
    }
    
    // compute gamma = 1 - alpha(i)*covar(i,i)
    //
    gamma = 1.0 - (old_alpha*tdata_a.covar_cholesky_d(i,i));
    
    // gamma can be negative if covar(i,i) ~= 1/old_alpha
    // due to numerical imprecision. only error if the result
    // is not close to zero.
    //
    if (gamma < 0.0) {
      if (!Integral::almostEqual(gamma, 0.0)) {
	tdata_a.A_d.debug(L"A");
	tdata_a.covar_cholesky_d.debug(L"covar_cholesky");
	gamma.debug(L"gamma");
	return Error::handle(name(), L"train:invalid gamma", ERR,
			     __FILE__, __LINE__);
      }
      else {
	gamma = -gamma;
      }
    }
    sum_gamma += gamma;
    
    // error checking
    // if the weight is already zero then alpha(i) will go to infinity
    // set it to a very large value instead
    //
    if (tdata_a.curr_weights_d(i) == (float)0.0) {
      tmp_alpha = tdata_a.alpha_thresh_d * 10;
    }
    else {
      if (use_mackay_a) {
	tmp_alpha = (float)gamma / tdata_a.curr_weights_d(i) / tdata_a.curr_weights_d(i);
      }
      else {
	tmp_alpha = (float)gamma / ((tdata_a.curr_weights_d(i) / (float)gamma *
			     tdata_a.curr_weights_d(i)) -
			     tdata_a.covar_cholesky_d(i,i));
	if (tmp_alpha <= 0) {
	  tmp_alpha = tdata_a.alpha_thresh_d * 10;
	}
      }
    }

    
    // compute momentum term
    //
    if (use_momentum) {
      float momentum_term = 0.0;
      if ((tdata_a.last_hyperparams_d(i) < tdata_a.alpha_thresh_d) &&
	  (tdata_a.twoback_hyperparams_d(i) < tdata_a.alpha_thresh_d)) {

	momentum_term = tdata_a.momentum_d * (tdata_a.last_hyperparams_d(i) -
					      tdata_a.twoback_hyperparams_d(i));

	if (debug_level_d > Integral::BRIEF) {
	  String tmpstr;
	  tmpstr.clear();
	  tmpstr.concat(L"momentum_term = ");
	  tmpstr.concat(momentum_term);
	  Console::put(tmpstr);
	}
	
	// the hyperparameter can not be negative so we do not want
	// to decrease toward zero too quickly and we should never
	// allow it to go below zero so we only take moderate
	// negative steps
	//
	if (momentum_term > (-0.5 * tmp_alpha)) {
	  tmp_alpha = tmp_alpha + momentum_term;
	}
      }
    }
    
    tdata_a.A_d.setValue(i, i, tmp_alpha);
    
    // debugging information
    //
    if (debug_level_d > Integral::DETAILED) {
      gamma.debug(L"gamma");
    }
    
    // zero the weights to be pruned
    //
    if (tmp_alpha > tdata_a.alpha_thresh_d) {
      tdata_a.curr_weights_d(i) = 0.0;
      updated_a = true;
    }
    
    // check for convergence in A
    //
    if (!Integral::almostEqual(tmp_alpha, old_alpha, 0.001)) {
      updated_a = true;
    }
  }
  if (debug_level_d > Integral::BRIEF) {
    sum_gamma.debug(L"Sum Gamma");
  }

  // exit gracefully
  //
  return true;
}

// method: finalizeTraining
//
// argument:
//  RVMTrainData& tdata: data stored for training the models
//
// return: boolean value indicating status
//
// finalize the trained model making it ready to write to file or use
// in prediction
//
boolean RelevanceVectorMachine::finalizeTraining(RVMTrainData& tdata_a,
						 ALGORITHM mode_a) {

  if (algorithm_d == FULL) {

    // perform a final check on the range of the weights and manually prune any
    // that have fallen below the minimum allowed weight value
    //
    if (Integral::abs(bias_d) < tdata_a.min_allowed_weight_d) {
      bias_d = 0.0;
    }
    for (long i = 0; i < weights_d.length(); i++) {
      if (Integral::abs(weights_d(i)) < tdata_a.min_allowed_weight_d) {
	weights_d(i) = 0.0;
      }
    }
    
    // prune any remaining zero-weights with large hyperparameters
    //
    pruneAndUpdate(tdata_a);
    
    // run a final iteration of IRLS training to get the final hessian
    //
    irlsTrain(tdata_a);
    
    // compute the final inverse hessian and assign
    //
    inv_hessian_d.inverse(tdata_a.hessian_d);
    inv_hessian_d.neg();
    
    // exit gracefully
    //
    return true;
  }
  else if (algorithm_d == ITERATIVE_REFINEMENT) {

    // get final hessian
    //
    // run a final iteration of IRLS training to get the final hessian
    //
    irlsTrain(tdata_a);
    
    // compute the final inverse hessian and assign
    //
    inv_hessian_d.inverse(tdata_a.hessian_d);
    inv_hessian_d.neg();

    // create the final vector list
    //
    SingleLinkedList<VectorFloat> final_vecs;
    SingleLinkedList<Byte> final_targets;
    long num_hyps = tdata_a.hyperparams_d.length();
    long num_vecs = 0;

    for (long i = 1; i < num_hyps; i++) {
      if ((tdata_a.hyperparams_d(i) < tdata_a.alpha_thresh_d) &&
	  (Integral::abs(tdata_a.weights_d(i)) >
	   tdata_a.min_allowed_weight_d)) {
	final_vecs.insert(&(vectors_d(i-1)));
	final_targets.insert(&(targets_d(i-1)));
	num_vecs++;
      }
    }
    vectors_d.setLength(num_vecs);
    targets_d.setLength(num_vecs);

    for (long i = 0; i < num_vecs; i++) {
      targets_d(i).assign(*final_targets.getFirst());
      vectors_d(i).assign(*final_vecs.getFirst());
      final_targets.removeFirst();
      final_vecs.removeFirst();
    }
    
    // exit gracefully
    //
    return true;
  }

  // improper mode
  //
  return false;
}

// method: computeSigma
//
// arguments:
//  RVMTrainData& tdata: (input) training data storage
//
// return: boolean value indicating status
//
// this method computes the sigma valued defined on pp. 218 of [1].
//
boolean RelevanceVectorMachine::computeSigma(RVMTrainData& tdata_a) {

  // declare local variables
  //
  tdata_a.phi_d.vmult(tdata_a.sigma_d, tdata_a.curr_weights_d);
  tdata_a.sigma_d.neg();
  tdata_a.sigma_d.exp();
  tdata_a.sigma_d.add(1.0);
  tdata_a.sigma_d.inverse();

  // exit gracefully
  //
  return true;
}
  
// method: updateHyperparametersIncremental
//
// argument:
//  boolean& updated: takes a 'true' value if the hyperparameter matrix was
//                    modified on this iteration - indicating that either
//                    a hyperparameter went to zero or 
//  RVMTrainData& tdata: data stored for training the models
//
// return: boolean value indicating status
//
// update the hyperparameter values
//
boolean RelevanceVectorMachine::updateHyperparametersIncremental(
						  boolean& updated_a,
						  RVMTrainData& tdata_a) {
  updated_a = false;
  
  // declare local variables
  //
  String tmpstr;
  long num_hyperparams = tdata_a.hyperparams_d.length();
  long size_C = tdata_a.B_d.getNumRows();
  VectorFloat tmp1(size_C);
  VectorFloat tmp2(size_C);
  VectorFloat phi_m(size_C);
  VectorFloat tmp;
  VectorFloat tmp_targets;
  MatrixFloat tmp_matrix;
  long index = -1;
  float S = 0;
  float Q = 0;
  float alpha = 0;
  float theta = 0;
  float weight = 0;
  float total_likelihood = 0.0;
  
  // compute updates for the hyperparameters already in the model
  //
  tdata_a.hessian_d.inverse();  // an O(M^3) operation
  long j = 0;
  Float gamma = 0.0;
  float tmp_alpha = 0.0;
  float old_alpha = 0.0;
  if (((tdata_a.last_changed_d + 1) % 50) == 0) {
    Console::put(L"using new update rule");
  }
  for (long i = 0; i < num_hyperparams; i++) {

    alpha = tdata_a.hyperparams_d(i);
    weight = tdata_a.weights_d(i);
    
    if (alpha < tdata_a.alpha_thresh_d) {

      // only deal with those parameters that can be updated
      //
      if (!tdata_a.active_params_d(i)) {
	// increment the position in the working hyperparameter matrix
	//
	j++;
	continue;
      }
    
      // equation (16): alpha = (1 - alpha(i) * covar(i,i)) / (weights^2)
      //
      old_alpha = tdata_a.A_d.getValue(j,j);

      // error checking
      //
      if (old_alpha < 0.0) {
	tdata_a.A_d.debug(L"A");
	return Error::handle(name(), L"train:invalid A", ERR,
			     __FILE__, __LINE__);        
      }
      // this is actually the negative covariance so all entries should
      // be negative
      //
      if (tdata_a.hessian_d(j,j) > 0.0) {
	tmpstr.clear();
	tmpstr.concat(L"invalid covar at entry: ");
	tmpstr.concat(j);
	tmpstr.concat(L"  :  ");
	tmpstr.concat(tdata_a.hessian_d(j,j));
	Console::put(tmpstr);
	tdata_a.hessian_d.debug(L"covariance");
	return Error::handle(name(), L"train:invalid covar", ERR,
			     __FILE__, __LINE__);        
      }
    
      // compute gamma = 1 - alpha(i)*covar(i,i)
      // hessian_d now holds -covar
      //
      gamma = 1.0 + (old_alpha*tdata_a.hessian_d(j,j)); 

      if (tdata_a.curr_weights_d(j) == (float)0.0) {
	tmp_alpha = tdata_a.alpha_thresh_d * 10;
      }
      else {
	tmp_alpha = gamma / tdata_a.curr_weights_d(j) /
	  tdata_a.curr_weights_d(j);
      }
      if (tmp_alpha < 0.0) {
	tmp_alpha = tdata_a.alpha_thresh_d * 10;
      }

      // compute momentum term
      //
      float momentum_term = 0.0;
      if ((tdata_a.last_hyperparams_d(i) < tdata_a.alpha_thresh_d) &&
	  (tdata_a.twoback_hyperparams_d(i) < tdata_a.alpha_thresh_d)) {

	momentum_term = tdata_a.momentum_d * (tdata_a.last_hyperparams_d(i) -
					    tdata_a.twoback_hyperparams_d(i));

	if (debug_level_d > Integral::BRIEF) {
	  tmpstr.clear();
	  tmpstr.concat(L"momentum_term = ");
	  tmpstr.concat(momentum_term);
	  Console::put(tmpstr);
	}
	
	// the hyperparameter can not be negative so we do not want
	// to decrease toward zero too quickly and we should never
	// allow it to go below zero so we only take moderate
	// negative steps
	//
	if (momentum_term > (-0.5 * tmp_alpha)) {
	  tmp_alpha = tmp_alpha + momentum_term;
	}
      }

      tdata_a.hyperparams_d(i) = tmp_alpha;
      
      // debugging information
      //
      if (debug_level_d > Integral::DETAILED) {
	gamma.debug(L"gamma");
      }
      
      // zero the weights to be pruned
      //
      if (tmp_alpha > tdata_a.alpha_thresh_d) {
	tdata_a.last_changed_d = 0;
	tdata_a.weights_d(i) = 0.0;
	tdata_a.hyperparams_d(i) = tdata_a.alpha_thresh_d * 10.0;

	if (debug_level_d > Integral::BRIEF) {
	  String tmp(L"pruning weight: ");
	  tmp.concat(i);
	  Console::put(tmp);
	}
	updated_a = true;
      }
      
      // check for convergence in A
      //
      else if (Integral::abs(Integral::log(tmp_alpha) -
			     Integral::log(old_alpha)) > 10e-3) {

	if (debug_level_d > Integral::BRIEF) {
	  String tmp(L"updating weight: ");
	  tmp.concat(i);
	  if (debug_level_d > Integral::BRIEF) {
	    tmp.concat(L" old_alpha:  ");
	    tmp.concat(old_alpha);
	    tmp.concat(L" new_alpha:  ");
	    tmp.concat(tmp_alpha);
	  }
	  Console::put(tmp);
	}
	updated_a = true;
      }
      else {
	if (debug_level_d > Integral::BRIEF) {
	  String tmp(L"copying weight: ");
	  tmp.concat(i);
	  if (debug_level_d > Integral::BRIEF) {
	    tmp.concat(L" old_alpha:  ");
	    tmp.concat(old_alpha);
	    tmp.concat(L" new_alpha:  ");
	    tmp.concat(tmp_alpha);
	  }
	  Console::put(tmp);
	}
      }
      
      // increment the position in the working hyperparameter matrix
      //
      j++;
    }
  }

  if (tdata_a.last_changed_d == 0) {
    tdata_a.last_hyperparams_d.assign(tdata_a.alpha_thresh_d * 10);
    tdata_a.twoback_hyperparams_d.assign(tdata_a.alpha_thresh_d * 10);
  }
  else {

    // check for cycling
    //
    if (tdata_a.last_hyperparams_d.almostEqual(tdata_a.hyperparams_d) ||
	tdata_a.twoback_hyperparams_d.almostEqual(tdata_a.hyperparams_d)) {

      if (debug_level_d > Integral::NONE) {
	Console::put(L"Cycle found - adding new relevance vectors");
      }
      updated_a = false;
    }
  }

  // copy the last hyperparams over
  //
  tdata_a.twoback_hyperparams_d.assign(tdata_a.last_hyperparams_d);
  tdata_a.last_hyperparams_d.assign(tdata_a.hyperparams_d);
      
  // if the model was not updated or pruned, then look for potential
  // additions
  //
  if ((!updated_a) || (tdata_a.last_changed_d > tdata_a.max_update_its_d)) {
    if (debug_level_d > Integral::NONE) {
      Console::put(L"adding new relevance vectors");
    }
    VectorFloat theta_values(num_hyperparams);
    VectorLong max_thetas(num_hyperparams);
  
    if (tdata_a.last_changed_d > tdata_a.max_update_its_d) {
      tdata_a.last_changed_d = 0;

      if (debug_level_d > Integral::BRIEF) {
	Console::put(L"Bumping the system to move it out of saddle point");
      }
    }

    // compute t_hat = t - sigma
    //
    tmp_targets.sub(tdata_a.targets_d, tdata_a.sigma_d);

    // inverse(C) = B + B*PHI'*inverse(Hessian)*PHI*B'
    // precompute B*PHI' and overwrite the phi matrix since we no longer
    // need it. Also precompute the inverse(Hessian)*PHI*B' matrix
    // since it is O(M*N) storage
    //
    tdata_a.phi_d.mult(tdata_a.B_d); // an O(M*N) operation
    tmp_matrix.setDimensions(0, 0, false, Integral::FULL);

    // inverse(Hessian) * PHI * B' an O(N*M^2) operation
    //
    tmp_matrix.mult(tdata_a.hessian_d, tdata_a.phi_d); 
    tdata_a.S_d.assign(0.0);
    tdata_a.Q_d.assign(0.0);
    
    total_likelihood = 0.0;
    for (long i = 0; i < num_hyperparams; i++) {

      // only deal with hyperparameters that can be updated
      //
      if (!tdata_a.active_params_d(i)) {
	continue;
      }

      alpha = tdata_a.hyperparams_d(i);
      weight = tdata_a.weights_d(i);
      Q = 0.0;
      S = 0.0;
      theta = 0.0;
      
      // get the row of the full phi matrix
      //
      getPhiRowFromCache(phi_m, i, tdata_a);
      
      // compute phi_m * (B * PHI) - an O(M*N) operation
      //
      tdata_a.phi_d.multv(tmp1, phi_m);
      
      // compute inv(Hessian) * PHI * B * phi_m - an O(M*N) operation
      //
      tmp_matrix.multv(tmp2, phi_m);
      
      // compute the first part of S(i)
      //  S(i)_1 = phi_m * B * PHI * inv(Hessian) * PHI * B * phi_m'
      //  an O(N) operation
      //
      S = tmp1.dotProduct(tmp2);
      
      // compute the second part of S(i)
      //  S(i)_2 = phi_m * B * phi_m' - an O(2N) operation
      //
      tdata_a.B_d.multv(tmp1, phi_m);
      S += tmp1.dotProduct(phi_m);
      
      // compute Q(i)
      //
      Q = phi_m.dotProduct(tmp_targets);

      if (alpha > tdata_a.alpha_thresh_d) {

	theta = Q * Q - S;
	if (theta > tdata_a.min_theta_d) {
	  //	  theta_values(i) = theta / S + Integral::log(S/Q/Q);
	  theta_values(i) = theta;
	}
      }

      // update the incremental statistics
      //
      tdata_a.S_d(i) = S;
      tdata_a.Q_d(i) = Q;
    }

    // sort the hyperparameters by their potential likelihood increase
    //
    theta_values.index(max_thetas, Integral::DESCENDING);

    for (long i = 0; i < tdata_a.max_additions_d; i++) {
      
      index = max_thetas(i);
      alpha = tdata_a.hyperparams_d(index);
      S = tdata_a.S_d(index);
      Q = tdata_a.Q_d(index);
      weight = tdata_a.weights_d(index);
      if (theta_values(index) <= tdata_a.min_theta_d) {
	continue;
      }
      if (alpha >= tdata_a.alpha_thresh_d) {
	theta = theta_values(index); // Q * Q - S;
	updated_a = true;
	alpha = S / theta * S;
	weight = 0.0;

	if (debug_level_d > Integral::NONE) {
	  String tmp(L"adding weight ");
	  tmp.concat(index);
	  if (debug_level_d > Integral::BRIEF) {
	    tmp.concat(L"  likelihood = ");
	    tmp.concat(theta_values(index));
	    tmp.concat(L"  theta = ");
	    tmp.concat(theta);
	    tmp.concat(L"  Q = ");
	    tmp.concat(Q);
	    tmp.concat(L"  S = ");
	    tmp.concat(S);
	  }
	  Console::put(tmp);
	}

	tdata_a.last_changed_d = 0;
	tdata_a.hyperparams_d(index) = alpha;
	tdata_a.weights_d(index) = weight;
      }
    }

    // write out the restart data if necessary
    //
    if (updated_a && tdata_a.save_restart_d) {
      writeRestartData(tdata_a);
    }
  }
  else {
    tdata_a.last_changed_d++;
  }
  
  if (tdata_a.last_changed_d == 0) {
    tdata_a.last_hyperparams_d.assign(tdata_a.alpha_thresh_d * 10);
    tdata_a.twoback_hyperparams_d.assign(tdata_a.alpha_thresh_d * 10);
  }

  // debugging information
  //
  if (debug_level_d > Integral::BRIEF) {
    tdata_a.S_d.debug(L"S");
    tdata_a.Q_d.debug(L"Q");
    tdata_a.hyperparams_d.debug(L"hyperparams");
    tdata_a.weights_d.debug(L"weights");
  }
  
  // exit gracefully
  //
  return true;
}

// method: pruneWeights
//
// arguments:
//  RVMTrainData& tdata: (input) training data storage
//
// return: boolean value indicating status
//
// this method prunes off vectors which attain a zero weight during
// training. Auxiliary data structures that are indexed according to the
// working set are also updated.
//
boolean RelevanceVectorMachine::pruneWeights(RVMTrainData& tdata_a) {

  // declare local variables
  //
  long weights_pruned = 0;
  
  // the A matrix is diagonal so we can get a temporary version of it
  // into a vector - this makes it easier to work with
  //
  VectorFloat tmp_A;
  tdata_a.A_d.getDiagonal(tmp_A);

  // get the dimensions of the phi matrix
  //
  long num_rows = tdata_a.phi_d.getNumRows();
  long num_cols = tdata_a.phi_d.getNumColumns();

  // because of the klugey way the weights are stored in the object (note
  // to self - fix that) we have to take care of when we are pruning the
  // bias versus pruning one of the regular weights
  //
  boolean offset = false;
  if (weights_d.length() != tmp_A.length()) {
    offset = true;
  }
  long k = 0;

  // position the active parameter index
  //
  long ac_idx = 0;
  String tmpstr;
  if (debug_level_d > Integral::BRIEF) {
    tdata_a.active_params_d.debug(L"active_params_d");
  }
  while ((ac_idx < tdata_a.active_params_d.length()) &&
	 (tdata_a.active_params_d(ac_idx) == false)) {
    ac_idx++;
  }
  
  // loop over the weights and check for zero-valued weights
  //
  for (long i = 0; i < tmp_A.length(); i++) {

    // prune any weight which has been floored. we prune off the corresponding
    // hyperparameter as well as the row of the phi matrix corresponding to
    // this weight
    //
    if ((tdata_a.curr_weights_d(i) == (float)0.0) &&
	(tmp_A(i) > tdata_a.alpha_thresh_d)) {

      weights_pruned++;
      tdata_a.active_params_d(ac_idx) = false;
      while ((ac_idx < tdata_a.active_params_d.length()) &&
	     (tdata_a.active_params_d(ac_idx) == false)) {
	ac_idx++;
      }

      // remove the weight
      //
      tdata_a.curr_weights_d.deleteRange(i, 1);
      
      // remove the hyperparameter
      //
      tmp_A.deleteRange(i, 1);

      // remove the weight, target and vector
      //
      if (offset) {
	if (i > 0) {
	  weights_d.deleteRange(i - 1, 1);
	  targets_d.deleteRange(i - 1, 1);
	  vectors_d.deleteRange(i - 1, 1);
	}
      }
      else {
	  weights_d.deleteRange(i, 1);
	  targets_d.deleteRange(i, 1);
	  vectors_d.deleteRange(i, 1);
      }
      if (weights_d.length() == tmp_A.length()) {
	offset = false;
      }
  
      // update the phi matrix. we move the row for this
      // element to the extremities of the matrix. they will actually be
      // pruned later by a matrix redimensioning operation
      //
      for (long j = i; j < num_rows - 1; j++) {
	tdata_a.phi_d.swapRows(j, j + 1);
      }
      num_rows--;

      // because we have removed an element we need to stay at this
      // entry index and update the stop condition
      //
      i--;
      tdata_a.dimA_d--;
    }
    else {
      ac_idx++;
      while ((ac_idx < tdata_a.active_params_d.length()) &&
	     (tdata_a.active_params_d(ac_idx) == false)) {
	ac_idx++;
      }

    }
    k++;
  }

  // recreate the A and phi matrices with appropriate dimensions.
  //
  tdata_a.A_d.makeDiagonal(tmp_A);
  tdata_a.phi_d.setDimensions(num_rows, num_cols, true);

  // debugging information
  //
  if (debug_level_d > Integral::BRIEF) {
    Console::put(L"Pruning completed");
    bias_d.debug(L"bias_d");
    weights_d.debug(L"weights_d");
    targets_d.debug(L"targets_d");
    vectors_d.debug(L"vectors_d");
    tdata_a.curr_weights_d.debug(L"curr_weights_d");
  }
  
  // debugging information
  //
  if (debug_level_d > Integral::NONE) {
    String out(L"Pruning ");
    out.concat(weights_pruned);
    out.concat(L" weights...");
    out.concat(tdata_a.dimA_d);
    out.concat(L" remain");
    Console::put(out);
  }
  
  if (debug_level_d > Integral::BRIEF) {
    tdata_a.active_params_d.debug(L"active params after pruning");
  }
  
  // exit gracefully
  //
  return true;
}
  
// method: computeLikelihood
//
// arguments:
//  RVMTrainData& tdata: (input) training data storage
//
// return: float value with computed likelihood value from (24) of [1]
//
// this method computes the log likelihood of the weights given the
// data. We would expect this value to increase as training proceeds
//
float RelevanceVectorMachine::computeLikelihood(RVMTrainData& tdata_a) const {

  // declare local variables
  //
  float result = 0;
  float divisor = tdata_a.phi_d.getNumColumns();
  
  // compute the 1/2 * w * A * w' component of the likelihood
  //
  tdata_a.A_d.quadratic(result, tdata_a.curr_weights_d);
  result *= 0.5;
  result /= divisor;
  
  // compute the summation in the log likelihood
  //  L = sum [log(sigma) * t + (1-t) * log(1-sigma)]
  //
  long len = tdata_a.sigma_d.length();
  float t;
  float sig;
  for (long i = 0; i < len; i++) {

    t = tdata_a.targets_d(i);
    sig = tdata_a.sigma_d(i);

    // t == 0
    //
    if (t < 0.5) {
      result -= Integral::log(1 - sig) / divisor;
    }

    // t == 1
    //
    else {
      result -= Integral::log(sig) / divisor;
    }
  }

  // exit gracefully
  //
  return result;
}

// method: computeVarianceCholesky
//
// arguments:
//  RVMTrainData& tdata: (input) training data storage
//
// return: logical value indicating status
//
// this method computes the diagonal elements of the inverse from the
// cholesky decomposition
//
boolean RelevanceVectorMachine::computeVarianceCholesky(RVMTrainData& tdata_a) {

  // find the size of the cholesky decomposition matrix
  //
  long nrows = tdata_a.covar_cholesky_d.getNumRows();
  float sum = 0.0;

  // compute the inverse of the lower triangular cholesky decomposition
  //
  for (long i = 0; i < nrows; i++) {
    tdata_a.covar_cholesky_d.setValue(i,i, (1.0 /
					    tdata_a.covar_cholesky_d(i,i)));
    for (long j = i+1; j < nrows; j++) {
      sum = 0.0;
      for (long k = i; k < j; k++) {
	sum -= tdata_a.covar_cholesky_d(j,k) * tdata_a.covar_cholesky_d(k,i);
      }
      tdata_a.covar_cholesky_d.setValue(j,i, (sum /
					      tdata_a.covar_cholesky_d(j,j)));
    }
  }

  // loop over columns and get the sumSquare values to put on the diagonals.
  // This simulates a matrix multiply but we only retain the diagonal values.
  //
  VectorFloat tmp;
  for (long i = 0; i < nrows; i++) {
    tdata_a.covar_cholesky_d.getColumn(tmp, i);
    float val = tmp.sumSquare();
    tdata_a.covar_cholesky_d.setValue(i,i,val);
  }

  // exit gracefully
  //
  return true;
}

// method: getPhiRow
//
// arguments:
//  VectorFloat& ovec: (output) row of the full PHI matrix - to save memory
//                      we do not actually compute this matrix
//  long index: (input) index into the full PHI matrix
//
// return: logical value indicating status
//
// this method computes and returns a row of the full phi matrix
//
boolean RelevanceVectorMachine::getPhiRow(VectorFloat& ovec_a, long index_a) {

  // initialize the output vector
  //
  ovec_a.setLength(vectors_d.length());
  
  // trivial case if the index is the bias vector
  //
  if (index_a == 0) {
    ovec_a.assign(1.0);
    return true;
  }
  
  float kernel_value;
  for (long j = 0; j < vectors_d.length(); j++) {
    kernel_d.compute(kernel_value, vectors_d(index_a - 1), vectors_d(j));
    ovec_a(j) = kernel_value;
  }

  // exit gracefully
  //
  return true;
}


// method: writeRestartData
//
// arguments:
//  const RVMTrainData& tdata: (input) training data storage
//
// return: logical value indicating status
//
// this method writes out any data needed to later restart the training
// from the current point. All data is written to the Sof file indicated
// by the RVMTrainData::restart_save_file_d.
//
boolean RelevanceVectorMachine::writeRestartData(RVMTrainData& tdata_a) {

  // open the Sof file
  //
  Sof out_sof;
  if (!out_sof.open(tdata_a.restart_save_file_d, File::WRITE_ONLY,
		    File::BINARY)) {
    return Error::handle(name(), L"writeRestartData::invalid file", ERR,
			 __FILE__, __LINE__);
  }
		   
  // write the hyperparameters
  //
  if (!tdata_a.hyperparams_d.write(out_sof, 0)) {
    return Error::handle(name(), L"readRestartData::invalid file", ERR,
			 __FILE__, __LINE__);
  }
  
  // write the weights
  //
  if (!tdata_a.weights_d.write(out_sof, 1)) {
    return Error::handle(name(), L"readRestartData::invalid file", ERR,
			 __FILE__, __LINE__);
  }

  // close the sof file
  //
  return out_sof.close();
}
  
// method: readRestartData
//
// arguments:
//  RVMTrainData& tdata: (input) training data storage
//
// return: logical value indicating status
//
// this method reads any data needed to restart the training
// from the point at which the data was saved. All data is read from
// the Sof file indicated by the RVMTrainData::restart_load_file_d.
//
boolean RelevanceVectorMachine::readRestartData(RVMTrainData& tdata_a) {

  // open the Sof file
  //
  Sof in_sof;
  if (!in_sof.open(tdata_a.restart_load_file_d, File::READ_ONLY)) {
    return Error::handle(name(), L"readRestartData::invalid file", ERR,
			 __FILE__, __LINE__);
  }
		   
  // read the hyperparameters
  //
  if (!tdata_a.hyperparams_d.read(in_sof, 0)) {
    return Error::handle(name(), L"readRestartData::invalid file", ERR,
			 __FILE__, __LINE__);
  }
  
  // read the weights
  //
  if (!tdata_a.weights_d.read(in_sof, 1)) {
    return Error::handle(name(), L"readRestartData::invalid file", ERR,
			 __FILE__, __LINE__);
  }

  // close the sof file
  //
  return in_sof.close();
}


// method: rebuildPhiCache
//
// arguments:
//  RVMTrainData& tdata: (input) training data storage
//
// return: logical value indicating status
//
// this method rebuilds the phi cache according to the current subset in
// use
//
boolean RelevanceVectorMachine::rebuildPhiCache(RVMTrainData& tdata_a) {

  // reset the cache index table
  //
  long num_vecs = tdata_a.hyperparams_d.length();
  tdata_a.cache_index_table_d.setLength(num_vecs);
  tdata_a.cache_index_table_d.assign(-1);

  // build a list of all indices that should be mapped because they are
  // currently in the model
  //
  long needed_cache_size = 0;
  for (long i = 0; i < num_vecs; i++) {
    if (tdata_a.hyperparams_d(i) < tdata_a.alpha_thresh_d) {
      tdata_a.cache_index_table_d(i) = needed_cache_size++;
    }
  }

  // add to the cache all elements in the current subset
  //
  long ii = tdata_a.active_subset_d;
  long subset_length = tdata_a.subsets_d(ii).length();
  for (long i = 0; i < subset_length; i++) {
    if ((long)(tdata_a.cache_index_table_d(tdata_a.subsets_d(ii)(i))) <
	(long)0) {
     tdata_a.cache_index_table_d(tdata_a.subsets_d(ii)(i))=needed_cache_size++;
    }
  }

  // set the new cache_size
  //
  tdata_a.phi_cache_d.setLength(needed_cache_size);

  // build the cache
  //
  long cache_index = -1;
  for (long i = 0; i < num_vecs; i++) {
    cache_index = tdata_a.cache_index_table_d(i);
    if (cache_index >= 0) {
      getPhiRow(tdata_a.phi_cache_d(cache_index), i);
    }
  }
  
  // exit gracefully
  //
  return true;
}

// method: getPhiRowFromCache
//
// arguments:
//  VectorFloat& ovec: (output) row of the full PHI matrix - to save memory
//                     we do not actually compute this matrix
//  long index: (input) index into the full PHI matrix
//  RVMTrainData& tdata: (input) training data storage
//
// return: logical value indicating status
//
// this method returns a cached value of a row in the phi matrix
//
boolean RelevanceVectorMachine::getPhiRowFromCache(VectorFloat& ovec_a,
						   long index_a,
						   RVMTrainData& tdata_a) {

  // initialize the output vector
  //
  ovec_a.setLength(vectors_d.length());

  // find the location of the row in the cache
  //
  long cache_index = tdata_a.cache_index_table_d(index_a);

  if (cache_index < 0) {
    return Error::handle(name(), L"getPhiRowFromCache:row not in cache", ERR,
			 __FILE__, __LINE__);
  }
  
  // assign the vector
  //
  ovec_a.assign(tdata_a.phi_cache_d(cache_index));
  
  // exit gracefully
  //
  return true;
}

// method: resetActiveParams
//
// arguments:
//  RVMTrainData& tdata: (input) training data storage
//
// return: logical value indicating status
//
// this method sets flags for those parameters that can be updated
//
boolean RelevanceVectorMachine::resetActiveParams(RVMTrainData& tdata_a) {

  // unset all parameters
  //
  Boolean f = false;
  tdata_a.active_params_d.assign(f);

  // set only those that are in the current subset or that are in the
  // current model. Only those in the current subset can be added. Those
  // in the current model or current subset can be modified or pruned.
  //
  long num_vecs = tdata_a.hyperparams_d.length();

  // build a list of all indices that should be mapped because they are
  // currently in the model
  //
  for (long i = 0; i < num_vecs; i++) {
    if (tdata_a.hyperparams_d(i) < tdata_a.alpha_thresh_d) {
      tdata_a.active_params_d(i) = true;
    }
  }

  long ii = tdata_a.active_subset_d;
  long subset_length = tdata_a.subsets_d(ii).length();
  for (long i = 0; i < subset_length; i++) {
    long subset_id = (long)tdata_a.subsets_d(ii)(i);
    tdata_a.active_params_d(subset_id) = true;
  }

  // exit gracefully
  //
  return true;
}
