2 * This file is part of the GROMACS molecular simulation package.
4 * Copyright (c) 2016,2017,2018, by the GROMACS development team, led by
5 * Mark Abraham, David van der Spoel, Berk Hess, and Erik Lindahl,
6 * and including many others, as listed in the AUTHORS file in the
7 * top-level source directory and at http://www.gromacs.org.
9 * GROMACS is free software; you can redistribute it and/or
10 * modify it under the terms of the GNU Lesser General Public License
11 * as published by the Free Software Foundation; either version 2.1
12 * of the License, or (at your option) any later version.
14 * GROMACS is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17 * Lesser General Public License for more details.
19 * You should have received a copy of the GNU Lesser General Public
20 * License along with GROMACS; if not, see
21 * http://www.gnu.org/licenses, or write to the Free Software Foundation,
22 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
24 * If you want to redistribute modifications to GROMACS, please
25 * consider that scientific software is very special. Version
26 * control is crucial - bugs must be traceable. We will be happy to
27 * consider code for inclusion in the official distribution, but
28 * derived work must not be called official GROMACS. Details are found
29 * in the README & COPYING files - if they are missing, get the
30 * official version at http://www.gromacs.org.
32 * To help us fund GROMACS development, we humbly ask that you cite
33 * the research papers on the package. Check out http://www.gromacs.org.
38 * Implements classes for cubic spline table functions
40 * \author Erik Lindahl <erik.lindahl@gmail.com>
41 * \ingroup module_tables
45 #include "cubicsplinetable.h"
51 #include <initializer_list>
55 #include "gromacs/tables/tableinput.h"
56 #include "gromacs/utility/alignedallocator.h"
57 #include "gromacs/utility/arrayref.h"
58 #include "gromacs/utility/exceptions.h"
59 #include "gromacs/utility/real.h"
61 #include "splineutil.h"
69 /*! \brief Calculate table elements from function/derivative data
71 * \param functionValue0 Function value for the present table index
72 * \param functionValue1 Function value for the next table index
73 * \param derivativeValue0 Derivative value for the present table index
74 * \param derivativeValue1 Derivative value for the next table index
75 * \param spacing Distance between table points
76 * \param Y Function value for table index
77 * \param F Component to multiply with offset eps
78 * \param G Component to multiply with eps^2
79 * \param H Component to multiply with eps^3
82 calculateCubicSplineCoefficients(double functionValue0
,
83 double functionValue1
,
84 double derivativeValue0
,
85 double derivativeValue1
,
93 *F
= spacing
* derivativeValue0
;
94 *G
= 3.0*( functionValue1
- functionValue0
) - spacing
* (derivativeValue1
+ 2.0 * derivativeValue0
);
95 *H
= -2.0*( functionValue1
- functionValue0
) + spacing
* (derivativeValue1
+ derivativeValue0
);
98 /*! \brief Perform cubic spline interpolation in interval from function/derivative
100 * \param functionValue0 Function value for the present table index
101 * \param functionValue1 Function value for the next table index
102 * \param derivativeValue0 Derivative value for the present table index
103 * \param derivativeValue1 Derivative value for the next table index
104 * \param spacing Distance between table points
105 * \param eps Offset from lower table point for evaluation
106 * \param[out] interpolatedFunctionValue Output function value
107 * \param[out] interpolatedDerivativeValue Output derivative value
110 cubicSplineInterpolationFromFunctionAndDerivative(double functionValue0
,
111 double functionValue1
,
112 double derivativeValue0
,
113 double derivativeValue1
,
116 double *interpolatedFunctionValue
,
117 double *interpolatedDerivativeValue
)
121 calculateCubicSplineCoefficients(functionValue0
, functionValue1
,
122 derivativeValue0
, derivativeValue1
,
126 double Fp
= fma(fma(H
, eps
, G
), eps
, F
);
128 *interpolatedFunctionValue
= fma(Fp
, eps
, Y
);
129 *interpolatedDerivativeValue
= fma(eps
, fma(2.0*eps
, H
, G
), Fp
)/spacing
;
134 /*! \brief Construct the data for a single cubic table from analytical functions
136 * \param[in] function Analytical functiojn
137 * \param[in] derivative Analytical derivative
138 * \param[in] range Upper/lower limit of region to tabulate
139 * \param[in] spacing Distance between table points
140 * \param[out] yfghTableData Output cubic spline table with Y,F,G,H entries
143 fillSingleCubicSplineTableData(const std::function
<double(double)> &function
,
144 const std::function
<double(double)> &derivative
,
145 const std::pair
<real
, real
> &range
,
147 std::vector
<real
> *yfghTableData
)
149 int endIndex
= static_cast<int>(range
.second
/ spacing
+ 2);
151 yfghTableData
->resize(4*endIndex
);
153 double maxMagnitude
= 0.0001*GMX_REAL_MAX
;
154 bool functionIsInRange
= true;
155 std::size_t lastIndexInRange
= endIndex
- 1;
157 for (int i
= endIndex
- 1; i
>= 0; i
--)
159 double x
= i
* spacing
;
160 double tmpFunctionValue
;
161 double tmpDerivativeValue
;
162 double nextHigherFunction
;
163 double nextHigherDerivative
;
166 if (range
.first
> 0 && i
== 0)
168 // Avoid x==0 if it is not in the range, since it can lead to
169 // singularities even if the value for i==1 was within or required magnitude
170 functionIsInRange
= false;
173 if (functionIsInRange
)
175 tmpFunctionValue
= function(x
);
176 tmpDerivativeValue
= derivative(x
);
177 nextHigherFunction
= ((i
+1) < endIndex
) ? function(x
+spacing
) : 0.0;
178 nextHigherDerivative
= ((i
+1) < endIndex
) ? derivative(x
+spacing
) : 0.0;
180 if (std::abs(tmpFunctionValue
) > maxMagnitude
|| std::abs(tmpDerivativeValue
) > maxMagnitude
)
182 functionIsInRange
= false; // Once this happens, it never resets to true again
186 if (functionIsInRange
)
188 calculateCubicSplineCoefficients(tmpFunctionValue
, nextHigherFunction
,
189 tmpDerivativeValue
, nextHigherDerivative
,
196 double lastIndexY
= (*yfghTableData
)[4*lastIndexInRange
];
197 double lastIndexF
= (*yfghTableData
)[4*lastIndexInRange
+ 1];
199 Y
= lastIndexY
+ lastIndexF
* (i
- lastIndexInRange
);
205 (*yfghTableData
)[4*i
] = Y
;
206 (*yfghTableData
)[4*i
+1] = F
;
207 (*yfghTableData
)[4*i
+2] = G
;
208 (*yfghTableData
)[4*i
+3] = H
;
213 /*! \brief Construct the data for a single cubic table from vector data
215 * \param[in] function Input vector with function data
216 * \param[in] derivative Input vector with derivative data
217 * \param[in] inputSpacing Distance between points in input vectors
218 * \param[in] range Upper/lower limit of region to tabulate
219 * \param[in] spacing Distance between table points
220 * \param[out] yfghTableData Output cubic spline table with Y,F,G,H entries
223 fillSingleCubicSplineTableData(ArrayRef
<const double> function
,
224 ArrayRef
<const double> derivative
,
226 const std::pair
<real
, real
> &range
,
228 std::vector
<real
> *yfghTableData
)
230 int endIndex
= static_cast<int>(range
.second
/ spacing
+ 2);
232 std::vector
<double> tmpFunction(endIndex
);
233 std::vector
<double> tmpDerivative(endIndex
);
235 double maxMagnitude
= 0.0001*GMX_REAL_MAX
;
236 bool functionIsInRange
= true;
237 std::size_t lastIndexInRange
= endIndex
- 1;
239 // Interpolate function and derivative values in positions needed for output
240 for (int i
= endIndex
- 1; i
>= 0; i
--)
242 double x
= i
* spacing
;
243 double xtab
= x
/ inputSpacing
;
244 int index
= static_cast<int>(xtab
);
245 double eps
= xtab
- index
;
247 if (range
.first
> 0 && i
== 0)
249 // Avoid x==0 if it is not in the range, since it can lead to
250 // singularities even if the value for i==1 was within or required magnitude
251 functionIsInRange
= false;
254 if (functionIsInRange
&& (std::abs(function
[index
]) > maxMagnitude
|| std::abs(derivative
[index
]) > maxMagnitude
))
256 functionIsInRange
= false; // Once this happens, it never resets to true again
259 if (functionIsInRange
)
261 cubicSplineInterpolationFromFunctionAndDerivative(function
[index
],
268 &(tmpDerivative
[i
]));
273 double lastIndexFunction
= tmpFunction
[lastIndexInRange
];
274 double lastIndexDerivative
= tmpDerivative
[lastIndexInRange
];
275 tmpFunction
[i
] = lastIndexFunction
+ lastIndexDerivative
* (i
- lastIndexInRange
) * spacing
;
276 tmpDerivative
[i
] = lastIndexDerivative
;
280 yfghTableData
->resize(4*endIndex
);
282 for (int i
= 0; i
< endIndex
; i
++)
286 double nextFunction
= ((i
+1) < endIndex
) ? tmpFunction
[i
+1] : 0.0;
287 double nextDerivative
= ((i
+1) < endIndex
) ? tmpDerivative
[i
+1] : 0.0;
289 calculateCubicSplineCoefficients(tmpFunction
[i
], nextFunction
,
290 tmpDerivative
[i
], nextDerivative
,
293 (*yfghTableData
)[4*i
] = Y
;
294 (*yfghTableData
)[4*i
+1] = F
;
295 (*yfghTableData
)[4*i
+2] = G
;
296 (*yfghTableData
)[4*i
+3] = H
;
307 CubicSplineTable::defaultTolerance
= 1e-10;
310 CubicSplineTable::defaultTolerance
= 10.0 * GMX_FLOAT_EPS
;
313 CubicSplineTable::CubicSplineTable(std::initializer_list
<AnalyticalSplineTableInput
> analyticalInputList
,
314 const std::pair
<real
, real
> &range
,
316 : numFuncInTable_(analyticalInputList
.size()), range_(range
)
318 // Sanity check on input values
319 if (range_
.first
< 0.0 || (range_
.second
-range_
.first
) < 0.001)
321 GMX_THROW(InvalidInputError("Range to tabulate cannot include negative values and must span at least 0.001"));
324 if (tolerance
< GMX_REAL_EPS
)
326 GMX_THROW(ToleranceError("Table tolerance cannot be smaller than GMX_REAL_EPS"));
329 double minQuotient
= GMX_REAL_MAX
;
331 // loop over all functions to find smallest spacing
332 for (const auto &thisFuncInput
: analyticalInputList
)
336 internal::throwUnlessDerivativeIsConsistentWithFunction(thisFuncInput
.function
, thisFuncInput
.derivative
, range_
);
338 catch (gmx::GromacsException
&ex
)
340 ex
.prependContext("Error generating cubic spline table for function '" + thisFuncInput
.desc
+ "'");
343 // Calculate the required table spacing h. The error we make with a third order polynomial
344 // (second order for derivative) will be described by the fourth-derivative correction term.
346 // This means we can compute the required spacing as h = 0.5*cbrt(72*sqrt(3)*tolerance**min(f'/f'''')),
347 // where f'/f'''' is the first and fourth derivative of the function, respectively.
348 // Since we already have an analytical form of the derivative, we reduce the numerical
349 // errors by calculating the quotient of the function and third derivative of the
350 // input-derivative-analytical function instead.
352 double thisMinQuotient
= internal::findSmallestQuotientOfFunctionAndThirdDerivative(thisFuncInput
.derivative
, range_
);
354 minQuotient
= std::min(minQuotient
, thisMinQuotient
);
357 double spacing
= 0.5 * std::cbrt(72.0 * std::sqrt(3.0) * tolerance
* minQuotient
);
359 tableScale_
= 1.0 / spacing
;
361 if (range_
.second
* tableScale_
> 2e6
)
363 GMX_THROW(ToleranceError("Over a million points would be required for table; decrease range or increase tolerance"));
366 // Loop over all tables again.
367 // Here we create the actual table for each function, and then
368 // combine them into a multiplexed table function.
369 std::size_t funcIndex
= 0;
371 for (const auto &thisFuncInput
: analyticalInputList
)
375 std::vector
<real
> tmpYfghTableData
;
377 fillSingleCubicSplineTableData(thisFuncInput
.function
,
378 thisFuncInput
.derivative
,
383 internal::fillMultiplexedTableData(tmpYfghTableData
, &yfghMultiTableData_
,
384 4, numFuncInTable_
, funcIndex
);
388 catch (gmx::GromacsException
&ex
)
390 ex
.prependContext("Error generating cubic spline table for function '" + thisFuncInput
.desc
+ "'");
397 CubicSplineTable::CubicSplineTable(std::initializer_list
<NumericalSplineTableInput
> numericalInputList
,
398 const std::pair
<real
, real
> &range
,
400 : numFuncInTable_(numericalInputList
.size()), range_(range
)
402 // Sanity check on input values
403 if (range
.first
< 0.0 || (range
.second
-range
.first
) < 0.001)
405 GMX_THROW(InvalidInputError("Range to tabulate cannot include negative values and must span at least 0.001"));
408 if (tolerance
< GMX_REAL_EPS
)
410 GMX_THROW(ToleranceError("Table tolerance cannot be smaller than GMX_REAL_EPS"));
413 double minQuotient
= GMX_REAL_MAX
;
415 // loop over all functions to find smallest spacing
416 for (auto thisFuncInput
: numericalInputList
)
420 // We do not yet know what the margin is, but we need to test that we at least cover
421 // the requested range before starting to calculate derivatives
422 if (thisFuncInput
.function
.size() < range_
.second
/ thisFuncInput
.spacing
+ 1)
424 GMX_THROW(InconsistentInputError("Table input vectors must cover requested range, and a margin beyond the upper endpoint"));
427 if (thisFuncInput
.function
.size() != thisFuncInput
.derivative
.size())
429 GMX_THROW(InconsistentInputError("Function and derivative vectors have different lengths"));
432 internal::throwUnlessDerivativeIsConsistentWithFunction(thisFuncInput
.function
, thisFuncInput
.derivative
, thisFuncInput
.spacing
, range_
);
434 catch (gmx::GromacsException
&ex
)
436 ex
.prependContext("Error generating cubic spline table for function '" + thisFuncInput
.desc
+ "'");
439 // Calculate the required table spacing h. The error we make with linear interpolation
440 // of the derivative will be described by the third-derivative correction term.
441 // This means we can compute the required spacing as h = sqrt(12*tolerance*min(f'/f''')),
442 // where f'/f''' is the first and third derivative of the function, respectively.
444 double thisMinQuotient
= internal::findSmallestQuotientOfFunctionAndThirdDerivative(thisFuncInput
.derivative
, thisFuncInput
.spacing
, range_
);
446 minQuotient
= std::min(minQuotient
, thisMinQuotient
);
449 double spacing
= std::cbrt(72.0 * std::sqrt(3.0) * tolerance
* minQuotient
);
451 tableScale_
= 1.0 / spacing
;
453 if (range_
.second
* tableScale_
> 1e6
)
455 GMX_THROW(ToleranceError("Requested tolerance would require over a million points in table"));
458 // Loop over all tables again.
459 // Here we create the actual table for each function, and then
460 // combine them into a multiplexed table function.
461 std::size_t funcIndex
= 0;
463 for (auto thisFuncInput
: numericalInputList
)
467 if (spacing
< thisFuncInput
.spacing
)
469 GMX_THROW(ToleranceError("Input vector spacing cannot achieve tolerance requested"));
472 std::vector
<real
> tmpYfghTableData
;
474 fillSingleCubicSplineTableData(thisFuncInput
.function
,
475 thisFuncInput
.derivative
,
476 thisFuncInput
.spacing
,
481 internal::fillMultiplexedTableData(tmpYfghTableData
, &yfghMultiTableData_
,
482 4, numFuncInTable_
, funcIndex
);
486 catch (gmx::GromacsException
&ex
)
488 ex
.prependContext("Error generating cubic spline table for function '" + thisFuncInput
.desc
+ "'");