From bb201d926ebf225439d0007e7015365a9fbc3578 Mon Sep 17 00:00:00 2001 From: Luc Maisonobe Date: Thu, 10 Nov 2011 20:08:55 +0000 Subject: [PATCH] Added adapters for simple bounds constraints optimization. The adapters are useful only for optimizers that do not support simple bounds constraints by themselves (i.e. Nelder-Mead and Torczon's multidirectional). Two adapters are available, one performs a mapping between the whole real range and the bounded range (bounds being set component wise), and one uses a penalty function. JIRA: MATH-196 git-svn-id: https://svn.apache.org/repos/asf/commons/proper/math/trunk@1200516 13f79535-47bb-0310-9956-ffa450edef68 --- ...ultivariateRealFunctionMappingAdapter.java | 300 ++++++++++++++++++ ...ultivariateRealFunctionPenaltyAdapter.java | 188 +++++++++++ .../optimization/direct/SimplexOptimizer.java | 12 + src/site/xdoc/changes.xml | 5 + src/site/xdoc/userguide/optimization.xml | 44 ++- ...variateRealFunctionMappingAdapterTest.java | 190 +++++++++++ ...variateRealFunctionPenaltyAdapterTest.java | 192 +++++++++++ 7 files changed, 926 insertions(+), 5 deletions(-) create mode 100644 src/main/java/org/apache/commons/math/optimization/direct/MultivariateRealFunctionMappingAdapter.java create mode 100644 src/main/java/org/apache/commons/math/optimization/direct/MultivariateRealFunctionPenaltyAdapter.java create mode 100644 src/test/java/org/apache/commons/math/optimization/direct/MultivariateRealFunctionMappingAdapterTest.java create mode 100644 src/test/java/org/apache/commons/math/optimization/direct/MultivariateRealFunctionPenaltyAdapterTest.java diff --git a/src/main/java/org/apache/commons/math/optimization/direct/MultivariateRealFunctionMappingAdapter.java b/src/main/java/org/apache/commons/math/optimization/direct/MultivariateRealFunctionMappingAdapter.java new file mode 100644 index 000000000..911da318f --- /dev/null +++ b/src/main/java/org/apache/commons/math/optimization/direct/MultivariateRealFunctionMappingAdapter.java @@ -0,0 +1,300 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.commons.math.optimization.direct; + +import org.apache.commons.math.analysis.MultivariateRealFunction; +import org.apache.commons.math.analysis.UnivariateRealFunction; +import org.apache.commons.math.analysis.function.Logit; +import org.apache.commons.math.analysis.function.Sigmoid; +import org.apache.commons.math.exception.DimensionMismatchException; +import org.apache.commons.math.exception.MathIllegalArgumentException; +import org.apache.commons.math.exception.NumberIsTooSmallException; +import org.apache.commons.math.util.FastMath; +import org.apache.commons.math.util.MathUtils; + +/** + *

Adapter for mapping bounded {@link MultivariateRealFunction} to unbounded ones.

+ * + *

+ * This adapter can be used to wrap functions subject to simple bounds on + * parameters so they can be used by optimizers that do not directly + * support simple bounds. + *

+ *

+ * The principle is that the user function that will be wrapped will see its + * parameters bounded as required, i.e when its {@code value} method is called + * with argument array {@code point}, the elements array will fulfill requirement + * {@code lower[i] <= point[i] <= upper[i]} for all i. Some of the components + * may be unbounded or bounded only on one side if the corresponding bound is + * set to an infinite value. The optimizer will not manage the user function by + * itself, but it will handle this adapter and it is this adapter that will take + * care the bounds are fulfilled. The adapter {@link #value(double[])} method will + * be called by the optimizer with unbound parameters, and the adapter will map + * the unbounded value to the bounded range using appropriate functions like + * {@link Sigmoid} for double bounded elements for example. + *

+ *

+ * As the optimizer sees only unbounded parameters, it should be noted that the + * start point or simplex expected by the optimizer should be unbounded, so the + * user is responsible for converting his bounded point to unbounded by calling + * {@link #boundedToUnbounded(double[])} before providing them to the optimizer. + * For the same reason, the point returned by the {@link + * org.apache.commons.math.optimization.BaseMultivariateRealOptimizer#optimize(int, + * MultivariateRealFunction, org.apache.commons.math.optimization.GoalType, double[])} + * method is unbounded. So to convert this point to bounded, users must call + * {@link #unboundedToBounded(double[])} by themselves!

+ *

+ * This adapter is only a poor man solution to simple bounds optimization constraints + * that can be used with simple optimizers like {@link SimplexOptimizer} with {@link + * NelderMeadSimplex} or {@link MultiDirectionalSimplex}. A better solution is to use + * an optimizer that directly supports simple bounds like {@link CMAESOptimizer} or + * {@link BOBYQAOptimizer}. One caveat of this poor man solution is that behavior near + * the bounds may be numerically unstable as bounds are mapped from infinite values. + * Another caveat is that convergence values are evaluated by the optimizer with respect + * to unbounded variables, so there will be scales differences when converted to bounded + * variables. + *

+ * + * @see MultivariateRealFunctionPenaltyAdapter + * + * @version $Id$ + * @since 3.0 + */ + +public class MultivariateRealFunctionMappingAdapter implements MultivariateRealFunction { + + /** Underlying bounded function. */ + private final MultivariateRealFunction bounded; + + /** Mapping functions. */ + private final Mapper[] mappers; + + /** Simple constructor. + * @param bounded bounded function + * @param lower lower bounds for each element of the input parameters array + * (some elements may be set to {@code Double.NEGATIVE_INFINITY} for + * unbounded values) + * @param upper upper bounds for each element of the input parameters array + * (some elements may be set to {@code Double.POSITIVE_INFINITY} for + * unbounded values) + * @exception MathIllegalArgumentException if lower and upper bounds are not + * consistent, either according to dimension or to values + */ + public MultivariateRealFunctionMappingAdapter(final MultivariateRealFunction bounded, + final double[] lower, final double[] upper) { + + // safety checks + MathUtils.checkNotNull(lower); + MathUtils.checkNotNull(upper); + if (lower.length != upper.length) { + throw new DimensionMismatchException(lower.length, upper.length); + } + for (int i = 0; i < lower.length; ++i) { + // note the following test is written in such a way it also fails for NaN + if (!(upper[i] >= lower[i])) { + throw new NumberIsTooSmallException(upper[i], lower[i], true); + } + } + + this.bounded = bounded; + this.mappers = new Mapper[lower.length]; + for (int i = 0; i < mappers.length; ++i) { + if (Double.isInfinite(lower[i])) { + if (Double.isInfinite(upper[i])) { + // element is unbounded, no transformation is needed + mappers[i] = new NoBoundsMapper(); + } else { + // element is simple-bounded on the upper side + mappers[i] = new UpperBoundMapper(upper[i]); + } + } else { + if (Double.isInfinite(upper[i])) { + // element is simple-bounded on the lower side + mappers[i] = new LowerBoundMapper(lower[i]); + } else { + // element is double-bounded + mappers[i] = new LowerUpperBoundMapper(lower[i], upper[i]); + } + } + } + + } + + /** Map an array from unbounded to bounded. + * @param x unbounded value + * @return bounded value + */ + public double[] unboundedToBounded(double[] point) { + + // map unbounded input point to bounded point + final double[] mapped = new double[mappers.length]; + for (int i = 0; i < mappers.length; ++i) { + mapped[i] = mappers[i].unboundedToBounded(point[i]); + } + + return mapped; + + } + + /** Map an array from bounded to unbounded. + * @param y bounded value + * @return unbounded value + */ + public double[] boundedToUnbounded(double[] point) { + + // map bounded input point to unbounded point + final double[] mapped = new double[mappers.length]; + for (int i = 0; i < mappers.length; ++i) { + mapped[i] = mappers[i].boundedToUnbounded(point[i]); + } + + // call underlying function + return mapped; + + } + + /** Compute the underlying function value from an unbounded point. + *

+ * This method simply bounds the unbounded point using the mappings + * set up at construction and calls the underlying function using + * the bounded point. + *

+ * @see #unboundedToBounded(double[]) + */ + public double value(double[] point) { + return bounded.value(unboundedToBounded(point)); + } + + /** Mapping interface. */ + private static interface Mapper { + + /** Map a value from unbounded to bounded. + * @param y unbounded value + * @return bounded value + */ + public double unboundedToBounded(double y); + + /** Map a value from bounded to unbounded. + * @param x bounded value + * @return unbounded value + */ + public double boundedToUnbounded(double x); + + } + + /** Local class for no bounds mapping. */ + private static class NoBoundsMapper implements Mapper { + + /** Simple constructor. + */ + public NoBoundsMapper() { + } + + /** {@inheritDoc} */ + public double unboundedToBounded(final double y) { + return y; + } + + /** {@inheritDoc} */ + public double boundedToUnbounded(final double x) { + return x; + } + + } + + /** Local class for lower bounds mapping. */ + private static class LowerBoundMapper implements Mapper { + + /** Low bound. */ + private final double lower; + + /** Simple constructor. + * @param lower lower bound + */ + public LowerBoundMapper(final double lower) { + this.lower = lower; + } + + /** {@inheritDoc} */ + public double unboundedToBounded(final double y) { + return lower + FastMath.exp(y); + } + + /** {@inheritDoc} */ + public double boundedToUnbounded(final double x) { + return FastMath.log(x - lower); + } + + } + + /** Local class for upper bounds mapping. */ + private static class UpperBoundMapper implements Mapper { + + /** Upper bound. */ + private final double upper; + + /** Simple constructor. + * @param upper upper bound + */ + public UpperBoundMapper(final double upper) { + this.upper = upper; + } + + /** {@inheritDoc} */ + public double unboundedToBounded(final double y) { + return upper - FastMath.exp(-y); + } + + /** {@inheritDoc} */ + public double boundedToUnbounded(final double x) { + return -FastMath.log(upper - x); + } + + } + + /** Local class for lower and bounds mapping. */ + private static class LowerUpperBoundMapper implements Mapper { + + /** Function from unbounded to bounded. */ + private final UnivariateRealFunction boundingFunction; + + /** Function from bounded to unbounded. */ + private final UnivariateRealFunction unboundingFunction; + + /** Simple constructor. + * @param lower lower bound + * @param upper upper bound + */ + public LowerUpperBoundMapper(final double lower, final double upper) { + boundingFunction = new Sigmoid(lower, upper); + unboundingFunction = new Logit(lower, upper); + } + + /** {@inheritDoc} */ + public double unboundedToBounded(final double y) { + return boundingFunction.value(y); + } + + /** {@inheritDoc} */ + public double boundedToUnbounded(final double x) { + return unboundingFunction.value(x); + } + + } + +} diff --git a/src/main/java/org/apache/commons/math/optimization/direct/MultivariateRealFunctionPenaltyAdapter.java b/src/main/java/org/apache/commons/math/optimization/direct/MultivariateRealFunctionPenaltyAdapter.java new file mode 100644 index 000000000..56429b494 --- /dev/null +++ b/src/main/java/org/apache/commons/math/optimization/direct/MultivariateRealFunctionPenaltyAdapter.java @@ -0,0 +1,188 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.commons.math.optimization.direct; + +import org.apache.commons.math.analysis.MultivariateRealFunction; +import org.apache.commons.math.exception.DimensionMismatchException; +import org.apache.commons.math.exception.MathIllegalArgumentException; +import org.apache.commons.math.exception.NumberIsTooSmallException; +import org.apache.commons.math.util.FastMath; +import org.apache.commons.math.util.MathUtils; + +/** + *

Adapter extending bounded {@link MultivariateRealFunction} to an unbouded + * domain using a penalty function.

+ * + *

+ * This adapter can be used to wrap functions subject to simple bounds on + * parameters so they can be used by optimizers that do not directly + * support simple bounds. + *

+ *

+ * The principle is that the user function that will be wrapped will see its + * parameters bounded as required, i.e when its {@code value} method is called + * with argument array {@code point}, the elements array will fulfill requirement + * {@code lower[i] <= point[i] <= upper[i]} for all i. Some of the components + * may be unbounded or bounded only on one side if the corresponding bound is + * set to an infinite value. The optimizer will not manage the user function by + * itself, but it will handle this adapter and it is this adapter that will take + * care the bounds are fulfilled. The adapter {@link #value(double[])} method will + * be called by the optimizer with unbound parameters, and the adapter will check + * if the parameters is within range or not. If it is in range, then the underlying + * user function will be called, and if it is not the value of a penalty function + * will be returned instead. + *

+ *

+ * This adapter is only a poor man solution to simple bounds optimization constraints + * that can be used with simple optimizers like {@link SimplexOptimizer} with {@link + * NelderMeadSimplex} or {@link MultiDirectionalSimplex}. A better solution is to use + * an optimizer that directly supports simple bounds like {@link CMAESOptimizer} or + * {@link BOBYQAOptimizer}. One caveat of this poor man solution is that if start point + * or start simplex is completely outside of the allowed range, only the penalty function + * is used, and the optimizer may converge without ever entering the range. + *

+ * + * @see MultivariateRealFunctionMappingAdapter + * + * @version $Id$ + * @since 3.0 + */ + +public class MultivariateRealFunctionPenaltyAdapter implements MultivariateRealFunction { + + /** Underlying bounded function. */ + private final MultivariateRealFunction bounded; + + /** Lower bounds. */ + private final double[] lower; + + /** Upper bounds. */ + private final double[] upper; + + /** Penalty offset. */ + private final double offset; + + /** Penalty scales. */ + private final double[] scale; + + /** Simple constructor. + *

+ * When the optimizer provided points are out of range, the value of the + * penalty function will be used instead of the value of the underlying + * function. In order for this penalty to be effective in rejecting this + * point during the optimization process, the penalty function value should + * be defined with care. This value is computed as: + *

+     *   penalty(point) = offset + ∑i[scale[i] * √|point[i]-boundary[i]|]
+     * 
+ * where indices i correspond to all the components that violates their boundaries. + *

+ *

+ * So when attempting a function minimization, offset should be larger than + * the maximum expected value of the underlying function and scale components + * should all be positive. When attempting a function maximization, offset + * should be lesser than the minimum expected value of the underlying function + * and scale components should all be negative. + * minimization, and lesser than the minimum expected value of the underlying + * function when attempting maximization. + *

+ *

+ * These choices for the penalty function have two properties. First, all out + * of range points will return a function value that is worse than the value + * returned by any in range point. Second, the penalty is worse for large + * boundaries violation than for small violations, so the optimizer has an hint + * about the direction in which it should search for acceptable points. + *

+ * @param bounded bounded function + * @param lower lower bounds for each element of the input parameters array + * (some elements may be set to {@code Double.NEGATIVE_INFINITY} for + * unbounded values) + * @param upper upper bounds for each element of the input parameters array + * (some elements may be set to {@code Double.POSITIVE_INFINITY} for + * unbounded values) + * @param offset base offset of the penalty function + * @param scale scale of the penalty function + * @exception MathIllegalArgumentException if lower bounds, upper bounds and + * scales are not consistent, either according to dimension or to bounadary + * values + */ + public MultivariateRealFunctionPenaltyAdapter(final MultivariateRealFunction bounded, + final double[] lower, final double[] upper, + final double offset, final double[] scale) { + + // safety checks + MathUtils.checkNotNull(lower); + MathUtils.checkNotNull(upper); + MathUtils.checkNotNull(scale); + if (lower.length != upper.length) { + throw new DimensionMismatchException(lower.length, upper.length); + } + if (lower.length != scale.length) { + throw new DimensionMismatchException(lower.length, scale.length); + } + for (int i = 0; i < lower.length; ++i) { + // note the following test is written in such a way it also fails for NaN + if (!(upper[i] >= lower[i])) { + throw new NumberIsTooSmallException(upper[i], lower[i], true); + } + } + + this.bounded = bounded; + this.lower = lower.clone(); + this.upper = upper.clone(); + this.offset = offset; + this.scale = scale.clone(); + + } + + /** Compute the underlying function value from an unbounded point. + *

+ * This method simply bounds the unbounded point using the mappings + * set up at construction and calls the underlying function using + * the bounded point. + *

+ * @see #unboundedToBounded(double[]) + */ + public double value(double[] point) { + + for (int i = 0; i < scale.length; ++i) { + if ((point[i] < lower[i]) || (point[i] > upper[i])) { + // bound violation starting at this component + double sum = 0; + for (int j = i; j < scale.length; ++j) { + final double overshoot; + if (point[j] < lower[j]) { + overshoot = lower[j] - point[j]; + } else if (point[j] > upper[j]) { + overshoot = point[j] - upper[j]; + } else { + overshoot = 0; + } + sum += FastMath.sqrt(overshoot); + } + return offset + sum; + } + } + + // all boundaries are fulfilled, we are in the expected + // domain of the underlying function + return bounded.value(point); + + } + +} diff --git a/src/main/java/org/apache/commons/math/optimization/direct/SimplexOptimizer.java b/src/main/java/org/apache/commons/math/optimization/direct/SimplexOptimizer.java index 1143b1978..d8c37eea7 100644 --- a/src/main/java/org/apache/commons/math/optimization/direct/SimplexOptimizer.java +++ b/src/main/java/org/apache/commons/math/optimization/direct/SimplexOptimizer.java @@ -66,8 +66,20 @@ import org.apache.commons.math.optimization.MultivariateRealOptimizer; * previous and current simplex to the convergence checker, not the best * ones. *

+ *

+ * This simplex optimizer implementation does not directly support constrained + * optimization with simple bounds, so for such optimizations, either a more + * dedicated method must be used like {@link CMAESOptimizer} or {@link + * BOBYQAOptimizer}, or the optimized method must be wrapped in an adapter like + * {@link MultivariateRealFunctionMappingAdapter} or {@link + * MultivariateRealFunctionPenaltyAdapter}. + *

* * @see AbstractSimplex + * @see MultivariateRealFunctionMappingAdapter + * @see MultivariateRealFunctionPenaltyAdapter + * @see CMAESOptimizer + * @see BOBYQAOptimizer * @version $Id$ * @since 3.0 */ diff --git a/src/site/xdoc/changes.xml b/src/site/xdoc/changes.xml index 27e4e48e6..61fb88757 100644 --- a/src/site/xdoc/changes.xml +++ b/src/site/xdoc/changes.xml @@ -52,6 +52,11 @@ The type attribute can be add,update,fix,remove. If the output is not quite correct, check for invisible trailing spaces! --> + + Added adapters for simple bound constraints optimization that can be + used for all direct optimization methods, including the ones that do not + support constraints by themselves. + CMA-ES optimizer input sigma is now consistent with boundaries range units. diff --git a/src/site/xdoc/userguide/optimization.xml b/src/site/xdoc/userguide/optimization.xml index b0f3ef390..83189db20 100644 --- a/src/site/xdoc/userguide/optimization.xml +++ b/src/site/xdoc/userguide/optimization.xml @@ -154,11 +154,45 @@ already provided by the minimizes method).

- The direct package provides two solvers. The first one is the classical - - Nelder-Mead method. The second one is Virginia Torczon's - - multi-directional method. + The direct package provides four solvers: +

+

+

+ The first two simplex-based methods do not handle simple bounds constraints by themselves. + However there are two adapters ( + MultivariateRealFunctionMappingAdapter and + MultivariateRealFunctionPenaltyAdapter) that can be used to wrap the user function in + such a way the wrapped function is unbounded and can be used with these optimizers, despite + the fact the underlying function is still bounded and will be called only with feasible + points that fulfill the constraints. Note however that using these adapters is only a + apter is only a poor man solution to simple bounds optimization constraints. A better solution + is to use an optimizer that directly supports simple bounds. Two caveats of the mapping adapter + solution is that behavior near the bounds may be numerically unstable as bounds are mapped from + infinite values, and that convergence values are evaluated by the optimizer with respect + to unbounded variables, so there will be scales differences when converted to bounded variables. + One caveat of penalty adapter is that if start point or start simplex is outside of the allowed + range, only the penalty function is used, and the optimizer may converge without ever entering + the alowed range. +

+

+ The last methods do handle simple bounds constraints directly, so the adapters are not needed + with them.

diff --git a/src/test/java/org/apache/commons/math/optimization/direct/MultivariateRealFunctionMappingAdapterTest.java b/src/test/java/org/apache/commons/math/optimization/direct/MultivariateRealFunctionMappingAdapterTest.java new file mode 100644 index 000000000..1b008978a --- /dev/null +++ b/src/test/java/org/apache/commons/math/optimization/direct/MultivariateRealFunctionMappingAdapterTest.java @@ -0,0 +1,190 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.commons.math.optimization.direct; + + +import org.apache.commons.math.analysis.MultivariateRealFunction; +import org.apache.commons.math.optimization.GoalType; +import org.apache.commons.math.optimization.RealPointValuePair; +import org.junit.Assert; +import org.junit.Test; + +public class MultivariateRealFunctionMappingAdapterTest { + + @Test + public void testStartSimplexInsideRange() { + + final BiQuadratic biQuadratic = new BiQuadratic(2.0, 2.5, 1.0, 3.0, 2.0, 3.0); + final MultivariateRealFunctionMappingAdapter wrapped = + new MultivariateRealFunctionMappingAdapter(biQuadratic, + biQuadratic.getLower(), + biQuadratic.getUpper()); + + SimplexOptimizer optimizer = new SimplexOptimizer(1e-10, 1e-30); + optimizer.setSimplex(new NelderMeadSimplex(new double[][] { + wrapped.boundedToUnbounded(new double[] { 1.5, 2.75 }), + wrapped.boundedToUnbounded(new double[] { 1.5, 2.95 }), + wrapped.boundedToUnbounded(new double[] { 1.7, 2.90 }) + })); + + final RealPointValuePair optimum + = optimizer.optimize(300, wrapped, GoalType.MINIMIZE, + wrapped.boundedToUnbounded(new double[] { 1.5, 2.25 })); + final double[] bounded = wrapped.unboundedToBounded(optimum.getPoint()); + + Assert.assertEquals(biQuadratic.getBoundedXOptimum(), bounded[0], 2e-7); + Assert.assertEquals(biQuadratic.getBoundedYOptimum(), bounded[1], 2e-7); + + } + + @Test + public void testOptimumOutsideRange() { + + final BiQuadratic biQuadratic = new BiQuadratic(4.0, 0.0, 1.0, 3.0, 2.0, 3.0); + final MultivariateRealFunctionMappingAdapter wrapped = + new MultivariateRealFunctionMappingAdapter(biQuadratic, + biQuadratic.getLower(), + biQuadratic.getUpper()); + + SimplexOptimizer optimizer = new SimplexOptimizer(1e-10, 1e-30); + optimizer.setSimplex(new NelderMeadSimplex(new double[][] { + wrapped.boundedToUnbounded(new double[] { 1.5, 2.75 }), + wrapped.boundedToUnbounded(new double[] { 1.5, 2.95 }), + wrapped.boundedToUnbounded(new double[] { 1.7, 2.90 }) + })); + + final RealPointValuePair optimum + = optimizer.optimize(100, wrapped, GoalType.MINIMIZE, + wrapped.boundedToUnbounded(new double[] { 1.5, 2.25 })); + final double[] bounded = wrapped.unboundedToBounded(optimum.getPoint()); + + Assert.assertEquals(biQuadratic.getBoundedXOptimum(), bounded[0], 2e-7); + Assert.assertEquals(biQuadratic.getBoundedYOptimum(), bounded[1], 2e-7); + + } + + @Test + public void testUnbounded() { + + final BiQuadratic biQuadratic = new BiQuadratic(4.0, 0.0, + Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, + Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY); + final MultivariateRealFunctionMappingAdapter wrapped = + new MultivariateRealFunctionMappingAdapter(biQuadratic, + biQuadratic.getLower(), + biQuadratic.getUpper()); + + SimplexOptimizer optimizer = new SimplexOptimizer(1e-10, 1e-30); + optimizer.setSimplex(new NelderMeadSimplex(new double[][] { + wrapped.boundedToUnbounded(new double[] { 1.5, 2.75 }), + wrapped.boundedToUnbounded(new double[] { 1.5, 2.95 }), + wrapped.boundedToUnbounded(new double[] { 1.7, 2.90 }) + })); + + final RealPointValuePair optimum + = optimizer.optimize(300, wrapped, GoalType.MINIMIZE, + wrapped.boundedToUnbounded(new double[] { 1.5, 2.25 })); + final double[] bounded = wrapped.unboundedToBounded(optimum.getPoint()); + + Assert.assertEquals(biQuadratic.getBoundedXOptimum(), bounded[0], 2e-7); + Assert.assertEquals(biQuadratic.getBoundedYOptimum(), bounded[1], 2e-7); + + } + + @Test + public void testHalfBounded() { + + final BiQuadratic biQuadratic = new BiQuadratic(4.0, 4.0, + 1.0, Double.POSITIVE_INFINITY, + Double.NEGATIVE_INFINITY, 3.0); + final MultivariateRealFunctionMappingAdapter wrapped = + new MultivariateRealFunctionMappingAdapter(biQuadratic, + biQuadratic.getLower(), + biQuadratic.getUpper()); + + SimplexOptimizer optimizer = new SimplexOptimizer(1e-13, 1e-30); + optimizer.setSimplex(new NelderMeadSimplex(new double[][] { + wrapped.boundedToUnbounded(new double[] { 1.5, 2.75 }), + wrapped.boundedToUnbounded(new double[] { 1.5, 2.95 }), + wrapped.boundedToUnbounded(new double[] { 1.7, 2.90 }) + })); + + final RealPointValuePair optimum + = optimizer.optimize(200, wrapped, GoalType.MINIMIZE, + wrapped.boundedToUnbounded(new double[] { 1.5, 2.25 })); + final double[] bounded = wrapped.unboundedToBounded(optimum.getPoint()); + + Assert.assertEquals(biQuadratic.getBoundedXOptimum(), bounded[0], 1e-7); + Assert.assertEquals(biQuadratic.getBoundedYOptimum(), bounded[1], 1e-7); + + } + + private static class BiQuadratic implements MultivariateRealFunction { + + private final double xOptimum; + private final double yOptimum; + + private final double xMin; + private final double xMax; + private final double yMin; + private final double yMax; + + public BiQuadratic(final double xOptimum, final double yOptimum, + final double xMin, final double xMax, + final double yMin, final double yMax) { + this.xOptimum = xOptimum; + this.yOptimum = yOptimum; + this.xMin = xMin; + this.xMax = xMax; + this.yMin = yMin; + this.yMax = yMax; + } + + public double value(double[] point) { + + // the function should never be called with out of range points + Assert.assertTrue(point[0] >= xMin); + Assert.assertTrue(point[0] <= xMax); + Assert.assertTrue(point[1] >= yMin); + Assert.assertTrue(point[1] <= yMax); + + final double dx = point[0] - xOptimum; + final double dy = point[1] - yOptimum; + return dx * dx + dy * dy; + + } + + public double[] getLower() { + return new double[] { xMin, yMin }; + } + + public double[] getUpper() { + return new double[] { xMax, yMax }; + } + + public double getBoundedXOptimum() { + return (xOptimum < xMin) ? xMin : ((xOptimum > xMax) ? xMax : xOptimum); + } + + public double getBoundedYOptimum() { + return (yOptimum < yMin) ? yMin : ((yOptimum > yMax) ? yMax : yOptimum); + } + + } + +} diff --git a/src/test/java/org/apache/commons/math/optimization/direct/MultivariateRealFunctionPenaltyAdapterTest.java b/src/test/java/org/apache/commons/math/optimization/direct/MultivariateRealFunctionPenaltyAdapterTest.java new file mode 100644 index 000000000..c1a7a1458 --- /dev/null +++ b/src/test/java/org/apache/commons/math/optimization/direct/MultivariateRealFunctionPenaltyAdapterTest.java @@ -0,0 +1,192 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.commons.math.optimization.direct; + + +import org.apache.commons.math.analysis.MultivariateRealFunction; +import org.apache.commons.math.optimization.GoalType; +import org.apache.commons.math.optimization.RealPointValuePair; +import org.apache.commons.math.optimization.SimpleRealPointChecker; +import org.junit.Assert; +import org.junit.Test; + +public class MultivariateRealFunctionPenaltyAdapterTest { + + @Test + public void testStartSimplexInsideRange() { + + final BiQuadratic biQuadratic = new BiQuadratic(2.0, 2.5, 1.0, 3.0, 2.0, 3.0); + final MultivariateRealFunctionPenaltyAdapter wrapped = + new MultivariateRealFunctionPenaltyAdapter(biQuadratic, + biQuadratic.getLower(), + biQuadratic.getUpper(), + 1000.0, new double[] { 100.0, 100.0 }); + + SimplexOptimizer optimizer = new SimplexOptimizer(1e-10, 1e-30); + optimizer.setSimplex(new NelderMeadSimplex(new double[] { 1.0, 0.5 })); + + final RealPointValuePair optimum + = optimizer.optimize(300, wrapped, GoalType.MINIMIZE, new double[] { 1.5, 2.25 }); + + Assert.assertEquals(biQuadratic.getBoundedXOptimum(), optimum.getPoint()[0], 2e-7); + Assert.assertEquals(biQuadratic.getBoundedYOptimum(), optimum.getPoint()[1], 2e-7); + + } + + @Test + public void testStartSimplexOutsideRange() { + + final BiQuadratic biQuadratic = new BiQuadratic(2.0, 2.5, 1.0, 3.0, 2.0, 3.0); + final MultivariateRealFunctionPenaltyAdapter wrapped = + new MultivariateRealFunctionPenaltyAdapter(biQuadratic, + biQuadratic.getLower(), + biQuadratic.getUpper(), + 1000.0, new double[] { 100.0, 100.0 }); + + SimplexOptimizer optimizer = new SimplexOptimizer(1e-10, 1e-30); + optimizer.setSimplex(new NelderMeadSimplex(new double[] { 1.0, 0.5 })); + + final RealPointValuePair optimum + = optimizer.optimize(300, wrapped, GoalType.MINIMIZE, new double[] { -1.5, 4.0 }); + + Assert.assertEquals(biQuadratic.getBoundedXOptimum(), optimum.getPoint()[0], 2e-7); + Assert.assertEquals(biQuadratic.getBoundedYOptimum(), optimum.getPoint()[1], 2e-7); + + } + + @Test + public void testOptimumOutsideRange() { + + final BiQuadratic biQuadratic = new BiQuadratic(4.0, 0.0, 1.0, 3.0, 2.0, 3.0); + final MultivariateRealFunctionPenaltyAdapter wrapped = + new MultivariateRealFunctionPenaltyAdapter(biQuadratic, + biQuadratic.getLower(), + biQuadratic.getUpper(), + 1000.0, new double[] { 100.0, 100.0 }); + + SimplexOptimizer optimizer = new SimplexOptimizer(new SimpleRealPointChecker(1.0e-11, 1.0e-20)); + optimizer.setSimplex(new NelderMeadSimplex(new double[] { 1.0, 0.5 })); + + final RealPointValuePair optimum + = optimizer.optimize(600, wrapped, GoalType.MINIMIZE, new double[] { -1.5, 4.0 }); + + Assert.assertEquals(biQuadratic.getBoundedXOptimum(), optimum.getPoint()[0], 2e-7); + Assert.assertEquals(biQuadratic.getBoundedYOptimum(), optimum.getPoint()[1], 2e-7); + + } + + @Test + public void testUnbounded() { + + final BiQuadratic biQuadratic = new BiQuadratic(4.0, 0.0, + Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, + Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY); + final MultivariateRealFunctionPenaltyAdapter wrapped = + new MultivariateRealFunctionPenaltyAdapter(biQuadratic, + biQuadratic.getLower(), + biQuadratic.getUpper(), + 1000.0, new double[] { 100.0, 100.0 }); + + SimplexOptimizer optimizer = new SimplexOptimizer(1e-10, 1e-30); + optimizer.setSimplex(new NelderMeadSimplex(new double[] { 1.0, 0.5 })); + + final RealPointValuePair optimum + = optimizer.optimize(300, wrapped, GoalType.MINIMIZE, new double[] { -1.5, 4.0 }); + + Assert.assertEquals(biQuadratic.getBoundedXOptimum(), optimum.getPoint()[0], 2e-7); + Assert.assertEquals(biQuadratic.getBoundedYOptimum(), optimum.getPoint()[1], 2e-7); + + } + + @Test + public void testHalfBounded() { + + final BiQuadratic biQuadratic = new BiQuadratic(4.0, 4.0, + 1.0, Double.POSITIVE_INFINITY, + Double.NEGATIVE_INFINITY, 3.0); + final MultivariateRealFunctionPenaltyAdapter wrapped = + new MultivariateRealFunctionPenaltyAdapter(biQuadratic, + biQuadratic.getLower(), + biQuadratic.getUpper(), + 1000.0, new double[] { 100.0, 100.0 }); + + SimplexOptimizer optimizer = new SimplexOptimizer(new SimpleRealPointChecker(1.0e-10, 1.0e-20)); + optimizer.setSimplex(new NelderMeadSimplex(new double[] { 1.0, 0.5 })); + + final RealPointValuePair optimum + = optimizer.optimize(400, wrapped, GoalType.MINIMIZE, new double[] { -1.5, 4.0 }); + + Assert.assertEquals(biQuadratic.getBoundedXOptimum(), optimum.getPoint()[0], 2e-7); + Assert.assertEquals(biQuadratic.getBoundedYOptimum(), optimum.getPoint()[1], 2e-7); + + } + + private static class BiQuadratic implements MultivariateRealFunction { + + private final double xOptimum; + private final double yOptimum; + + private final double xMin; + private final double xMax; + private final double yMin; + private final double yMax; + + public BiQuadratic(final double xOptimum, final double yOptimum, + final double xMin, final double xMax, + final double yMin, final double yMax) { + this.xOptimum = xOptimum; + this.yOptimum = yOptimum; + this.xMin = xMin; + this.xMax = xMax; + this.yMin = yMin; + this.yMax = yMax; + } + + public double value(double[] point) { + + // the function should never be called with out of range points + Assert.assertTrue(point[0] >= xMin); + Assert.assertTrue(point[0] <= xMax); + Assert.assertTrue(point[1] >= yMin); + Assert.assertTrue(point[1] <= yMax); + + final double dx = point[0] - xOptimum; + final double dy = point[1] - yOptimum; + return dx * dx + dy * dy; + + } + + public double[] getLower() { + return new double[] { xMin, yMin }; + } + + public double[] getUpper() { + return new double[] { xMax, yMax }; + } + + public double getBoundedXOptimum() { + return (xOptimum < xMin) ? xMin : ((xOptimum > xMax) ? xMax : xOptimum); + } + + public double getBoundedYOptimum() { + return (yOptimum < yMin) ? yMin : ((yOptimum > yMax) ? yMax : yOptimum); + } + + } + +}