LineMultiOptimizer.java

/*
 * Copyright (C) 2012 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.optimization;

import com.irurueta.numerical.DirectionalEvaluator;
import com.irurueta.numerical.EvaluationException;
import com.irurueta.numerical.LockedException;
import com.irurueta.numerical.MultiDimensionFunctionEvaluatorListener;
import com.irurueta.numerical.NotAvailableException;
import com.irurueta.numerical.NumericalException;

/**
 * Abstract class to search for a local minimum on a multidimensional function
 * along a given line of input parameters.
 * Line minimization implementations exist such as Powell's method or conjugate
 * gradients. Among those, usually conjugate gradients provide faster
 * convergence because search directions during the estimation don't need to be
 * perpendicular.
 */
public abstract class LineMultiOptimizer extends MultiOptimizer {
    /**
     * n-dimensional point containing a minimum in a given line.
     */
    protected double[] p;

    /**
     * Direction to make the search.
     */
    protected double[] xi;

    /**
     * Number of dimensions on function being evaluated.
     */
    private int n;

    /**
     * Class in charge of evaluating a function through a given line.
     */
    private DirectionalEvaluator evaluator;

    /**
     * Internal optimizer to find a minimum of a function along a line of
     * input values. Hence, input is converted to a single dimension using a
     * DirectionalEvaluator.
     */
    private BrentSingleOptimizer brent;

    /**
     * Empty constructor.
     */
    protected LineMultiOptimizer() {
        super();
        p = xi = null;
        n = 0;
        evaluator = null;
        brent = null;
    }

    /**
     * Constructor.
     *
     * @param listener Listener to evaluate a multi-dimension function.
     */
    protected LineMultiOptimizer(final MultiDimensionFunctionEvaluatorListener listener) {
        super(listener);
        p = xi = null;
        n = 0;
        evaluator = null;
        brent = null;
    }

    /**
     * Constructor.
     *
     * @param listener  Listener to evaluate a multi-dimension function.
     * @param point     Start point where algorithm will be started. Start point
     *                  should be close to the local minimum to be found. Provided array must
     *                  have a length equal to the number of dimensions of the function being
     *                  evaluated, otherwise and exception will be raised when searching for the
     *                  minimum.
     * @param direction Direction to start looking for a minimum. Provided array
     *                  must have the same length as the number of dimensions of the function
     *                  being evaluated. Provided direction is considered as a vector pointing
     *                  to the minimum to be found.
     * @throws IllegalArgumentException Raised if provided point and direction
     *                                  don't have the same length.
     */
    protected LineMultiOptimizer(
            final MultiDimensionFunctionEvaluatorListener listener, final double[] point, final double[] direction) {
        super(listener);
        internalSetStartPointAndDirection(point, direction);
        n = 0;
        evaluator = null;
        brent = null;
    }

    /**
     * Returns boolean indicating whether start point has already been provided
     * and is ready for retrieval.
     *
     * @return True if available, false otherwise.
     */
    public boolean isStartPointAvailable() {
        return p != null;
    }

    /**
     * Returns start point where algorithm will be started. Start point should
     * be close to the local minimum to be found.
     *
     * @return Start point where algorithm will be started.
     * @throws NotAvailableException Raised if start point has not yet been
     *                               provided and is not available.
     */
    public double[] getStartPoint() throws NotAvailableException {
        if (!isStartPointAvailable()) {
            throw new NotAvailableException();
        }
        return p;
    }

    /**
     * Returns boolean indicating whether direction has already been provided
     * and is ready for retrieval.
     *
     * @return True if available, false otherwise.
     */
    public boolean isDirectionAvailable() {
        return xi != null;
    }

    /**
     * Returns direction to start looking for a minimum. Provided array must
     * have the same length as the number of dimensions of the function being
     * evaluated. Provided direction is considered as a vector pointing to the
     * minimum to be found.
     *
     * @return Direction to start looking for a minimum.
     * @throws NotAvailableException Raised if direction has not yet been
     *                               provided and is not available.
     */
    public double[] getDirection() throws NotAvailableException {
        if (!isDirectionAvailable()) {
            throw new NotAvailableException();
        }
        return xi;
    }

    /**
     * Internal method to set start point and direction to start the search for
     * a local minimum.
     *
     * @param point     Start point where algorithm will be started. Start point
     *                  should be close to the local minimum to be found. Provided array must
     *                  have a length equal to the number of dimensions of the function being
     *                  evaluated, otherwise and exception will be raised when searching for the
     *                  minimum.
     * @param direction Direction to start looking for a minimum. Provided array
     *                  must have the same length as the number of dimensions of the function
     *                  being evaluated. Provided direction is considered as a vector pointing
     *                  to the minimum to be found.
     * @throws LockedException          Raised if this instance is locked.
     * @throws IllegalArgumentException Raised if provided point and direction
     *                                  don't have the same length.
     */
    public void setStartPointAndDirection(final double[] point, final double[] direction) throws LockedException {
        if (isLocked()) {
            throw new LockedException();
        }
        internalSetStartPointAndDirection(point, direction);
    }

    /**
     * Returns boolean indicating whether this instance is considered to be
     * ready to start the estimation of a minimum.
     * This instance is considered to be ready once a listener, start point and
     * direction are provided.
     *
     * @return True if this instance is ready, false otherwise.
     */
    @Override
    public boolean isReady() {
        return isListenerAvailable() && isStartPointAvailable() && isDirectionAvailable();
    }

    /**
     * Searches for a minimum along a given line of input values.
     * The line being searched is obtained by using a start point and direction.
     *
     * @return Returns function evaluation at minimum that has been found.
     */
    @SuppressWarnings("Duplicates")
    protected double linmin() {
        final double ax;
        final double xx;
        final double linxmin;
        n = p.length;

        if (evaluator == null) {
            // attempt to reuse evaluator
            evaluator = new DirectionalEvaluator(listener, p, xi);
        }
        if (evaluator.getListener() != listener) {
            // update listener
            evaluator.setListener(listener);
        }
        if (evaluator.getPoint() != p || evaluator.getDirection() != xi) {
            evaluator.setPointAndDirection(p, xi);
        }

        ax = 0.0;
        xx = 1.0;

        try {
            if (brent == null) {
                // attempt to reuse brent single optimizer
                brent = new BrentSingleOptimizer(
                        point -> evaluator.evaluateAt(point),
                        BracketedSingleOptimizer.DEFAULT_MIN_EVAL_POINT,
                        BracketedSingleOptimizer.DEFAULT_MIDDLE_EVAL_POINT,
                        BracketedSingleOptimizer.DEFAULT_MAX_EVAL_POINT,
                        BrentSingleOptimizer.DEFAULT_TOLERANCE);
            }

            brent.computeBracket(ax, xx);
            brent.minimize();
            linxmin = brent.getResult();

            for (var j = 0; j < n; j++) {
                xi[j] *= linxmin;
                p[j] += xi[j];
            }

            return brent.getEvaluationAtResult();
        } catch (final NumericalException e) {
            // if minimization fails, try to evaluate at best point found so far
            try {
                return listener.evaluate(p);
            } catch (EvaluationException e2) {
                // if minimization fails here we assume that obtained result is
                // the worst possible one
                return Double.MAX_VALUE;
            }
        }
    }

    /**
     * Internal method to set start point and direction to start the search for
     * a local minimum.
     * This method does not check whether this instance is locked.
     *
     * @param point     Start point where algorithm will be started. Start point
     *                  should be close to the local minimum to be found. Provided array must
     *                  have a length equal to the number of dimensions of the function being
     *                  evaluated, otherwise and exception will be raised when searching for the
     *                  minimum.
     * @param direction Direction to start looking for a minimum. Provided array
     *                  must have the same length as the number of dimensions of the function
     *                  being evaluated. Provided direction is considered as a vector pointing
     *                  to the minimum to be found.
     * @throws IllegalArgumentException Raised if provided point and direction
     *                                  don't have the same length.
     */
    private void internalSetStartPointAndDirection(final double[] point, final double[] direction) {
        if (point.length != direction.length) {
            throw new IllegalArgumentException();
        }
        p = point;
        xi = direction;
    }
}