/*
 *    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.
 */

/*
 *    NeuralNetwork.java
 *    Copyright (C) 2008 University of Waikato, Hamilton, New Zealand
 *
 */

package weka.classifiers.pmml.consumer;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;

import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import weka.core.Attribute;
import weka.core.Instance;
import weka.core.Instances;
import weka.core.RevisionUtils;
import weka.core.Utils;
import weka.core.pmml.*;

/**
 * Class implementing import of PMML Neural Network model. Can be used as a Weka
 * classifier for prediction (buildClassifier() raises an Exception).
 * 
 * @author Mark Hall (mhall{[at]}pentaho{[dot]}com)
 * @version $Revision 1.0 $
 */
public class NeuralNetwork extends PMMLClassifier {
  
  /**
   * For serialization
   */
  private static final long serialVersionUID = -4545904813133921249L;

  /**
   * Small inner class for a NeuralInput (essentially just
   * wraps a DerivedField and adds an ID)
   */
  static class NeuralInput implements Serializable {
    
    /**
     * For serialization
     */
    private static final long serialVersionUID = -1902233762824835563L;
    
    /** Field that this input refers to */
    private DerivedFieldMetaInfo m_field;
    
    /** ID string */
    private String m_ID = null;
    
    private String getID() {
      return m_ID;
    }
    
    protected NeuralInput(Element input, MiningSchema miningSchema) throws Exception {
      m_ID = input.getAttribute("id");
      
      NodeList fL = input.getElementsByTagName("DerivedField");
      if (fL.getLength() != 1) {
        throw new Exception("[NeuralInput] expecting just one derived field!");
      }
      
      Element dF = (Element)fL.item(0);
      Instances allFields = miningSchema.getFieldsAsInstances();
      ArrayList<Attribute> fieldDefs = new ArrayList<Attribute>();
      for (int i = 0; i < allFields.numAttributes(); i++) {
        fieldDefs.add(allFields.attribute(i));
      }
      m_field = new DerivedFieldMetaInfo(dF, fieldDefs, miningSchema.getTransformationDictionary());
    }
    
    protected double getValue(double[] incoming) throws Exception {
      return m_field.getDerivedValue(incoming);
    }
    
    public String toString() {
      StringBuffer temp = new StringBuffer();
      
      temp.append("Nueral input (" + getID() + ")\n");
      temp.append(m_field);
      
      return temp.toString();
    }
  }
  
  /**
   * Inner class representing a layer in the network.
   */
  class NeuralLayer implements Serializable {
    
    /**
     * For serialization
     */
    private static final long serialVersionUID = -8386042001675763922L;

    /** The number of neurons in this layer */
    private int m_numNeurons = 0;
    
    /** Activation function (if defined, overrides one in NeuralNetwork) */
    private ActivationFunction m_layerActivationFunction = null;
    
    /** Threshold (if defined overrides one in NeuralNetwork) */
    private double m_layerThreshold = Double.NaN; 
    
    /** Width (if defined overrides one in NeuralNetwork) */
    private double m_layerWidth = Double.NaN;
    
    /** Altitude (if defined overrides one in NeuralNetwork) */
    private double m_layerAltitude = Double.NaN;
    
    /** Normalization (if defined overrides one in NeuralNetwork) */
    private Normalization m_layerNormalization = null;
    
    /** The neurons at this hidden layer */
    private Neuron[] m_layerNeurons = null;
    
    /** Stores the output of this layer (for given inputs) */
    private HashMap<String, Double> m_layerOutput = new HashMap<String, Double>();
    
    protected NeuralLayer(Element layerE) {
      
      String activationFunction = layerE.getAttribute("activationFunction");
      if (activationFunction != null && activationFunction.length() > 0) {
        for (ActivationFunction a : ActivationFunction.values()) {
          if (a.toString().equals(activationFunction)) {
            m_layerActivationFunction = a;
            break;
          }
        }
      } else {
        // use the network-level activation function
        m_layerActivationFunction = m_activationFunction;
      }
      
      String threshold = layerE.getAttribute("threshold");
      if (threshold != null && threshold.length() > 0) {
        m_layerThreshold = Double.parseDouble(threshold);
      } else {
        // use network-level threshold
        m_layerThreshold = m_threshold;
      }
      
      String width = layerE.getAttribute("width");
      if (width != null && width.length() > 0) {
        m_layerWidth = Double.parseDouble(width);
      } else {
        // use network-level width
        m_layerWidth = m_width;
      }
      
      String altitude = layerE.getAttribute("altitude");
      if (altitude != null && altitude.length() > 0) {
        m_layerAltitude = Double.parseDouble(altitude);
      } else {
        // use network-level altitude
        m_layerAltitude = m_altitude;
      }
      
      String normMethod = layerE.getAttribute("normalizationMethod");
      if (normMethod != null && normMethod.length() > 0) {
        for (Normalization n : Normalization.values()) {
          if (n.toString().equals(normMethod)) {
            m_layerNormalization = n;
            break;
          }
        }
      } else {
        // use network-level normalization method
        m_layerNormalization = m_normalizationMethod;
      }
      
      NodeList neuronL = layerE.getElementsByTagName("Neuron");
      m_numNeurons = neuronL.getLength();
      m_layerNeurons = new Neuron[m_numNeurons];
      for (int i = 0; i < neuronL.getLength(); i++) {
        Node neuronN = neuronL.item(i);
        if (neuronN.getNodeType() == Node.ELEMENT_NODE) {
          m_layerNeurons[i] = new Neuron((Element)neuronN, this);
        }
      }
    }
    
    protected ActivationFunction getActivationFunction() {
      return m_layerActivationFunction;
    }
    
    protected double getThreshold() {
      return m_layerThreshold;
    }
    
    protected double getWidth() {
      return m_layerWidth;
    }
    
    protected double getAltitude() {
      return m_layerAltitude;
    }
    
    protected Normalization getNormalization() {
      return m_layerNormalization;
    }
    
    /**
     * Compute the output values for this layer.
     * 
     * @param incoming the incoming values
     * @return the output values for this layer
     * @throws Exception if there is a problem computing the outputs
     */
    protected HashMap<String, Double> computeOutput(HashMap<String, Double> incoming) 
      throws Exception {
      
      m_layerOutput.clear();
      
      double normSum = 0;
      for (int i = 0; i < m_layerNeurons.length; i++) {
        double neuronOut = m_layerNeurons[i].getValue(incoming);
        String neuronID = m_layerNeurons[i].getID();

        if (m_layerNormalization == Normalization.SOFTMAX) {
          normSum += Math.exp(neuronOut);
        } else if (m_layerNormalization == Normalization.SIMPLEMAX) {
          normSum += neuronOut;
        }
        //System.err.println("Inserting ID " + neuronID + " " + neuronOut);
        m_layerOutput.put(neuronID, neuronOut);
      }
      
      // apply the normalization (if necessary)
      if (m_layerNormalization != Normalization.NONE) {
        for (int i = 0; i < m_layerNeurons.length; i++) {
          double val = m_layerOutput.get(m_layerNeurons[i].getID());
//          System.err.println("Normalizing ID " + m_layerNeurons[i].getID() + " " + val);
          if (m_layerNormalization == Normalization.SOFTMAX) {
            val = Math.exp(val) / normSum;
          } else {
            val = (val / normSum);
          }
          m_layerOutput.put(m_layerNeurons[i].getID(), val);
        }
      }
      return m_layerOutput;
    }
    
    public String toString() {
      StringBuffer temp = new StringBuffer();
      
      temp.append("activation: " + getActivationFunction() + "\n");
      if (!Double.isNaN(getThreshold())) {
        temp.append("threshold: " + getThreshold() + "\n");
      }
      if (!Double.isNaN(getWidth())) {
        temp.append("width: " + getWidth() + "\n");
      }
      if (!Double.isNaN(getAltitude())) {
        temp.append("altitude: " + getAltitude() + "\n");
      }
      temp.append("normalization: " + m_layerNormalization + "\n");
      for (int i = 0; i < m_numNeurons; i++) {
        temp.append(m_layerNeurons[i] + "\n");
      }

      return temp.toString();
    }
  }
  
  /**
   * Inner class encapsulating a Neuron
   */
  static class Neuron implements Serializable {
    
    /**
     * For serialization
     */
    private static final long serialVersionUID = -3817434025682603443L;

    /** ID string */
    private String m_ID = null;
    
    /** The layer we belong to (for accessing activation function, threshold etc.) */
    private NeuralLayer m_layer;
    
    /** The bias */
    private double m_bias = 0.0;
    
    /** The width (if defined overrides the one in NeuralLayer or NeuralNetwork) */
    private double m_neuronWidth = Double.NaN;
    
    /** The altitude (if defined overrides the one in NeuralLayer or NeuralNetwork) */
    private double m_neuronAltitude = Double.NaN;
    
    /** The IDs of the neurons/neural inputs that we are connected to */
    private String[] m_connectionIDs = null;
    
    /** The weights corresponding to the connections */
    private double[] m_weights = null;
    
    protected Neuron(Element neuronE, NeuralLayer layer) {
      m_layer = layer;
      
      m_ID = neuronE.getAttribute("id");
      
      String bias = neuronE.getAttribute("bias");
      if (bias != null && bias.length() > 0) {
        m_bias = Double.parseDouble(bias);
      }
      
      String width = neuronE.getAttribute("width");
      if (width != null && width.length() > 0) {
        m_neuronWidth = Double.parseDouble(width);
      }
      
      String altitude = neuronE.getAttribute("altitude");
      if (altitude != null && altitude.length() > 0) {
        m_neuronAltitude = Double.parseDouble(altitude);
      }
      
      // get the connection details
      NodeList conL = neuronE.getElementsByTagName("Con");
      m_connectionIDs = new String[conL.getLength()];
      m_weights = new double[conL.getLength()];
      for (int i = 0; i < conL.getLength(); i++) {
        Node conN = conL.item(i);
        if (conN.getNodeType() == Node.ELEMENT_NODE) {
          Element conE = (Element)conN;
          m_connectionIDs[i] = conE.getAttribute("from");
          String weight = conE.getAttribute("weight");
          m_weights[i] = Double.parseDouble(weight);
        }
      }
    }
    
    protected String getID() {
      return m_ID;
    }    
    
    /**
     * Compute the output of this Neuron.
     * 
     * @param incoming a Map of input values. The keys are the IDs
     * of incoming connections (either neural inputs or neurons) and
     * the values are the output values of the neural input/neuron in
     * question.
     * 
     * @return the output of this neuron
     * @throws Exception if any of our incoming connection IDs cannot be
     * located in the Map
     */
    protected double getValue(HashMap<String, Double> incoming) throws Exception {
      
      double z = 0;
      double result = Double.NaN;
      
      double width = (Double.isNaN(m_neuronWidth))
        ? m_layer.getWidth()
        : m_neuronWidth;

      z = m_bias;
      for (int i = 0; i < m_connectionIDs.length; i++) {
        Double inVal = incoming.get(m_connectionIDs[i]);
        if (inVal == null) {
          throw new Exception("[Neuron] unable to find connection " 
              + m_connectionIDs[i] + " in input Map!");
        }

        if (m_layer.getActivationFunction() != ActivationFunction.RADIALBASIS) {
          // multiply with weight
          double inV = inVal.doubleValue() * m_weights[i];
          z += inV;
        } else {
          // Euclidean distance to the center (stored in m_weights)
          double inV = Math.pow((inVal.doubleValue() - m_weights[i]), 2.0);
          z += inV;
        }
      }
      
      // apply the width if necessary
      if (m_layer.getActivationFunction() == ActivationFunction.RADIALBASIS) {
        z /= (2.0 * (width * width));
      }

      double threshold = m_layer.getThreshold();
      double altitude = (Double.isNaN(m_neuronAltitude))
        ? m_layer.getAltitude()
        : m_neuronAltitude;
        
      double fanIn = m_connectionIDs.length;        
      result = m_layer.getActivationFunction().eval(z, threshold, altitude, fanIn);
      
      return result;
    }
    
    public String toString() {
      StringBuffer temp = new StringBuffer();
      temp.append("Nueron (" + m_ID + ") [bias:" + m_bias);
      if (!Double.isNaN(m_neuronWidth)) {
        temp.append(" width:" + m_neuronWidth);
      }
      if (!Double.isNaN(m_neuronAltitude)) {
        temp.append(" altitude:" + m_neuronAltitude);
      }
      temp.append("]\n");
      temp.append("  con. (ID:weight): ");
      for (int i = 0; i < m_connectionIDs.length; i++) {
        temp.append(m_connectionIDs[i] + ":" + Utils.doubleToString(m_weights[i], 2));
        if ((i + 1) % 10 == 0 || i == m_connectionIDs.length - 1) {
          temp.append("\n                    ");
        } else {
          temp.append(", ");
        }
      }
      return temp.toString();
    }
  }
  
  static class NeuralOutputs implements Serializable {
    
    /**
     * For serialization
     */
    private static final long serialVersionUID = -233611113950482952L;

    /** The neurons we are mapping */
    private String[] m_outputNeurons = null;
    
    /**
     *  In the case of a nominal class, the index of the value
     * being predicted by each output neuron
     */
    private int[] m_categoricalIndexes = null;
    
    /** The class attribute we are mapping to */
    private Attribute m_classAttribute = null;
    
    /** Used when the class is numeric */
    private NormContinuous m_regressionMapping = null;
        
    protected NeuralOutputs(Element outputs, MiningSchema miningSchema) throws Exception {
      m_classAttribute = miningSchema.getMiningSchemaAsInstances().classAttribute();
      
      int vals = (m_classAttribute.isNumeric())
        ? 1
        : m_classAttribute.numValues();
      
      m_outputNeurons = new String[vals];
      m_categoricalIndexes = new int[vals];
      
      NodeList outputL = outputs.getElementsByTagName("NeuralOutput");
      if (outputL.getLength() != m_outputNeurons.length) {
        throw new Exception("[NeuralOutputs] the number of neural outputs does not match "
            + "the number expected!");
      }
      
      for (int i = 0; i < outputL.getLength(); i++) {
        Node outputN = outputL.item(i);
        if (outputN.getNodeType() == Node.ELEMENT_NODE) {
          Element outputE = (Element)outputN;
          // get the ID for this output neuron
          m_outputNeurons[i] = outputE.getAttribute("outputNeuron");
          
          if (m_classAttribute.isNumeric()) {
            // get the single norm continuous
            NodeList contL = outputE.getElementsByTagName("NormContinuous");
            if (contL.getLength() != 1) {
              throw new Exception("[NeuralOutputs] Should be exactly one norm continuous element "
                  + "for numeric class!");
            }
            Node normContNode = contL.item(0);
            String attName = ((Element)normContNode).getAttribute("field");
            Attribute dummyTargetDef = new Attribute(attName);
            ArrayList<Attribute> dummyFieldDefs = new ArrayList<Attribute>();
            dummyFieldDefs.add(dummyTargetDef);
            
            m_regressionMapping = new NormContinuous((Element)normContNode, 
                FieldMetaInfo.Optype.CONTINUOUS, dummyFieldDefs);
            break;
          } else {
            // we just need to grab the categorical value (out of the NormDiscrete element)
            // that this output neuron is associated with
            NodeList discL = outputE.getElementsByTagName("NormDiscrete");
            if (discL.getLength() != 1) {
              throw new Exception("[NeuralOutputs] Should be only one norm discrete element "
                  + "per derived field/neural output for a nominal class!");
            }
            Node normDiscNode = discL.item(0);
            String attValue = ((Element)normDiscNode).getAttribute("value");
            int index = m_classAttribute.indexOfValue(attValue);
            if (index < 0) {
              throw new Exception("[NeuralOutputs] Can't find specified target value "
                  + attValue + " in class attribute " + m_classAttribute.name());
            }
            m_categoricalIndexes[i] = index;
          }
        }
      }
    }
    
    /**
     * Compute the output. Either a probability distribution or a single
     * value (regression).
     * 
     * @param incoming the values from the last hidden layer
     * @param preds the array to fill with predicted values
     * @throws Exception if there is a problem computing the output
     */
    protected void getOuput(HashMap<String, Double> incoming, double[] preds) throws Exception {
      
      if (preds.length != m_outputNeurons.length) {
        throw new Exception("[NeuralOutputs] Incorrect number of predictions requested: "
            + preds.length + "requested, " + m_outputNeurons.length + " expected");
      }
      for (int i = 0; i < m_outputNeurons.length; i++) {
        Double neuronOut = incoming.get(m_outputNeurons[i]);
        if (neuronOut == null) {
          throw new Exception("[NeuralOutputs] Unable to find output neuron "
              + m_outputNeurons[i] + " in the incoming HashMap!!");
        }
        if (m_classAttribute.isNumeric()) {
          // will be only one output neuron anyway
          preds[0] = neuronOut.doubleValue();
          
          preds[0] = m_regressionMapping.getResultInverse(preds);
        } else {

          // clip at zero
          // preds[m_categoricalIndexes[i]] = (neuronOut < 0) ? 0.0 : neuronOut;
          preds[m_categoricalIndexes[i]] = neuronOut;
        }
      }
      
      if (m_classAttribute.isNominal()) {
        // check for negative values and adjust
        double min = preds[Utils.minIndex(preds)];
        if (min < 0) {
          for (int i = 0; i < preds.length; i++) {
            preds[i] -= min;
          }
        }
        // do a simplemax normalization
        Utils.normalize(preds);
      }
    }
    
    public String toString() {
      StringBuffer temp = new StringBuffer();
      
      for (int i = 0; i < m_outputNeurons.length; i++) {
        temp.append("Output neuron (" + m_outputNeurons[i] + ")\n");
        temp.append("mapping:\n");
        if (m_classAttribute.isNumeric()) {
          temp.append(m_regressionMapping +"\n");
        } else {
          temp.append(m_classAttribute.name() + " = " 
              + m_classAttribute.value(m_categoricalIndexes[i]) + "\n");
        }
      }
      
      return temp.toString();
    }
  }
  
  /**
   * Enumerated type for the mining function
   */
  enum MiningFunction {
    CLASSIFICATION,
    REGRESSION;
  }
  
  /** The mining function */
  protected MiningFunction m_functionType = MiningFunction.CLASSIFICATION;
  
  /**
   * Enumerated type for the activation function.
   */
  enum ActivationFunction {
    THRESHOLD("threshold") {
      double eval(double z, double threshold, double altitude, double fanIn) {
        if (z > threshold) {
          return 1.0;
        }
        return 0.0;
      }
    },
    LOGISTIC("logistic") {
      double eval(double z, double threshold, double altitude, double fanIn) {
        return 1.0 / (1.0 + Math.exp(-z));
      }
    },
    TANH("tanh") {
      double eval(double z, double threshold, double altitude, double fanIn) {
        double a = Math.exp( z );
        double b = Math.exp( -z );
        return ((a-b)/(a+b));
        //return (1.0 - Math.exp(-2.0 * z)) / (1.0 + Math.exp(-2.0 * z));
      }
    },
    IDENTITY("identity") {
      double eval(double z, double threshold, double altitude, double fanIn) {
        return z;
      }
    },
    EXPONENTIAL("exponential") {
      double eval(double z, double threshold, double altitude, double fanIn) {
        return Math.exp(z);
      }
    },
    RECIPROCAL("reciprocal") {
      double eval(double z, double threshold, double altitude, double fanIn) {
        return 1.0 / z;
      }
    },
    SQUARE("square") {
      double eval(double z, double threshold, double altitude, double fanIn) {
        return  z * z;
      }
    },
    GAUSS("gauss") {
      double eval(double z, double threshold, double altitude, double fanIn) {
        return Math.exp(-(z * z));
      }
    },
    SINE("sine") {
      double eval(double z, double threshold, double altitude, double fanIn) {
        return Math.sin(z);
      }
    },
    COSINE("cosine") {
      double eval(double z, double threshold, double altitude, double fanIn) {
        return Math.cos(z);
      }
    },
    ELLICOT("ellicot") {
      double eval(double z, double threshold, double altitude, double fanIn) {
        return z / (1.0 + Math.abs(z));
      }
    },
    ARCTAN("arctan") {
      double eval(double z, double threshold, double altitude, double fanIn) {
        return 2.0 * Math.atan(z) / Math.PI;
      }
    },
    RADIALBASIS("radialBasis") {
      double eval(double z, double threshold, double altitude, double fanIn) {
        return Math.exp(fanIn * Math.log(altitude) - z);
      }
    };
    
    abstract double eval(double z, double threshold, double altitude, double fanIn);
    
    private final String m_stringVal;
    
    ActivationFunction(String name) {
      m_stringVal = name;
    }
    
    public String toString() {
      return m_stringVal;
    }
  }
  
  /** The activation function to use */
  protected ActivationFunction m_activationFunction = ActivationFunction.ARCTAN;
  
  /**
   * Enumerated type for the normalization method
   */
  enum Normalization {
    NONE ("none"),
    SIMPLEMAX ("simplemax"),
    SOFTMAX ("softmax");
    
    private final String m_stringVal;
    
    Normalization(String name) {
      m_stringVal = name;
    }
    
    public String toString() {
      return m_stringVal;
    }
  }
    
  /** The normalization method */
  protected Normalization m_normalizationMethod = Normalization.NONE;
  
  /** Threshold activation */
  protected double m_threshold = 0.0; // default = 0
  
  /** Width for radial basis */
  protected double m_width = Double.NaN; // no default
  
  /** Altitude for radial basis */
  protected double m_altitude = 1.0; // default = 1
  
  /** The number of inputs to the network */
  protected int m_numberOfInputs = 0;
  
  /** Number of hidden layers in the network */
  protected int m_numberOfLayers = 0;
  
  /** The inputs to the network */
  protected NeuralInput[] m_inputs = null;
  
  /** A map for storing network input values (computed from an incoming instance) */
  protected HashMap<String, Double> m_inputMap = new HashMap<String, Double>();
    
  /** The hidden layers in the network */
  protected NeuralLayer[] m_layers = null;
  
  /** The outputs of the network */
  protected NeuralOutputs m_outputs = null;
  
  public NeuralNetwork(Element model, Instances dataDictionary,
                       MiningSchema miningSchema) throws Exception {
    
    super(dataDictionary, miningSchema);
    
    String fn = model.getAttribute("functionName");
    if (fn.equals("regression")) {
      m_functionType = MiningFunction.REGRESSION;
    }
    
    String act = model.getAttribute("activationFunction");
    if (act == null || act.length() == 0) {
      throw new Exception("[NeuralNetwork] no activation functon defined");
    }
    
    // get the activation function
    for (ActivationFunction a : ActivationFunction.values()) {
      if (a.toString().equals(act)) {
        m_activationFunction = a;
        break;
      }
    }
    
    // get the normalization method (if specified)
    String norm = model.getAttribute("normalizationMethod");
    if (norm != null && norm.length() > 0) {
      for (Normalization n : Normalization.values()) {
        if (n.toString().equals(norm)) {
          m_normalizationMethod = n;
          break;
        }
      }
    }
    
    String thresh = model.getAttribute("threshold");
    if (thresh != null && thresh.length() > 0) {
      m_threshold = Double.parseDouble(thresh);
    }
    String width = model.getAttribute("width");
    if (width != null && width.length() > 0) {
      m_width = Double.parseDouble(width);
    }
    String alt = model.getAttribute("altitude");
    if (alt != null && alt.length() > 0) {
      m_altitude = Double.parseDouble(alt);
    }
    
    // get all the inputs
    NodeList inputL = model.getElementsByTagName("NeuralInput");
    m_numberOfInputs = inputL.getLength();
    m_inputs = new NeuralInput[m_numberOfInputs];
    for (int i = 0; i < m_numberOfInputs; i++) {
      Node inputN = inputL.item(i);
      if (inputN.getNodeType() == Node.ELEMENT_NODE) {
        NeuralInput nI = new NeuralInput((Element)inputN, m_miningSchema);
        m_inputs[i] = nI;
      }
    }
    
    // get the layers
    NodeList layerL = model.getElementsByTagName("NeuralLayer");
    m_numberOfLayers = layerL.getLength();
    m_layers = new NeuralLayer[m_numberOfLayers];
    for (int i = 0; i < m_numberOfLayers; i++) {
      Node layerN = layerL.item(i);
      if (layerN.getNodeType() == Node.ELEMENT_NODE) {
        NeuralLayer nL = new NeuralLayer((Element)layerN);
        m_layers[i] = nL;
      }
    }
    
    // get the outputs
    NodeList outputL = model.getElementsByTagName("NeuralOutputs");
    if (outputL.getLength() != 1) {
      throw new Exception("[NeuralNetwork] Should be just one NeuralOutputs element defined!");
    }
    
    m_outputs = new NeuralOutputs((Element)outputL.item(0), m_miningSchema);
  }

  /* (non-Javadoc)
   * @see weka.core.RevisionHandler#getRevision()
   */
  public String getRevision() {
    return RevisionUtils.extract("$Revision: 5987 $");
  }
  
  /**                                                                                                             
   * Classifies the given test instance. The instance has to belong to a                                          
   * dataset when it's being classified.                                                          
   *                                                                                                              
   * @param inst the instance to be classified                                                                
   * @return the predicted most likely class for the instance or                                                  
   * Utils.missingValue() if no prediction is made                                                             
   * @exception Exception if an error occurred during the prediction                                              
   */
  public double[] distributionForInstance(Instance inst) throws Exception {
    if (!m_initialized) {
      mapToMiningSchema(inst.dataset());
    }
    double[] preds = null;
    
    if (m_miningSchema.getFieldsAsInstances().classAttribute().isNumeric()) {
      preds = new double[1];
    } else {
      preds = new double[m_miningSchema.getFieldsAsInstances().classAttribute().numValues()];
    }
    
    double[] incoming = m_fieldsMap.instanceToSchema(inst, m_miningSchema);
    
    boolean hasMissing = false;
    for (int i = 0; i < incoming.length; i++) {
      if (i != m_miningSchema.getFieldsAsInstances().classIndex() && 
          Double.isNaN(incoming[i])) {
        hasMissing = true;
        //System.err.println("Missing value for att : " + m_miningSchema.getFieldsAsInstances().attribute(i).name());
        break;
      }
    }
    
    if (hasMissing) {
      if (!m_miningSchema.hasTargetMetaData()) {
        String message = "[NeuralNetwork] WARNING: Instance to predict has missing value(s) but "
          + "there is no missing value handling meta data and no "
          + "prior probabilities/default value to fall back to. No "
          + "prediction will be made (" 
          + ((m_miningSchema.getFieldsAsInstances().classAttribute().isNominal()
              || m_miningSchema.getFieldsAsInstances().classAttribute().isString())
              ? "zero probabilities output)."
              : "NaN output).");
        if (m_log == null) {
          System.err.println(message);
        } else {
          m_log.logMessage(message);
        }
        
        if (m_miningSchema.getFieldsAsInstances().classAttribute().isNumeric()) {
          preds[0] = Utils.missingValue();
        }
        return preds;
      } else {
        // use prior probablilities/default value
        TargetMetaInfo targetData = m_miningSchema.getTargetMetaData();
        if (m_miningSchema.getFieldsAsInstances().classAttribute().isNumeric()) {
          preds[0] = targetData.getDefaultValue();
        } else {
          Instances miningSchemaI = m_miningSchema.getFieldsAsInstances();
          for (int i = 0; i < miningSchemaI.classAttribute().numValues(); i++) {
            preds[i] = targetData.getPriorProbability(miningSchemaI.classAttribute().value(i));
          }
        }
        return preds;
      }
    } else {
      
      // construct the input to the network for this instance
      m_inputMap.clear();
      for (int i = 0; i < m_inputs.length; i++) {
        double networkInVal = m_inputs[i].getValue(incoming);
        String ID = m_inputs[i].getID();
        m_inputMap.put(ID, networkInVal);
      }
      
      // now compute the output of each layer
      HashMap<String, Double> layerOut = m_layers[0].computeOutput(m_inputMap);
      for (int i = 1; i < m_layers.length; i++) {
        layerOut = m_layers[i].computeOutput(layerOut);
      }
      
      // now do the output
      m_outputs.getOuput(layerOut, preds);
    }
    
    return preds;
  }

  public String toString() {
    StringBuffer temp = new StringBuffer();
    
    temp.append("PMML version " + getPMMLVersion());
    if (!getCreatorApplication().equals("?")) {
      temp.append("\nApplication: " + getCreatorApplication());
    }
    temp.append("\nPMML Model: Neural network");
    temp.append("\n\n");
    temp.append(m_miningSchema);
    
    temp.append("Inputs:\n");
    for (int i = 0; i < m_inputs.length; i++) {
      temp.append(m_inputs[i] + "\n");
    }

    for (int i = 0; i < m_layers.length; i++) {
      temp.append("Layer: " + (i+1) + "\n");
      temp.append(m_layers[i] + "\n");
    }
    
    temp.append("Outputs:\n");
    temp.append(m_outputs);
    
    return temp.toString();
  }
}
