View Javadoc
1   // ******************************************************************************
2   //
3   // Title:       Force Field X.
4   // Description: Force Field X - Software for Molecular Biophysics.
5   // Copyright:   Copyright (c) Michael J. Schnieders 2001-2025.
6   //
7   // This file is part of Force Field X.
8   //
9   // Force Field X is free software; you can redistribute it and/or modify it
10  // under the terms of the GNU General Public License version 3 as published by
11  // the Free Software Foundation.
12  //
13  // Force Field X is distributed in the hope that it will be useful, but WITHOUT
14  // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
15  // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
16  // details.
17  //
18  // You should have received a copy of the GNU General Public License along with
19  // Force Field X; if not, write to the Free Software Foundation, Inc., 59 Temple
20  // Place, Suite 330, Boston, MA 02111-1307 USA
21  //
22  // Linking this library statically or dynamically with other modules is making a
23  // combined work based on this library. Thus, the terms and conditions of the
24  // GNU General Public License cover the whole combination.
25  //
26  // As a special exception, the copyright holders of this library give you
27  // permission to link this library with independent modules to produce an
28  // executable, regardless of the license terms of these independent modules, and
29  // to copy and distribute the resulting executable under terms of your choice,
30  // provided that you also meet, for each linked independent module, the terms
31  // and conditions of the license of that module. An independent module is a
32  // module which is not derived from or based on this library. If you modify this
33  // library, you may extend this exception to your version of the library, but
34  // you are not obligated to do so. If you do not wish to do so, delete this
35  // exception statement from your version.
36  //
37  // ******************************************************************************
38  package ffx.numerics.special;
39  
40  import static org.apache.commons.math3.util.FastMath.abs;
41  import static org.apache.commons.math3.util.FastMath.exp;
42  import static org.apache.commons.math3.util.FastMath.log;
43  import static org.apache.commons.math3.util.FastMath.sqrt;
44  
45  /**
46   * Implementation of the modified Bessel function of the first kind using Chebyshev polynomials.
47   * <p>
48   * Adapted from the CERN "cern.jet.math.tdouble" package as included with the
49   * <a href="http://sourceforge.net/projects/parallelcolt">ParallelColt library</a>.
50   */
51  public class ModifiedBessel {
52  
53    private ModifiedBessel() {
54      // Prevent instantiation.
55    }
56  
57    /**
58     * Chebyshev coefficients for exp(-x) i0(x) in the interval [0,8].
59     * <br>
60     * lim(x->0){ exp(-x) i0(x) } = 1.
61     */
62    private static final double[] A_i0 = {
63        -4.41534164647933937950E-18,
64        3.33079451882223809783E-17,
65        -2.43127984654795469359E-16,
66        1.71539128555513303061E-15,
67        -1.16853328779934516808E-14,
68        7.67618549860493561688E-14,
69        -4.85644678311192946090E-13,
70        2.95505266312963983461E-12,
71        -1.72682629144155570723E-11,
72        9.67580903537323691224E-11,
73        -5.18979560163526290666E-10,
74        2.65982372468238665035E-9,
75        -1.30002500998624804212E-8,
76        6.04699502254191894932E-8,
77        -2.67079385394061173391E-7,
78        1.11738753912010371815E-6,
79        -4.41673835845875056359E-6,
80        1.64484480707288970893E-5,
81        -5.75419501008210370398E-5,
82        1.88502885095841655729E-4,
83        -5.76375574538582365885E-4,
84        1.63947561694133579842E-3,
85        -4.32430999505057594430E-3,
86        1.05464603945949983183E-2,
87        -2.37374148058994688156E-2,
88        4.93052842396707084878E-2,
89        -9.49010970480476444210E-2,
90        1.71620901522208775349E-1,
91        -3.04682672343198398683E-1,
92        6.76795274409476084995E-1
93    };
94    /**
95     * Chebyshev coefficients for exp(-x) sqrt(x) i0(x) in the inverted interval [8,infinity].
96     * <br>
97     * lim(x->inf){ exp(-x) sqrt(x) i0(x) } = 1/sqrt(2pi).
98     */
99    private static final double[] B_i0 = {
100       -7.23318048787475395456E-18,
101       -4.83050448594418207126E-18,
102       4.46562142029675999901E-17,
103       3.46122286769746109310E-17,
104       -2.82762398051658348494E-16,
105       -3.42548561967721913462E-16,
106       1.77256013305652638360E-15,
107       3.81168066935262242075E-15,
108       -9.55484669882830764870E-15,
109       -4.15056934728722208663E-14,
110       1.54008621752140982691E-14,
111       3.85277838274214270114E-13,
112       7.18012445138366623367E-13,
113       -1.79417853150680611778E-12,
114       -1.32158118404477131188E-11,
115       -3.14991652796324136454E-11,
116       1.18891471078464383424E-11,
117       4.94060238822496958910E-10,
118       3.39623202570838634515E-9,
119       2.26666899049817806459E-8,
120       2.04891858946906374183E-7,
121       2.89137052083475648297E-6,
122       6.88975834691682398426E-5,
123       3.36911647825569408990E-3,
124       8.04490411014108831608E-1
125   };
126   /**
127    * Chebyshev coefficients for exp(-x) i1(x) / x in the interval [0,8].
128    * <br>
129    * lim(x->0){ exp(-x) i1(x) / x } = 1/2.
130    */
131   private static final double[] A_i1 = {
132       2.77791411276104639959E-18,
133       -2.11142121435816608115E-17,
134       1.55363195773620046921E-16,
135       -1.10559694773538630805E-15,
136       7.60068429473540693410E-15,
137       -5.04218550472791168711E-14,
138       3.22379336594557470981E-13,
139       -1.98397439776494371520E-12,
140       1.17361862988909016308E-11,
141       -6.66348972350202774223E-11,
142       3.62559028155211703701E-10,
143       -1.88724975172282928790E-9,
144       9.38153738649577178388E-9,
145       -4.44505912879632808065E-8,
146       2.00329475355213526229E-7,
147       -8.56872026469545474066E-7,
148       3.47025130813767847674E-6,
149       -1.32731636560394358279E-5,
150       4.78156510755005422638E-5,
151       -1.61760815825896745588E-4,
152       5.12285956168575772895E-4,
153       -1.51357245063125314899E-3,
154       4.15642294431288815669E-3,
155       -1.05640848946261981558E-2,
156       2.47264490306265168283E-2,
157       -5.29459812080949914269E-2,
158       1.02643658689847095384E-1,
159       -1.76416518357834055153E-1,
160       2.52587186443633654823E-1
161   };
162   /**
163    * Chebyshev coefficients for exp(-x) sqrt(x) i1(x) in the inverted interval [8,infinity].
164    * <br>
165    * lim(x->inf){ exp(-x) sqrt(x) i1(x) } = 1/sqrt(2pi).
166    */
167   private static final double[] B_i1 = {
168       7.51729631084210481353E-18,
169       4.41434832307170791151E-18,
170       -4.65030536848935832153E-17,
171       -3.20952592199342395980E-17,
172       2.96262899764595013876E-16,
173       3.30820231092092828324E-16,
174       -1.88035477551078244854E-15,
175       -3.81440307243700780478E-15,
176       1.04202769841288027642E-14,
177       4.27244001671195135429E-14,
178       -2.10154184277266431302E-14,
179       -4.08355111109219731823E-13,
180       -7.19855177624590851209E-13,
181       2.03562854414708950722E-12,
182       1.41258074366137813316E-11,
183       3.25260358301548823856E-11,
184       -1.89749581235054123450E-11,
185       -5.58974346219658380687E-10,
186       -3.83538038596423702205E-9,
187       -2.63146884688951950684E-8,
188       -2.51223623787020892529E-7,
189       -3.88256480887769039346E-6,
190       -1.10588938762623716291E-4,
191       -9.76109749136146840777E-3,
192       7.78576235018280120474E-1
193   };
194 
195   /**
196    * Modified zero-order Bessel function.
197    * <p>
198    * The function is defined as i0(x) = J0( ix ). The range is partitioned into the two intervals
199    * [0,8] and (8, infinity). Chebyshev polynomial expansions are employed in each interval.
200    *
201    * @param x input parameter
202    * @return i0(x)
203    */
204   public static double i0(double x) {
205     // Handle special cases
206     if (Double.isNaN(x)) {
207       return Double.NaN;
208     }
209     if (Double.isInfinite(x)) {
210       return 0.0;
211     }
212 
213     double y;
214     if (x < 0) {
215       x = -x;
216     }
217     if (x <= 8.0) {
218       y = (x * 0.5) - 2.0;
219       return (eToThe(x) * evaluateChebyshev(y, A_i0, 30));
220     }
221     double ix = 1.0 / x;
222     return (eToThe(x) * evaluateChebyshev(32.0 * ix - 2.0, B_i0, 25) * sqrt(ix));
223   }
224 
225   /**
226    * Modified 1st-order Bessel function.
227    * <p>
228    * The function is defined as i1(x) = -i j1( ix ). The range is partitioned into the two intervals
229    * [0,8] and (8, infinity). Chebyshev polynomial expansions are employed in each interval.
230    *
231    * @param x input parameter.
232    * @return i1(x).
233    */
234   public static double i1(double x) {
235     // Handle special cases
236     if (Double.isNaN(x)) {
237       return Double.NaN;
238     }
239     if (Double.isInfinite(x)) {
240       return 0.0;
241     }
242 
243     double y, z;
244 
245     z = abs(x);
246     if (z <= 8.0) {
247       y = (z * 0.5) - 2.0;
248       z = evaluateChebyshev(y, A_i1, 29) * z * eToThe(z);
249     } else {
250       double iz = 1.0 / z;
251       z = eToThe(z) * evaluateChebyshev(32.0 * iz - 2.0, B_i1, 25) * sqrt(iz);
252     }
253     if (x < 0.0) {
254       z = -z;
255     }
256     return (z);
257   }
258 
259   /**
260    * Compute the ratio of i1(x) to i0(x).
261    * <p>
262    * This implementation avoids redundant calculations by computing both i1(x) and i0(x)
263    * efficiently for different ranges of x.
264    *
265    * @param x input parameter
266    * @return i1(x) / i0(x)
267    */
268   public static double i1OverI0(double x) {
269     // Handle special cases
270     if (Double.isNaN(x)) {
271       return Double.NaN;
272     }
273     if (Double.isInfinite(x)) {
274       return 0.0; // Both i1(±∞) and i0(±∞) are 0, but i1/i0 approaches 0 as x approaches ±∞
275     }
276     if (x == 0.0) {
277       return 0.0; // i1(0) = 0, i0(0) = 1
278     }
279 
280     // For small values, use the direct calculation to avoid precision loss
281     if (abs(x) < 1.0) {
282       return i1(x) / i0(x);
283     }
284 
285     double absX = abs(x);
286     double result;
287 
288     if (absX <= 8.0) {
289       // For moderate values, calculate both functions with shared operations
290       double y = (absX * 0.5) - 2.0;
291       double expX = eToThe(absX);
292       double i0Val = expX * evaluateChebyshev(y, A_i0, 30);
293       double i1Val = evaluateChebyshev(y, A_i1, 29) * absX * expX;
294       result = i1Val / i0Val;
295     } else {
296       // For large values, use the asymptotic behavior
297       double ix = 1.0 / absX;
298       double expX = eToThe(absX);
299       double sqrtIx = sqrt(ix);
300       double i0Val = expX * evaluateChebyshev(32.0 * ix - 2.0, B_i0, 25) * sqrtIx;
301       double i1Val = expX * evaluateChebyshev(32.0 * ix - 2.0, B_i1, 25) * sqrtIx;
302       result = i1Val / i0Val;
303     }
304 
305     // Adjust sign for negative x
306     return (x < 0.0) ? -result : result;
307   }
308 
309   /**
310    * Compute the natural log(i0(x)).
311    * <p>
312    * This implementation computes log(i0(x)) directly to avoid overflow for large x values.
313    * For large x, i0(x) ~ exp(x)/sqrt(2*pi*x), so log(i0(x)) ~ x - 0.5*log(2*pi*x).
314    *
315    * @param x input parameter.
316    * @return the natural log(i0(x)).
317    */
318   public static double lnI0(double x) {
319     // Handle special cases
320     if (Double.isNaN(x)) {
321       return Double.NaN;
322     }
323     if (Double.isInfinite(x)) {
324       return Double.NEGATIVE_INFINITY; // log(0) = -∞
325     }
326 
327     // For small values, use the direct calculation to avoid precision loss
328     if (abs(x) <= 8.0) {
329       double i0Val = i0(x);
330       // Avoid log(0) which would return -Infinity
331       return i0Val > 0.0 ? log(i0Val) : Double.NEGATIVE_INFINITY;
332     }
333 
334     // For large values, use asymptotic formula to avoid overflow
335     double absX = abs(x);
336     double ix = 1.0 / absX;
337 
338     // Compute log(i0(x)) directly using the asymptotic formula
339     // log(i0(x)) = x + log(evaluateChebyshev(...) * sqrt(ix))
340     double chebyshevTerm = evaluateChebyshev(32.0 * ix - 2.0, B_i0, 25);
341 
342     // Handle potential underflow in the Chebyshev evaluation
343     if (chebyshevTerm <= 0.0) {
344       return absX; // Fallback to the dominant term
345     }
346 
347     return absX + log(chebyshevTerm * sqrt(ix));
348   }
349 
350   /**
351    * Evaluates Chebyshev polynomials (first kind) at x/2.
352    * <p>
353    * NOTE: Argument x must first be transformed to the interval (-1,1).
354    * <p>
355    * NOTE: Coefficients are in reverse; zero-order term is last.
356    *
357    * @param x            argument to the polynomial.
358    * @param coefficients the coefficients of the polynomial.
359    * @param N            the number of coefficients.
360    * @return the result
361    */
362   private static double evaluateChebyshev(double x, double[] coefficients, int N) {
363     double b0, b1, b2;
364 
365     int p = 0;
366     int i;
367 
368     b0 = coefficients[p++];
369     b1 = 0.0;
370     i = N - 1;
371 
372     do {
373       b2 = b1;
374       b1 = b0;
375       b0 = x * b1 - b2 + coefficients[p++];
376     } while (--i > 0);
377 
378     return (0.5 * (b0 - b2));
379   }
380 
381   /**
382    * Computes exp(x) with protection against overflow and underflow.
383    * <p>
384    * Returns Double.MAX_VALUE in place of Double.POSITIVE_INFINITY and
385    * Double.MIN_VALUE in place of Double.NEGATIVE_INFINITY. This prevents
386    * arithmetic operations from generating NaN results when overflow occurs.
387    *
388    * @param x input parameter
389    * @return exp(x) with overflow/underflow protection
390    */
391   private static double eToThe(double x) {
392     // Handle NaN input
393     if (Double.isNaN(x)) {
394       return Double.NaN;
395     }
396 
397     // Handle extreme values directly to avoid unnecessary computation
398     if (x > 709.0) { // Approximate threshold where exp(x) overflows to Infinity
399       return Double.MAX_VALUE;
400     }
401     if (x < -745.0) { // Approximate threshold where exp(x) underflows to 0
402       return Double.MIN_VALUE;
403     }
404 
405     double res = exp(x);
406     if (res == Double.POSITIVE_INFINITY) {
407       return Double.MAX_VALUE;
408     } else if (res == Double.NEGATIVE_INFINITY || res == 0.0) {
409       return Double.MIN_VALUE;
410     }
411     return res;
412   }
413 }