/*
 * RpropExtender.java
 *
 * Created on September 14, 2004, 3:29 PM
 */

package org.joone.engine.extenders;

import org.joone.engine.RpropParameters;
import org.joone.log.*;

/**
 * This class changes the delta value in such a way that it implements the
 * RPROP algorithm.
 *
 * @author Boris Jansen
 */
public class RpropExtender extends DeltaRuleExtender {
    
    // Note the gradient passed by the ExtendableLearner is multiplied by
    // the learning rate. However, the RPROP learning algorithm looks only at
    // the sign of the gradient. So as long the learning algorithm is positive
    // there is no problem.
    
    /** Logger */
    private static final ILogger log = LoggerFactory.getLogger(RpropExtender.class);
    
    /**
     * Each weight has its own individual update-value (delta_ij(t)) represented
     * by the next object.
     *
     * The weight update deltaW_ij(t) is defined as follows (dE(t) / dW_ij is the
     * summed gradient for a single epoch):
     *                | -delta_ij(t),    if dE(t) / dW_ij > 0
     * deltaW_ij(t) = | delta_ij(t),     if dE(t) / dW_ij < 0
     *                | 0                otherwise
     *
     * The delta_ij values are updated as follows:
     *               | eta_inc * delta_ij(t-1),     if dE(t-1)/ dW_ij * dE(t)/ dW_ij > 0
     * delta_ij(t) = | eta_dec * delta_ij(t-1),     if dE(t-1)/ dW_ij * dE(t)/ dW_ij < 0
     *               | delta_ij(t-1),               otherwise
     * where 0 < eta_dec < 1 < eta_inc
     */
    protected double[][] theDeltas;
    
    /** The gradient pattern of the previous epoch (dE(t-1)/dW_ij). */
    protected double[][] thePreviousGradients;
    
    /** The parameters for the RPROP learning algorithm. */
    protected RpropParameters theRpropParameters;
    
    /** The current som of the gradients of all patterns seen so far. The number
     * of summed gradients is smaller the "batch size" and will be reset to zero,
     * if the number of sums becomes equal to "batch size" after the weights/biases
     * have been updated. */
    protected double[][] theSummedGradients;
    
    /** Creates a new instance of RpropExtender */
    public RpropExtender() {
    }
    
    /**
     * (Re)Initializes this RPROP learner.
     */
    public void reinit() {
        if(getLearner().getMonitor().getLearningRate() != 1) {
            log.warn("RPROP learning rate should be equal to 1.");
        }
        
        if(getLearner().getLayer() != null) {
            thePreviousGradients = new double[getLearner().getLayer().getRows()][1];
            theSummedGradients = new double[thePreviousGradients.length][1];
            theDeltas = new double[thePreviousGradients.length][1];
        } else if (getLearner().getSynapse() != null) {
            int myRows = getLearner().getSynapse().getInputDimension();
            int myCols = getLearner().getSynapse().getOutputDimension();
            thePreviousGradients = new double[myRows][myCols];
            theSummedGradients = new double[myRows][myCols];
            theDeltas = new double[myRows][myCols];
        }
        
        for(int i = 0; i < theDeltas.length; i++) {
            for(int j = 0; j < theDeltas[0].length; j++) {
                theDeltas[i][j] = getParameters().getInitialDelta(i, j);
            }
        }
    }
    
    public double getDelta(double[] currentGradientOuts, int j, double aPreviousDelta) {
        // we will hold our delta's in memory ourselves only when the weights will be 
        // stored we will pass on the calculated delta value !! Therefore, some 
        // DeltaExtenders executed after this delta might not work correct...
        // Please think about the order of the delta extenders... !!
        double myDelta = 0;
        
        // Note:
        // dE/dw = sum(dE/de * de/dy * ...) *...
        // de/dy = -1, however * -1 is neglected, so aCurrentGradientOuts has a different sign than dE/dw
        // we fix this here by multiplying aCurrentGradientOuts by -1.0
        // remove -> theSummedGradients[i][0] += -1.0 * aCurrentGradientOuts[i];
        // theSummedGradients[i][0] += -1.0 * getGradientBias(aCurrentGradientOuts, i);
        theSummedGradients[j][0] -= aPreviousDelta;
            
        if(getLearner().getUpdateWeightExtender().storeWeightsBiases()) {
            // biases will be stored this cycle
            if(thePreviousGradients[j][0] * theSummedGradients[j][0] > 0) {
                theDeltas[j][0] = Math.min(theDeltas[j][0] * getParameters().getEtaInc(),
                    getParameters().getMaxDelta());
                myDelta = -1.0 * sign(theSummedGradients[j][0]) * theDeltas[j][0];
                thePreviousGradients[j][0] = theSummedGradients[j][0];
            } else if(thePreviousGradients[j][0] * theSummedGradients[j][0] < 0) {
                theDeltas[j][0] = Math.max(theDeltas[j][0] * getParameters().getEtaDec(),
                    getParameters().getMinDelta());
                // sign changed -> the previous step was to large and the minimum was missed,
                // the previous weight-update is reverted
                myDelta = -1.0 * getLearner().getLayer().getBias().delta[j][0];
                // due the backtracking step the derivative is supposed to change its sign
                // again in the following step. To prevent double punishement we set the
                // gradient to 0
                thePreviousGradients[j][0] = 0;
            } else {
                myDelta = -1.0 * sign(theSummedGradients[j][0]) * theDeltas[j][0];
                thePreviousGradients[j][0] = theSummedGradients[j][0];
            }
            theSummedGradients[j][0] = 0; // reset to zero so we can start somming up again...
        }
        return myDelta;
    }
    
    public double getDelta(double[] currentInps, int j, double[] currentPattern, int k, double aPreviousDelta) {
        // read comments getDelta (for bias)...
        double myDelta = 0;
        
        // * -1.0, because de/dy = -1, but is neglected in  aCurrentPattern
        // remove -> theSummedGradients[i][j] += aCurrentPattern[j] * aCurrentInps[i] * - 1.0;
        // theSummedGradients[i][j] += -1.0 * getGradientWeights(aCurrentInps, i, aCurrentPattern, j);
        theSummedGradients[j][k] -= aPreviousDelta;
        
        if(getLearner().getUpdateWeightExtender().storeWeightsBiases()) {
            // weights will be stored this cycle
            if(thePreviousGradients[j][k] * theSummedGradients[j][k] > 0) {
                theDeltas[j][k] = Math.min(theDeltas[j][k] * getParameters().getEtaInc(),
                    getParameters().getMaxDelta());
                myDelta = -1.0 * sign(theSummedGradients[j][k]) * theDeltas[j][k];
                thePreviousGradients[j][k] = theSummedGradients[j][k];
            } else if(thePreviousGradients[j][k] * theSummedGradients[j][k] < 0) {
                theDeltas[j][k] = Math.max(theDeltas[j][k] * getParameters().getEtaDec(),
                    getParameters().getMinDelta());
                // sign changed -> the previous step was to large and the minimum was missed,
                // the previous weight-update is reverted
                myDelta = -1.0 * getLearner().getSynapse().getWeights().delta[j][k];
                // due the backtracking step the derivative is supposed to change its sign
                // again in the following step. To prevent double punishement we set the
                // gradient to 0
                thePreviousGradients[j][k] = 0;
            } else {
                myDelta = -1.0 * sign(theSummedGradients[j][k]) * theDeltas[j][k];
                thePreviousGradients[j][k] = theSummedGradients[j][k];
            }
            theSummedGradients[j][k] = 0;
        }
        return myDelta;
    }
    
    public void postBiasUpdate(double[] currentGradientOuts) {
    }
    
    public void postWeightUpdate(double[] currentPattern, double[] currentInps) {
    }
    
    public void preBiasUpdate(double[] currentGradientOuts) {
        if(theDeltas == null || theDeltas.length != getLearner().getLayer().getRows()) {
            // first time or dimensions have changed
            reinit();
        }
    }
    
    public void preWeightUpdate(double[] currentPattern, double[] currentInps) {
        if(theDeltas == null || theDeltas.length != getLearner().getSynapse().getInputDimension() 
            || theDeltas[0].length != getLearner().getSynapse().getOutputDimension()) 
        {
            reinit();
        }
    }
    
    /**
     * Gets the parameters of this learning algorithm.
     *
     * @return the parameters of this learning algorithm.
     */
    public RpropParameters getParameters() {
        if(theRpropParameters == null) {
            // create default parameters
            theRpropParameters = new RpropParameters();
            theRpropParameters.setParameteresByParams();
        }
        return theRpropParameters;
    }
    
    /**
     * Sets the parameters for this learning algorithm.
     *
     * @param aParameters the parameters for this learning algorithm.
     */
    public void setParameters(RpropParameters aParameters) {
        theRpropParameters = aParameters;
    }
    
    /**
     * Gets the sign of a double.
     *
     * return the sign of a double (-1, 0, 1).
     */
    protected double sign(double d) {
        if(d > 0) {
            return 1.0;
        } else if(d < 0) {
            return -1.0;
        }
        return 0;
    }
}
