PolynomialEstimator.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.Matrix;
import com.irurueta.numerical.LockedException;
import com.irurueta.numerical.NotReadyException;
import com.irurueta.numerical.polynomials.Polynomial;

import java.util.Arrays;
import java.util.List;

/**
 * This class defines the interface for an estimator of a polynomial of a given
 * degree using points where polynomials are evaluated.
 */
@SuppressWarnings("Duplicates")
public abstract class PolynomialEstimator {

    /**
     * Minimum allowed degree to be estimated
     */
    public static final int MIN_DEGREE = 1;

    /**
     * Default estimator type.
     */
    public static final PolynomialEstimatorType DEFAULT_ESTIMATOR_TYPE =
            PolynomialEstimatorType.LMSE_POLYNOMIAL_ESTIMATOR;

    /**
     * Degree of polynomial to be estimated.
     */
    protected int degree;

    /**
     * Collection of polynomial evaluations and their corresponding point of
     * evaluation used to determine a polynomial of required degree.
     */
    protected List<PolynomialEvaluation> evaluations;

    /**
     * True when estimator is estimating radial distortion.
     */
    protected boolean locked;

    /**
     * Listener to be notified of events such as when estimation starts, ends or
     * estimation progress changes.
     */
    protected PolynomialEstimatorListener listener;

    /**
     * Constructor.
     */
    protected PolynomialEstimator() {
        degree = MIN_DEGREE;
    }

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

    /**
     * Constructor.
     *
     * @param evaluations collection of polynomial evaluations.
     */
    protected PolynomialEstimator(final List<PolynomialEvaluation> evaluations) {
        this();
        this.evaluations = evaluations;
    }

    /**
     * Constructor.
     *
     * @param listener listener to be notified of events.
     */
    protected PolynomialEstimator(final PolynomialEstimatorListener listener) {
        this();
        this.listener = listener;
    }

    /**
     * Constructor.
     *
     * @param degree      degree of polynomial to be estimated.
     * @param evaluations collection of polynomial evaluations.
     * @throws IllegalArgumentException if provided degree is less than 1.
     */
    protected PolynomialEstimator(final int degree, final List<PolynomialEvaluation> evaluations) {
        this(degree);
        this.evaluations = evaluations;
    }

    /**
     * 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.
     */
    protected PolynomialEstimator(final int degree, final PolynomialEstimatorListener listener) {
        this(degree);
        this.listener = listener;
    }

    /**
     * Constructor.
     *
     * @param evaluations collection of polynomial evaluations.
     * @param listener    listener to be notified of events.
     */
    protected PolynomialEstimator(
            final List<PolynomialEvaluation> evaluations, final PolynomialEstimatorListener listener) {
        this(evaluations);
        this.listener = listener;
    }

    /**
     * Constructor.
     *
     * @param degree      degree of polynomial to be estimated.
     * @param evaluations collection of polynomial evaluations.
     * @param listener    listener to be notified of events.
     * @throws IllegalArgumentException if provided degree is less than 1.
     */
    protected PolynomialEstimator(
            final int degree, final List<PolynomialEvaluation> evaluations,
            final PolynomialEstimatorListener listener) {
        this(degree, evaluations);
        this.listener = listener;
    }

    /**
     * Gets degree of polynomial to be estimated.
     *
     * @return degree of polynomial to be estimated.
     */
    public int getDegree() {
        return degree;
    }

    /**
     * Sets degree of polynomial to be estimated.
     *
     * @param degree degree of polynomial to be estimated.
     * @throws IllegalArgumentException if provided degree is less than 1.
     * @throws LockedException          if this instance is locked.
     */
    public void setDegree(final int degree) throws LockedException {
        if (isLocked()) {
            throw new LockedException();
        }

        internalSetDegree(degree);
    }

    /**
     * Gets collection of polynomial evaluations and their corresponding point
     * of evaluation used to determine a polynomial of required degree.
     *
     * @return collection of polynomial evaluations.
     */
    public List<PolynomialEvaluation> getEvaluations() {
        return evaluations;
    }

    /**
     * Sets collection of polynomial evaluations and their corresponding point
     * of evaluation used to determine a polynomial of required degree.
     *
     * @param evaluations collection of polynomial evaluations.
     * @throws LockedException if this instance is locked.
     */
    public void setEvaluations(final List<PolynomialEvaluation> evaluations) throws LockedException {
        if (isLocked()) {
            throw new LockedException();
        }

        this.evaluations = evaluations;
    }

    /**
     * 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.
     * @throws IllegalArgumentException if provided degree is less than 1.
     * @throws LockedException          if this instance is locked.
     */
    public void setDegreeAndEvaluations(
            final int degree, final List<PolynomialEvaluation> evaluations) throws LockedException {
        setDegree(degree);
        setEvaluations(evaluations);
    }

    /**
     * Determines whether estimation is ready to start with the given data
     * and required degree of polynomial to be estimated
     *
     * @return true if estimator is ready, false otherwise.
     */
    public boolean isReady() {

        final var nParams = degree + 1;
        if (evaluations == null || evaluations.size() < nParams) {
            return false;
        }

        // also ensure that at least a direct or integral evaluation exists
        var count = 0;
        for (final var eval : evaluations) {
            if (eval.getType() == PolynomialEvaluationType.DIRECT_EVALUATION
                    || eval.getType() == PolynomialEvaluationType.INTEGRAL_EVALUATION
                    || eval.getType() == PolynomialEvaluationType.INTEGRAL_INTERVAL) {
                count++;
            }
        }

        return count >= 1 && evaluations.size() >= nParams;
    }

    /**
     * Gets minimum number of evaluations required to estimate a polynomial of
     * the specified degree.
     *
     * @param degree degree of polynomial to be estimated.
     * @return number of required evaluations.
     * @throws IllegalArgumentException if provided degree is less than 1.
     */
    public static int getMinNumberOfEvaluations(final int degree) {
        if (degree < MIN_DEGREE) {
            throw new IllegalArgumentException();
        }

        return degree + 1;
    }

    /**
     * Gets minimum number of evaluations required to estimate a polynomial of
     * the specified degree.
     *
     * @return number of required evaluations.
     */
    public int getMinNumberOfEvaluations() {
        return getMinNumberOfEvaluations(degree);
    }

    /**
     * Indicates whether this instance is locked.
     *
     * @return true if this estimator is busy estimating a polynomial, false
     * otherwise.
     */
    public boolean isLocked() {
        return locked;
    }

    /**
     * Gets listener to be notified of events such as when estimation starts,
     * ends or estimation progress changes.
     *
     * @return listener to be notified of events.
     */
    public PolynomialEstimatorListener getListener() {
        return listener;
    }

    /**
     * Sets listener to be notified of events such as when estimation starts,
     * ends or estimation progress changes.
     *
     * @param listener listener to be notified of events.
     * @throws LockedException if estimator is locked.
     */
    public void setListener(final PolynomialEstimatorListener listener) throws LockedException {
        if (isLocked()) {
            throw new LockedException();
        }

        this.listener = listener;
    }

    /**
     * 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.
     */
    public abstract Polynomial estimate() throws LockedException, NotReadyException, PolynomialEstimationException;

    /**
     * Returns type of polynomial estimator.
     *
     * @return type of polynomial estimator.
     */
    public abstract PolynomialEstimatorType getType();

    /**
     * Creates an instance of a polynomial estimator using default
     * type.
     *
     * @return an instance of a polynomial estimator.
     */
    public static PolynomialEstimator create() {
        return create(DEFAULT_ESTIMATOR_TYPE);
    }

    /**
     * Creates an instance of a polynomial estimator using provided degree and
     * default type.
     *
     * @param degree degree of polynomial to be estimated.
     * @return an instance of a polynomial estimator.
     * @throws IllegalArgumentException if provided degree is less than 1.
     */
    public static PolynomialEstimator create(final int degree) {
        return create(degree, DEFAULT_ESTIMATOR_TYPE);
    }

    /**
     * Creates an instance of a polynomial estimator using provided evaluations,
     * and default type and degree.
     *
     * @param evaluations collection of polynomial evaluations.
     * @return an instance of a polynomial estimator.
     */
    public static PolynomialEstimator create(final List<PolynomialEvaluation> evaluations) {
        return create(evaluations, DEFAULT_ESTIMATOR_TYPE);
    }

    /**
     * Creates an instance of a polynomial estimator using provided listener
     * and default type and degree.
     *
     * @param listener listener to be notified of events.
     * @return an instance of a polynomial estimator.
     */
    public static PolynomialEstimator create(final PolynomialEstimatorListener listener) {
        return create(listener, DEFAULT_ESTIMATOR_TYPE);
    }

    /**
     * Creates an instance of a polynomial estimator using provided degree,
     * polynomial evaluations and default type.
     *
     * @param degree      degree of polynomial to be estimated.
     * @param evaluations collection of polynomial evaluations.
     * @return an instance of a polynomial estimator.
     * @throws IllegalArgumentException if provided degree is less than 1.
     */
    public static PolynomialEstimator create(final int degree, final List<PolynomialEvaluation> evaluations) {
        return create(degree, evaluations, DEFAULT_ESTIMATOR_TYPE);
    }

    /**
     * Creates an instance of a polynomial estimator using provided degree,
     * listener and default type.
     *
     * @param degree   degree of polynomial to be estimated.
     * @param listener listener to be notified of events.
     * @return an instance of a polynomial estimator.
     * @throws IllegalArgumentException if provided degree is less than 1.
     */
    public static PolynomialEstimator create(final int degree, final PolynomialEstimatorListener listener) {
        return create(degree, listener, DEFAULT_ESTIMATOR_TYPE);
    }

    /**
     * Creates an instance of a polynomial estimator using provided evaluations,
     * listener and default type.
     *
     * @param evaluations collection of polynomial evaluations.
     * @param listener    listener to be notified of events.
     * @return an instance of a polynomial estimator.
     */
    public static PolynomialEstimator create(
            final List<PolynomialEvaluation> evaluations, final PolynomialEstimatorListener listener) {
        return create(evaluations, listener, DEFAULT_ESTIMATOR_TYPE);
    }

    /**
     * Creates an instance of a polynomial estimator using provided degree,
     * evaluations, listener and default type.
     *
     * @param degree      degree of polynomial to be estimated.
     * @param evaluations collection of polynomial evaluations.
     * @param listener    listener to be notified of events.
     * @return an instance of a polynomial estimator.
     * @throws IllegalArgumentException if provided degree is less than 1.
     */
    public static PolynomialEstimator create(
            final int degree, final List<PolynomialEvaluation> evaluations,
            final PolynomialEstimatorListener listener) {
        return create(degree, evaluations, listener, DEFAULT_ESTIMATOR_TYPE);
    }

    /**
     * Creates an instance of a polynomial estimator using provided type and
     * default degree.
     *
     * @param type type of polynomial estimator
     * @return an instance of a polynomial estimator.
     */
    public static PolynomialEstimator create(final PolynomialEstimatorType type) {
        if (type == PolynomialEstimatorType.WEIGHTED_POLYNOMIAL_ESTIMATOR) {
            return new WeightedPolynomialEstimator();
        } else {
            return new LMSEPolynomialEstimator();
        }
    }

    /**
     * Creates an instance of a polynomial estimator using provided degree and
     * type.
     *
     * @param degree degree of polynomial to be estimated.
     * @param type   type of polynomial estimator.
     * @return an instance of a polynomial estimator.
     * @throws IllegalArgumentException if provided degree is less than 1.
     */
    public static PolynomialEstimator create(final int degree, final PolynomialEstimatorType type) {
        if (type == PolynomialEstimatorType.WEIGHTED_POLYNOMIAL_ESTIMATOR) {
            return new WeightedPolynomialEstimator(degree);
        } else {
            return new LMSEPolynomialEstimator(degree);
        }
    }

    /**
     * Creates an instance of a polynomial estimator using provided evaluations
     * and type.
     *
     * @param evaluations collection of polynomial evaluations.
     * @param type        type of polynomial estimator.
     * @return an instance of a polynomial estimator.
     */
    public static PolynomialEstimator create(
            final List<PolynomialEvaluation> evaluations, final PolynomialEstimatorType type) {
        switch (type) {
            case WEIGHTED_POLYNOMIAL_ESTIMATOR:
                final var weights = new double[evaluations.size()];
                Arrays.fill(weights, 1.0);
                return new WeightedPolynomialEstimator(evaluations, weights);
            case LMSE_POLYNOMIAL_ESTIMATOR:
            default:
                return new LMSEPolynomialEstimator(evaluations);
        }
    }

    /**
     * Creates an instance of a polynomial estimator using provided listener
     * and type.
     *
     * @param listener listener to be notified of events.
     * @param type     type of polynomial estimator.
     * @return an instance of a polynomial estimator.
     */
    public static PolynomialEstimator create(
            final PolynomialEstimatorListener listener, final PolynomialEstimatorType type) {
        if (type == PolynomialEstimatorType.WEIGHTED_POLYNOMIAL_ESTIMATOR) {
            return new WeightedPolynomialEstimator(listener);
        } else {
            return new LMSEPolynomialEstimator(listener);
        }
    }

    /**
     * Creates an instance of a polynomial estimator using provided degree,
     * evaluations and type.
     *
     * @param degree      degree of polynomial to be estimated.
     * @param evaluations collection of polynomial evaluations.
     * @param type        type of polynomial estimator.
     * @return an instance of a polynomial estimator.
     * @throws IllegalArgumentException if provided degree is less than 1.
     */
    public static PolynomialEstimator create(
            final int degree, final List<PolynomialEvaluation> evaluations, final PolynomialEstimatorType type) {
        switch (type) {
            case WEIGHTED_POLYNOMIAL_ESTIMATOR:
                final var weights = new double[evaluations.size()];
                Arrays.fill(weights, 1.0);
                return new WeightedPolynomialEstimator(degree, evaluations, weights);
            case LMSE_POLYNOMIAL_ESTIMATOR:
            default:
                return new LMSEPolynomialEstimator(degree, evaluations);
        }
    }

    /**
     * Creates an instance of a polynomial estimator using provided degree,
     * listener and type.
     *
     * @param degree   degree of polynomial to be estimated.
     * @param listener listener to be notified of events.
     * @param type     type of polynomial estimator.
     * @return an instance of a polynomial estimator.
     * @throws IllegalArgumentException if provided degree is less than 1.
     */
    public static PolynomialEstimator create(
            final int degree, final PolynomialEstimatorListener listener, final PolynomialEstimatorType type) {
        if (type == PolynomialEstimatorType.WEIGHTED_POLYNOMIAL_ESTIMATOR) {
            return new WeightedPolynomialEstimator(degree, listener);
        } else {
            return new LMSEPolynomialEstimator(degree, listener);
        }
    }

    /**
     * Creates an instance of a polynomial estimator using provided evaluations,
     * listener and type.
     *
     * @param evaluations collection of polynomial evaluations.
     * @param listener    listener to be notified of events.
     * @param type        type of polynomial estimator.
     * @return an instance of a polynomial estimator.
     */
    public static PolynomialEstimator create(
            final List<PolynomialEvaluation> evaluations, final PolynomialEstimatorListener listener,
            final PolynomialEstimatorType type) {
        switch (type) {
            case WEIGHTED_POLYNOMIAL_ESTIMATOR:
                final var weights = new double[evaluations.size()];
                Arrays.fill(weights, 1.0);
                return new WeightedPolynomialEstimator(evaluations, weights, listener);
            case LMSE_POLYNOMIAL_ESTIMATOR:
            default:
                return new LMSEPolynomialEstimator(evaluations, listener);
        }
    }

    /**
     * Creates an instance of a polynomial estimator using provided degree,
     * evaluations, listener and type.
     *
     * @param degree      degree of polynomial to be estimated.
     * @param evaluations collection of polynomial evaluations.
     * @param listener    listener to be notified of events.
     * @param type        type of polynomial estimator.
     * @return an instance of a polynomial estimator.
     * @throws IllegalArgumentException if provided degree is less than 1.
     */
    public static PolynomialEstimator create(
            final int degree, final List<PolynomialEvaluation> evaluations, final PolynomialEstimatorListener listener,
            final PolynomialEstimatorType type) {
        switch (type) {
            case WEIGHTED_POLYNOMIAL_ESTIMATOR:
                final var weights = new double[evaluations.size()];
                Arrays.fill(weights, 1.0);
                return new WeightedPolynomialEstimator(degree, evaluations, weights, listener);
            case LMSE_POLYNOMIAL_ESTIMATOR:
            default:
                return new LMSEPolynomialEstimator(degree, evaluations, listener);
        }
    }

    /**
     * Fills row of system of equations for a direct polynomial evaluation.
     *
     * @param evaluation a direct polynomial evaluation.
     * @param a          system matrix.
     * @param b          values matrix.
     * @param row        row to be filled.
     */
    protected void fillDirectEvaluation(
            final DirectPolynomialEvaluation evaluation, final Matrix a, final Matrix b, final int row) {

        var powX = 1.0;
        final var x = evaluation.getX();
        for (var i = 0; i < a.getColumns(); i++) {
            a.setElementAt(row, i, powX);
            powX *= x;
        }

        b.setElementAtIndex(row, evaluation.getEvaluation());
    }

    /**
     * Fills row of system of equations for a derivative polynomial evaluation.
     *
     * @param evaluation a derivative polynomial evaluation.
     * @param a          system matrix.
     * @param b          values matrix.
     * @param row        row to be filled.
     */
    protected void fillDerivativeEvaluation(
            final DerivativePolynomialEvaluation evaluation, final Matrix a, final Matrix b, final int row) {

        final var order = evaluation.getDerivativeOrder();

        for (var i = 0; i < order; i++) {
            a.setElementAt(row, i, 0.0);
        }

        var powX = 1.0;
        final var x = evaluation.getX();
        for (var i = order; i < a.getColumns(); i++) {
            var param = i;
            for (var j = 1; j < order; j++) {
                param *= i - j;
            }
            a.setElementAt(row, i, param * powX);
            powX *= x;
        }

        b.setElementAtIndex(row, evaluation.getEvaluation());
    }

    /**
     * Fills row of system of equations for an integral polynomial evaluation.
     *
     * @param evaluation an integral polynomial evaluation.
     * @param a          system matrix.
     * @param b          values matrix.
     * @param row        row to be filled.
     * @throws PolynomialEstimationException if constant terms does not have
     *                                       proper size (it must be null or have order length).
     */
    protected void fillIntegralEvaluation(
            final IntegralPolynomialEvaluation evaluation, final Matrix a, final Matrix b, final int row)
            throws PolynomialEstimationException {

        final var order = evaluation.getIntegralOrder();
        final var constants = evaluation.getConstants();
        if (constants != null && constants.length != order) {
            throw new PolynomialEstimationException();
        }

        var accum = 0.0;
        var powX = 1.0;
        final var x = evaluation.getX();
        for (var i = 0; i < order; i++) {
            if (constants != null) {
                var param = 1;
                for (var k = 1; k <= i; k++) {
                    param *= k;
                }
                accum += constants[i] / param * powX;
            }
            powX *= x;
        }

        for (int i = 0, j = order; i < a.getColumns(); i++, j++) {
            var param = j;
            for (var k = 1; k < order; k++) {
                param *= j - k;
            }
            a.setElementAt(row, i, powX / param);
            powX *= x;
        }

        b.setElementAtIndex(row, evaluation.getEvaluation() - accum);
    }

    /**
     * Fills row of system of equations for a polynomial evaluation of an
     * interval integration.
     *
     * @param evaluation an interval integration of a polynomial evaluation.
     * @param a          system matrix.
     * @param b          values matrix.
     * @param row        row to be filled.
     * @throws PolynomialEstimationException if constant terms does not have
     *                                       proper size (it must be null or have order length).
     */
    protected void fillIntegralIntervalEvaluation(
            final IntegralIntervalPolynomialEvaluation evaluation, final Matrix a, final Matrix b, final int row)
            throws PolynomialEstimationException {

        final var order = evaluation.getIntegralOrder();
        final var constants = evaluation.getConstants();
        if (constants != null && constants.length != order) {
            throw new PolynomialEstimationException();
        }

        var accum = 0.0;
        var powStartX = 1.0;
        var powEndX = 1.0;
        final var startX = evaluation.getStartX();
        final var endX = evaluation.getEndX();
        for (var i = 0; i < order; i++) {
            if (constants != null) {
                var param = 1;
                for (var k = 1; k <= i; k++) {
                    param *= k;
                }
                accum += constants[i] / param * (powEndX - powStartX);
            }
            powStartX *= startX;
            powEndX *= endX;
        }

        for (int i = 0, j = order; i < a.getColumns(); i++, j++) {
            var param = j;
            for (var k = 1; k < order; k++) {
                param *= j - k;
            }
            a.setElementAt(row, i, (powEndX - powStartX) / param);
            powStartX *= startX;
            powEndX *= endX;
        }

        b.setElementAtIndex(row, evaluation.getEvaluation() - accum);
    }

    /**
     * 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.
     */
    protected void normalize(final Matrix a, final Matrix b, final int row) {
        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);

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

    /**
     * Internal method to set degree of polynomial to be estimated.
     *
     * @param degree degree of polynomial to be estimated.
     * @throws IllegalArgumentException if provided degree is less than 1.
     */
    private void internalSetDegree(final int degree) {
        if (degree < MIN_DEGREE) {
            throw new IllegalArgumentException("degree must be at least 1");
        }
        this.degree = degree;
    }
}