This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-numbers.git
The following commit(s) were added to refs/heads/master by this push:
new 55b5539c NUMBERS-210: BrentSolver to validate convergence arguments
55b5539c is described below
commit 55b5539c69d00b49d666020d483ead2c93e4a0e2
Author: Alex Herbert <[email protected]>
AuthorDate: Thu Aug 20 18:03:04 2026 +0100
NUMBERS-210: BrentSolver to validate convergence arguments
All accuracies must be positive and finite.
Function value must not be NaN.
Initial bracket [min, max] must be finite.
---
.../commons/numbers/rootfinder/BrentSolver.java | 67 +++++++++++++++++++---
.../numbers/rootfinder/SolverException.java | 6 ++
.../numbers/rootfinder/BrentSolverTest.java | 55 ++++++++++++++++++
src/changes/changes.xml | 6 ++
4 files changed, 127 insertions(+), 7 deletions(-)
diff --git
a/commons-numbers-rootfinder/src/main/java/org/apache/commons/numbers/rootfinder/BrentSolver.java
b/commons-numbers-rootfinder/src/main/java/org/apache/commons/numbers/rootfinder/BrentSolver.java
index f363d313..cf0fd552 100644
---
a/commons-numbers-rootfinder/src/main/java/org/apache/commons/numbers/rootfinder/BrentSolver.java
+++
b/commons-numbers-rootfinder/src/main/java/org/apache/commons/numbers/rootfinder/BrentSolver.java
@@ -46,13 +46,23 @@ public class BrentSolver {
/**
* Construct a solver.
*
+ * <p>The accuracies must be finite and non-negative.
+ * Accuracies of zero are accepted, but the
+ * search may then only terminate when it finds a value within 1 ULP of
+ * zero.
+ *
* @param relativeAccuracy Relative accuracy.
* @param absoluteAccuracy Absolute accuracy.
* @param functionValueAccuracy Function value accuracy.
+ * @throws IllegalArgumentException if any accuracy is NaN, infinite or
+ * negative.
*/
public BrentSolver(double relativeAccuracy,
double absoluteAccuracy,
double functionValueAccuracy) {
+ checkAccuracy("relative", relativeAccuracy);
+ checkAccuracy("absolute", absoluteAccuracy);
+ checkAccuracy("function value", functionValueAccuracy);
this.relativeAccuracy = relativeAccuracy;
this.absoluteAccuracy = absoluteAccuracy;
this.functionValueAccuracy = functionValueAccuracy;
@@ -61,15 +71,17 @@ public class BrentSolver {
/**
* Search the function's zero within the given interval.
*
- * @param func Function to solve.
+ * @param function Function to solve.
* @param min Lower bound.
* @param max Upper bound.
* @return the root.
- * @throws IllegalArgumentException if {@code min > max}.
+ * @throws IllegalArgumentException if {@code min > max}; or
+ * {@code min} or {@code max} are non-finite.
* @throws IllegalArgumentException if the given interval does
* not bracket the root.
+ * @throws IllegalArgumentException if the function evaluates as NaN.
*/
- public double findRoot(DoubleUnaryOperator func,
+ public double findRoot(DoubleUnaryOperator function,
double min,
double max) {
// Avoid overflow computing the initial value: 0.5 * (min + max)
@@ -78,27 +90,31 @@ public class BrentSolver {
// if min is not the root within the configured function accuracy;
// otherwise min is returned.
final double initial = min == max ? min : 0.5 * min + 0.5 * max;
- return findRoot(func, min, initial, max);
+ return findRoot(function, min, initial, max);
}
/**
* Search the function's zero within the given interval,
* starting from the given estimate.
*
- * @param func Function to solve.
+ * @param function Function to solve.
* @param min Lower bound.
* @param initial Initial guess.
* @param max Upper bound.
* @return the root.
- * @throws IllegalArgumentException if {@code min > max} or
+ * @throws IllegalArgumentException if {@code min > max};
+ * {@code min} or {@code max} are non-finite; or
* {@code initial} is not in the {@code [min, max]} interval.
* @throws IllegalArgumentException if the given interval does
* not bracket the root.
+ * @throws IllegalArgumentException if the function evaluates as NaN.
*/
- public double findRoot(DoubleUnaryOperator func,
+ public double findRoot(DoubleUnaryOperator function,
double min,
double initial,
double max) {
+ checkFinite("min", min);
+ checkFinite("max", max);
if (min > max) {
throw new SolverException(SolverException.TOO_LARGE, min, max);
}
@@ -107,6 +123,15 @@ public class BrentSolver {
throw new SolverException(SolverException.OUT_OF_RANGE, initial,
min, max);
}
+ // A NaN invalidates convergence
+ final DoubleUnaryOperator func = x -> {
+ final double fx = function.applyAsDouble(x);
+ if (Double.isNaN(fx)) {
+ throw new SolverException(SolverException.NAN, x);
+ }
+ return fx;
+ };
+
// Return the initial guess if it is good enough.
final double yInitial = func.applyAsDouble(initial);
if (Math.abs(yInitial) <= functionValueAccuracy) {
@@ -257,4 +282,32 @@ public class BrentSolver {
private static boolean equalsZero(double value) {
return Math.abs(value) <= Double.MIN_VALUE;
}
+
+ /**
+ * Check the accuracy is finite and non-negative.
+ * Negative, infinite and NaN accuracies prevent valid convergence checks.
+ *
+ * @param name Name of the accuracy.
+ * @param accuracy Accuracy.
+ * @throws IllegalArgumentException if {@code accuracy} is NaN, infinite
+ * or negative.
+ */
+ private static void checkAccuracy(String name, double accuracy) {
+ if (!(accuracy >= 0 && accuracy <= Double.MAX_VALUE)) {
+ throw new SolverException(SolverException.INVALID_ACCURACY, name,
accuracy);
+ }
+ }
+
+ /**
+ * Check the value is finite.
+ *
+ * @param name Name of the value.
+ * @param value Value.
+ * @throws IllegalArgumentException if {@code value} is NaN or infinite.
+ */
+ private static void checkFinite(String name, double value) {
+ if (!Double.isFinite(value)) {
+ throw new SolverException(SolverException.NON_FINITE, name, value);
+ }
+ }
}
diff --git
a/commons-numbers-rootfinder/src/main/java/org/apache/commons/numbers/rootfinder/SolverException.java
b/commons-numbers-rootfinder/src/main/java/org/apache/commons/numbers/rootfinder/SolverException.java
index c6f671c3..14b37421 100644
---
a/commons-numbers-rootfinder/src/main/java/org/apache/commons/numbers/rootfinder/SolverException.java
+++
b/commons-numbers-rootfinder/src/main/java/org/apache/commons/numbers/rootfinder/SolverException.java
@@ -26,6 +26,12 @@ class SolverException extends IllegalArgumentException {
static final String OUT_OF_RANGE = "%s is out of range [%s, %s]";
/** Error message for "failed bracketing" condition. */
static final String BRACKETING = "No bracketing: f(%s)=%s, f(%s)=%s";
+ /** Error message for an invalid named solver accuracy. */
+ static final String INVALID_ACCURACY = "Invalid %s accuracy: %s";
+ /** Error message for a function evaluation of NaN. */
+ static final String NAN = "Function value at %s is NaN";
+ /** Error message for a named non-finite number. */
+ static final String NON_FINITE = "%s is non-finite: %s";
/** Serializable version identifier. */
private static final long serialVersionUID = 20190602L;
diff --git
a/commons-numbers-rootfinder/src/test/java/org/apache/commons/numbers/rootfinder/BrentSolverTest.java
b/commons-numbers-rootfinder/src/test/java/org/apache/commons/numbers/rootfinder/BrentSolverTest.java
index 07986706..504af762 100644
---
a/commons-numbers-rootfinder/src/test/java/org/apache/commons/numbers/rootfinder/BrentSolverTest.java
+++
b/commons-numbers-rootfinder/src/test/java/org/apache/commons/numbers/rootfinder/BrentSolverTest.java
@@ -30,6 +30,61 @@ class BrentSolverTest {
private static final double DEFAULT_RELATIVE_ACCURACY = 1e-14;
private static final double DEFAULT_FUNCTION_ACCURACY = 1e-15;
+ @Test
+ void testInvalidAccuracies() {
+ // The accuracies feed the convergence tolerance 2 * eps * abs(b) + t.
+ // A NaN accuracy makes every loop exit criterion false (an infinite
+ // loop); negative and infinite accuracies are meaningless. All must
+ // be rejected at construction.
+ for (final double bad : new double[] {Double.NaN, -1,
-Double.MIN_VALUE,
+ Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY}) {
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> new BrentSolver(bad, DEFAULT_ABSOLUTE_ACCURACY,
DEFAULT_FUNCTION_ACCURACY),
+ () -> "relative accuracy: " + bad);
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> new BrentSolver(DEFAULT_RELATIVE_ACCURACY, bad,
DEFAULT_FUNCTION_ACCURACY),
+ () -> "absolute accuracy: " + bad);
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> new BrentSolver(DEFAULT_RELATIVE_ACCURACY,
DEFAULT_ABSOLUTE_ACCURACY, bad),
+ () -> "function value accuracy: " + bad);
+ }
+ // Zero accuracies remain supported (see testSubNormalBracket,
NUMBERS-168).
+ Assertions.assertDoesNotThrow(() -> new BrentSolver(0, 0, 0));
+ }
+
+ @Test
+ void testNonFiniteBracketValue() {
+ final DoubleUnaryOperator f = x -> x;
+ final BrentSolver solver = new BrentSolver(DEFAULT_RELATIVE_ACCURACY,
DEFAULT_ABSOLUTE_ACCURACY,
+ DEFAULT_FUNCTION_ACCURACY);
+ final SolverException ex =
Assertions.assertThrows(SolverException.class,
+ () -> solver.findRoot(f, 0, Double.POSITIVE_INFINITY));
+ Assertions.assertNotEquals(-1, ex.getMessage().indexOf("non-finite"));
+
+ final SolverException ex2 =
Assertions.assertThrows(SolverException.class,
+ () -> solver.findRoot(f, Double.NEGATIVE_INFINITY, 0));
+ Assertions.assertNotEquals(-1, ex2.getMessage().indexOf("non-finite"));
+ }
+
+ @Test
+ void testNaNFunctionValue() {
+ // A function with a NaN "hole" strictly inside the bracket, e.g. from
+ // sqrt/log of an out-of-domain sub-expression. The solver must fail
+ // fast instead of degrading to a tolerance-sized step per evaluation
+ // (and possibly returning a point where the function value is NaN).
+ final DoubleUnaryOperator f = x -> {
+ if (x < 0.1) {
+ return -1;
+ }
+ return x < 0.4 ? Double.NaN : 1;
+ };
+ final BrentSolver solver = new BrentSolver(DEFAULT_RELATIVE_ACCURACY,
DEFAULT_ABSOLUTE_ACCURACY,
+ DEFAULT_FUNCTION_ACCURACY);
+ final SolverException ex =
Assertions.assertThrows(SolverException.class,
+ () -> solver.findRoot(f, 0, 0.05, 0.5));
+ Assertions.assertNotEquals(-1, ex.getMessage().indexOf("NaN"));
+ }
+
@Test
void testSinZero() {
// The sinus function is behaved well around the root at pi. The second
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 04a41560..b1eb79ff 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -56,6 +56,12 @@ If the output is not quite correct, check for invisible
trailing spaces!
<release version="1.4" date="TBD" description="
New features, updates and bug fixes.
">
+ <action dev="aherbert" type="fix" due-to="Security scan, Alex Herbert"
issue="NUMBERS-210">
+ "BrentSolver": Avoid non-convergence by validating the convergence
accuracies
+ are positive and finite. Throw an exception if a function evaluation
is NaN
+ as this invalidates the bracket update step. Vaidate the initial
bracket [min, max]
+ has finite values.
+ </action>
<action dev="aherbert" type="fix" due-to="Security scan, Alex Herbert"
issue="NUMBERS-209">
"Trigamma": Avoid an infinite loop on large negative arguments. All
negative
arguments are now computed using the reflection formula to map the
computation