[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:
parent
3a45bc5b6d
commit
54a7796ff2
|
@ -51,6 +51,12 @@ If the output is not quite correct, check for invisible trailing spaces!
|
||||||
</properties>
|
</properties>
|
||||||
<body>
|
<body>
|
||||||
<release version="3.3" date="TBD" description="TBD">
|
<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">
|
<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
|
Added new class "ClusterEvaluator" to evaluate the result of a clustering algorithm
|
||||||
and refactored existing evaluation code in "MultiKMeansPlusPlusClusterer"
|
and refactored existing evaluation code in "MultiKMeansPlusPlusClusterer"
|
||||||
|
|
|
@ -18,7 +18,9 @@ package org.apache.commons.math3.optim.linear;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.apache.commons.math3.exception.TooManyIterationsException;
|
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.optim.PointValuePair;
|
||||||
import org.apache.commons.math3.util.Precision;
|
import org.apache.commons.math3.util.Precision;
|
||||||
|
|
||||||
|
@ -75,6 +77,12 @@ public class SimplexSolver extends LinearOptimizer {
|
||||||
*/
|
*/
|
||||||
private final double cutOff;
|
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.
|
* Builds a simplex solver with default settings.
|
||||||
*/
|
*/
|
||||||
|
@ -114,6 +122,53 @@ public class SimplexSolver extends LinearOptimizer {
|
||||||
this.cutOff = cutOff;
|
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.
|
* Returns the column with the most negative coefficient in the objective function row.
|
||||||
*
|
*
|
||||||
|
@ -278,6 +333,13 @@ public class SimplexSolver extends LinearOptimizer {
|
||||||
throws TooManyIterationsException,
|
throws TooManyIterationsException,
|
||||||
UnboundedSolutionException,
|
UnboundedSolutionException,
|
||||||
NoFeasibleSolutionException {
|
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 =
|
final SimplexTableau tableau =
|
||||||
new SimplexTableau(getFunction(),
|
new SimplexTableau(getFunction(),
|
||||||
getConstraints(),
|
getConstraints(),
|
||||||
|
@ -290,6 +352,11 @@ public class SimplexSolver extends LinearOptimizer {
|
||||||
solvePhase1(tableau);
|
solvePhase1(tableau);
|
||||||
tableau.dropPhase1Objective();
|
tableau.dropPhase1Objective();
|
||||||
|
|
||||||
|
// after phase 1, we are sure to have a feasible solution
|
||||||
|
if (solutionCallback != null) {
|
||||||
|
solutionCallback.setTableau(tableau);
|
||||||
|
}
|
||||||
|
|
||||||
while (!tableau.isOptimal()) {
|
while (!tableau.isOptimal()) {
|
||||||
doIteration(tableau);
|
doIteration(tableau);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,7 +18,10 @@ package org.apache.commons.math3.optim.linear;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.apache.commons.math3.exception.TooManyIterationsException;
|
||||||
import org.apache.commons.math3.optim.MaxIter;
|
import org.apache.commons.math3.optim.MaxIter;
|
||||||
import org.apache.commons.math3.optim.nonlinear.scalar.GoalType;
|
import org.apache.commons.math3.optim.nonlinear.scalar.GoalType;
|
||||||
import org.apache.commons.math3.optim.PointValuePair;
|
import org.apache.commons.math3.optim.PointValuePair;
|
||||||
|
@ -318,7 +321,20 @@ public class SimplexSolverTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testMath930() {
|
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, -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.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));
|
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}, 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, 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));
|
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));
|
||||||
|
return constraints;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -710,6 +718,49 @@ public class SimplexSolverTest {
|
||||||
Assert.assertEquals(7518.0, solution.getValue(), .0000001);
|
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}.
|
* Converts a test string to a {@link LinearConstraint}.
|
||||||
* Ex: x0 + x1 + x2 + x3 - x12 = 0
|
* Ex: x0 + x1 + x2 + x3 - x12 = 0
|
||||||
|
@ -772,4 +823,42 @@ public class SimplexSolverTest {
|
||||||
|
|
||||||
return true;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue