/*
 * (c) Copyright Christian P. Fries, Germany. All rights reserved. Contact: email@christian-fries.de.
 *
 * Created on 09.02.2006
 */
package net.finmath.montecarlo;

import java.util.Arrays;
import java.util.function.DoubleBinaryOperator;
import java.util.function.DoubleUnaryOperator;
import java.util.function.IntToDoubleFunction;
import java.util.stream.DoubleStream;
import java.util.stream.IntStream;

import net.finmath.functions.DoubleTernaryOperator;
import net.finmath.stochastic.RandomVariableInterface;

import org.apache.commons.math3.util.FastMath;

/**
 * Implements a Monte-Carlo random variable (like RandomVariable using
 * late evaluation of Java 8 streams
 *
 * Accesses performed exclusively through the interface
 * RandomVariableInterface is thread safe (and does not mutate the class).
 *
 * The implementation require Java 8 or better.
 *
 * @TODO The implementation of getAverage does not use a Kahan summation, while RandomVariable does.
 *
 * @author Christian Fries
 * @author OSC
 * @version 2.0
 */
public class RandomVariableLazyEvaluation implements RandomVariableInterface {

    private final double            time;	                // Time (filtration)

    // Operator
    private       IntToDoubleFunction   realizations;
    private final int                   size;
    // Data model for the non-stochastic case (if realizations==null)
    private final double                valueIfNonStochastic;

    private transient double[] realizationsArray = null;

    /**
     * Create a random variable from a given other implementation of RandomVariableInterface.
     *
     * @param value Object implementing RandomVariableInterface.
     */
    public RandomVariableLazyEvaluation(RandomVariableInterface value) {
        super();
        this.time = value.getFiltrationTime();
        this.realizations = value.isDeterministic() ? null : value::get;
        this.size = value.size();
        this.valueIfNonStochastic = value.isDeterministic() ? value.get(0) : Double.NaN;
    }

    /**
     * Create a non stochastic random variable, i.e. a constant.
     *
     * @param value the value, a constant.
     */
    public RandomVariableLazyEvaluation(double value) {
        this(0.0, value);
    }

    /**
     * Create a random variable by applying a function to a given other implementation of RandomVariableInterface.
     *
     * @param value Object implementing RandomVariableInterface.
     * @param function A function mapping double to double.
     */
    public RandomVariableLazyEvaluation(RandomVariableInterface value, DoubleUnaryOperator function) {
        super();
        this.time = value.getFiltrationTime();
        this.realizations = value.isDeterministic() ? null : i -> function.applyAsDouble(value.get(i));
        this.size = value.size();
        this.valueIfNonStochastic = value.isDeterministic() ? function.applyAsDouble(value.get(0)) : Double.NaN;
    }

    /**
     * Create a non stochastic random variable, i.e. a constant.
     *
     * @param time the filtration time, set to 0.0 if not used.
     * @param value the value, a constant.
     */
    public RandomVariableLazyEvaluation(double time, double value) {
        super();
        this.time = time;
        this.realizations = null;
        this.size = 1;
        this.valueIfNonStochastic = value;
    }

    /**
     * Create a non stochastic random variable, i.e. a constant.
     *
     * @param time the filtration time, set to 0.0 if not used.
     * @param value the value, a constant.
     */
    public RandomVariableLazyEvaluation(double time, int numberOfPath, double value) {
        super();
        this.time = time;
        this.size = numberOfPath;
        this.realizations = i -> value;
        this.valueIfNonStochastic = Double.NaN;
    }

    /**
     * Create a stochastic random variable.
     *
     * @param time the filtration time, set to 0.0 if not used.
     * @param realisations the vector of realizations.
     */
    public RandomVariableLazyEvaluation(double time, double[] realisations) {
        super();
        this.time = time;
        this.size = realisations.length;
        this.realizations = i->realisations[i];
        this.valueIfNonStochastic = Double.NaN;
        this.realizationsArray = realisations;
    }

    /**
     * Create a stochastic random variable.
     *
     * @param time the filtration time, set to 0.0 if not used.
     * @param realisations the vector of realizations.
     */
    public RandomVariableLazyEvaluation(double time, IntToDoubleFunction realisations, int size) {
        super();
        this.time = time;
        this.realizations = realisations;
        this.size = size;
        this.valueIfNonStochastic = Double.NaN;
    }

    @Override
    @Deprecated
    public RandomVariableInterface getMutableCopy() {
        return this;
    }

    /* (non-Javadoc)
     * @see net.finmath.stochastic.RandomVariableInterface#equals(net.finmath.montecarlo.RandomVariable)
     */
    @Override
    public boolean equals(RandomVariableInterface randomVariable) {
        if(this.time != randomVariable.getFiltrationTime()) return false;
        if(this.isDeterministic() && randomVariable.isDeterministic()) {
            return this.valueIfNonStochastic == randomVariable.get(0);
        }

        if(this.isDeterministic() != randomVariable.isDeterministic()) return false;

        for(int i=0; i quantileEnd) return getQuantileExpectation(quantileEnd, quantileStart);

        double[] realizationsSorted = getRealizations().clone();
        Arrays.sort(realizationsSorted);

        int indexOfQuantileValueStart	= Math.min(Math.max((int)Math.round((size()+1) * quantileStart - 1), 0), size()-1);
        int indexOfQuantileValueEnd		= Math.min(Math.max((int)Math.round((size()+1) * quantileEnd - 1), 0), size()-1);

        double quantileExpectation = 0.0;
        for (int i=indexOfQuantileValueStart; i<=indexOfQuantileValueEnd;i++) {
            quantileExpectation += realizationsSorted[i];
        }
        quantileExpectation /= indexOfQuantileValueEnd-indexOfQuantileValueStart+1;

        return quantileExpectation;
    }

    /* (non-Javadoc)
     * @see net.finmath.stochastic.RandomVariableInterface#getHistogram()
     */
    @Override
    public double[] getHistogram(double[] intervalPoints)
    {
        double[] histogramValues = new double[intervalPoints.length+1];

        if(isDeterministic()) {
        	/*
        	 * If the random variable is deterministic we will return an array
        	 * consisting of 0's and one and only one 1.
        	 */
            Arrays.fill(histogramValues, 0.0);
            for (int intervalIndex=0; intervalIndex intervalPoints[intervalIndex]) {
                    histogramValues[intervalIndex] = 1.0;
                    break;
                }
            }
            histogramValues[intervalPoints.length] = 1.0;
        }
        else {
        	/*
        	 * If the random variable is deterministic we will return an array
        	 * representing a density, where the sum of the entries is one.
        	 * There is one exception:
        	 * If the size of the random variable is 0, all entries will be zero.
        	 */
            double[] realizationsSorted = getRealizations().clone();
            Arrays.sort(realizationsSorted);

            int sampleIndex=0;
            for (int intervalIndex=0; intervalIndex 0) {
                for(int i=0; i valueIfNonStochastic);
        }
        else {
            return IntStream.range(0,size()).mapToDouble(realizations).parallel();
        }
    }

    @Override
    public RandomVariableInterface apply(DoubleUnaryOperator operator) {
        if(isDeterministic()) {
            return new RandomVariableLazyEvaluation(time, operator.applyAsDouble(valueIfNonStochastic));
        }
        else
        {
            IntToDoubleFunction newRealizations = i -> operator.applyAsDouble(realizations.applyAsDouble(i));
            return new RandomVariableLazyEvaluation(time, newRealizations, size());
        }
    }

    @Override
    public RandomVariableInterface cache() {
        synchronized (this)
          {
            if(realizationsArray == null) {
                realizationsArray = getRealizationsStream().toArray();
                realizations = i -> realizationsArray[i];
            }
        }
        return this;
    }

    @Override
    public RandomVariableInterface apply(DoubleBinaryOperator operator, final RandomVariableInterface argument) {

        double      newTime           = Math.max(time, argument.getFiltrationTime());

        if(isDeterministic() && argument.isDeterministic()) {
            return new RandomVariableLazyEvaluation(newTime, operator.applyAsDouble(valueIfNonStochastic, argument.get(0)));
        }
        else if(!isDeterministic() && argument.isDeterministic()) {
	        IntToDoubleFunction newRealizations = i -> operator.applyAsDouble(realizations.applyAsDouble(i), argument.get(0));
	        return new RandomVariableLazyEvaluation(newTime, newRealizations, size());
        }
        else if(isDeterministic() && !argument.isDeterministic()) {
	        if(false) {
		        final IntToDoubleFunction argumentRealizations = argument.getOperator();
		        IntToDoubleFunction newRealizations = i -> operator.applyAsDouble(valueIfNonStochastic, argumentRealizations.applyAsDouble(i));
		        return new RandomVariableLazyEvaluation(newTime, newRealizations, argument.size());
	        }
	        else {
		        final double[] argumentRealizations = argument.getRealizations();
		        IntToDoubleFunction newRealizations = i -> operator.applyAsDouble(valueIfNonStochastic, argumentRealizations[i]);
		        return new RandomVariableLazyEvaluation(newTime, newRealizations, argument.size());
	        }
        }
        else
        {
	        if(false) {
		        final IntToDoubleFunction argumentRealizations = argument.getOperator();
		        IntToDoubleFunction newRealizations = i -> operator.applyAsDouble(realizations.applyAsDouble(i), argumentRealizations.applyAsDouble(i));
		        return new RandomVariableLazyEvaluation(newTime, newRealizations, size());
	        }
	        else {
		        final double[] argumentRealizations = argument.getRealizations();
		        IntToDoubleFunction newRealizations = i -> operator.applyAsDouble(realizations.applyAsDouble(i), argumentRealizations[i]);
		        return new RandomVariableLazyEvaluation(newTime, newRealizations, size());
	        }
        }

    }

    public RandomVariableInterface apply(DoubleBinaryOperator operatorOuter, DoubleBinaryOperator operatorInner, RandomVariableInterface argument1, RandomVariableInterface argument2)
    {
        double newTime = Math.max(time, argument1.getFiltrationTime());
        newTime = Math.max(newTime, argument2.getFiltrationTime());

        if(this.isDeterministic() && argument1.isDeterministic() && argument2.isDeterministic()) {
            return new RandomVariableLazyEvaluation(newTime, operatorOuter.applyAsDouble(valueIfNonStochastic, operatorInner.applyAsDouble(argument1.get(0), argument2.get(0))));
        }
        else {
            int newSize = Math.max(Math.max(this.size(), argument1.size()), argument2.size());

            if(false) {
            	if(argument1.isDeterministic() && argument2.isDeterministic()) {
            		final double	argument1Realization = argument1.get(0);
            		final double	argument2Realization = argument2.get(0);
            		final double	innerResult = operatorInner.applyAsDouble(argument1Realization, argument2Realization);
            		return new RandomVariableLazyEvaluation(newTime,(int i) -> operatorOuter.applyAsDouble(realizations.applyAsDouble(i), innerResult), newSize);
            	}
            	else {
            		return new RandomVariableLazyEvaluation(newTime,(int i) -> operatorOuter.applyAsDouble(realizations.applyAsDouble(i), operatorInner.applyAsDouble(argument1.get(i), argument2.get(i))), newSize);
            	}
            }
            else {
                IntToDoubleFunction innerResult;
            	if(argument1.isDeterministic() && argument2.isDeterministic()) {
            		final double	argument1Realization = argument1.get(0);
            		final double	argument2Realization = argument2.get(0);
            		innerResult = i -> operatorInner.applyAsDouble(argument1Realization, argument2Realization);
                }
            	else if(argument1.isDeterministic() && !argument2.isDeterministic()) {
            		final double	argument1Realization	= argument1.get(0);
            		final double[]	argument2Realizations	= argument2.getRealizations();
                    innerResult = i -> operatorInner.applyAsDouble(argument1Realization, argument2Realizations[i]);
            	}
            	else if(!argument1.isDeterministic() && argument2.isDeterministic()) {
            		final double[]	argument1Realizations	= argument1.getRealizations();
            		final double	argument2Realization	= argument2.get(0);
                    innerResult = i -> operatorInner.applyAsDouble(argument1Realizations[i], argument2Realization);
            	}
            	else {// if(!argument1.isDeterministic() && !argument2.isDeterministic()) {
            		final double[]	argument1Realizations	= argument1.getRealizations();
            		final double[]	argument2Realizations	= argument2.getRealizations();
                    innerResult = i -> operatorInner.applyAsDouble(argument1Realizations[i], argument2Realizations[i]);
            	}

                if(isDeterministic()) {
                    return new RandomVariableLazyEvaluation(newTime,(int i) -> operatorOuter.applyAsDouble(valueIfNonStochastic,          innerResult.applyAsDouble(i)), newSize);
                }
                else {
                    return new RandomVariableLazyEvaluation(newTime,(int i) -> operatorOuter.applyAsDouble(realizations.applyAsDouble(i), innerResult.applyAsDouble(i)), newSize);
                }

            }
        }
    }

    public RandomVariableInterface apply(DoubleTernaryOperator operator, RandomVariableInterface argument1, RandomVariableInterface argument2)
    {
        double newTime = Math.max(time, argument1.getFiltrationTime());
        newTime = Math.max(newTime, argument2.getFiltrationTime());

        if(this.isDeterministic() && argument1.isDeterministic() && argument2.isDeterministic()) {
            return new RandomVariableLazyEvaluation(newTime, operator.applyAsDouble(valueIfNonStochastic, argument1.get(0), argument2.get(0)));
        }
        else {
            int newSize = Math.max(Math.max(this.size(), argument1.size()), argument2.size());
            IntToDoubleFunction result;
            if(argument1.isDeterministic() && argument2.isDeterministic()) {
                final double	argument1Realization = argument1.get(0);
                final double	argument2Realization = argument2.get(0);
                if(isDeterministic()) {
                    result = i -> operator.applyAsDouble(valueIfNonStochastic, argument1Realization, argument2Realization);
                }
                else {
                    result = i -> operator.applyAsDouble(realizations.applyAsDouble(i), argument1Realization, argument2Realization);

                }
            }
            else if(argument1.isDeterministic() && !argument2.isDeterministic()) {
                final double	argument1Realization	= argument1.get(0);
                final double[]	argument2Realizations	= argument2.getRealizations();
                if(isDeterministic()) {
                    result = i -> operator.applyAsDouble(valueIfNonStochastic, argument1Realization, argument2Realizations[i]);
                }
                else {
                    result = i -> operator.applyAsDouble(realizations.applyAsDouble(i), argument1Realization, argument2Realizations[i]);

                }
            }
            else if(!argument1.isDeterministic() && argument2.isDeterministic()) {
                final double[]	argument1Realizations	= argument1.getRealizations();
                final double	argument2Realization	= argument2.get(0);
                if(isDeterministic()) {
                    result = i -> operator.applyAsDouble(valueIfNonStochastic, argument1Realizations[i], argument2Realization);
                }
                else {
                    result = i -> operator.applyAsDouble(realizations.applyAsDouble(i), argument1Realizations[i], argument2Realization);

                }
            }
            else {// if(!argument1.isDeterministic() && !argument2.isDeterministic()) {
                final double[]	argument1Realizations	= argument1.getRealizations();
                final double[]	argument2Realizations	= argument2.getRealizations();
                if(isDeterministic()) {
                    result = i -> operator.applyAsDouble(valueIfNonStochastic, argument1Realizations[i], argument2Realizations[i]);
                }
                else {
                    result = i -> operator.applyAsDouble(realizations.applyAsDouble(i), argument1Realizations[i], argument2Realizations[i]);

                }
            }

            return new RandomVariableLazyEvaluation(newTime, result, newSize);
        }
    }

    /* (non-Javadoc)
     * @see net.finmath.stochastic.RandomVariableInterface#cap(double)
     */
    @Override
    public RandomVariableInterface cap(double cap) {
        return apply(x -> Math.min(x, cap));
    }

    /* (non-Javadoc)
     * @see net.finmath.stochastic.RandomVariableInterface#floor(double)
     */
    @Override
    public RandomVariableInterface floor(double floor) {
        return apply(x -> Math.max(x, floor));
    }

    /* (non-Javadoc)
     * @see net.finmath.stochastic.RandomVariableInterface#add(double)
     */
    @Override
    public RandomVariableInterface add(double value) {
        return apply(x -> x + value);
    }

    /* (non-Javadoc)
     * @see net.finmath.stochastic.RandomVariableInterface#sub(double)
     */
    @Override
    public RandomVariableInterface sub(double value) {
        return apply(x -> x - value);
    }

    /* (non-Javadoc)
     * @see net.finmath.stochastic.RandomVariableInterface#mult(double)
     */
    @Override
    public RandomVariableInterface mult(double value) {
        return apply(x -> x * value);
    }

    /* (non-Javadoc)
     * @see net.finmath.stochastic.RandomVariableInterface#div(double)
     */
    @Override
    public RandomVariableInterface div(double value) {
        return apply(x -> x / value);
    }

    /* (non-Javadoc)
     * @see net.finmath.stochastic.RandomVariableInterface#pow(double)
     */
    @Override
    public RandomVariableInterface pow(double exponent) {
        return apply(x -> FastMath.pow(x, exponent));
    }

    /* (non-Javadoc)
     * @see net.finmath.stochastic.RandomVariableInterface#squared()
     */
    @Override
    public RandomVariableInterface squared() {
        return apply(x -> x * x);
    }

    /* (non-Javadoc)
     * @see net.finmath.stochastic.RandomVariableInterface#sqrt()
     */
    @Override
    public RandomVariableInterface sqrt() {
        return apply(FastMath::sqrt);
    }

    /* (non-Javadoc)
     * @see net.finmath.stochastic.RandomVariableInterface#exp()
     */
    @Override
    public RandomVariableInterface exp() {
        return apply(FastMath::exp);
    }

    /* (non-Javadoc)
     * @see net.finmath.stochastic.RandomVariableInterface#log()
     */
    @Override
    public RandomVariableInterface log() {
        return apply(FastMath::log);
    }

    /* (non-Javadoc)
     * @see net.finmath.stochastic.RandomVariableInterface#sin()
     */
    @Override
    public RandomVariableInterface sin() {
        return apply(FastMath::sin);
    }

    /* (non-Javadoc)
     * @see net.finmath.stochastic.RandomVariableInterface#cos()
     */
    @Override
    public RandomVariableInterface cos() {
        return apply(FastMath::cos);
    }

    /* (non-Javadoc)
     * @see net.finmath.stochastic.RandomVariableInterface#add(net.finmath.stochastic.RandomVariableInterface)
     */
    @Override
    public RandomVariableInterface add(RandomVariableInterface randomVariable) {
        return apply((x, y) -> x + y, randomVariable);

    }

    /* (non-Javadoc)
     * @see net.finmath.stochastic.RandomVariableInterface#sub(net.finmath.stochastic.RandomVariableInterface)
     */
    @Override
    public RandomVariableInterface sub(RandomVariableInterface randomVariable) {
        return apply((x, y) -> x - y, randomVariable);
    }

    /* (non-Javadoc)
     * @see net.finmath.stochastic.RandomVariableInterface#mult(net.finmath.stochastic.RandomVariableInterface)
     */
    @Override
    public RandomVariableInterface mult(RandomVariableInterface randomVariable) {
        return apply((x, y) -> x * y, randomVariable);
    }

    /* (non-Javadoc)
     * @see net.finmath.stochastic.RandomVariableInterface#div(net.finmath.stochastic.RandomVariableInterface)
     */
    @Override
    public RandomVariableInterface div(RandomVariableInterface randomVariable) {
        return apply((x, y) -> x / y, randomVariable);
    }

    /* (non-Javadoc)
     * @see net.finmath.stochastic.RandomVariableInterface#cap(net.finmath.stochastic.RandomVariableInterface)
     */
    @Override
    public RandomVariableInterface cap(RandomVariableInterface cap) {
        return apply(FastMath::min, cap);
    }

    /* (non-Javadoc)
     * @see net.finmath.stochastic.RandomVariableInterface#floor(net.finmath.stochastic.RandomVariableInterface)
     */
    @Override
    public RandomVariableInterface floor(RandomVariableInterface floor) {
        return apply(FastMath::max, floor);
    }

    /* (non-Javadoc)
     * @see net.finmath.stochastic.RandomVariableInterface#accrue(net.finmath.stochastic.RandomVariableInterface, double)
     */
    @Override
    public RandomVariableInterface accrue(RandomVariableInterface rate, double periodLength) {
        return apply((x, y) -> x * (1.0 + y * periodLength), rate);
    }

    /* (non-Javadoc)
     * @see net.finmath.stochastic.RandomVariableInterface#discount(net.finmath.stochastic.RandomVariableInterface, double)
     */
    @Override
    public RandomVariableInterface discount(RandomVariableInterface rate, double periodLength) {
        return apply((x, y) -> x / (1.0 + y * periodLength), rate);
    }

    /* (non-Javadoc)
     * @see net.finmath.stochastic.RandomVariableInterface#barrier(net.finmath.stochastic.RandomVariableInterface, net.finmath.stochastic.RandomVariableInterface, net.finmath.stochastic.RandomVariableInterface)
     */
    @Override
    public RandomVariableInterface barrier(RandomVariableInterface trigger, RandomVariableInterface valueIfTriggerNonNegative, RandomVariableInterface valueIfTriggerNegative) {
        return trigger.apply(( x, y, z) -> (x >= 0 ? y : z), valueIfTriggerNonNegative, valueIfTriggerNegative);
    }

    @Override
    public RandomVariableInterface barrier(RandomVariableInterface trigger, RandomVariableInterface valueIfTriggerNonNegative, double valueIfTriggerNegative) {
        return this.barrier(trigger, valueIfTriggerNonNegative, new RandomVariableLazyEvaluation(valueIfTriggerNonNegative.getFiltrationTime(), valueIfTriggerNegative));
    }

    /* (non-Javadoc)
     * @see net.finmath.stochastic.RandomVariableInterface#invert()
     */
    @Override
    public RandomVariableInterface invert() {
        return apply(x -> 1.0 / x);
    }

    /* (non-Javadoc)
     * @see net.finmath.stochastic.RandomVariableInterface#abs()
     */
    @Override
    public RandomVariableInterface abs() {
        return apply(Math::abs);
    }

    /* (non-Javadoc)
     * @see net.finmath.stochastic.RandomVariableInterface#addProduct(net.finmath.stochastic.RandomVariableInterface, double)
     */
    @Override
    public RandomVariableInterface addProduct(final RandomVariableInterface factor1, final double factor2) {
        return apply((x, y) -> x + y * factor2, factor1);
    }


    /* (non-Javadoc)
     * @see net.finmath.stochastic.RandomVariableInterface#addProduct(net.finmath.stochastic.RandomVariableInterface, net.finmath.stochastic.RandomVariableInterface)
     */
    @Override
    public RandomVariableInterface addProduct(RandomVariableInterface factor1, RandomVariableInterface factor2) {
        return apply((x,y) -> x + y, (x, y) -> x * y, factor1, factor2);
    }

    /* (non-Javadoc)
     * @see net.finmath.stochastic.RandomVariableInterface#addRatio(net.finmath.stochastic.RandomVariableInterface, net.finmath.stochastic.RandomVariableInterface)
     */
    @Override
    public RandomVariableInterface addRatio(RandomVariableInterface numerator, RandomVariableInterface denominator) {
        return apply((x, y) -> x + y, (x, y) -> x / y, numerator, denominator);
    }

    /* (non-Javadoc)
     * @see net.finmath.stochastic.RandomVariableInterface#subRatio(net.finmath.stochastic.RandomVariableInterface, net.finmath.stochastic.RandomVariableInterface)
     */
    @Override
    public RandomVariableInterface subRatio(RandomVariableInterface numerator, RandomVariableInterface denominator) {
        return apply((x,y) -> x - y, (x, y) -> x / y, numerator, denominator);
    }

    @Override
	public RandomVariableInterface isNaN() {
		if(isDeterministic()) {
			return new RandomVariableLazyEvaluation(time, Double.isNaN(valueIfNonStochastic) ? 1.0 : 0.0);
		}
		else {
			double[] newRealizations = new double[size()];
			for(int i=0; i 

Ads help maintain this website.