package hex.pca;

import java.util.Arrays;

import water.Job.*;
import water.*;
import water.api.DocGen;
import water.fvec.*;
import water.util.RString;

/**
 * Principal Components Scoring
 * This algorithm maps a dataset into the subspace generated by the principal components.
 * If A = dataset to be scored, and B = eigenvector matrix (rows = features, cols = components),
 * then the score is simply A * B, assuming the column features match up exactly.
 * <a href = "http://en.wikipedia.org/wiki/Principal_component_analysis">PCA on Wikipedia</a>
 * @author anqi_fu
 *
 */
public class PCAScore extends FrameJob {
  static final int API_WEAVER = 1;
  static public DocGen.FieldDoc[] DOC_FIELDS;
  static final String DOC_GET = "pca_score";

  @API(help = "PCA model to use for scoring", required = true, filter = Default.class)
  PCAModel model;
  // hex.DPCA.PCAModel model;

  @API(help = "Number of principal components to return", filter = Default.class, lmin = 1, lmax = 10000)
  int num_pc = 1;

  // Note: Source data MUST contain all features (matched by name) used to build PCA model!
  // If additional columns exist in source, they are automatically ignored in scoring
  @Override protected void exec() {
    /*String[] fnames = new String[model._va._cols.length];
    for(int i = 0; i < fnames.length; i++)
      fnames[i] = model._va._cols[i]._name;

    Frame fr = subset(source, fnames);
    int nfeat = model._eigVec.length;
    Vec[] vecs = Arrays.copyOf(fr.vecs(), nfeat + num_pc);
    for(int i = 0; i < num_pc; i++)
      vecs[nfeat+i] = vecs[0].makeZero();
    PCAScoreTask tsk = new PCAScoreTask(this, nfeat, num_pc, model._eigVec, model._pcaParams._standardized); */

    Frame fr = subset(source, model._names);
    int nfeat = model._names.length;
    Vec[] vecs = Arrays.copyOf(fr.vecs(), nfeat + num_pc);
    for(int i = 0; i < num_pc; i++)
      vecs[nfeat+i] = vecs[0].makeZero();
    // PCAScoreTask tsk = new PCAScoreTask(this, nfeat, num_pc, model.eigVec, model.params.standardize);
    boolean temp = model.params.standardize == 1 ? true : false;
    PCAScoreTask tsk = new PCAScoreTask(this, nfeat, num_pc, model.eigVec, temp);
    tsk.doIt(new Frame(vecs));
    Vec[] outputVecs = Arrays.copyOfRange(tsk._fr.vecs(), nfeat, nfeat + num_pc);
    String [] names = new String[num_pc];
    for(int i = 0; i < num_pc; i++) names[i] = "PC" + i;
    Frame f = new Frame(names, outputVecs);
    DKV.put(destination_key, f);
  }

  @Override protected void init() {
    super.init();
    if(model != null && num_pc > model.num_pc)
      throw new IllegalArgumentException("Argument 'num_pc' must be between 1 and " + model.num_pc);
  }

  /* @Override public float progress() {
    ChunkProgress progress = UKV.get(progressKey());
    return (progress != null ? progress.progress() : 0);
  } */

  public final Frame subset(Frame data, String[] feat) {
    Vec[] dvecs = new Vec[feat.length];
    String[] dnames = new String[feat.length];

    Vec[] vecs = data.vecs();
    String[] names = data.names();
    for(int i = 0; i < feat.length; i++) {
      int idx = data.find(feat[i]);
      if(idx == -1)
        throw new IllegalArgumentException("Incompatible dataset: Column " + feat[i] + " does not exist in source!");
      dvecs[i] = vecs[idx];
      dnames[i] = names[idx];
    }
    return new Frame(dnames, dvecs);
  }

  public static String link(Key modelKey, String content) {
    return link("model", modelKey, content);
  }

  public static String link(String key_param, Key k, String content) {
    RString rs = new RString("<a href='PCAScore.query?%key_param=%$key'>%content</a>");
    rs.replace("key_param", key_param);
    rs.replace("key", k.toString());
    rs.replace("content", content);
    return rs.toString();
  }

  public static class PCAScoreTask extends MRTask2<PCAScoreTask> {
    final Job _job;
    double[] _normSub;
    double[] _normMul;
    final boolean _standardize;
    final int _nfeat;           // number of features
    final int _ncomp;           // number of principal components (<= nfeat)
    final double[][] _eigvec;   // eigenvector matrix

    public PCAScoreTask(Job job, int nfeat, int ncomp, double[][] eigvec, boolean standardize) {
      _job = job;
      _normSub = null;
      _normMul = null;
      _standardize = standardize;
      _nfeat = nfeat;
      _ncomp = ncomp;
      _eigvec = eigvec;
    }

    // Matrix multiplication A * B, where A is a skinny matrix (# rows >> # cols) and B is a
    // small matrix that fits on a single node. For PCA scoring, the cols of A (rows of B) are
    // the features of the input dataset, while the cols of B are the principal components.
    @Override public void map(Chunk [] chunks) {
      int rows = chunks[0]._len;
      ROW_LOOP:
      for(int r = 0; r < rows; r++) {
        for(int c = 0; c < _ncomp; c++) {
         double x = 0;
         for(int d = 0; d < _nfeat; d++) {
           if(chunks[d].isNA0(r)) {
             for(int i = 0; i < _ncomp; i++)
               chunks[_nfeat+i].setNA0(r);
               continue ROW_LOOP;
           }
           x += (chunks[d].at0(r) - _normSub[d])*_normMul[d]*_eigvec[d][c];
         }
         chunks[_nfeat+c].set0(r,x);
        }
      }
      // if(_job != null) _job.updateProgress(1);
    }

    public PCAScoreTask doIt(Frame fr) {
        final Vec[] vecs = fr.vecs();
        _normSub = MemoryManager.malloc8d(_nfeat);
        _normMul = MemoryManager.malloc8d(_nfeat); Arrays.fill(_normMul, 1);

        if(_standardize) {
          for(int i = 0; i < _nfeat; ++i) {
          _normSub[i] = vecs[i].mean();
          _normMul[i] = 1.0/vecs[i].sigma();
          }
        }
      dfork(fr);
      return getResult();
    }
  }
}

@@ -3574,16 +3574,19 @@
 a href='
+/2/
 PCAScore

Fix of PCA score link.