2 * This file is part of the GROMACS molecular simulation package.
4 * Copyright (c) 2016,2017,2018,2019, 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
81 void calculateCubicSplineCoefficients(double functionValue0
,
82 double functionValue1
,
83 double derivativeValue0
,
84 double derivativeValue1
,
92 *F
= spacing
* derivativeValue0
;
93 *G
= 3.0 * (functionValue1
- functionValue0
) - spacing
* (derivativeValue1
+ 2.0 * derivativeValue0
);
94 *H
= -2.0 * (functionValue1
- functionValue0
) + spacing
* (derivativeValue1
+ derivativeValue0
);
97 /*! \brief Perform cubic spline interpolation in interval from function/derivative
99 * \param functionValue0 Function value for the present table index
100 * \param functionValue1 Function value for the next table index
101 * \param derivativeValue0 Derivative value for the present table index
102 * \param derivativeValue1 Derivative value for the next table index
103 * \param spacing Distance between table points
104 * \param eps Offset from lower table point for evaluation
105 * \param[out] interpolatedFunctionValue Output function value
106 * \param[out] interpolatedDerivativeValue Output derivative value
108 void cubicSplineInterpolationFromFunctionAndDerivative(double functionValue0
,
109 double functionValue1
,
110 double derivativeValue0
,
111 double derivativeValue1
,
114 double* interpolatedFunctionValue
,
115 double* interpolatedDerivativeValue
)
119 calculateCubicSplineCoefficients(functionValue0
, functionValue1
, derivativeValue0
,
120 derivativeValue1
, spacing
, &Y
, &F
, &G
, &H
);
122 double Fp
= fma(fma(H
, eps
, G
), eps
, F
);
124 *interpolatedFunctionValue
= fma(Fp
, eps
, Y
);
125 *interpolatedDerivativeValue
= fma(eps
, fma(2.0 * eps
, H
, G
), Fp
) / spacing
;
129 /*! \brief Construct the data for a single cubic table from analytical functions
131 * \param[in] function Analytical functiojn
132 * \param[in] derivative Analytical derivative
133 * \param[in] range Upper/lower limit of region to tabulate
134 * \param[in] spacing Distance between table points
135 * \param[out] yfghTableData Output cubic spline table with Y,F,G,H entries
137 void fillSingleCubicSplineTableData(const std::function
<double(double)>& function
,
138 const std::function
<double(double)>& derivative
,
139 const std::pair
<real
, real
>& range
,
141 std::vector
<real
>* yfghTableData
)
143 int endIndex
= static_cast<int>(range
.second
/ spacing
+ 2);
145 yfghTableData
->resize(4 * endIndex
);
147 double maxMagnitude
= 0.0001 * GMX_REAL_MAX
;
148 bool functionIsInRange
= true;
149 std::size_t lastIndexInRange
= endIndex
- 1;
151 for (int i
= endIndex
- 1; i
>= 0; i
--)
153 double x
= i
* spacing
;
154 double tmpFunctionValue
;
155 double tmpDerivativeValue
;
156 double nextHigherFunction
;
157 double nextHigherDerivative
;
160 if (range
.first
> 0 && i
== 0)
162 // Avoid x==0 if it is not in the range, since it can lead to
163 // singularities even if the value for i==1 was within or required magnitude
164 functionIsInRange
= false;
167 if (functionIsInRange
)
169 tmpFunctionValue
= function(x
);
170 tmpDerivativeValue
= derivative(x
);
171 nextHigherFunction
= ((i
+ 1) < endIndex
) ? function(x
+ spacing
) : 0.0;
172 nextHigherDerivative
= ((i
+ 1) < endIndex
) ? derivative(x
+ spacing
) : 0.0;
174 if (std::abs(tmpFunctionValue
) > maxMagnitude
|| std::abs(tmpDerivativeValue
) > maxMagnitude
)
176 functionIsInRange
= false; // Once this happens, it never resets to true again
180 if (functionIsInRange
)
182 calculateCubicSplineCoefficients(tmpFunctionValue
, nextHigherFunction
, tmpDerivativeValue
,
183 nextHigherDerivative
, spacing
, &Y
, &F
, &G
, &H
);
188 double lastIndexY
= (*yfghTableData
)[4 * lastIndexInRange
];
189 double lastIndexF
= (*yfghTableData
)[4 * lastIndexInRange
+ 1];
191 Y
= lastIndexY
+ lastIndexF
* (i
- lastIndexInRange
);
197 (*yfghTableData
)[4 * i
] = Y
;
198 (*yfghTableData
)[4 * i
+ 1] = F
;
199 (*yfghTableData
)[4 * i
+ 2] = G
;
200 (*yfghTableData
)[4 * i
+ 3] = H
;
205 /*! \brief Construct the data for a single cubic table from vector data
207 * \param[in] function Input vector with function data
208 * \param[in] derivative Input vector with derivative data
209 * \param[in] inputSpacing Distance between points in input vectors
210 * \param[in] range Upper/lower limit of region to tabulate
211 * \param[in] spacing Distance between table points
212 * \param[out] yfghTableData Output cubic spline table with Y,F,G,H entries
214 void fillSingleCubicSplineTableData(ArrayRef
<const double> function
,
215 ArrayRef
<const double> derivative
,
217 const std::pair
<real
, real
>& range
,
219 std::vector
<real
>* yfghTableData
)
221 int endIndex
= static_cast<int>(range
.second
/ spacing
+ 2);
223 std::vector
<double> tmpFunction(endIndex
);
224 std::vector
<double> tmpDerivative(endIndex
);
226 double maxMagnitude
= 0.0001 * GMX_REAL_MAX
;
227 bool functionIsInRange
= true;
228 std::size_t lastIndexInRange
= endIndex
- 1;
230 // Interpolate function and derivative values in positions needed for output
231 for (int i
= endIndex
- 1; i
>= 0; i
--)
233 double x
= i
* spacing
;
234 double xtab
= x
/ inputSpacing
;
235 int index
= static_cast<int>(xtab
);
236 double eps
= xtab
- index
;
238 if (range
.first
> 0 && i
== 0)
240 // Avoid x==0 if it is not in the range, since it can lead to
241 // singularities even if the value for i==1 was within or required magnitude
242 functionIsInRange
= false;
245 if (functionIsInRange
246 && (std::abs(function
[index
]) > maxMagnitude
|| std::abs(derivative
[index
]) > maxMagnitude
))
248 functionIsInRange
= false; // Once this happens, it never resets to true again
251 if (functionIsInRange
)
253 cubicSplineInterpolationFromFunctionAndDerivative(
254 function
[index
], function
[index
+ 1], derivative
[index
], derivative
[index
+ 1],
255 inputSpacing
, eps
, &(tmpFunction
[i
]), &(tmpDerivative
[i
]));
260 double lastIndexFunction
= tmpFunction
[lastIndexInRange
];
261 double lastIndexDerivative
= tmpDerivative
[lastIndexInRange
];
262 tmpFunction
[i
] = lastIndexFunction
+ lastIndexDerivative
* (i
- lastIndexInRange
) * spacing
;
263 tmpDerivative
[i
] = lastIndexDerivative
;
267 yfghTableData
->resize(4 * endIndex
);
269 for (int i
= 0; i
< endIndex
; i
++)
273 double nextFunction
= ((i
+ 1) < endIndex
) ? tmpFunction
[i
+ 1] : 0.0;
274 double nextDerivative
= ((i
+ 1) < endIndex
) ? tmpDerivative
[i
+ 1] : 0.0;
276 calculateCubicSplineCoefficients(tmpFunction
[i
], nextFunction
, tmpDerivative
[i
],
277 nextDerivative
, spacing
, &Y
, &F
, &G
, &H
);
278 (*yfghTableData
)[4 * i
] = Y
;
279 (*yfghTableData
)[4 * i
+ 1] = F
;
280 (*yfghTableData
)[4 * i
+ 2] = G
;
281 (*yfghTableData
)[4 * i
+ 3] = H
;
289 const real
CubicSplineTable::defaultTolerance
= 1e-10;
291 const real
CubicSplineTable::defaultTolerance
= 10.0 * GMX_FLOAT_EPS
;
294 CubicSplineTable::CubicSplineTable(std::initializer_list
<AnalyticalSplineTableInput
> analyticalInputList
,
295 const std::pair
<real
, real
>& range
,
297 numFuncInTable_(analyticalInputList
.size()),
300 // Sanity check on input values
301 if (range_
.first
< 0.0 || (range_
.second
- range_
.first
) < 0.001)
303 GMX_THROW(InvalidInputError(
304 "Range to tabulate cannot include negative values and must span at least 0.001"));
307 if (tolerance
< GMX_REAL_EPS
)
309 GMX_THROW(ToleranceError("Table tolerance cannot be smaller than GMX_REAL_EPS"));
312 double minQuotient
= GMX_REAL_MAX
;
314 // loop over all functions to find smallest spacing
315 for (const auto& thisFuncInput
: analyticalInputList
)
319 internal::throwUnlessDerivativeIsConsistentWithFunction(
320 thisFuncInput
.function
, thisFuncInput
.derivative
, range_
);
322 catch (gmx::GromacsException
& ex
)
324 ex
.prependContext("Error generating cubic spline table for function '"
325 + thisFuncInput
.desc
+ "'");
328 // Calculate the required table spacing h. The error we make with a third order polynomial
329 // (second order for derivative) will be described by the fourth-derivative correction term.
331 // This means we can compute the required spacing as h =
332 // 0.5*cbrt(72*sqrt(3)*tolerance**min(f'/f'''')), where f'/f'''' is the first and fourth
333 // derivative of the function, respectively. Since we already have an analytical form of the
334 // derivative, we reduce the numerical errors by calculating the quotient of the function
335 // and third derivative of the input-derivative-analytical function instead.
337 double thisMinQuotient
= internal::findSmallestQuotientOfFunctionAndThirdDerivative(
338 thisFuncInput
.derivative
, range_
);
340 minQuotient
= std::min(minQuotient
, thisMinQuotient
);
343 double spacing
= 0.5 * std::cbrt(72.0 * std::sqrt(3.0) * tolerance
* minQuotient
);
345 tableScale_
= 1.0 / spacing
;
347 if (range_
.second
* tableScale_
> 2e6
)
350 ToleranceError("Over a million points would be required for table; decrease range "
351 "or increase tolerance"));
354 // Loop over all tables again.
355 // Here we create the actual table for each function, and then
356 // combine them into a multiplexed table function.
357 std::size_t funcIndex
= 0;
359 for (const auto& thisFuncInput
: analyticalInputList
)
363 std::vector
<real
> tmpYfghTableData
;
365 fillSingleCubicSplineTableData(thisFuncInput
.function
, thisFuncInput
.derivative
, range_
,
366 spacing
, &tmpYfghTableData
);
368 internal::fillMultiplexedTableData(tmpYfghTableData
, &yfghMultiTableData_
, 4,
369 numFuncInTable_
, funcIndex
);
373 catch (gmx::GromacsException
& ex
)
375 ex
.prependContext("Error generating cubic spline table for function '"
376 + thisFuncInput
.desc
+ "'");
383 CubicSplineTable::CubicSplineTable(std::initializer_list
<NumericalSplineTableInput
> numericalInputList
,
384 const std::pair
<real
, real
>& range
,
386 numFuncInTable_(numericalInputList
.size()),
389 // Sanity check on input values
390 if (range
.first
< 0.0 || (range
.second
- range
.first
) < 0.001)
392 GMX_THROW(InvalidInputError(
393 "Range to tabulate cannot include negative values and must span at least 0.001"));
396 if (tolerance
< GMX_REAL_EPS
)
398 GMX_THROW(ToleranceError("Table tolerance cannot be smaller than GMX_REAL_EPS"));
401 double minQuotient
= GMX_REAL_MAX
;
403 // loop over all functions to find smallest spacing
404 for (auto thisFuncInput
: numericalInputList
)
408 // We do not yet know what the margin is, but we need to test that we at least cover
409 // the requested range before starting to calculate derivatives
410 if (thisFuncInput
.function
.size() < range_
.second
/ thisFuncInput
.spacing
+ 1)
413 InconsistentInputError("Table input vectors must cover requested range, "
414 "and a margin beyond the upper endpoint"));
417 if (thisFuncInput
.function
.size() != thisFuncInput
.derivative
.size())
419 GMX_THROW(InconsistentInputError(
420 "Function and derivative vectors have different lengths"));
423 internal::throwUnlessDerivativeIsConsistentWithFunction(
424 thisFuncInput
.function
, thisFuncInput
.derivative
, thisFuncInput
.spacing
, range_
);
426 catch (gmx::GromacsException
& ex
)
428 ex
.prependContext("Error generating cubic spline table for function '"
429 + thisFuncInput
.desc
+ "'");
432 // Calculate the required table spacing h. The error we make with linear interpolation
433 // of the derivative will be described by the third-derivative correction term.
434 // This means we can compute the required spacing as h = sqrt(12*tolerance*min(f'/f''')),
435 // where f'/f''' is the first and third derivative of the function, respectively.
437 double thisMinQuotient
= internal::findSmallestQuotientOfFunctionAndThirdDerivative(
438 thisFuncInput
.derivative
, thisFuncInput
.spacing
, range_
);
440 minQuotient
= std::min(minQuotient
, thisMinQuotient
);
443 double spacing
= std::cbrt(72.0 * std::sqrt(3.0) * tolerance
* minQuotient
);
445 tableScale_
= 1.0 / spacing
;
447 if (range_
.second
* tableScale_
> 1e6
)
450 ToleranceError("Requested tolerance would require over a million points in table"));
453 // Loop over all tables again.
454 // Here we create the actual table for each function, and then
455 // combine them into a multiplexed table function.
456 std::size_t funcIndex
= 0;
458 for (auto thisFuncInput
: numericalInputList
)
462 if (spacing
< thisFuncInput
.spacing
)
465 ToleranceError("Input vector spacing cannot achieve tolerance requested"));
468 std::vector
<real
> tmpYfghTableData
;
470 fillSingleCubicSplineTableData(thisFuncInput
.function
, thisFuncInput
.derivative
,
471 thisFuncInput
.spacing
, range
, spacing
, &tmpYfghTableData
);
473 internal::fillMultiplexedTableData(tmpYfghTableData
, &yfghMultiTableData_
, 4,
474 numFuncInTable_
, funcIndex
);
478 catch (gmx::GromacsException
& ex
)
480 ex
.prependContext("Error generating cubic spline table for function '"
481 + thisFuncInput
.desc
+ "'");