/*
 *    DeepBeliefNetwork.java
 *    Copyright (C) 2012-2013 Aristotle University of Thessaloniki, Greece
 */
package dbn.classifier.neural;

import com.syvys.jaRBM.Layers.Layer;
import com.syvys.jaRBM.Layers.LinearLayer;
import com.syvys.jaRBM.Layers.StochasticBinaryLayer;
import com.syvys.jaRBM.Math.Matrix;
import com.syvys.jaRBM.RBM;
import com.syvys.jaRBM.RBMImpl;
import com.syvys.jaRBM.RBMNet;
import java.util.ArrayList;
import java.util.List;
import mulan.classifier.neural.model.*;

/**
 * The structure of a Deep Belief Network (DBN). It extends the {@link RBMNet}
 * class and all its methods. The main difference from the {@link RBMNet} is
 * the constructor, which takes as a parameter an integers array with the sizes
 * of all the labels and builds itself the network (you don't have to do it
 * manually).<br />
 * This structure consists a number of {@link RBMImpl} neural networks that are going to be
 * trained greedy. That means that when the first will be trained, then its output
 * becomes the input to the second and so on.<br />
 * This class implements the {@link Cloneable} interface, so you can clone its instances
 * (objects).
 * 
 * @author Evripidis Gkanias
 * @see RBMNet
 * @see NeuralNet
 */
public class DeepBeliefNetwork extends RBMNet implements NeuralNet, Cloneable {
    
    double[] lastInput = null;
    
    /**
     * Builds the {@link DeepBeliefNetwork} using an array with the sizes of 
     * layers. If the parameter array has length 5, our new DeepBeliefNetwork 
     * will have 4 {@link RBMImpl}s stacked.
     * 
     * @param numUnitPerLayer an array with the sizes of each layer
     * @throws IllegalArgumentException if number of layers are less then 3
     */
    public DeepBeliefNetwork(int[] numUnitPerLayer) {
        super(
            new RBMImpl(new LinearLayer(numUnitPerLayer[0]),
            new StochasticBinaryLayer(numUnitPerLayer[1])));
        
        if (numUnitPerLayer == null || numUnitPerLayer.length < 3)
            throw new IllegalArgumentException("Deep Belief Network must have at least 3 layers.");
        
        Layer visibleLayer;
        Layer hiddenLayer = super.getRBM(0).getHiddenLayer();
        for (int i=2; i < numUnitPerLayer.length; i++) {
            int  hiddenUnits = numUnitPerLayer[i];
            //*
            visibleLayer = hiddenLayer.clone();
            hiddenLayer = new StochasticBinaryLayer(hiddenUnits);
            //*/
            /*
            visibleLayer = new LinearLayer(hiddenLayer.getNumUnits());
            hiddenLayer = new StochasticBinaryLayer(hiddenUnits);
            //*/
            try {
                super.AddRBM(new RBMImpl(visibleLayer, hiddenLayer));
            } catch (Exception ex) {}
        }
    }
    
    /**
     * Builds a DBN using a {@link RBMNet} object. It takes only the data from the 
     * {@link RBMNet}.
     * It could cause an {@link Exception} if the {@link RBMNet} is <code>null</code>.
     * @param rbmnet the {@link RBMNet} whose data we want to use
     * @throws Exception 
     */
    public DeepBeliefNetwork (RBMNet rbmnet) throws Exception {
        super(rbmnet.getRBM(0).clone());
        for (int i=1; i < rbmnet.getNumRBMs(); i++) {
            this.AddRBM(rbmnet.getRBM(i).clone());
        }
    }

    // <editor-fold defaultstate="collapsed" desc="Parameter Getters">
    /**
     * Gets the size/dimension of the input layer of the neural network.
     * This is the size of input pattern the neural network can process.
     *
     * @return the network input size
     */
    @Override
    public int getNetInputSize() {
        return this.getRBM(0).getNumVisibleUnits();
    }

    /**
     * Gets the size/dimension of the output layer of the neural network.
     * This is the size of output pattern the neural network produces.
     *
     * @return the network output size
     */
    @Override
    public int getNetOutputSize() {
        return this.getRBM(this.getNumRBMs()-1).getNumHiddenUnits();
    }

    /**
     * Returns a total number of layers of the neural network.
     *
     * @return the number of layers in the neural network
     */
    @Override
    public int getLayersCount() {
        return this.getNumRBMs() + 1;
    }

    /**
     * Returns units of a particular layer of the neural network.
     * The valid indexes for layers are from 0 to N-1, where N is the total number of layers
     * <br />
     * The first layer (index = 0) is always input layer and the
     * last (index = N-1) always output layer.
     *
     * @param layerIndex
     * @return returns an unmodifiable list of units of the particular layer
     * @throws IndexOutOfBoundsException if the index is out of range
     * @see java.util.Collections#unmodifiableList(List)
     */
    @Override
    public List<Neuron> getLayerUnits(int layerIndex) {
        List<Neuron> neurons = new ArrayList();
        boolean last;
        if (last = layerIndex >= this.getNumRBMs()) {
            layerIndex = this.getNumRBMs() - 1;
        }
        Layer layer = last ? this.getRBM(layerIndex).getVisibleLayer() :
                             this.getRBM(layerIndex).getHiddenLayer();
        int dim =   last ? this.getRBM(layerIndex-1).getNumVisibleUnits() :
                             this.getRBM(layerIndex).getNumVisibleUnits();
        double[] biases = layer.getBiases();
        for (double bias : biases) {
            neurons.add(new Neuron(new ActivationTANH(), dim, bias));
        }
        return neurons;
    }
    // </editor-fold>

    /**
     * Propagates the input pattern through the network.
     *
     * @param inputPattern the input pattern for the network to process
     * @return the output of the network
     * @throws IllegalArgumentException if input pattern is null or does not match network input dimension
     */
    @Override
    public double[] feedForward(double[] inputPattern) {
        return this.feedForward(inputPattern, this.getNumRBMs()-1);
    }

    /**
     * Propagates the input pattern through the network.
     *
     * @param inputPattern the input pattern for the network to process
     * @param lastRBMIndex the index of the last RBM to process
     * @return the output of the network
     * @throws IllegalArgumentException if input pattern is null or does not match network input dimension
     * @throws ArrayIndexOutOfBoundsException if last RBM index is invalid
     */
    public double[] feedForward(double[] inputPattern, int lastRBMIndex) {
        
        if (lastRBMIndex >= this.getNumRBMs() || lastRBMIndex < 0)
            throw new ArrayIndexOutOfBoundsException("Invalide RBM index!");
        if (inputPattern == null || inputPattern.length < this.getNetInputSize())
            throw new IllegalArgumentException("Input pattern does not match input dimantions!");
        
        double[] input = new double[this.getNetInputSize()];
        if (input.length < inputPattern.length)
            System.arraycopy(inputPattern, 0, input, 0, input.length);
        else
            input = inputPattern.clone();
        
        this.lastInput = inputPattern.clone();
        double[] output = input.clone();
        for (int i = 0; i <= lastRBMIndex; i++) {
            output = this.getRBM(i).getHiddenActivitiesFromVisibleData(output);
        }
        return output;
    }

    /**
     * Generates a feed forward bipartition.
     * @param input
     * @return the status of the labels of the last layer in a bipartition 
     * (<code>true</code> or <code>false</code>)
     */
    public boolean[] generateFeedForwardUnits(double[] input) {
        double[] hiddenActivities = super.GenerateHiddenUnits(input);
        boolean[] output = new boolean[hiddenActivities.length];
        for (int i = 0; i < output.length; i++) {
            output[i] = hiddenActivities[i] >= 0.5;
        }
        return output;
    }
    
    /**
     * Back propagates the input pattern through the network.
     *
     * @param inputPattern the input pattern for the network to process from the back
     * @param fromRBMIndex the index of the last RBM to process
     * @return the output of the top layer of the network
     * @throws IllegalArgumentException if input pattern is null or does not 
     * match network highest layers dimension
     * @throws ArrayIndexOutOfBoundsException if last RBM index is invalid
     */
    public double[] feedBackward(double[] inputPattern, int fromRBMIndex) {
        
        if (fromRBMIndex >= this.getNumRBMs() || fromRBMIndex < 0)
            throw new ArrayIndexOutOfBoundsException("Invalide RBM index!");
        if (inputPattern == null || inputPattern.length < this.getNetInputSize())
            throw new IllegalArgumentException("Input pattern does not match input dimantions!");
        
        double[] input = new double[this.getNetInputSize()];
        if (input.length < inputPattern.length)
            System.arraycopy(inputPattern, 0, input, 0, input.length);
        else
            input = inputPattern;
        
        this.lastInput = inputPattern;
        double[] output = input.clone();
        for (int i = fromRBMIndex; i >= 0; i--) {
            output = this.getRBM(i).getVisibleActivitiesFromHiddenData(input);
        }
        return output;
    }
    
    /**
     * Back propagates the input pattern through the network.
     *
     * @param inputPattern the input pattern for the network to process from the back
     * @return the output of the network on the top
     * @throws IllegalArgumentException if input pattern is null or does not 
     * match network higher layers dimension
     */
    public double[] feedBackward(double[] inputPattern) {
        return this.feedBackward(inputPattern, this.getNumRBMs()-1);
    }
    
    /**
     * Generates a feed backward bipartition.
     * @param input
     * @return the status of the labels of the first layer in a bipartition 
     * (<code>true</code> or <code>false</code>)
     */
    public boolean[] generateFeedBackwardUnits(double[] input) {
        double[] hiddenActivities = super.GenerateVisibleUnits(input);
        boolean[] output = new boolean[hiddenActivities.length];
        for (int i = 0; i < output.length; i++) {
            output[i] = hiddenActivities[i] >= 0.5;
        }
        return output;
    }
    
    /**
     * Returns the actual output of the neural network,
     * which is a result of last processed input pattern.
     *
     * @return the output of the network.
     * 			Returns null if network is reset or no input pattern was processed
     */
    @Override
    public double[] getOutput() {
        return this.feedForward(this.lastInput);
    }

    /**
     * Perform reset, re-initialization of neural network.
     * All learned knowledge stored in the network will be lost.
     */
    @Override
    public void reset() {
        this.lastInput = null;
        boolean first = true;
        for (RBM rbm : (ArrayList<RBM>) this._arrlistRBMs) {
            int vunits = rbm.getNumVisibleUnits();
            int hunits = rbm.getNumHiddenUnits();
            Layer vlayer, hlayer;
            if (first) {
                vlayer = new LinearLayer(vunits);
                first = false;
            } else {
                vlayer = new StochasticBinaryLayer(vunits);
            }
            hlayer = new StochasticBinaryLayer(hunits);
            rbm.setVisibleLayerType(vlayer);
            rbm.setHiddenLayerType(hlayer);
        }
        
    }
    
    /**
     * 
     * @return an exact copy of this {@link DeepBeliefNetwork}.
     */
    @Override
    public DeepBeliefNetwork clone() {
        
        DeepBeliefNetwork mydbn;
        try {
            mydbn = new DeepBeliefNetwork(this);
        } catch (Exception ex1) {
            int[] layers = new int[this.getNumRBMs() + 1];
            layers[0] = this.getRBM(0).getNumVisibleUnits();
            for (int i = 1; i < layers.length; i++) {
                layers[i] = this.getRBM(i-1).getNumHiddenUnits();
            }
            mydbn = new DeepBeliefNetwork(layers);
            
            for (int i = 0; i < this.getNumRBMs(); i++) {
                try {
                    mydbn.setRBM(this.getRBM(i), i);
                } catch (Exception ex2) {}
            }
        }
        mydbn.setConnectionWeightVector(this.getConnectionWeightVector());
        
        return mydbn;
    }
    
    /**
     * 
     * @return a string representation of the object
     */
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        
        System.out.println("WEIGHTS:");
        for (int i = 0; i < this.getNumRBMs(); i++) {
            sb.append("\nRBM ").append(i + 1).append(": (");
            sb.append(this.getRBM(i).getNumVisibleUnits()).append(", ");
            sb.append(this.getRBM(i).getNumHiddenUnits()).append(")\n");
            sb.append(Matrix.toString(this.getRBM(i).getConnectionWeights()));
        }
        
        return sb.toString();
    }
    
}
