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

Reply via email to