[MATH-970] Added SolutionCallback to SimplexSolver to retrieve the best solution found.

git-svn-id: https://svn.apache.org/repos/asf/commons/proper/math/trunk@1543169 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Thomas Neidhart 2013-11-18 21:24:32 +00:00
parent 3a45bc5b6d
commit 54a7796ff2
4 changed files with 227 additions and 10 deletions

View File

@ -51,6 +51,12 @@ If the output is not quite correct, check for invisible trailing spaces!
</properties>
<body>
<release version="3.3" date="TBD" description="TBD">
<action dev="tn" type="add" issue="MATH-970">
Added possibility to retrieve the best found solution of the "SimplexSolver" in case
the iteration limit has been reached. The "optimize(OptimizationData...)" method now
supports a "SolutionCallback" which provides access to the best solution if
a feasible solution could be found (phase 2 of the Two-Phase simplex method has been reached).
</action>
<action dev="tn" type="update" issue="MATH-1031" due-to="Thorsten Schäfer">
Added new class "ClusterEvaluator" to evaluate the result of a clustering algorithm
and refactored existing evaluation code in "MultiKMeansPlusPlusClusterer"

View File

@ -18,7 +18,9 @@ package org.apache.commons.math3.optim.linear;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.math3.exception.TooManyIterationsException;
import org.apache.commons.math3.optim.OptimizationData;
import org.apache.commons.math3.optim.PointValuePair;
import org.apache.commons.math3.util.Precision;
@ -75,6 +77,12 @@ public class SimplexSolver extends LinearOptimizer {
*/
private final double cutOff;
/**
* The solution callback to access the best solution found so far in case
* the optimizer fails to find an optimal solution within the iteration limits.
*/
private SolutionCallback solutionCallback;
/**
* Builds a simplex solver with default settings.
*/
@ -114,6 +122,53 @@ public class SimplexSolver extends LinearOptimizer {
this.cutOff = cutOff;
}
/**
* {@inheritDoc}
*
* @param optData Optimization data. In addition to those documented in
* {@link LinearOptimizer#optimize(OptimizationData...)
* LinearOptimizer}, this method will register the following data:
* <ul>
* <li>{@link SolutionCallback}</li>
* </ul>
*
* @return {@inheritDoc}
* @throws TooManyIterationsException if the maximal number of iterations is exceeded.
*/
@Override
public PointValuePair optimize(OptimizationData... optData)
throws TooManyIterationsException {
// Set up base class and perform computation.
return super.optimize(optData);
}
/**
* {@inheritDoc}
*
* @param optData Optimization data.
* In addition to those documented in
* {@link LinearOptimizer#parseOptimizationData(OptimizationData[])
* LinearOptimizer}, this method will register the following data:
* <ul>
* <li>{@link SolutionCallback}</li>
* </ul>
*/
@Override
protected void parseOptimizationData(OptimizationData... optData) {
// Allow base class to register its own data.
super.parseOptimizationData(optData);
// reset the callback before parsing
solutionCallback = null;
for (OptimizationData data : optData) {
if (data instanceof SolutionCallback) {
solutionCallback = (SolutionCallback) data;
continue;
}
}
}
/**
* Returns the column with the most negative coefficient in the objective function row.
*
@ -278,6 +333,13 @@ public class SimplexSolver extends LinearOptimizer {
throws TooManyIterationsException,
UnboundedSolutionException,
NoFeasibleSolutionException {
// reset the tableau to indicate a non-feasible solution in case
// we do not pass phase 1 successfully
if (solutionCallback != null) {
solutionCallback.setTableau(null);
}
final SimplexTableau tableau =
new SimplexTableau(getFunction(),
getConstraints(),
@ -290,6 +352,11 @@ public class SimplexSolver extends LinearOptimizer {
solvePhase1(tableau);
tableau.dropPhase1Objective();
// after phase 1, we are sure to have a feasible solution
if (solutionCallback != null) {
solutionCallback.setTableau(tableau);
}
while (!tableau.isOptimal()) {
doIteration(tableau);
}

View File

@ -0,0 +1,55 @@
/*
* 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.math3.optim.linear;
import org.apache.commons.math3.optim.OptimizationData;
import org.apache.commons.math3.optim.PointValuePair;
/**
* A constraint for a linear optimization problem indicating whether all
* variables must be restricted to non-negative values.
*
* @version $Id$
* @since 3.3
*/
public class SolutionCallback implements OptimizationData {
/** The SimplexTableau used by the SimplexSolver. */
private SimplexTableau tableau;
/**
* Set the simplex tableau used during the optimization once a feasible
* solution has been found.
*
* @param tableau the simplex tableau containing a feasible solution
*/
void setTableau(final SimplexTableau tableau) {
this.tableau = tableau;
}
/**
* Retrieve the best solution found so far.
* <p>
* <b>Note:</b> the returned solution may not be optimal, e.g. in case
* the optimizer did reach the iteration limits.
*
* @return the best solution found so far by the optimizer, or {@code null} if
* no feasible solution could be found
*/
public PointValuePair getSolution() {
return tableau != null ? tableau.getSolution() : null;
}
}

View File

@ -18,7 +18,10 @@ package org.apache.commons.math3.optim.linear;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.apache.commons.math3.exception.TooManyIterationsException;
import org.apache.commons.math3.optim.MaxIter;
import org.apache.commons.math3.optim.nonlinear.scalar.GoalType;
import org.apache.commons.math3.optim.PointValuePair;
@ -318,7 +321,20 @@ public class SimplexSolverTest {
@Test
public void testMath930() {
Collection<LinearConstraint> constraints = new ArrayList<LinearConstraint>();
Collection<LinearConstraint> constraints = createMath930Constraints();
double[] objFunctionCoeff = new double[33];
objFunctionCoeff[3] = 1;
LinearObjectiveFunction f = new LinearObjectiveFunction(objFunctionCoeff, 0);
SimplexSolver solver = new SimplexSolver(1e-4, 10, 1e-6);
PointValuePair solution = solver.optimize(new MaxIter(1000), f, new LinearConstraintSet(constraints),
GoalType.MINIMIZE, new NonNegativeConstraint(true));
Assert.assertEquals(0.3752298, solution.getValue(), 1e-4);
}
private List<LinearConstraint> createMath930Constraints() {
List<LinearConstraint> constraints = new ArrayList<LinearConstraint>();
constraints.add(new LinearConstraint(new double[] {1, -1, -1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, -1, -1, 1, 1, -1, -1, 1, -1, 1, 1, -1, 0}, Relationship.GEQ, 0.0));
constraints.add(new LinearConstraint(new double[] {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1}, Relationship.GEQ, 0.0));
constraints.add(new LinearConstraint(new double[] {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1}, Relationship.LEQ, 0.0));
@ -416,15 +432,7 @@ public class SimplexSolverTest {
constraints.add(new LinearConstraint(new double[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0}, Relationship.GEQ, 0.0));
constraints.add(new LinearConstraint(new double[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, -0.028754}, Relationship.LEQ, 0.0));
constraints.add(new LinearConstraint(new double[] {0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, Relationship.EQ, 1.0));
double[] objFunctionCoeff = new double[33];
objFunctionCoeff[3] = 1;
LinearObjectiveFunction f = new LinearObjectiveFunction(objFunctionCoeff, 0);
SimplexSolver solver = new SimplexSolver(1e-4, 10, 1e-6);
PointValuePair solution = solver.optimize(new MaxIter(1000), f, new LinearConstraintSet(constraints),
GoalType.MINIMIZE, new NonNegativeConstraint(true));
Assert.assertEquals(0.3752298, solution.getValue(), 1e-4);
return constraints;
}
@Test
@ -710,6 +718,49 @@ public class SimplexSolverTest {
Assert.assertEquals(7518.0, solution.getValue(), .0000001);
}
@Test
public void testSolutionCallback() {
// re-use the problem from testcase for MATH-930
// it normally requires 186 iterations
final List<LinearConstraint> constraints = createMath930Constraints();
double[] objFunctionCoeff = new double[33];
objFunctionCoeff[3] = 1;
LinearObjectiveFunction f = new LinearObjectiveFunction(objFunctionCoeff, 0);
SimplexSolver solver = new SimplexSolver(1e-4, 10, 1e-6);
final SolutionCallback callback = new SolutionCallback();
// 1. iteration limit is too low to reach phase 2 -> no feasible solution
try {
// we need to use a DeterministicLinearConstraintSet to always get the same behavior
solver.optimize(new MaxIter(100), f, new DeterministicLinearConstraintSet(constraints),
GoalType.MINIMIZE, new NonNegativeConstraint(true), callback);
Assert.fail("expected TooManyIterationsException");
} catch (TooManyIterationsException ex) {
// expected
}
final PointValuePair solution1 = callback.getSolution();
Assert.assertNull(solution1);
// 2. iteration limit allows to reach phase 2, but too low to find an optimal solution
try {
// we need to use a DeterministicLinearConstraintSet to always get the same behavior
solver.optimize(new MaxIter(180), f, new DeterministicLinearConstraintSet(constraints),
GoalType.MINIMIZE, new NonNegativeConstraint(true), callback);
Assert.fail("expected TooManyIterationsException");
} catch (TooManyIterationsException ex) {
// expected
}
final PointValuePair solution2 = callback.getSolution();
Assert.assertNotNull(solution2);
Assert.assertTrue(validSolution(solution2, constraints, 1e-4));
// the solution is clearly not optimal
Assert.assertEquals(0.3752298, solution2.getValue(), 5e-1);
}
/**
* Converts a test string to a {@link LinearConstraint}.
* Ex: x0 + x1 + x2 + x3 - x12 = 0
@ -772,4 +823,42 @@ public class SimplexSolverTest {
return true;
}
/**
* Needed for deterministic tests, as the original LinearConstraintSet uses as HashSet.
*/
public class DeterministicLinearConstraintSet extends LinearConstraintSet {
/** Set of constraints. */
private final List<LinearConstraint> linearConstraints = new ArrayList<LinearConstraint>();
/**
* Creates a set containing the given constraints.
*
* @param constraints Constraints.
*/
public DeterministicLinearConstraintSet(LinearConstraint... constraints) {
for (LinearConstraint c : constraints) {
linearConstraints.add(c);
}
}
/**
* Creates a set containing the given constraints.
*
* @param constraints Constraints.
*/
public DeterministicLinearConstraintSet(Collection<LinearConstraint> constraints) {
linearConstraints.addAll(constraints);
}
/**
* Gets the set of linear constraints.
*
* @return the constraints.
*/
public Collection<LinearConstraint> getConstraints() {
return Collections.unmodifiableList(linearConstraints);
}
}
}