/*
 *    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., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

/*
 * LibSVM.java
 * Copyright (C) 2005 Yasser EL-Manzalawy (original code)
 * Copyright (C) 2005 University of Waikato, Hamilton, NZ (adapted code)
 * 
 */

package weka.classifiers.functions;

import weka.classifiers.Classifier;
import weka.core.Capabilities;
import weka.core.Instance;
import weka.core.Instances;
import weka.core.Option;
import weka.core.RevisionUtils;
import weka.core.SelectedTag;
import weka.core.Tag;
import weka.core.TechnicalInformation;
import weka.core.TechnicalInformationHandler;
import weka.core.Utils;
import weka.core.Capabilities.Capability;
import weka.core.TechnicalInformation.Type;
import weka.filters.Filter;
import weka.filters.unsupervised.attribute.Normalize;
import weka.filters.unsupervised.attribute.ReplaceMissingValues;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Enumeration;
import java.util.StringTokenizer;
import java.util.Vector;
import weka.classifiers.AbstractClassifier;

/*
 * Modifications by FracPete:
 * - complete overhaul to make it useable in Weka
 * - accesses libsvm classes only via Reflection to make Weka compile without
 *   the libsvm classes
 * - uses more efficient code to transfer the data into the libsvm sparse format
 */

/** 
 <!-- globalinfo-start -->
 * A wrapper class for the libsvm tools (the libsvm classes, typically the jar file, need to be in the classpath to use this classifier).<br/>
 * LibSVM runs faster than SMO since it uses LibSVM to build the SVM classifier.<br/>
 * LibSVM allows users to experiment with One-class SVM, Regressing SVM, and nu-SVM supported by LibSVM tool. LibSVM reports many useful statistics about LibSVM classifier (e.g., confusion matrix,precision, recall, ROC score, etc.).<br/>
 * <br/>
 * Yasser EL-Manzalawy (2005). WLSVM. URL http://www.cs.iastate.edu/~yasser/wlsvm/.<br/>
 * <br/>
 * Chih-Chung Chang, Chih-Jen Lin (2001). LIBSVM - A Library for Support Vector Machines. URL http://www.csie.ntu.edu.tw/~cjlin/libsvm/.
 * <p/>
 <!-- globalinfo-end -->
 *
 <!-- technical-bibtex-start -->
 * BibTeX:
 * <pre>
 * &#64;misc{EL-Manzalawy2005,
 *    author = {Yasser EL-Manzalawy},
 *    note = {You don't need to include the WLSVM package in the CLASSPATH},
 *    title = {WLSVM},
 *    year = {2005},
 *    URL = {http://www.cs.iastate.edu/\~yasser/wlsvm/}
 * }
 * 
 * &#64;misc{Chang2001,
 *    author = {Chih-Chung Chang and Chih-Jen Lin},
 *    note = {The Weka classifier works with version 2.82 of LIBSVM},
 *    title = {LIBSVM - A Library for Support Vector Machines},
 *    year = {2001},
 *    URL = {http://www.csie.ntu.edu.tw/\~cjlin/libsvm/}
 * }
 * </pre>
 * <p/>
 <!-- technical-bibtex-end -->
 *
 <!-- options-start -->
 * Valid options are: <p/>
 * 
 * <pre> -S &lt;int&gt;
 *  Set type of SVM (default: 0)
 *    0 = C-SVC
 *    1 = nu-SVC
 *    2 = one-class SVM
 *    3 = epsilon-SVR
 *    4 = nu-SVR</pre>
 * 
 * <pre> -K &lt;int&gt;
 *  Set type of kernel function (default: 2)
 *    0 = linear: u'*v
 *    1 = polynomial: (gamma*u'*v + coef0)^degree
 *    2 = radial basis function: exp(-gamma*|u-v|^2)
 *    3 = sigmoid: tanh(gamma*u'*v + coef0)</pre>
 * 
 * <pre> -D &lt;int&gt;
 *  Set degree in kernel function (default: 3)</pre>
 * 
 * <pre> -G &lt;double&gt;
 *  Set gamma in kernel function (default: 1/k)</pre>
 * 
 * <pre> -R &lt;double&gt;
 *  Set coef0 in kernel function (default: 0)</pre>
 * 
 * <pre> -C &lt;double&gt;
 *  Set the parameter C of C-SVC, epsilon-SVR, and nu-SVR
 *   (default: 1)</pre>
 * 
 * <pre> -N &lt;double&gt;
 *  Set the parameter nu of nu-SVC, one-class SVM, and nu-SVR
 *   (default: 0.5)</pre>
 * 
 * <pre> -Z
 *  Turns on normalization of input data (default: off)</pre>
 * 
 * <pre> -J
 *  Turn off nominal to binary conversion.
 *  WARNING: use only if your data is all numeric!</pre>
 * 
 * <pre> -V
 *  Turn off missing value replacement.
 *  WARNING: use only if your data has no missing values.</pre>
 * 
 * <pre> -P &lt;double&gt;
 *  Set the epsilon in loss function of epsilon-SVR (default: 0.1)</pre>
 * 
 * <pre> -M &lt;double&gt;
 *  Set cache memory size in MB (default: 40)</pre>
 * 
 * <pre> -E &lt;double&gt;
 *  Set tolerance of termination criterion (default: 0.001)</pre>
 * 
 * <pre> -H
 *  Turns the shrinking heuristics off (default: on)</pre>
 * 
 * <pre> -W &lt;double&gt;
 *  Set the parameters C of class i to weight[i]*C, for C-SVC
 *  E.g., for a 3-class problem, you could use "1 1 1" for equally
 *  weighted classes.
 *  (default: 1 for all classes)</pre>
 * 
 * <pre> -B
 *  Trains a SVC model instead of a SVR one (default: SVR)</pre>
 * 
 * <pre> -D
 *  If set, classifier is run in debug mode and
 *  may output additional info to the console</pre>
 * 
 <!-- options-end -->
 *
 * @author  Yasser EL-Manzalawy
 * @author  FracPete (fracpete at waikato dot ac dot nz)
 * @version $Revision: 5523 $
 * @see     weka.core.converters.LibSVMLoader
 * @see     weka.core.converters.LibSVMSaver
 */
public class LibSVM  extends AbstractClassifier implements TechnicalInformationHandler {
  
  /** the svm classname */
  protected final static String CLASS_SVM = "libsvm.svm";
  
  /** the svm_model classname */
  protected final static String CLASS_SVMMODEL = "libsvm.svm_model";
  
  /** the svm_problem classname */
  protected final static String CLASS_SVMPROBLEM = "libsvm.svm_problem";
  
  /** the svm_parameter classname */
  protected final static String CLASS_SVMPARAMETER = "libsvm.svm_parameter";
  
  /** the svm_node classname */
  protected final static String CLASS_SVMNODE = "libsvm.svm_node";
  
  /** serial UID */
  protected static final long serialVersionUID = 14172;
  
  /** LibSVM Model */
  protected Object m_Model;
  
  /** for normalizing the data */
  protected Filter m_Filter = null;
    
  /** The filter used to get rid of missing values. */
  protected ReplaceMissingValues m_ReplaceMissingValues;
  
  /** normalize input data */
  protected boolean m_Normalize = false;
  
  /** If true, the replace missing values filter is not applied */
  private boolean m_noReplaceMissingValues;
  
  /** SVM type C-SVC (classification) */
  public static final int SVMTYPE_C_SVC = 0;
  /** SVM type nu-SVC (classification) */
  public static final int SVMTYPE_NU_SVC = 1;
  /** SVM type one-class SVM (classification) */
  public static final int SVMTYPE_ONE_CLASS_SVM = 2;
  /** SVM type epsilon-SVR (regression) */
  public static final int SVMTYPE_EPSILON_SVR = 3;
  /** SVM type nu-SVR (regression) */
  public static final int SVMTYPE_NU_SVR = 4;
  /** SVM types */
  public static final Tag[] TAGS_SVMTYPE = {
    new Tag(SVMTYPE_C_SVC, "C-SVC (classification)"),
    new Tag(SVMTYPE_NU_SVC, "nu-SVC (classification)"),
    new Tag(SVMTYPE_ONE_CLASS_SVM, "one-class SVM (classification)"),
    new Tag(SVMTYPE_EPSILON_SVR, "epsilon-SVR (regression)"),
    new Tag(SVMTYPE_NU_SVR, "nu-SVR (regression)")
  };
  
  /** the SVM type */
  protected int m_SVMType = SVMTYPE_C_SVC;
  
  /** kernel type linear: u'*v */
  public static final int KERNELTYPE_LINEAR = 0;
  /** kernel type polynomial: (gamma*u'*v + coef0)^degree */
  public static final int KERNELTYPE_POLYNOMIAL = 1;
  /** kernel type radial basis function: exp(-gamma*|u-v|^2) */
  public static final int KERNELTYPE_RBF = 2;
  /** kernel type sigmoid: tanh(gamma*u'*v + coef0) */
  public static final int KERNELTYPE_SIGMOID = 3;
  /** the different kernel types */
  public static final Tag[] TAGS_KERNELTYPE = {
    new Tag(KERNELTYPE_LINEAR, "linear: u'*v"),
    new Tag(KERNELTYPE_POLYNOMIAL, "polynomial: (gamma*u'*v + coef0)^degree"),
    new Tag(KERNELTYPE_RBF, "radial basis function: exp(-gamma*|u-v|^2)"),
    new Tag(KERNELTYPE_SIGMOID, "sigmoid: tanh(gamma*u'*v + coef0)")
  };
  
  /** the kernel type */
  protected int m_KernelType = KERNELTYPE_RBF;
  
  /** for poly - in older versions of libsvm declared as a double.
   * At least since 2.82 it is an int. */
  protected int m_Degree = 3;
  
  /** for poly/rbf/sigmoid */
  protected double m_Gamma = 0;
  
  /** for poly/rbf/sigmoid (the actual gamma) */
  protected double m_GammaActual = 0;
  
  /** for poly/sigmoid */
  protected double m_Coef0 = 0;
  
  /** in MB */
  protected double m_CacheSize = 40;
  
  /** stopping criteria */
  protected double m_eps = 1e-3;
  
  /** cost, for C_SVC, EPSILON_SVR and NU_SVR */
  protected double m_Cost = 1;
  
  /** for C_SVC */
  protected int[] m_WeightLabel = new int[0];
  
  /** for C_SVC */
  protected double[] m_Weight = new double[0];
  
  /** for NU_SVC, ONE_CLASS, and NU_SVR */
  protected double m_nu = 0.5;
  
  /** loss, for EPSILON_SVR */
  protected double m_Loss = 0.1;
  
  /** use the shrinking heuristics */
  protected boolean m_Shrinking = true;	
  
  /** whether to generate probability estimates instead of +1/-1 in case of 
   * classification problems */
  protected boolean m_ProbabilityEstimates = false;
    
  /** whether the libsvm classes are in the Classpath */
  protected static boolean m_Present = false;
  static {
    try {
      Class.forName(CLASS_SVM);
      m_Present = true;
    }
    catch (Exception e) {
      m_Present = false;
    }
  }
  
  /**
   * Returns a string describing classifier
   * 
   * @return a description suitable for displaying in the
   *         explorer/experimenter gui
   */
  public String globalInfo() {
    return 
      "A wrapper class for the libsvm tools (the libsvm classes, typically "
    + "the jar file, need to be in the classpath to use this classifier).\n"
    + "LibSVM runs faster than SMO since it uses LibSVM to build the SVM "
    + "classifier.\n"
    + "LibSVM allows users to experiment with One-class SVM, Regressing SVM, "
    + "and nu-SVM supported by LibSVM tool. LibSVM reports many useful "
    + "statistics about LibSVM classifier (e.g., confusion matrix,"
    + "precision, recall, ROC score, etc.).\n"
    + "\n"
    + getTechnicalInformation().toString();
  }

  /**
   * Returns an instance of a TechnicalInformation object, containing 
   * detailed information about the technical background of this class,
   * e.g., paper reference or book this class is based on.
   * 
   * @return the technical information about this class
   */
  public TechnicalInformation getTechnicalInformation() {
    TechnicalInformation 	result;
    TechnicalInformation 	additional;
    
    result = new TechnicalInformation(Type.MISC);
    result.setValue(TechnicalInformation.Field.AUTHOR, "Yasser EL-Manzalawy");
    result.setValue(TechnicalInformation.Field.YEAR, "2005");
    result.setValue(TechnicalInformation.Field.TITLE, "WLSVM");
    result.setValue(TechnicalInformation.Field.NOTE, "LibSVM was originally developed as 'WLSVM'");
    result.setValue(TechnicalInformation.Field.URL, "http://www.cs.iastate.edu/~yasser/wlsvm/");
    result.setValue(TechnicalInformation.Field.NOTE, "You don't need to include the WLSVM package in the CLASSPATH");
    
    additional = result.add(Type.MISC);
    additional.setValue(TechnicalInformation.Field.AUTHOR, "Chih-Chung Chang and Chih-Jen Lin");
    additional.setValue(TechnicalInformation.Field.TITLE, "LIBSVM - A Library for Support Vector Machines");
    additional.setValue(TechnicalInformation.Field.YEAR, "2001");
    additional.setValue(TechnicalInformation.Field.URL, "http://www.csie.ntu.edu.tw/~cjlin/libsvm/");
    additional.setValue(TechnicalInformation.Field.NOTE, "The Weka classifier works with version 2.82 of LIBSVM");
    
    return result;
  }
  
  /**
   * Returns an enumeration describing the available options.
   * 
   * @return an enumeration of all the available options.
   */
  public Enumeration listOptions() {
    Vector      result;
    
    result = new Vector();
    
    result.addElement(
        new Option(
            "\tSet type of SVM (default: 0)\n"
            + "\t\t 0 = C-SVC\n" 
            + "\t\t 1 = nu-SVC\n"
            + "\t\t 2 = one-class SVM\n" 
            + "\t\t 3 = epsilon-SVR\n"
            + "\t\t 4 = nu-SVR", 
            "S", 1, "-S <int>"));
    
    result.addElement(
        new Option(
            "\tSet type of kernel function (default: 2)\n"
            + "\t\t 0 = linear: u'*v\n"
            + "\t\t 1 = polynomial: (gamma*u'*v + coef0)^degree\n"
            + "\t\t 2 = radial basis function: exp(-gamma*|u-v|^2)\n"
            + "\t\t 3 = sigmoid: tanh(gamma*u'*v + coef0)",
            "K", 1, "-K <int>"));
    
    result.addElement(
        new Option(
            "\tSet degree in kernel function (default: 3)", 
            "D", 1, "-D <int>"));
    
    result.addElement(
        new Option(
            "\tSet gamma in kernel function (default: 1/k)", 
            "G", 1, "-G <double>"));
    
    result.addElement(
        new Option(
            "\tSet coef0 in kernel function (default: 0)", 
            "R", 1, "-R <double>"));
    
    result.addElement(
        new Option(
            "\tSet the parameter C of C-SVC, epsilon-SVR, and nu-SVR\n"
            + "\t (default: 1)",
            "C", 1, "-C <double>"));
    
    result.addElement(
        new Option(
            "\tSet the parameter nu of nu-SVC, one-class SVM, and nu-SVR\n"
            + "\t (default: 0.5)",
            "N", 1, "-N <double>"));
    
    result.addElement(
        new Option(
            "\tTurns on normalization of input data (default: off)", 
            "Z", 0, "-Z"));
    
    result.addElement(
        new Option("\tTurn off nominal to binary conversion."
            + "\n\tWARNING: use only if your data is all numeric!",
            "J", 0, "-J"));
    
    result.addElement(
        new Option("\tTurn off missing value replacement."
            + "\n\tWARNING: use only if your data has no missing "
            + "values.", "V", 0, "-V"));
    
    result.addElement(
        new Option(
            "\tSet the epsilon in loss function of epsilon-SVR (default: 0.1)",
            "P", 1, "-P <double>"));
    
    result.addElement(
        new Option(
            "\tSet cache memory size in MB (default: 40)", 
            "M", 1, "-M <double>"));
    
    result.addElement(
        new Option(
            "\tSet tolerance of termination criterion (default: 0.001)",
            "E", 1, "-E <double>"));
    
    result.addElement(
        new Option(
            "\tTurns the shrinking heuristics off (default: on)",
            "H", 0, "-H"));
    
    result.addElement(
        new Option(
            "\tSet the parameters C of class i to weight[i]*C, for C-SVC\n" 
            + "\tE.g., for a 3-class problem, you could use \"1 1 1\" for equally\n"
            + "\tweighted classes.\n"
            + "\t(default: 1 for all classes)",
            "W", 1, "-W <double>"));
    
    result.addElement(
        new Option(
            "\tTrains a SVC model instead of a SVR one (default: SVR)",
            "B", 0, "-B"));

    Enumeration en = super.listOptions();
    while (en.hasMoreElements())
      result.addElement(en.nextElement());
    
    return result.elements();
  }
  
  /**
   * Sets the classifier options <p/>
   * 
   <!-- options-start -->
   * Valid options are: <p/>
   * 
   * <pre> -S &lt;int&gt;
   *  Set type of SVM (default: 0)
   *    0 = C-SVC
   *    1 = nu-SVC
   *    2 = one-class SVM
   *    3 = epsilon-SVR
   *    4 = nu-SVR</pre>
   * 
   * <pre> -K &lt;int&gt;
   *  Set type of kernel function (default: 2)
   *    0 = linear: u'*v
   *    1 = polynomial: (gamma*u'*v + coef0)^degree
   *    2 = radial basis function: exp(-gamma*|u-v|^2)
   *    3 = sigmoid: tanh(gamma*u'*v + coef0)</pre>
   * 
   * <pre> -D &lt;int&gt;
   *  Set degree in kernel function (default: 3)</pre>
   * 
   * <pre> -G &lt;double&gt;
   *  Set gamma in kernel function (default: 1/k)</pre>
   * 
   * <pre> -R &lt;double&gt;
   *  Set coef0 in kernel function (default: 0)</pre>
   * 
   * <pre> -C &lt;double&gt;
   *  Set the parameter C of C-SVC, epsilon-SVR, and nu-SVR
   *   (default: 1)</pre>
   * 
   * <pre> -N &lt;double&gt;
   *  Set the parameter nu of nu-SVC, one-class SVM, and nu-SVR
   *   (default: 0.5)</pre>
   * 
   * <pre> -Z
   *  Turns on normalization of input data (default: off)</pre>
   * 
   * <pre> -J
   *  Turn off nominal to binary conversion.
   *  WARNING: use only if your data is all numeric!</pre>
   * 
   * <pre> -V
   *  Turn off missing value replacement.
   *  WARNING: use only if your data has no missing values.</pre>
   * 
   * <pre> -P &lt;double&gt;
   *  Set the epsilon in loss function of epsilon-SVR (default: 0.1)</pre>
   * 
   * <pre> -M &lt;double&gt;
   *  Set cache memory size in MB (default: 40)</pre>
   * 
   * <pre> -E &lt;double&gt;
   *  Set tolerance of termination criterion (default: 0.001)</pre>
   * 
   * <pre> -H
   *  Turns the shrinking heuristics off (default: on)</pre>
   * 
   * <pre> -W &lt;double&gt;
   *  Set the parameters C of class i to weight[i]*C, for C-SVC
   *  E.g., for a 3-class problem, you could use "1 1 1" for equally
   *  weighted classes.
   *  (default: 1 for all classes)</pre>
   * 
   * <pre> -B
   *  Trains a SVC model instead of a SVR one (default: SVR)</pre>
   * 
   * <pre> -D
   *  If set, classifier is run in debug mode and
   *  may output additional info to the console</pre>
   * 
   <!-- options-end -->
   *
   * @param options     the options to parse
   * @throws Exception  if parsing fails
   */
  public void setOptions(String[] options) throws Exception {
    String      tmpStr;
    
    tmpStr = Utils.getOption('S', options);
    if (tmpStr.length() != 0)
      setSVMType(
          new SelectedTag(Integer.parseInt(tmpStr), TAGS_SVMTYPE));
    else
      setSVMType(
          new SelectedTag(SVMTYPE_C_SVC, TAGS_SVMTYPE));
    
    tmpStr = Utils.getOption('K', options);
    if (tmpStr.length() != 0)
      setKernelType(
          new SelectedTag(Integer.parseInt(tmpStr), TAGS_KERNELTYPE));
    else
      setKernelType(
          new SelectedTag(KERNELTYPE_RBF, TAGS_KERNELTYPE));
    
    tmpStr = Utils.getOption('D', options);
    if (tmpStr.length() != 0)
      setDegree(Integer.parseInt(tmpStr));
    else
      setDegree(3);
    
    tmpStr = Utils.getOption('G', options);
    if (tmpStr.length() != 0)
      setGamma(Double.parseDouble(tmpStr));
    else
      setGamma(0);
    
    tmpStr = Utils.getOption('R', options);
    if (tmpStr.length() != 0)
      setCoef0(Double.parseDouble(tmpStr));
    else
      setCoef0(0);
    
    tmpStr = Utils.getOption('N', options);
    if (tmpStr.length() != 0)
      setNu(Double.parseDouble(tmpStr));
    else
      setNu(0.5);
    
    tmpStr = Utils.getOption('M', options);
    if (tmpStr.length() != 0)
      setCacheSize(Double.parseDouble(tmpStr));
    else
      setCacheSize(40);
    
    tmpStr = Utils.getOption('C', options);
    if (tmpStr.length() != 0)
      setCost(Double.parseDouble(tmpStr));
    else
      setCost(1);
    
    tmpStr = Utils.getOption('E', options);
    if (tmpStr.length() != 0)
      setEps(Double.parseDouble(tmpStr));
    else
      setEps(1e-3);
    
    setNormalize(Utils.getFlag('Z', options));
    
    setDoNotReplaceMissingValues(Utils.getFlag("V", options));
    
    tmpStr = Utils.getOption('P', options);
    if (tmpStr.length() != 0)
      setLoss(Double.parseDouble(tmpStr));
    else
      setLoss(0.1);
    
    setShrinking(!Utils.getFlag('H', options));
    
    setWeights(Utils.getOption('W', options));
    
    setProbabilityEstimates(Utils.getFlag('B', options));
  }
  
  /**
   * Returns the current options
   * 
   * @return            the current setup
   */
  public String[] getOptions() {
    Vector        result;
    
    result  = new Vector();
    
    result.add("-S");
    result.add("" + m_SVMType);
    
    result.add("-K");
    result.add("" + m_KernelType);
    
    result.add("-D");
    result.add("" + getDegree());
    
    result.add("-G");
    result.add("" + getGamma());
    
    result.add("-R");
    result.add("" + getCoef0());
    
    result.add("-N");
    result.add("" + getNu());
    
    result.add("-M");
    result.add("" + getCacheSize());
    
    result.add("-C");
    result.add("" + getCost());
    
    result.add("-E");
    result.add("" + getEps());
    
    result.add("-P");
    result.add("" + getLoss());
    
    if (!getShrinking())
      result.add("-H");
    
    if (getNormalize())
      result.add("-Z");
        
    if (getDoNotReplaceMissingValues())
      result.add("-V");
    
    if (getWeights().length() != 0) {
      result.add("-W");
      result.add("" + getWeights());
    }
    
    if (getProbabilityEstimates())
      result.add("-B");
    
    return (String[]) result.toArray(new String[result.size()]);
  }
  
  /**
   * returns whether the libsvm classes are present or not, i.e. whether the 
   * classes are in the classpath or not
   *
   * @return whether the libsvm classes are available
   */
  public static boolean isPresent() {
    return m_Present;
  }
  
  /**
   * Sets type of SVM (default SVMTYPE_C_SVC)
   * 
   * @param value       the type of the SVM
   */
  public void setSVMType(SelectedTag value) {
    if (value.getTags() == TAGS_SVMTYPE)
      m_SVMType = value.getSelectedTag().getID();
  }
  
  /**
   * Gets type of SVM
   * 
   * @return            the type of the SVM
   */
  public SelectedTag getSVMType() {
    return new SelectedTag(m_SVMType, TAGS_SVMTYPE);
  }
  
  /**
   * Returns the tip text for this property
   *
   * @return tip text for this property suitable for
   *         displaying in the explorer/experimenter gui
   */
  public String SVMTypeTipText() {
    return "The type of SVM to use.";
  }
  
  /**
   * Sets type of kernel function (default KERNELTYPE_RBF)
   * 
   * @param value       the kernel type
   */
  public void setKernelType(SelectedTag value) {
    if (value.getTags() == TAGS_KERNELTYPE)
      m_KernelType = value.getSelectedTag().getID();
  }
  
  /**
   * Gets type of kernel function
   * 
   * @return            the kernel type
   */
  public SelectedTag getKernelType() {
    return new SelectedTag(m_KernelType, TAGS_KERNELTYPE);
  }
  
  /**
   * Returns the tip text for this property
   *
   * @return tip text for this property suitable for
   *         displaying in the explorer/experimenter gui
   */
  public String kernelTypeTipText() {
    return "The type of kernel to use";
  }
  
  /**
   * Sets the degree of the kernel
   * 
   * @param value       the degree of the kernel
   */
  public void setDegree(int value) {
    m_Degree = value;
  }
  
  /**
   * Gets the degree of the kernel
   * 
   * @return            the degree of the kernel
   */
  public int getDegree() {
    return m_Degree;
  }
  
  /**
   * Returns the tip text for this property
   *
   * @return tip text for this property suitable for
   *         displaying in the explorer/experimenter gui
   */
  public String degreeTipText() {
    return "The degree of the kernel.";
  }
  
  /**
   * Sets gamma (default = 1/no of attributes)
   * 
   * @param value       the gamma value
   */
  public void setGamma(double value) {
    m_Gamma = value;
  }
  
  /**
   * Gets gamma
   * 
   * @return            the current gamma
   */
  public double getGamma() {
    return m_Gamma;
  }
  
  /**
   * Returns the tip text for this property
   *
   * @return tip text for this property suitable for
   *         displaying in the explorer/experimenter gui
   */
  public String gammaTipText() {
    return "The gamma to use, if 0 then 1/max_index is used.";
  }
  
  /**
   * Sets coef (default 0)
   * 
   * @param value       the coef
   */
  public void setCoef0(double value) {
    m_Coef0 = value;
  }
  
  /**
   * Gets coef
   * 
   * @return            the coef
   */
  public double getCoef0() {
    return m_Coef0;
  }
  
  /**
   * Returns the tip text for this property
   *
   * @return tip text for this property suitable for
   *         displaying in the explorer/experimenter gui
   */
  public String coef0TipText() {
    return "The coefficient to use.";
  }
  
  /**
   * Sets nu of nu-SVC, one-class SVM, and nu-SVR (default 0.5)
   * 
   * @param value       the new nu value
   */
  public void setNu(double value) {
    m_nu = value;
  }
  
  /**
   * Gets nu of nu-SVC, one-class SVM, and nu-SVR (default 0.5)
   * 
   * @return            the current nu value
   */
  public double getNu() {
    return m_nu;
  }
  
  /**
   * Returns the tip text for this property
   *
   * @return tip text for this property suitable for
   *         displaying in the explorer/experimenter gui
   */
  public String nuTipText() {
    return "The value of nu for nu-SVC, one-class SVM and nu-SVR.";
  }
  
  /**
   * Sets cache memory size in MB (default 40)
   * 
   * @param value       the memory size in MB
   */
  public void setCacheSize(double value) {
    m_CacheSize = value;
  }
  
  /**
   * Gets cache memory size in MB
   * 
   * @return            the memory size in MB
   */
  public double getCacheSize() {
    return m_CacheSize;
  }
  
  /**
   * Returns the tip text for this property
   *
   * @return tip text for this property suitable for
   *         displaying in the explorer/experimenter gui
   */
  public String cacheSizeTipText() {
    return "The cache size in MB.";
  }
  
  /**
   * Sets the parameter C of C-SVC, epsilon-SVR, and nu-SVR (default 1)
   * 
   * @param value       the cost value
   */
  public void setCost(double value) {
    m_Cost = value;
  }
  
  /**
   * Sets the parameter C of C-SVC, epsilon-SVR, and nu-SVR
   * 
   * @return            the cost value
   */
  public double getCost() {
    return m_Cost;
  }
  
  /**
   * Returns the tip text for this property
   *
   * @return tip text for this property suitable for
   *         displaying in the explorer/experimenter gui
   */
  public String costTipText() {
    return "The cost parameter C for C-SVC, epsilon-SVR and nu-SVR.";
  }
  
  /**
   * Sets tolerance of termination criterion (default 0.001)
   * 
   * @param value       the tolerance
   */
  public void setEps(double value) {
    m_eps = value;
  }
  
  /**
   * Gets tolerance of termination criterion
   * 
   * @return            the current tolerance
   */
  public double getEps() {
    return m_eps;
  }
  
  /**
   * Returns the tip text for this property
   *
   * @return tip text for this property suitable for
   *         displaying in the explorer/experimenter gui
   */
  public String epsTipText() {
    return "The tolerance of the termination criterion.";
  }
  
  /**
   * Sets the epsilon in loss function of epsilon-SVR (default 0.1)
   * 
   * @param value       the loss epsilon
   */
  public void setLoss(double value) {
    m_Loss = value;
  }
  
  /**
   * Gets the epsilon in loss function of epsilon-SVR
   * 
   * @return            the loss epsilon
   */
  public double getLoss() {
    return m_Loss;
  }
  
  /**
   * Returns the tip text for this property
   *
   * @return tip text for this property suitable for
   *         displaying in the explorer/experimenter gui
   */
  public String lossTipText() {
    return "The epsilon for the loss function in epsilon-SVR.";
  }
  
  /**
   * whether to use the shrinking heuristics
   * 
   * @param value       true uses shrinking
   */
  public void setShrinking(boolean value) {
    m_Shrinking = value;
  }
  
  /**
   * whether to use the shrinking heuristics
   * 
   * @return            true, if shrinking is used
   */
  public boolean getShrinking() {
    return m_Shrinking;
  }
  
  /**
   * Returns the tip text for this property
   *
   * @return tip text for this property suitable for
   *         displaying in the explorer/experimenter gui
   */
  public String shrinkingTipText() {
    return "Whether to use the shrinking heuristic.";
  }
  
  /**
   * whether to normalize input data
   * 
   * @param value       whether to normalize the data
   */
  public void setNormalize(boolean value) {
    m_Normalize = value;
  }
  
  /**
   * whether to normalize input data
   * 
   * @return            true, if the data is normalized
   */
  public boolean getNormalize() {
    return m_Normalize;
  }
  
  /**
   * Returns the tip text for this property
   *
   * @return tip text for this property suitable for
   *         displaying in the explorer/experimenter gui
   */
  public String normalizeTipText() {
    return "Whether to normalize the data.";
  }
    
  /**
   * Returns the tip text for this property
   *
   * @return tip text for this property suitable for
   *         displaying in the explorer/experimenter gui
   */
  public String doNotReplaceMissingValuesTipText() {
    return "Whether to turn off automatic replacement of missing "
      + "values. WARNING: set to true only if the data does not "
      + "contain missing values.";
  }
  
  /**
   * Whether to turn off automatic replacement of missing values.
   * Set to true only if the data does not contain missing values.
   * 
   * @param b true if automatic missing values replacement is
   * to be disabled.
   */
  public void setDoNotReplaceMissingValues(boolean b) {
    m_noReplaceMissingValues = b;
  }
  
  /**
   * Gets whether automatic replacement of missing values is
   * disabled.
   * 
   * @return true if automatic replacement of missing values
   * is disabled.
   */
  public boolean getDoNotReplaceMissingValues() {
    return m_noReplaceMissingValues;
  }
  
  /**
   * Sets the parameters C of class i to weight[i]*C, for C-SVC (default 1).
   * Blank separated list of doubles.
   * 
   * @param weightsStr          the weights (doubles, separated by blanks)
   */
  public void setWeights(String weightsStr) {
    StringTokenizer       tok;
    int                   i;
    
    tok           = new StringTokenizer(weightsStr, " ");
    m_Weight      = new double[tok.countTokens()];
    m_WeightLabel = new int[tok.countTokens()];
    
    if (m_Weight.length == 0)
      System.out.println(
          "Zero Weights processed. Default weights will be used");
    
    for (i = 0; i < m_Weight.length; i++) {
      m_Weight[i]      = Double.parseDouble(tok.nextToken());
      m_WeightLabel[i] = i;
    }
  }
  
  /**
   * Gets the parameters C of class i to weight[i]*C, for C-SVC (default 1).
   * Blank separated doubles.
   * 
   * @return            the weights (doubles separated by blanks)
   */
  public String getWeights() {
    String      result;
    int         i;
    
    result = "";
    for (i = 0; i < m_Weight.length; i++) {
      if (i > 0)
        result += " ";
      result += Double.toString(m_Weight[i]);
    }
    
    return result;
  }
  
  /**
   * Returns the tip text for this property
   *
   * @return tip text for this property suitable for
   *         displaying in the explorer/experimenter gui
   */
  public String weightsTipText() {
    return "The weights to use for the classes (blank-separated list, eg, \"1 1 1\" for a 3-class problem), if empty 1 is used by default.";
  }
  
  /**
   * Returns whether probability estimates are generated instead of -1/+1 for 
   * classification problems.
   * 
   * @param value       whether to predict probabilities
   */
  public void setProbabilityEstimates(boolean value) {
    m_ProbabilityEstimates = value;
  }
  
  /**
   * Sets whether to generate probability estimates instead of -1/+1 for 
   * classification problems.
   * 
   * @return            true, if probability estimates should be returned
   */
  public boolean getProbabilityEstimates() {
    return m_ProbabilityEstimates;
  }
  
  /**
   * Returns the tip text for this property
   *
   * @return tip text for this property suitable for
   *         displaying in the explorer/experimenter gui
   */
  public String probabilityEstimatesTipText() {
    return "Whether to generate probability estimates instead of -1/+1 for classification problems.";
  }
  
  /**
   * sets the specified field
   * 
   * @param o           the object to set the field for
   * @param name        the name of the field
   * @param value       the new value of the field
   */
  protected void setField(Object o, String name, Object value) {
    Field       f;
    
    try {
      f = o.getClass().getField(name);
      f.set(o, value);
    }
    catch (Exception e) {
      e.printStackTrace();
    }
  }
  
  /**
   * sets the specified field in an array
   * 
   * @param o           the object to set the field for
   * @param name        the name of the field
   * @param index       the index in the array
   * @param value       the new value of the field
   */
  protected void setField(Object o, String name, int index, Object value) {
    Field       f;
    
    try {
      f = o.getClass().getField(name);
      Array.set(f.get(o), index, value);
    }
    catch (Exception e) {
      e.printStackTrace();
    }
  }
  
  /**
   * returns the current value of the specified field
   * 
   * @param o           the object the field is member of
   * @param name        the name of the field
   * @return            the value
   */
  protected Object getField(Object o, String name) {
    Field       f;
    Object      result;
    
    try {
      f      = o.getClass().getField(name);
      result = f.get(o);
    }
    catch (Exception e) {
      e.printStackTrace();
      result = null;
    }
    
    return result;
  }
  
  /**
   * sets a new array for the field
   * 
   * @param o           the object to set the array for
   * @param name        the name of the field
   * @param type        the type of the array
   * @param length      the length of the one-dimensional array
   */
  protected void newArray(Object o, String name, Class type, int length) {
    newArray(o, name, type, new int[]{length});
  }
  
  /**
   * sets a new array for the field
   * 
   * @param o           the object to set the array for
   * @param name        the name of the field
   * @param type        the type of the array
   * @param dimensions  the dimensions of the array
   */
  protected void newArray(Object o, String name, Class type, int[] dimensions) {
    Field       f;
    
    try {
      f = o.getClass().getField(name);
      f.set(o, Array.newInstance(type, dimensions));
    }
    catch (Exception e) {
      e.printStackTrace();
    }
  }
  
  /**
   * executes the specified method and returns the result, if any
   * 
   * @param o                   the object the method should be called from
   * @param name                the name of the method
   * @param paramClasses        the classes of the parameters
   * @param paramValues         the values of the parameters
   * @return                    the return value of the method, if any (in that case null)
   */
  protected Object invokeMethod(Object o, String name, Class[] paramClasses, Object[] paramValues) {
    Method      m;
    Object      result;
    
    result = null;
    
    try {
      m      = o.getClass().getMethod(name, paramClasses);
      result = m.invoke(o, paramValues);
    }
    catch (Exception e) {
      e.printStackTrace();
      result = null;
    }
    
    return result;
  }
  
  /**
   * transfers the local variables into a svm_parameter object
   * 
   * @return the configured svm_parameter object
   */
  protected Object getParameters() {
    Object      result;
    int         i;
    
    try {
      result = Class.forName(CLASS_SVMPARAMETER).newInstance();
      
      setField(result, "svm_type", new Integer(m_SVMType));
      setField(result, "kernel_type", new Integer(m_KernelType));
      setField(result, "degree", new Integer(m_Degree));
      setField(result, "gamma", new Double(m_GammaActual));
      setField(result, "coef0", new Double(m_Coef0));
      setField(result, "nu", new Double(m_nu));
      setField(result, "cache_size", new Double(m_CacheSize));
      setField(result, "C", new Double(m_Cost));
      setField(result, "eps", new Double(m_eps));
      setField(result, "p", new Double(m_Loss));
      setField(result, "shrinking", new Integer(m_Shrinking ? 1 : 0));
      setField(result, "nr_weight", new Integer(m_Weight.length));
      setField(result, "probability", new Integer(m_ProbabilityEstimates ? 1 : 0));
      
      newArray(result, "weight", Double.TYPE, m_Weight.length);
      newArray(result, "weight_label", Integer.TYPE, m_Weight.length);
      for (i = 0; i < m_Weight.length; i++) {
        setField(result, "weight", i, new Double(m_Weight[i]));
        setField(result, "weight_label", i, new Integer(m_WeightLabel[i]));
      }
    }
    catch (Exception e) {
      e.printStackTrace();
      result = null;
    }
    
    return result;
  }
  
  /**
   * returns the svm_problem
   * 
   * @param vx the x values
   * @param vy the y values
   * @return the svm_problem object
   */
  protected Object getProblem(Vector vx, Vector vy) {
    Object      result;
    
    try {
      result = Class.forName(CLASS_SVMPROBLEM).newInstance();
      
      setField(result, "l", new Integer(vy.size()));
      
      newArray(result, "x", Class.forName(CLASS_SVMNODE), new int[]{vy.size(), 0});
      for (int i = 0; i < vy.size(); i++)
        setField(result, "x", i, vx.elementAt(i));
      
      newArray(result, "y", Double.TYPE, vy.size());
      for (int i = 0; i < vy.size(); i++)
        setField(result, "y", i, vy.elementAt(i));
    }
    catch (Exception e) {
      e.printStackTrace();
      result = null;
    }
    
    return result;
  }
  
  /**
   * returns an instance into a sparse libsvm array
   * 
   * @param instance	the instance to work on
   * @return		the libsvm array
   * @throws Exception	if setup of array fails
   */
  protected Object instanceToArray(Instance instance) throws Exception {
    int		index;
    int		count;
    int 	i;
    Object 	result;
    
    // determine number of non-zero attributes
    /*for (i = 0; i < instance.numAttributes(); i++) {
      if (i == instance.classIndex())
	continue;
      if (instance.value(i) != 0)
	count++;
    } */
    count = 0;
    for (i = 0; i < instance.numValues(); i++) {
      if (instance.index(i) == instance.classIndex())
        continue;
      if (instance.valueSparse(i) != 0)
        count++;
    }

    // fill array
    /* result = Array.newInstance(Class.forName(CLASS_SVMNODE), count);
    index  = 0;
    for (i = 0; i < instance.numAttributes(); i++) {
      if (i == instance.classIndex())
	continue;
      if (instance.value(i) == 0)
	continue;

      Array.set(result, index, Class.forName(CLASS_SVMNODE).newInstance());
      setField(Array.get(result, index), "index", new Integer(i + 1));
      setField(Array.get(result, index), "value", new Double(instance.value(i)));
      index++;
    } */
    
    result = Array.newInstance(Class.forName(CLASS_SVMNODE), count);
    index  = 0;
    for (i = 0; i < instance.numValues(); i++) {
      
      int idx = instance.index(i);
      if (idx == instance.classIndex())
        continue;
      if (instance.valueSparse(i) == 0)
        continue;

      Array.set(result, index, Class.forName(CLASS_SVMNODE).newInstance());
      setField(Array.get(result, index), "index", new Integer(idx + 1));
      setField(Array.get(result, index), "value", new Double(instance.valueSparse(i)));
      index++;
    }
    
    return result;
  }
  
  /**
   * Computes the distribution for a given instance. 
   * In case of 1-class classification, 1 is returned at index 0 if libsvm 
   * returns 1 and NaN (= missing) if libsvm returns -1.
   *
   * @param instance 		the instance for which distribution is computed
   * @return 			the distribution
   * @throws Exception 		if the distribution can't be computed successfully
   */
  public double[] distributionForInstance (Instance instance) throws Exception {	
    int[] labels = new int[instance.numClasses()];
    double[] prob_estimates = null;

    if (m_ProbabilityEstimates) {
      invokeMethod(
	  Class.forName(CLASS_SVM).newInstance(),
	  "svm_get_labels",
	  new Class[]{
	    Class.forName(CLASS_SVMMODEL), 
	    Array.newInstance(Integer.TYPE, instance.numClasses()).getClass()},
	    new Object[]{
	    m_Model, 
	    labels});

      prob_estimates = new double[instance.numClasses()];
    }
    
    if (!getDoNotReplaceMissingValues()) {
      m_ReplaceMissingValues.input(instance);
      m_ReplaceMissingValues.batchFinished();
      instance = m_ReplaceMissingValues.output();
    }
    
    if (m_Filter != null) {
      m_Filter.input(instance);
      m_Filter.batchFinished();
      instance = m_Filter.output();
    }

    Object x = instanceToArray(instance);
    double v;
    double[] result = new double[instance.numClasses()];
    if (    m_ProbabilityEstimates 
	 && ((m_SVMType == SVMTYPE_C_SVC) || (m_SVMType == SVMTYPE_NU_SVC)) ) {
      v = ((Double) invokeMethod(
          Class.forName(CLASS_SVM).newInstance(),
          "svm_predict_probability",
          new Class[]{
            Class.forName(CLASS_SVMMODEL), 
            Array.newInstance(Class.forName(CLASS_SVMNODE), Array.getLength(x)).getClass(),
            Array.newInstance(Double.TYPE, prob_estimates.length).getClass()},
          new Object[]{
            m_Model, 
            x,
            prob_estimates})).doubleValue();

      // Return order of probabilities to canonical weka attribute order
      for (int k = 0; k < prob_estimates.length; k++) {
        result[labels[k]] = prob_estimates[k];
      }
    }
    else {
      v = ((Double) invokeMethod(
          Class.forName(CLASS_SVM).newInstance(),
          "svm_predict",
          new Class[]{
            Class.forName(CLASS_SVMMODEL), 
            Array.newInstance(Class.forName(CLASS_SVMNODE), Array.getLength(x)).getClass()},
          new Object[]{
            m_Model, 
            x})).doubleValue();
      
      if (instance.classAttribute().isNominal()) {
	if (m_SVMType == SVMTYPE_ONE_CLASS_SVM) {
	  if (v > 0)
	    result[0] = 1;
	  else
	    result[0] = Double.NaN;  // outlier
	}
	else {
	  result[(int) v] = 1;
	}
      }
      else {
	result[0] = v;
      }
    }

    return result;                
  }

  /**
   * Returns default capabilities of the classifier.
   *
   * @return      the capabilities of this classifier
   */
  public Capabilities getCapabilities() {
    Capabilities result = super.getCapabilities();
    result.disableAll();

    // attributes
    result.enable(Capability.NOMINAL_ATTRIBUTES);
    result.enable(Capability.NUMERIC_ATTRIBUTES);
    result.enable(Capability.DATE_ATTRIBUTES);

    // class
    result.enableDependency(Capability.UNARY_CLASS);
    result.enableDependency(Capability.NOMINAL_CLASS);
    result.enableDependency(Capability.NUMERIC_CLASS);
    result.enableDependency(Capability.DATE_CLASS);

    switch (m_SVMType) {
      case SVMTYPE_C_SVC:
      case SVMTYPE_NU_SVC:
	result.enable(Capability.NOMINAL_CLASS);
	break;
	
      case SVMTYPE_ONE_CLASS_SVM:
	result.enable(Capability.UNARY_CLASS);
	break;
	
      case SVMTYPE_EPSILON_SVR:
      case SVMTYPE_NU_SVR:
	result.enable(Capability.NUMERIC_CLASS);
	result.enable(Capability.DATE_CLASS);
	break;
	
      default:
	throw new IllegalArgumentException("SVMType " + m_SVMType + " is not supported!");
    }
    result.enable(Capability.MISSING_CLASS_VALUES);
    
    return result;
  }
  
  /**
   * builds the classifier
   * 
   * @param insts       the training instances
   * @throws Exception  if libsvm classes not in classpath or libsvm
   *                    encountered a problem
   */
  public void buildClassifier(Instances insts) throws Exception {
    m_Filter = null;
    
    if (!isPresent())
      throw new Exception("libsvm classes not in CLASSPATH!");

    // remove instances with missing class
    insts = new Instances(insts);
    insts.deleteWithMissingClass();
    
    if (!getDoNotReplaceMissingValues()) {
      m_ReplaceMissingValues = new ReplaceMissingValues();
      m_ReplaceMissingValues.setInputFormat(insts);
      insts = Filter.useFilter(insts, m_ReplaceMissingValues);
    }
    
    // can classifier handle the data?
    // we check this here so that if the user turns off
    // replace missing values filtering, it will fail
    // if the data actually does have missing values
    getCapabilities().testWithFail(insts);
        
    if (getNormalize()) {
      m_Filter = new Normalize();
      m_Filter.setInputFormat(insts);
      insts = Filter.useFilter(insts, m_Filter);
    }
    
    Vector vy = new Vector();
    Vector vx = new Vector();
    int max_index = 0;
    
    for (int d = 0; d < insts.numInstances(); d++) {
      Instance inst = insts.instance(d);
      Object x = instanceToArray(inst);
      int m = Array.getLength(x);
      
      if (m > 0)
        max_index = Math.max(max_index, ((Integer) getField(Array.get(x, m - 1), "index")).intValue());
      vx.addElement(x);
      vy.addElement(new Double(inst.classValue()));
    }
    
    // calculate actual gamma
    if (getGamma() == 0)
      m_GammaActual = 1.0 / max_index;
    else
      m_GammaActual = m_Gamma;

    // check parameter
    String error_msg = (String) invokeMethod(
        Class.forName(CLASS_SVM).newInstance(), 
        "svm_check_parameter", 
        new Class[]{
          Class.forName(CLASS_SVMPROBLEM), 
          Class.forName(CLASS_SVMPARAMETER)},
        new Object[]{
          getProblem(vx, vy), 
          getParameters()});
    
    if (error_msg != null)
      throw new Exception("Error: " + error_msg);
    
    // train model
    m_Model = invokeMethod(
        Class.forName(CLASS_SVM).newInstance(), 
        "svm_train", 
        new Class[]{
          Class.forName(CLASS_SVMPROBLEM), 
          Class.forName(CLASS_SVMPARAMETER)},
        new Object[]{
          getProblem(vx, vy), 
          getParameters()});
  }
    
  /**
   * returns a string representation
   * 
   * @return            a string representation
   */
  public String toString() {
    return "LibSVM wrapper, original code by Yasser EL-Manzalawy (= WLSVM)";
  }
  
  /**
   * Returns the revision string.
   * 
   * @return		the revision
   */
  public String getRevision() {
    return RevisionUtils.extract("$Revision: 5523 $");
  }
  
  /**
   * Main method for testing this class.
   * 
   * @param args the options
   */
  public static void main(String[] args) {
    runClassifier(new LibSVM(), args);
  }
}
