WeightedPolynomialEstimator.java

/*
 * Copyright (C) 2016 Alberto Irurueta Carro (alberto@irurueta.com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.irurueta.numerical.polynomials.estimators;

import com.irurueta.algebra.AlgebraException;
import com.irurueta.algebra.Matrix;
import com.irurueta.algebra.Utils;
import com.irurueta.numerical.LockedException;
import com.irurueta.numerical.NotReadyException;
import com.irurueta.numerical.polynomials.Polynomial;
import com.irurueta.numerical.robust.WeightSelection;
import com.irurueta.sorting.SortingException;

import java.util.List;

/**
 * This class implements a polynomial estimator using weighted evaluations.
 * Weights can be used so that evaluations assumed to have a better quality
 * (i.e. more precisely estimated) are considered to be more relevant.
 * It is discouraged to use a large number of evaluations, even if they are
 * correctly weighted, since as the number of evaluations increase so do the
 * rounding errors.
 */
@SuppressWarnings("DuplicatedCode")
public class WeightedPolynomialEstimator extends PolynomialEstimator {

    /**
     * Default number of evaluations to be weighted and taken into account.
     */
    public static final int DEFAULT_MAX_EVALUATIONS = 50;

    /**
     * Indicates if weights are sorted by default so that largest weighted
     * evaluations are used first.
     */
    public static final boolean DEFAULT_SORT_WEIGHTS = true;

    /**
     * Maximum number of evaluations to be weighted and taken into account.
     */
    private int maxEvaluations = DEFAULT_MAX_EVALUATIONS;

    /**
     * Indicates if weights are sorted by default so that largest weighted
     * evaluations are used first.
     */
    private boolean sortWeights = DEFAULT_SORT_WEIGHTS;

    /**
     * Array containing weights for all evaluations.
     */
    private double[] weights;

    /**
     * Constructor.
     */
    public WeightedPolynomialEstimator() {
        super();
    }

    /**
     * Constructor.
     *
     * @param degree degree of polynomial to be estimated.
     * @throws IllegalArgumentException if provided degree is less than 1.
     */
    public WeightedPolynomialEstimator(final int degree) {
        super(degree);
    }

    /**
     * Constructor.
     *
     * @param evaluations collection of polynomial evaluations.
     * @param weights     array containing a weight amount for each evaluation. The
     *                    larger the value of a weight, the most significant the correspondence
     *                    will be.
     * @throws IllegalArgumentException if evaluations or weights are null or
     *                                  don't have the same size.
     */
    public WeightedPolynomialEstimator(
            final List<PolynomialEvaluation> evaluations, final double[] weights) {
        super();
        internalSetEvaluationsAndWeights(evaluations, weights);
    }

    /**
     * Constructor.
     *
     * @param listener listener to be notified of events.
     */
    public WeightedPolynomialEstimator(final PolynomialEstimatorListener listener) {
        super(listener);
    }

    /**
     * Constructor.
     *
     * @param degree      degree of polynomial to be estimated.
     * @param evaluations collection of polynomial evaluations.
     * @param weights     array containing a weight amount for each evaluation. The
     *                    larger the value of a weight, the most significant the correspondence
     *                    will be.
     * @throws IllegalArgumentException if evaluations or weights are null or
     *                                  don't have the same size, or if provided degree is less than 1.
     */
    public WeightedPolynomialEstimator(
            final int degree, final List<PolynomialEvaluation> evaluations, final double[] weights) {
        super(degree);
        internalSetEvaluationsAndWeights(evaluations, weights);
    }

    /**
     * Constructor.
     *
     * @param degree   degree of polynomial to be estimated.
     * @param listener listener to be notified of events.
     * @throws IllegalArgumentException if provided degree is less than 1.
     */
    public WeightedPolynomialEstimator(final int degree, final PolynomialEstimatorListener listener) {
        super(degree, listener);
    }

    /**
     * Constructor.
     *
     * @param evaluations collection of polynomial evaluations.
     * @param weights     array containing a weight amount for each evaluation. The
     *                    larger the value of a weight, the most significant the correspondence
     *                    will be.
     * @param listener    listener to be notified of events.
     * @throws IllegalArgumentException if evaluations or weights are null or
     *                                  don't have the same size.
     */
    public WeightedPolynomialEstimator(
            final List<PolynomialEvaluation> evaluations, final double[] weights,
            final PolynomialEstimatorListener listener) {
        super(listener);
        internalSetEvaluationsAndWeights(evaluations, weights);
    }

    /**
     * Constructor.
     *
     * @param degree      degree of polynomial to be estimated.
     * @param evaluations collection of polynomial evaluations.
     * @param weights     array containing a weight amount for each evaluation. The
     *                    larger the value of a weight, the most significant the correspondence
     *                    will be.
     * @param listener    listener to be notified of events.
     * @throws IllegalArgumentException if evaluations or weights are null or
     *                                  don't have the same size, or if provided degree is less than 1.
     */
    public WeightedPolynomialEstimator(
            final int degree, final List<PolynomialEvaluation> evaluations, final double[] weights,
            final PolynomialEstimatorListener listener) {
        super(degree, listener);
        internalSetEvaluationsAndWeights(evaluations, weights);
    }

    /**
     * Sets collection of polynomial evaluations and their corresponding point
     * of evaluation used to determine a polynomial of required degree.
     * This method override always throws an IllegalArgumentException because it
     * is expected to provide both evaluations and their weights.
     *
     * @param evaluations collection of polynomial evaluations.
     * @throws IllegalArgumentException always thrown.
     */
    @Override
    public void setEvaluations(final List<PolynomialEvaluation> evaluations) {
        throw new IllegalArgumentException("evaluations and weights must be provided at once");
    }

    /**
     * Sets collection of polynomial evaluations along with their corresponding
     * weights.
     *
     * @param evaluations collection of polynomial evaluations.
     * @param weights     array containing a weight amount for each polynomial
     *                    evaluation. The larger the value of a weight, the most significant the
     *                    evaluation will be.
     * @throws LockedException          if estimator is locked.
     * @throws IllegalArgumentException if evaluations or weights are null or
     *                                  don't have the same size.
     */
    public void setEvaluationsAndWeights(
            final List<PolynomialEvaluation> evaluations, final double[] weights) throws LockedException {
        if (isLocked()) {
            throw new LockedException();
        }
        internalSetEvaluationsAndWeights(evaluations, weights);
    }

    /**
     * Sets degree of polynomial to be estimated and collection of polynomial
     * evaluations and their corresponding point of evaluation used to determine
     * a polynomial of specified degree.
     *
     * @param degree      degree of polynomial to be estimated.
     * @param evaluations collection of polynomial evaluations.
     * @param weights     array containing a weight amount for each polynomial
     *                    evaluation. The larger the value of a weight, the most significant the
     *                    evaluation will be.
     * @throws IllegalArgumentException if provided degree is less than 1 or
     *                                  if evaluations or weights are null or don't have the same size.
     * @throws LockedException          if this instance is locked.
     */
    public void setDegreeEvaluationsAndWeights(
            final int degree, final List<PolynomialEvaluation> evaluations, final double[] weights)
            throws LockedException {
        setDegree(degree);
        setEvaluationsAndWeights(evaluations, weights);
    }


    /**
     * Returns array containing a weight amount for each polynomial evaluation.
     * The larger the value of a weight, the most significant the correspondence
     * will be.
     *
     * @return array containing weights for each correspondence.
     */
    public double[] getWeights() {
        return weights;
    }

    /**
     * Returns boolean indicating whether weights have been provided and are
     * available for retrieval.
     *
     * @return true if weights are available, false otherwise.
     */
    public boolean areWeightsAvailable() {
        return weights != null;
    }

    /**
     * Returns maximum number of evaluations to be weighted and taken into
     * account.
     *
     * @return maximum number of evaluations to be weighted.
     */
    public int getMaxEvaluations() {
        return maxEvaluations;
    }

    /**
     * Sets maximum number of evaluations to be weighted and taken into account.
     * This method must be called after setting degree, because the minimum
     * number of required evaluations will be checked based on degree of
     * polynomial to be estimated.
     *
     * @param maxEvaluations maximum number of evaluations to be weighted.
     * @throws IllegalArgumentException if provided value is less than the
     *                                  minimum number of required evaluations.
     * @throws LockedException          if this instance is locked.
     */
    public void setMaxEvaluations(final int maxEvaluations) throws LockedException {
        if (isLocked()) {
            throw new LockedException();
        }
        if (maxEvaluations < getMinNumberOfEvaluations()) {
            throw new IllegalArgumentException();
        }
        this.maxEvaluations = maxEvaluations;
    }

    /**
     * Indicates if weights are sorted by so that largest weighted evaluations
     * are used first.
     *
     * @return true if weights are sorted, false otherwise.
     */
    public boolean isSortWeightsEnabled() {
        return sortWeights;
    }

    /**
     * Specifies whether weights are sorted by so that largest weighted
     * evaluations are used first.
     *
     * @param sortWeights true if weights are sorted, false otherwise.
     * @throws LockedException if this instance is locked.
     */
    public void setSortWeightsEnabled(final boolean sortWeights) throws LockedException {
        if (isLocked()) {
            throw new LockedException();
        }

        this.sortWeights = sortWeights;
    }

    /**
     * Indicates if this estimator is ready to start the estimation.
     * Estimator will be ready once enough evaluations and weights are provided.
     *
     * @return true if estimator is ready, false otherwise.
     */
    @Override
    public boolean isReady() {
        return super.isReady() && areWeightsAvailable() && evaluations.size() == weights.length;
    }

    /**
     * Estimates a polynomial based on provided evaluations.
     *
     * @return estimated polynomial.
     * @throws LockedException               if estimator is locked.
     * @throws NotReadyException             if estimator is not ready.
     * @throws PolynomialEstimationException if polynomial estimation fails.
     */
    @Override
    public Polynomial estimate() throws LockedException, NotReadyException, PolynomialEstimationException {
        if (isLocked()) {
            throw new LockedException();
        }
        if (!isReady()) {
            throw new NotReadyException();
        }

        try {
            locked = true;
            if (listener != null) {
                listener.onEstimateStart(this);
            }

            final var selection = WeightSelection.selectWeights(weights, sortWeights, maxEvaluations);
            final var selected = selection.getSelected();
            final var nEvaluations = selection.getNumSelected();


            final var a = new Matrix(nEvaluations, degree + 1);
            final var b = new Matrix(nEvaluations, 1);

            var index = 0;
            var counter = 0;
            double weight;
            for (final var evaluation : evaluations) {
                if (selected[index]) {
                    weight = weights[index];

                    switch (evaluation.getType()) {
                        case DIRECT_EVALUATION:
                            fillDirectEvaluation((DirectPolynomialEvaluation) evaluation, a, b, counter);
                            break;
                        case DERIVATIVE_EVALUATION:
                            fillDerivativeEvaluation((DerivativePolynomialEvaluation) evaluation, a, b, counter);
                            break;
                        case INTEGRAL_EVALUATION:
                            fillIntegralEvaluation((IntegralPolynomialEvaluation) evaluation, a, b, counter);
                            break;
                        case INTEGRAL_INTERVAL:
                            fillIntegralIntervalEvaluation((IntegralIntervalPolynomialEvaluation) evaluation, a, b,
                                    counter);
                            break;
                        default:
                            continue;
                    }

                    normalize(a, b, counter, weight);
                    counter++;
                }

                index++;
            }

            final var params = Utils.solve(a, b);

            final var result = new Polynomial(params.toArray());

            if (listener != null) {
                listener.onEstimateEnd(this);
            }

            return result;
        } catch (final AlgebraException | SortingException e) {
            throw new PolynomialEstimationException(e);
        } finally {
            locked = false;
        }
    }

    /**
     * Returns type of polynomial estimator.
     *
     * @return type of polynomial estimator.
     */
    @Override
    public PolynomialEstimatorType getType() {
        return PolynomialEstimatorType.WEIGHTED_POLYNOMIAL_ESTIMATOR;
    }

    /**
     * Normalizes rows of system matrix and values matrix to increase accuracy
     * of linear system of equations to be solved.
     *
     * @param a      system matrix.
     * @param b      values matrix.
     * @param row    row to normalize.
     * @param weight weight.
     */
    private static void normalize(final Matrix a, final Matrix b, final int row, final double weight) {
        var sqrNorm = 0.0;
        for (var i = 0; i < a.getColumns(); i++) {
            sqrNorm += Math.pow(a.getElementAt(row, i), 2.0);
        }
        sqrNorm += Math.pow(b.getElementAtIndex(row), 2.0);

        final var norm = Math.sqrt(sqrNorm);
        final var factor = weight / norm;

        for (var i = 0; i < a.getColumns(); i++) {
            a.setElementAt(row, i, a.getElementAt(row, i) * factor);
        }
        b.setElementAtIndex(row, b.getElementAtIndex(row) * factor);
    }

    /**
     * Internal method to set evaluations and weights.
     *
     * @param evaluations evaluations.
     * @param weights     weights.
     * @throws IllegalArgumentException if evaluations or weights are null or
     *                                  don't have the same size.
     */
    private void internalSetEvaluationsAndWeights(
            final List<PolynomialEvaluation> evaluations, final double[] weights) {
        if (weights == null || evaluations == null || weights.length != evaluations.size()) {
            throw new IllegalArgumentException();
        }
        this.evaluations = evaluations;
        this.weights = weights;
    }
}