Apply clang-format-11
[alexxy/gromacs.git] / src / gromacs / tables / quadraticsplinetable.cpp
1 /*
2  * This file is part of the GROMACS molecular simulation package.
3  *
4  * Copyright (c) 2016,2017,2018,2019,2020,2021, 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.
8  *
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.
13  *
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.
18  *
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.
23  *
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.
31  *
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.
34  */
35
36 /*! \internal \file
37  * \brief
38  * Implements classes for quadratic spline table functions
39  *
40  * \author Erik Lindahl <erik.lindahl@gmail.com>
41  * \ingroup module_tables
42  */
43 #include "gmxpre.h"
44
45 #include "quadraticsplinetable.h"
46
47 #include <cmath>
48
49 #include <algorithm>
50 #include <functional>
51 #include <initializer_list>
52 #include <utility>
53 #include <vector>
54
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"
60
61 #include "splineutil.h"
62
63 namespace gmx
64 {
65
66 namespace
67 {
68
69 /*! \brief Construct the data for a single quadratic table from analytical functions
70  *
71  * \param[in]  function             Analytical function
72  * \param[in]  derivative           Analytical derivative
73  * \param[in]  range                Upper/lower limit of region to tabulate
74  * \param[in]  spacing              Distance between table points
75  * \param[out] functionTableData    Output table with function data
76  * \param[out] derivativeTableData  OUtput table with (adjusted) derivative data
77  */
78 void fillSingleQuadraticSplineTableData(const std::function<double(double)>& function,
79                                         const std::function<double(double)>& derivative,
80                                         const std::pair<real, real>&         range,
81                                         double                               spacing,
82                                         std::vector<real>*                   functionTableData,
83                                         std::vector<real>*                   derivativeTableData)
84 {
85     std::size_t endIndex = static_cast<std::size_t>(range.second / spacing + 2);
86
87     functionTableData->resize(endIndex);
88     derivativeTableData->resize(endIndex);
89
90     double      maxMagnitude      = 0.0001 * GMX_REAL_MAX;
91     bool        functionIsInRange = true;
92     std::size_t lastIndexInRange  = endIndex - 1;
93
94     for (int i = endIndex - 1; i >= 0; i--)
95     {
96         double x = i * spacing;
97         double tmpFunctionValue;
98         double tmpDerivativeValue;
99
100         if (range.first > 0 && i == 0)
101         {
102             // Avoid x==0 if it is not in the range, since it can lead to
103             // singularities even if the value for i==1 was within or required magnitude
104             functionIsInRange = false;
105         }
106
107         if (functionIsInRange)
108         {
109             tmpFunctionValue = function(x);
110
111             // Calculate third derivative term (2nd derivative of the derivative)
112             // Make sure we stay in range. In practice this means we use one-sided
113             // interpolation at the interval endpoints (indentical to an offset for 3-point formula)
114             const double h = std::pow(GMX_DOUBLE_EPS, 0.25);
115             double       y = std::min(std::max(x, range.first + h), range.second - h);
116             double       thirdDerivativeValue =
117                     (derivative(y + h) - 2.0 * derivative(y) + derivative(y - h)) / (h * h);
118
119             tmpDerivativeValue = derivative(x) - spacing * spacing * thirdDerivativeValue / 12.0;
120
121             if (std::abs(tmpFunctionValue) > maxMagnitude || std::abs(tmpDerivativeValue) > maxMagnitude)
122             {
123                 functionIsInRange = false; // Once this happens, it never resets to true again
124             }
125         }
126
127         if (functionIsInRange)
128         {
129             (*functionTableData)[i]   = tmpFunctionValue;
130             (*derivativeTableData)[i] = tmpDerivativeValue;
131             lastIndexInRange--;
132         }
133         else
134         {
135             // Once the function or derivative (more likely) has reached very large values,
136             // we simply make a linear function from the last in-range value of the derivative.
137             double lastIndexFunction   = (*functionTableData)[lastIndexInRange];
138             double lastIndexDerivative = (*derivativeTableData)[lastIndexInRange];
139             (*functionTableData)[i] =
140                     lastIndexFunction + lastIndexDerivative * (i - lastIndexInRange) * spacing;
141             (*derivativeTableData)[i] = lastIndexDerivative;
142         }
143     }
144 }
145
146
147 /*! \brief Construct the data for a single quadratic table from vector data
148  *
149  * \param[in]  function             Input vector with function data
150  * \param[in]  derivative           Input vector with derivative data
151  * \param[in]  inputSpacing         Distance between points in input vectors
152  * \param[in]  range                Upper/lower limit of region to tabulate
153  * \param[in]  spacing              Distance between table points
154  * \param[out] functionTableData    Output table with function data
155  * \param[out] derivativeTableData  OUtput table with (adjusted) derivative data
156  */
157 void fillSingleQuadraticSplineTableData(ArrayRef<const double>       function,
158                                         ArrayRef<const double>       derivative,
159                                         double                       inputSpacing,
160                                         const std::pair<real, real>& range,
161                                         double                       spacing,
162                                         std::vector<real>*           functionTableData,
163                                         std::vector<real>*           derivativeTableData)
164 {
165     std::size_t endIndex = static_cast<std::size_t>(range.second / spacing + 2);
166
167     functionTableData->resize(endIndex);
168     derivativeTableData->resize(endIndex);
169
170     std::vector<double> thirdDerivative(internal::vectorSecondDerivative(derivative, inputSpacing));
171
172     double maxMagnitude      = 0.0001 * GMX_REAL_MAX;
173     bool   functionIsInRange = true;
174     int    lastIndexInRange  = static_cast<int>(endIndex) - 1;
175
176     for (int i = lastIndexInRange; i >= 0; i--)
177     {
178         double x = i * spacing;
179         double tmpFunctionValue;
180         double tmpDerivativeValue;
181
182         if (range.first > 0 && i == 0)
183         {
184             // Avoid x==0 if it is not in the range, since it can lead to
185             // singularities even if the value for i==1 was within or required magnitude
186             functionIsInRange = false;
187         }
188
189         if (functionIsInRange)
190         {
191             // Step 1: Interpolate the function value at x from input table.
192             double inputXTab  = x / inputSpacing;
193             int    inputIndex = static_cast<std::size_t>(inputXTab);
194             double inputEps   = inputXTab - inputIndex;
195
196             // Linear interpolation of input derivative and third derivative
197             double thirdDerivativeValue = (1.0 - inputEps) * thirdDerivative[inputIndex]
198                                           + inputEps * thirdDerivative[inputIndex + 1];
199             double derivativeValue =
200                     (1.0 - inputEps) * derivative[inputIndex] + inputEps * derivative[inputIndex + 1];
201
202             // Quadratic interpolation for function value
203             tmpFunctionValue = function[inputIndex]
204                                + 0.5 * (derivative[inputIndex] + derivativeValue) * inputEps * inputSpacing;
205             tmpDerivativeValue = derivativeValue - spacing * spacing * thirdDerivativeValue / 12.0;
206
207             if (std::abs(tmpFunctionValue) > maxMagnitude || std::abs(tmpDerivativeValue) > maxMagnitude)
208             {
209                 functionIsInRange = false; // Once this happens, it never resets to true again
210             }
211         }
212
213         if (functionIsInRange)
214         {
215             (*functionTableData)[i]   = tmpFunctionValue;
216             (*derivativeTableData)[i] = tmpDerivativeValue;
217             lastIndexInRange--;
218         }
219         else
220         {
221             // Once the function or derivative (more likely) has reached very large values,
222             // we simply make a linear function from the last in-range value of the derivative.
223             GMX_ASSERT(lastIndexInRange >= 0, "Array index is unexpectedly negative.");
224             double lastIndexFunction   = (*functionTableData)[lastIndexInRange];
225             double lastIndexDerivative = (*derivativeTableData)[lastIndexInRange];
226             (*functionTableData)[i] =
227                     lastIndexFunction + lastIndexDerivative * (i - lastIndexInRange) * spacing;
228             (*derivativeTableData)[i] = lastIndexDerivative;
229         }
230     }
231 }
232
233 /*! \brief Create merged DDFZ vector from function & derivative data
234  *
235  *  \param functionTableData     Function values
236  *  \param derivativeTableData   Derivative values. We have already subtracted the
237  *                               small third derivative component when calling this
238  *                               function, but in practice it is just an arbitrary
239  *                               vector here.
240  *  \param ddfzTableData         Vector four times longer, filled with
241  *                               the derivative, the difference to the next derivative
242  *                               point, the function value, and zero.
243  *
244  *  \throws If the vector lengths do not match.
245  */
246 void fillDdfzTableData(const std::vector<real>& functionTableData,
247                        const std::vector<real>& derivativeTableData,
248                        std::vector<real>*       ddfzTableData)
249 {
250     GMX_ASSERT(functionTableData.size() == derivativeTableData.size(), "Mismatching vector lengths");
251
252     std::size_t points = functionTableData.size();
253
254     ddfzTableData->resize(4 * points);
255
256     for (std::size_t i = 0; i < points; i++)
257     {
258         (*ddfzTableData)[4 * i] = derivativeTableData[i];
259
260         double nextDerivative = (i < functionTableData.size() - 1) ? derivativeTableData[i + 1] : 0.0;
261
262         (*ddfzTableData)[4 * i + 1] = nextDerivative - derivativeTableData[i];
263         (*ddfzTableData)[4 * i + 2] = functionTableData[i];
264         (*ddfzTableData)[4 * i + 3] = 0.0;
265     }
266 }
267
268 } // namespace
269
270
271 const real QuadraticSplineTable::defaultTolerance = 10.0 * GMX_FLOAT_EPS;
272
273
274 QuadraticSplineTable::QuadraticSplineTable(std::initializer_list<AnalyticalSplineTableInput> analyticalInputList,
275                                            const std::pair<real, real>&                      range,
276                                            real tolerance) :
277     numFuncInTable_(analyticalInputList.size()), range_(range)
278 {
279     // Sanity check on input values
280     if (range_.first < 0.0 || (range_.second - range_.first) < 0.001)
281     {
282         GMX_THROW(InvalidInputError(
283                 "Range to tabulate cannot include negative values and must span at least 0.001"));
284     }
285
286     if (tolerance < GMX_REAL_EPS)
287     {
288         GMX_THROW(ToleranceError("Table tolerance cannot be smaller than GMX_REAL_EPS"));
289     }
290
291     double minQuotient = GMX_REAL_MAX;
292
293     // loop over all functions to find smallest spacing
294     for (const auto& thisFuncInput : analyticalInputList)
295     {
296         try
297         {
298             internal::throwUnlessDerivativeIsConsistentWithFunction(
299                     thisFuncInput.function, thisFuncInput.derivative, range_);
300         }
301         catch (gmx::GromacsException& ex)
302         {
303             ex.prependContext("Error generating quadratic spline table for function '"
304                               + thisFuncInput.desc + "'");
305             throw;
306         }
307         // Calculate the required table spacing h. The error we make with linear interpolation
308         // of the derivative will be described by the third-derivative correction term.
309         // This means we can compute the required spacing as h = sqrt(12*tolerance*min(f'/f''')),
310         // where f'/f''' is the first and third derivative of the function, respectively.
311
312         double thisMinQuotient = internal::findSmallestQuotientOfFunctionAndSecondDerivative(
313                 thisFuncInput.derivative, range_);
314
315         minQuotient = std::min(minQuotient, thisMinQuotient);
316     }
317
318     double spacing = std::sqrt(12.0 * tolerance * minQuotient);
319
320     halfSpacing_ = 0.5 * spacing;
321     tableScale_  = 1.0 / spacing;
322
323     if (range_.second * tableScale_ > 1e6)
324     {
325         GMX_THROW(
326                 ToleranceError("Over a million points would be required for table; decrease range "
327                                "or increase tolerance"));
328     }
329
330     // Loop over all tables again.
331     // Here we create the actual table for each function, and then
332     // combine them into a multiplexed table function.
333     std::size_t funcIndex = 0;
334
335     for (const auto& thisFuncInput : analyticalInputList)
336     {
337         try
338         {
339             std::vector<real> tmpFuncTableData;
340             std::vector<real> tmpDerTableData;
341             std::vector<real> tmpDdfzTableData;
342
343             fillSingleQuadraticSplineTableData(thisFuncInput.function,
344                                                thisFuncInput.derivative,
345                                                range_,
346                                                spacing,
347                                                &tmpFuncTableData,
348                                                &tmpDerTableData);
349
350             fillDdfzTableData(tmpFuncTableData, tmpDerTableData, &tmpDdfzTableData);
351
352             internal::fillMultiplexedTableData(
353                     tmpDerTableData, &derivativeMultiTableData_, 1, numFuncInTable_, funcIndex);
354
355             internal::fillMultiplexedTableData(
356                     tmpDdfzTableData, &ddfzMultiTableData_, 4, numFuncInTable_, funcIndex);
357
358             funcIndex++;
359         }
360         catch (gmx::GromacsException& ex)
361         {
362             ex.prependContext("Error generating quadratic spline table for function '"
363                               + thisFuncInput.desc + "'");
364             throw;
365         }
366     }
367 }
368
369
370 QuadraticSplineTable::QuadraticSplineTable(std::initializer_list<NumericalSplineTableInput> numericalInputList,
371                                            const std::pair<real, real>&                     range,
372                                            real tolerance) :
373     numFuncInTable_(numericalInputList.size()), range_(range)
374 {
375     // Sanity check on input values
376     if (range.first < 0.0 || (range.second - range.first) < 0.001)
377     {
378         GMX_THROW(InvalidInputError(
379                 "Range to tabulate cannot include negative values and must span at least 0.001"));
380     }
381
382     if (tolerance < GMX_REAL_EPS)
383     {
384         GMX_THROW(ToleranceError("Table tolerance cannot be smaller than GMX_REAL_EPS"));
385     }
386
387     double minQuotient = GMX_REAL_MAX;
388
389     // loop over all functions to find smallest spacing
390     for (auto thisFuncInput : numericalInputList)
391     {
392         try
393         {
394             // We do not yet know what the margin is, but we need to test that we at least cover
395             // the requested range before starting to calculate derivatives
396             if (thisFuncInput.function.size() < range_.second / thisFuncInput.spacing + 1)
397             {
398                 GMX_THROW(
399                         InconsistentInputError("Table input vectors must cover requested range, "
400                                                "and a margin beyond the upper endpoint"));
401             }
402
403             if (thisFuncInput.function.size() != thisFuncInput.derivative.size())
404             {
405                 GMX_THROW(InconsistentInputError(
406                         "Function and derivative vectors have different lengths"));
407             }
408
409             internal::throwUnlessDerivativeIsConsistentWithFunction(
410                     thisFuncInput.function, thisFuncInput.derivative, thisFuncInput.spacing, range_);
411         }
412         catch (gmx::GromacsException& ex)
413         {
414             ex.prependContext("Error generating quadratic spline table for function '"
415                               + thisFuncInput.desc + "'");
416             throw;
417         }
418         // Calculate the required table spacing h. The error we make with linear interpolation
419         // of the derivative will be described by the third-derivative correction term.
420         // This means we can compute the required spacing as h = sqrt(12*tolerance*min(f'/f''')),
421         // where f'/f''' is the first and third derivative of the function, respectively.
422         // Since we already have an analytical form of the derivative, we reduce the numerical
423         // errors by calculating the quotient of the function and second derivative of the
424         // input-derivative-analytical function instead.
425
426         double thisMinQuotient = internal::findSmallestQuotientOfFunctionAndSecondDerivative(
427                 thisFuncInput.derivative, thisFuncInput.spacing, range_);
428
429         minQuotient = std::min(minQuotient, thisMinQuotient);
430     }
431
432     double spacing = std::sqrt(12.0 * tolerance * minQuotient);
433
434     halfSpacing_ = 0.5 * spacing;
435     tableScale_  = 1.0 / spacing;
436
437     if (range_.second * tableScale_ > 1e6)
438     {
439         GMX_THROW(
440                 ToleranceError("Requested tolerance would require over a million points in table"));
441     }
442
443     // Loop over all tables again.
444     // Here we create the actual table for each function, and then
445     // combine them into a multiplexed table function.
446     std::size_t funcIndex = 0;
447
448     for (auto thisFuncInput : numericalInputList)
449     {
450         try
451         {
452             if (spacing < thisFuncInput.spacing)
453             {
454                 GMX_THROW(
455                         ToleranceError("Input vector spacing cannot achieve tolerance requested"));
456             }
457
458             std::vector<real> tmpFuncTableData;
459             std::vector<real> tmpDerTableData;
460             std::vector<real> tmpDdfzTableData;
461
462             fillSingleQuadraticSplineTableData(thisFuncInput.function,
463                                                thisFuncInput.derivative,
464                                                thisFuncInput.spacing,
465                                                range,
466                                                spacing,
467                                                &tmpFuncTableData,
468                                                &tmpDerTableData);
469
470             fillDdfzTableData(tmpFuncTableData, tmpDerTableData, &tmpDdfzTableData);
471
472             internal::fillMultiplexedTableData(
473                     tmpDerTableData, &derivativeMultiTableData_, 1, numFuncInTable_, funcIndex);
474
475             internal::fillMultiplexedTableData(
476                     tmpDdfzTableData, &ddfzMultiTableData_, 4, numFuncInTable_, funcIndex);
477
478             funcIndex++;
479         }
480         catch (gmx::GromacsException& ex)
481         {
482             ex.prependContext("Error generating quadratic spline table for function '"
483                               + thisFuncInput.desc + "'");
484             throw;
485         }
486     }
487 }
488
489 } // namespace gmx