Migrate gmx msd to the trajectoryanalysis framework
[alexxy/gromacs.git] / src / gromacs / trajectoryanalysis / modules / msd.cpp
1 /*
2  * This file is part of the GROMACS molecular simulation package.
3  *
4  * Copyright (c) 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 /*! \internal \file
36  * \brief
37  * Defines the trajectory analysis module for mean squared displacement calculations.
38  *
39  * \author Kevin Boyd <kevin44boyd@gmail.com>
40  * \ingroup module_trajectoryanalysis
41  */
42
43 #include "gmxpre.h"
44
45 #include "msd.h"
46
47 #include <numeric>
48
49 #include "gromacs/analysisdata/analysisdata.h"
50 #include "gromacs/analysisdata/modules/average.h"
51 #include "gromacs/analysisdata/modules/plot.h"
52 #include "gromacs/analysisdata/paralleloptions.h"
53 #include "gromacs/fileio/oenv.h"
54 #include "gromacs/fileio/trxio.h"
55 #include "gromacs/math/functions.h"
56 #include "gromacs/math/vectypes.h"
57 #include "gromacs/options/basicoptions.h"
58 #include "gromacs/options/filenameoption.h"
59 #include "gromacs/options/ioptionscontainer.h"
60 #include "gromacs/pbcutil/pbc.h"
61 #include "gromacs/selection/selectionoption.h"
62 #include "gromacs/statistics/statistics.h"
63 #include "gromacs/trajectoryanalysis/analysissettings.h"
64 #include "gromacs/trajectoryanalysis/topologyinformation.h"
65 #include "gromacs/trajectory/trajectoryframe.h"
66 #include "gromacs/utility/stringutil.h"
67 #include "gromacs/utility.h"
68
69 namespace gmx
70 {
71 namespace analysismodules
72 {
73
74 namespace
75 {
76
77 //! Convert nm^2/ps to 10e-5 cm^2/s
78 constexpr double c_diffusionConversionFactor = 1000.0;
79 //! Three dimensional diffusion coefficient multiplication constant.
80 constexpr double c_3DdiffusionDimensionFactor = 6.0;
81 //! Two dimensional diffusion coefficient multiplication constant.
82 constexpr double c_2DdiffusionDimensionFactor = 4.0;
83 //! One dimensional diffusion coefficient multiplication constant.
84 constexpr double c_1DdiffusionDimensionFactor = 2.0;
85
86
87 /*! \brief Mean Squared Displacement data accumulator
88  *
89  * This class is used to accumulate individual MSD data points
90  * and emit tau-averaged results once data is finished collecting. Displacements at each observed
91  * time difference (tau) are recorded from the trajectory. Because it is not known in advance which
92  * time differences will be observed from the trajectory, this data structure is built adaptively.
93  * New columns corresponding to observed time differences are added as needed, and additional
94  * observations at formerly observed time differences are added to those columns. Separate time lags
95  * will likely have differing total data points.
96  *
97  * Data columns per tau are accessed via operator[], which always guarantees
98  * a column is initialized and returns an MsdColumProxy to the column that can push data.
99  */
100 class MsdData
101 {
102 public:
103     //! Proxy to a MsdData tau column vector. Supports only push_back.
104     class MsdColumnProxy
105     {
106     public:
107         MsdColumnProxy(std::vector<double>& column) : column_(column) {}
108
109         void push_back(double value) { column_.push_back(value); }
110
111     private:
112         std::vector<double>& column_;
113     };
114     //! Returns a proxy to the column for the given tau index. Guarantees that the column is initialized.
115     MsdColumnProxy operator[](size_t index)
116     {
117         if (msds_.size() <= index)
118         {
119             msds_.resize(index + 1);
120         }
121         return MsdColumnProxy(msds_[index]);
122     }
123     /*! \brief Compute per-tau MSDs averaged over all added points.
124      *
125      * The resulting vector is size(max tau index). Any indices
126      * that have no data points have MSD set to 0.
127      *
128      * \return Average MSD per tau
129      */
130     [[nodiscard]] std::vector<real> averageMsds() const;
131
132 private:
133     //! Results - first indexed by tau, then data points
134     std::vector<std::vector<double>> msds_;
135 };
136
137
138 std::vector<real> MsdData::averageMsds() const
139 {
140     std::vector<real> msdSums;
141     msdSums.reserve(msds_.size());
142     for (gmx::ArrayRef<const double> msdValues : msds_)
143     {
144         if (msdValues.empty())
145         {
146             msdSums.push_back(0.0);
147             continue;
148         }
149         msdSums.push_back(std::accumulate(msdValues.begin(), msdValues.end(), 0.0, std::plus<>())
150                           / msdValues.size());
151     }
152     return msdSums;
153 }
154
155 /*! \brief Calculates 1,2, or 3D distance for two vectors.
156  *
157  * \todo Remove NOLINTs once clang-tidy is updated to v11, it should be able to handle constexpr.
158  *
159  * \tparam x If true, calculate x dimension of displacement
160  * \tparam y If true, calculate y dimension of displacement
161  * \tparam z If true, calculate z dimension of displacement
162  * \param c1 First point
163  * \param c2 Second point
164  * \return Euclidian distance for the given dimension.
165  */
166 template<bool x, bool y, bool z>
167 inline double calcSingleSquaredDistance(RVec c1, const RVec c2)
168 {
169     static_assert(x || y || z, "zero-dimensional MSD selected");
170     const DVec firstCoords  = c1.toDVec();
171     const DVec secondCoords = c2.toDVec();
172     double     result       = 0;
173     if constexpr (x)
174     {
175         result += (firstCoords[XX] - secondCoords[XX]) * (firstCoords[XX] - secondCoords[XX]);
176     }
177     if constexpr (y) // NOLINT(readability-misleading-indentation): clang-tidy-9 can't handle if constexpr (https://bugs.llvm.org/show_bug.cgi?id=32203)
178     {
179         result += (firstCoords[YY] - secondCoords[YY]) * (firstCoords[YY] - secondCoords[YY]);
180     }
181     if constexpr (z) // NOLINT(readability-misleading-indentation)
182     {
183         result += (firstCoords[ZZ] - secondCoords[ZZ]) * (firstCoords[ZZ] - secondCoords[ZZ]);
184     }
185     return result; // NOLINT(readability-misleading-indentation)
186 }
187
188 /*! \brief Calculate average displacement between sets of points
189  *
190  * Each displacement c1[i] - c2[i] is calculated and the distances
191  * are averaged.
192  *
193  * \tparam x If true, calculate x dimension of displacement
194  * \tparam y If true, calculate y dimension of displacement
195  * \tparam z If true, calculate z dimension of displacement
196  * \param c1 First vector
197  * \param c2 Second vector
198  * \param numberOfValues
199  * \return Per-particle averaged distance
200  */
201 template<bool x, bool y, bool z>
202 double calcAverageDisplacement(ArrayRef<const RVec> c1, ArrayRef<const RVec> c2)
203 {
204     double result = 0;
205     for (size_t i = 0; i < c1.size(); i++)
206     {
207         result += calcSingleSquaredDistance<x, y, z>(c1[i], c2[i]);
208     }
209     return result / c1.size();
210 }
211
212
213 //! Describes 1D MSDs, in the given dimension.
214 enum class SingleDimDiffType : int
215 {
216     X = 0,
217     Y,
218     Z,
219     Unused,
220     Count,
221 };
222
223 //! Describes 2D MSDs, in the plane normal to the given dimension.
224 enum class TwoDimDiffType : int
225 {
226     NormalToX = 0,
227     NormalToY,
228     NormalToZ,
229     Unused,
230     Count,
231 };
232
233 /*! \brief Removes jumps across periodic boundaries for currentFrame, based on the positions in
234  * previousFrame. Updates currentCoords in place.
235  */
236 void removePbcJumps(ArrayRef<RVec> currentCoords, ArrayRef<const RVec> previousCoords, t_pbc* pbc)
237 {
238     // There are two types of "pbc removal" in gmx msd. The first happens in the trajectoryanalysis
239     // framework, which makes molecules whole across periodic boundaries and is done
240     // automatically where the inputs support it. This lambda performs the second PBC correction, where
241     // any "jump" across periodic boundaries BETWEEN FRAMES is put back. The order of these
242     // operations is important - since the first transformation may only apply to part of a
243     // molecule (e.g., one half in/out of the box is put on one side of the box), the
244     // subsequent step needs to be applied to the molecule COM rather than individual atoms, or
245     // we'd have a clash where the per-mol PBC removal moves an atom that gets put back into
246     // it's original position by the second transformation. Therefore, this second transformation
247     // is applied *after* per molecule coordinates have been consolidated into COMs.
248     auto pbcRemover = [pbc](RVec in, RVec prev) {
249         rvec dx;
250         pbc_dx(pbc, in, prev, dx);
251         return prev + dx;
252     };
253     std::transform(
254             currentCoords.begin(), currentCoords.end(), previousCoords.begin(), currentCoords.begin(), pbcRemover);
255 }
256
257 //! Holds data needed for MSD calculations for a single molecule, if requested.
258 struct MoleculeData
259 {
260     //! Number of atoms in the molecule.
261     int atomCount = 0;
262     //! Total mass.
263     double mass = 0;
264     //! MSD accumulator and calculator for the molecule
265     MsdData msdData;
266     //! Calculated diffusion coefficient
267     real diffusionCoefficient = 0;
268 };
269
270 /*! \brief Handles coordinate operations for MSD calculations.
271  *
272  * Can be used to hold coordinates for individual atoms as well as molecules COMs. Handles PBC
273  * jump removal between consecutive frames.
274  *
275  * Only previous_ contains valid data between calls to buildCoordinates(), although both vectors
276  * are maintained at the correct size for the number of coordinates needed.
277  */
278 class MsdCoordinateManager
279 {
280 public:
281     MsdCoordinateManager(const int                    numAtoms,
282                          ArrayRef<const MoleculeData> molecules,
283                          ArrayRef<const int>          moleculeIndexMapping) :
284         current_(numAtoms),
285         previous_(numAtoms),
286         molecules_(molecules),
287         moleculeIndexMapping_(moleculeIndexMapping)
288     {
289     }
290     /*! \brief Prepares coordinates for the current frame.
291      *
292      * Reads in selection data, and returns an ArrayRef of the particle positions or molecule
293      * centers of mass (if the molecules input is not empty). Removes jumps across periodic
294      * boundaries based on the previous frame coordinates, except for the first frame built with
295      * builtCoordinates(), which has no previous frame as a reference.
296      *
297      * @param sel                   The selection object which holds coordinates
298      * @param molecules             Per-molecule description of atom counts and masses
299      * @param moleculeIndexMapping  Mapping of atom index to molecule index.
300      * @return                      The current frames coordinates in proper format.
301      */
302     ArrayRef<const RVec> buildCoordinates(const Selection& sel, t_pbc* pbc);
303
304 private:
305     //! The current coordinates.
306     std::vector<RVec> current_;
307     //! The previous frame's coordinates.
308     std::vector<RVec> previous_;
309     //! Molecule data.
310     ArrayRef<const MoleculeData> molecules_;
311     //! Mapping of atom indices to molecle indices;
312     ArrayRef<const int> moleculeIndexMapping_;
313     //! Tracks if a previous frame exists to compare with for PBC handling.
314     bool containsPreviousFrame_ = false;
315 };
316
317 ArrayRef<const RVec> MsdCoordinateManager::buildCoordinates(const Selection& sel, t_pbc* pbc)
318 {
319
320     if (molecules_.empty())
321     {
322         std::copy(sel.coordinates().begin(), sel.coordinates().end(), current_.begin());
323     }
324     else
325     {
326         // Prepare for molecule COM calculation, then sum up all positions per molecule.
327
328         std::fill(current_.begin(), current_.end(), RVec(0, 0, 0));
329         gmx::ArrayRef<const real> masses = sel.masses();
330         for (int i = 0; i < sel.posCount(); i++)
331         {
332             const int moleculeIndex = moleculeIndexMapping_[i];
333             // accumulate ri * mi, and do division at the end to minimize number of divisions.
334             current_[moleculeIndex] += RVec(sel.position(i).x()) * masses[i];
335         }
336         // Divide accumulated mass * positions to get COM.
337         std::transform(current_.begin(),
338                        current_.end(),
339                        molecules_.begin(),
340                        current_.begin(),
341                        [](const RVec& position, const MoleculeData& molecule) -> RVec {
342                            return position / molecule.mass;
343                        });
344     }
345
346     if (containsPreviousFrame_)
347     {
348         removePbcJumps(current_, previous_, pbc);
349     }
350     else
351     {
352         containsPreviousFrame_ = true;
353     }
354
355     // Previous is no longer needed, swap with current and return "current" coordinates which
356     // now reside in previous.
357     current_.swap(previous_);
358     return previous_;
359 }
360
361
362 //! Holds per-group coordinates, analysis, and results.
363 struct MsdGroupData
364 {
365     explicit MsdGroupData(const Selection&             inputSel,
366                           ArrayRef<const MoleculeData> molecules,
367                           ArrayRef<const int>          moleculeAtomMapping) :
368         sel(inputSel),
369         coordinateManager_(molecules.empty() ? sel.posCount() : molecules.size(), molecules, moleculeAtomMapping)
370     {
371     }
372
373     //! Selection associated with this group.
374     const Selection& sel;
375
376     //! Stored coordinates, indexed by frame then atom number.
377     std::vector<std::vector<RVec>> frames;
378
379     //! MSD result accumulator
380     MsdData msds;
381     //! Coordinate handler for the group.
382     MsdCoordinateManager coordinateManager_;
383     //! Collector for processed MSD averages per tau
384     std::vector<real> msdSums;
385     //! Fitted diffusion coefficient
386     real diffusionCoefficient = 0.0;
387     //! Uncertainty of diffusion coefficient
388     double sigma = 0.0;
389 };
390
391 } // namespace
392
393 /*! \brief Implements the gmx msd module
394  *
395  * \todo Implement -(no)mw. Right now, all calculations are mass-weighted with -mol, and not otherwise
396  * \todo Implement -tensor for full MSD tensor calculation
397  * \todo Implement -rmcomm for total-frame COM removal
398  * \todo Implement -pdb for molecule B factors
399  * \todo Implement -maxtau option proposed at https://gitlab.com/gromacs/gromacs/-/issues/3870
400  * \todo Update help text as options are added and clarifications decided on at https://gitlab.com/gromacs/gromacs/-/issues/3869
401  */
402 class Msd : public TrajectoryAnalysisModule
403 {
404 public:
405     Msd();
406
407     void initOptions(IOptionsContainer* options, TrajectoryAnalysisSettings* settings) override;
408     void optionsFinished(TrajectoryAnalysisSettings* settings) override;
409     void initAfterFirstFrame(const TrajectoryAnalysisSettings& settings, const t_trxframe& fr) override;
410     void initAnalysis(const TrajectoryAnalysisSettings& settings, const TopologyInformation& top) override;
411     void analyzeFrame(int                           frameNumber,
412                       const t_trxframe&             frame,
413                       t_pbc*                        pbc,
414                       TrajectoryAnalysisModuleData* pdata) override;
415     void finishAnalysis(int nframes) override;
416     void writeOutput() override;
417
418 private:
419     //! Selections for MSD output
420     SelectionList selections_;
421
422     //! MSD type information, for -type {x,y,z}
423     SingleDimDiffType singleDimType_ = SingleDimDiffType::Unused;
424     //! MSD type information, for -lateral {x,y,z}
425     TwoDimDiffType twoDimType_ = TwoDimDiffType::Unused;
426     //! Diffusion coefficient conversion factor
427     double diffusionCoefficientDimensionFactor_ = c_3DdiffusionDimensionFactor;
428     //! Method used to calculate MSD - changes based on dimensonality.
429     std::function<double(ArrayRef<const RVec>, ArrayRef<const RVec>)> calcMsd_ =
430             calcAverageDisplacement<true, true, true>;
431
432     //! Picoseconds between restarts
433     double trestart_ = 10.0;
434     //! Initial time
435     double t0_ = 0;
436     //! Inter-frame delta-t
437     std::optional<double> dt_ = std::nullopt;
438
439     //! First tau value to fit from for diffusion coefficient, defaults to 0.1 * max tau
440     real beginFit_ = -1.0;
441     //! Final tau value to fit to for diffusion coefficient, defaults to 0.9 * max tau
442     real endFit_ = -1.0;
443
444     //! All selection group-specific data stored here.
445     std::vector<MsdGroupData> groupData_;
446
447     //! Time of all frames.
448     std::vector<double> times_;
449     //! Taus for output - won't know the size until the end.
450     std::vector<double> taus_;
451     //! Tau indices for fitting.
452     size_t beginFitIndex_ = 0;
453     size_t endFitIndex_   = 0;
454
455     // MSD per-molecule stuff
456     //! Are we doing molecule COM-based MSDs?
457     bool molSelected_ = false;
458     //! Per molecule topology information and MSD accumulators.
459     std::vector<MoleculeData> molecules_;
460     //! Atom index -> mol index map
461     std::vector<int> moleculeIndexMappings_;
462
463     // Output stuff
464     AnalysisData msdPlotData_;
465     AnalysisData msdMoleculePlotData_;
466
467     AnalysisDataPlotSettings plotSettings_;
468     //! Per-tau MSDs for each selected group
469     std::string output_;
470     //! Per molecule diffusion coefficients if -mol is selected.
471     std::string moleculeOutput_;
472 };
473
474
475 Msd::Msd() = default;
476
477 void Msd::initOptions(IOptionsContainer* options, TrajectoryAnalysisSettings* settings)
478 {
479     static const char* const desc[] = {
480         "[THISMODULE] computes the mean square displacement (MSD) of atoms from",
481         "a set of initial positions. This provides an easy way to compute",
482         "the diffusion constant using the Einstein relation.",
483         "The time between the reference points for the MSD calculation",
484         "is set with [TT]-trestart[tt].",
485         "The diffusion constant is calculated by least squares fitting a",
486         "straight line (D*t + c) through the MSD(t) from [TT]-beginfit[tt] to",
487         "[TT]-endfit[tt] (note that t is time from the reference positions,",
488         "not simulation time). An error estimate given, which is the difference",
489         "of the diffusion coefficients obtained from fits over the two halves",
490         "of the fit interval.[PAR]",
491         "There are three, mutually exclusive, options to determine different",
492         "types of mean square displacement: [TT]-type[tt], [TT]-lateral[tt]",
493         "and [TT]-ten[tt]. Option [TT]-ten[tt] writes the full MSD tensor for",
494         "each group, the order in the output is: trace xx yy zz yx zx zy.[PAR]",
495         "If [TT]-mol[tt] is set, [THISMODULE] plots the MSD for individual molecules",
496         "(including making molecules whole across periodic boundaries): ",
497         "for each individual molecule a diffusion constant is computed for ",
498         "its center of mass. The chosen index group will be split into ",
499         "molecules. With -mol, only one index group can be selected.[PAR]",
500         "The diffusion coefficient is determined by linear regression of the MSD.",
501         "When [TT]-beginfit[tt] is -1, fitting starts at 10%",
502         "and when [TT]-endfit[tt] is -1, fitting goes to 90%.",
503         "Using this option one also gets an accurate error estimate",
504         "based on the statistics between individual molecules.",
505         "Note that this diffusion coefficient and error estimate are only",
506         "accurate when the MSD is completely linear between",
507         "[TT]-beginfit[tt] and [TT]-endfit[tt].[PAR]",
508     };
509     settings->setHelpText(desc);
510
511     // Selections
512     options->addOption(SelectionOption("sel")
513                                .storeVector(&selections_)
514                                .required()
515                                .onlyStatic()
516                                .multiValue()
517                                .description("Selections to compute MSDs for from the reference"));
518
519     // Select MSD type - defaults to 3D if neither option is selected.
520     EnumerationArray<SingleDimDiffType, const char*> enumTypeNames    = { "x", "y", "z", "unused" };
521     EnumerationArray<TwoDimDiffType, const char*>    enumLateralNames = { "x", "y", "z", "unused" };
522     options->addOption(EnumOption<SingleDimDiffType>("type")
523                                .enumValue(enumTypeNames)
524                                .store(&singleDimType_)
525                                .defaultValue(SingleDimDiffType::Unused));
526     options->addOption(EnumOption<TwoDimDiffType>("lateral")
527                                .enumValue(enumLateralNames)
528                                .store(&twoDimType_)
529                                .defaultValue(TwoDimDiffType::Unused));
530
531     options->addOption(DoubleOption("trestart")
532                                .description("Time between restarting points in trajectory (ps)")
533                                .defaultValue(10.0)
534                                .store(&trestart_));
535     options->addOption(RealOption("beginfit").description("").store(&beginFit_));
536     options->addOption(RealOption("endfit").description("").store(&endFit_));
537
538     // Output options
539     options->addOption(FileNameOption("o")
540                                .filetype(eftPlot)
541                                .outputFile()
542                                .store(&output_)
543                                .defaultBasename("msdout")
544                                .description("MSD output"));
545     options->addOption(
546             FileNameOption("mol")
547                     .filetype(eftPlot)
548                     .outputFile()
549                     .store(&moleculeOutput_)
550                     .storeIsSet(&molSelected_)
551                     .defaultBasename("diff_mol")
552                     .description("Report diffusion coefficients for each molecule in selection"));
553 }
554
555 void Msd::optionsFinished(TrajectoryAnalysisSettings gmx_unused* settings)
556 {
557     if (singleDimType_ != SingleDimDiffType::Unused && twoDimType_ != TwoDimDiffType::Unused)
558     {
559         std::string errorMessage =
560                 "Options -type and -lateral are mutually exclusive. Choose one or neither (for 3D "
561                 "MSDs).";
562         GMX_THROW(InconsistentInputError(errorMessage.c_str()));
563     }
564     if (selections_.size() > 1 && molSelected_)
565     {
566         std::string errorMessage =
567                 "Cannot have multiple groups selected with -sel when using -mol.";
568         GMX_THROW(InconsistentInputError(errorMessage.c_str()));
569     }
570 }
571
572
573 void Msd::initAnalysis(const TrajectoryAnalysisSettings& settings, const TopologyInformation& top)
574 {
575     plotSettings_ = settings.plotSettings();
576
577     // Enumeration helpers for dispatching the right MSD calculation type.
578     const EnumerationArray<SingleDimDiffType, decltype(&calcAverageDisplacement<true, true, true>)>
579             oneDimensionalMsdFunctions = { &calcAverageDisplacement<true, false, false>,
580                                            &calcAverageDisplacement<false, true, false>,
581                                            &calcAverageDisplacement<false, false, true> };
582     const EnumerationArray<TwoDimDiffType, decltype(&calcAverageDisplacement<true, true, true>)>
583             twoDimensionalMsdFunctions = { calcAverageDisplacement<false, true, true>,
584                                            calcAverageDisplacement<true, false, true>,
585                                            calcAverageDisplacement<true, true, false> };
586
587     // Parse dimensionality and assign the MSD calculating function.
588     // Note if we don't hit either of these cases, we're computing 3D MSDs.
589     if (singleDimType_ != SingleDimDiffType::Unused)
590     {
591         calcMsd_                             = oneDimensionalMsdFunctions[singleDimType_];
592         diffusionCoefficientDimensionFactor_ = c_1DdiffusionDimensionFactor;
593     }
594     else if (twoDimType_ != TwoDimDiffType::Unused)
595     {
596         calcMsd_                             = twoDimensionalMsdFunctions[twoDimType_];
597         diffusionCoefficientDimensionFactor_ = c_2DdiffusionDimensionFactor;
598     }
599
600     // TODO validate that we have mol info and not atom only - and masses, and topology.
601     if (molSelected_)
602     {
603         Selection& sel  = selections_[0];
604         const int  nMol = sel.initOriginalIdsToGroup(top.mtop(), INDEX_MOL);
605
606         gmx::ArrayRef<const int> mappedIds = selections_[0].mappedIds();
607         moleculeIndexMappings_.resize(selections_[0].posCount());
608         std::copy(mappedIds.begin(), mappedIds.end(), moleculeIndexMappings_.begin());
609
610         // Precalculate each molecules mass for speeding up COM calculations.
611         ArrayRef<const real> masses = sel.masses();
612
613         molecules_.resize(nMol);
614         for (int i = 0; i < sel.posCount(); i++)
615         {
616             molecules_[mappedIds[i]].atomCount++;
617             molecules_[mappedIds[i]].mass += masses[i];
618         }
619     }
620
621     // Accumulated frames and results
622     for (const Selection& sel : selections_)
623     {
624         groupData_.emplace_back(sel, molecules_, moleculeIndexMappings_);
625     }
626 }
627
628 void Msd::initAfterFirstFrame(const TrajectoryAnalysisSettings gmx_unused& settings, const t_trxframe& fr)
629 {
630     t0_ = std::round(fr.time);
631 }
632
633
634 void Msd::analyzeFrame(int gmx_unused                frameNumber,
635                        const t_trxframe&             frame,
636                        t_pbc*                        pbc,
637                        TrajectoryAnalysisModuleData* pdata)
638 {
639     const real time = std::round(frame.time);
640     // Need to populate dt on frame 2;
641     if (!dt_.has_value() && !times_.empty())
642     {
643         dt_ = time - times_[0];
644     }
645
646     // Each frame gets an entry in times, but frameTimes only updates if we're at a restart.
647     times_.push_back(time);
648
649     // Each frame will get a tau between it and frame 0, and all other frame combos should be
650     // covered by this.
651     // \todo this will no longer hold exactly when maxtau is added
652     taus_.push_back(time - times_[0]);
653
654     for (MsdGroupData& msdData : groupData_)
655     {
656         //NOLINTNEXTLINE(readability-static-accessed-through-instance)
657         const Selection& sel = pdata->parallelSelection(msdData.sel);
658
659         ArrayRef<const RVec> coords = msdData.coordinateManager_.buildCoordinates(sel, pbc);
660
661         // For each preceding frame, calculate tau and do comparison.
662         for (size_t i = 0; i < msdData.frames.size(); i++)
663         {
664             // If dt > trestart, the tau increment will be dt rather than trestart.
665             double  tau      = time - (t0_ + std::max<double>(trestart_, *dt_) * i);
666             int64_t tauIndex = gmx::roundToInt64(tau / *dt_);
667             msdData.msds[tauIndex].push_back(calcMsd_(coords, msdData.frames[i]));
668
669             for (size_t molInd = 0; molInd < molecules_.size(); molInd++)
670             {
671                 molecules_[molInd].msdData[tauIndex].push_back(
672                         calcMsd_(arrayRefFromArray(&coords[molInd], 1),
673                                  arrayRefFromArray(&msdData.frames[i][molInd], 1)));
674             }
675         }
676
677
678         // We only store the frame for the future if it's a restart per -trestart.
679         if (bRmod(time, t0_, trestart_))
680         {
681             msdData.frames.emplace_back(coords.begin(), coords.end());
682         }
683     }
684 }
685
686 //! Calculate the tau index for fitting. If userFitTau < 0, uses the default fraction of max tau.
687 static size_t calculateFitIndex(const int    userFitTau,
688                                 const double defaultTauFraction,
689                                 const int    numTaus,
690                                 const double dt)
691 {
692     if (userFitTau < 0)
693     {
694         return gmx::roundToInt((numTaus - 1) * defaultTauFraction);
695     }
696     return std::min<size_t>(numTaus - 1, gmx::roundToInt(static_cast<double>(userFitTau) / dt));
697 }
698
699
700 void Msd::finishAnalysis(int gmx_unused nframes)
701 {
702     static constexpr double c_defaultStartFitIndexFraction = 0.1;
703     static constexpr double c_defaultEndFitIndexFraction   = 0.9;
704     beginFitIndex_ = calculateFitIndex(beginFit_, c_defaultStartFitIndexFraction, taus_.size(), *dt_);
705     endFitIndex_   = calculateFitIndex(endFit_, c_defaultEndFitIndexFraction, taus_.size(), *dt_);
706     const int numTausForFit = 1 + endFitIndex_ - beginFitIndex_;
707
708     // These aren't used, except for correlationCoefficient, which is used to estimate error if
709     // enough points are available.
710     real b = 0.0, correlationCoefficient = 0.0, chiSquared = 0.0;
711
712     for (MsdGroupData& msdData : groupData_)
713     {
714         msdData.msdSums = msdData.msds.averageMsds();
715
716         if (numTausForFit >= 4)
717         {
718             const int halfNumTaus         = numTausForFit / 2;
719             const int secondaryStartIndex = beginFitIndex_ + halfNumTaus;
720             // Split the fit in 2, and compare the results of each fit;
721             real a = 0.0, a2 = 0.0;
722             lsq_y_ax_b_xdouble(halfNumTaus,
723                                &taus_[beginFitIndex_],
724                                &msdData.msdSums[beginFitIndex_],
725                                &a,
726                                &b,
727                                &correlationCoefficient,
728                                &chiSquared);
729             lsq_y_ax_b_xdouble(halfNumTaus,
730                                &taus_[secondaryStartIndex],
731                                &msdData.msdSums[secondaryStartIndex],
732                                &a2,
733                                &b,
734                                &correlationCoefficient,
735                                &chiSquared);
736             msdData.sigma = std::abs(a - a2);
737         }
738         lsq_y_ax_b_xdouble(numTausForFit,
739                            &taus_[beginFitIndex_],
740                            &msdData.msdSums[beginFitIndex_],
741                            &msdData.diffusionCoefficient,
742                            &b,
743                            &correlationCoefficient,
744                            &chiSquared);
745         msdData.diffusionCoefficient *= c_diffusionConversionFactor / diffusionCoefficientDimensionFactor_;
746         msdData.sigma *= c_diffusionConversionFactor / diffusionCoefficientDimensionFactor_;
747     }
748
749     for (MoleculeData& molecule : molecules_)
750     {
751         std::vector<real> msds = molecule.msdData.averageMsds();
752         lsq_y_ax_b_xdouble(numTausForFit,
753                            &taus_[beginFitIndex_],
754                            &msds[beginFitIndex_],
755                            &molecule.diffusionCoefficient,
756                            &b,
757                            &correlationCoefficient,
758                            &chiSquared);
759         molecule.diffusionCoefficient *= c_diffusionConversionFactor / diffusionCoefficientDimensionFactor_;
760     }
761 }
762
763 void Msd::writeOutput()
764 {
765     // AnalysisData currently doesn't support changing column counts after analysis has started.
766     // We can't determine the number of tau values until the trajectory is fully read, so analysis
767     // data construction and plotting are done here.
768     AnalysisDataPlotModulePointer msdPlotModule(new AnalysisDataPlotModule(plotSettings_));
769     msdPlotModule->setFileName(output_);
770     msdPlotModule->setTitle("Mean Squared Displacement");
771     msdPlotModule->setXLabel("tau (ps)");
772     msdPlotModule->setYLabel(R"(MSD (nm\\S2\\N))");
773     msdPlotModule->setYFormat(10, 6, 'g');
774     for (const auto& group : groupData_)
775     {
776         const real D = group.diffusionCoefficient;
777         if (D > 0.01 && D < 1e4)
778         {
779             msdPlotModule->appendLegend(formatString(
780                     "D[%10s] = %.4f (+/- %.4f) (1e-5 cm^2/s)", group.sel.name(), D, group.sigma));
781         }
782         else
783         {
784             msdPlotModule->appendLegend(formatString(
785                     "D[%10s] = %.4g (+/- %.4f) (1e-5 cm^2/s)", group.sel.name(), D, group.sigma));
786         }
787     }
788     msdPlotData_.addModule(msdPlotModule);
789     msdPlotData_.setDataSetCount(groupData_.size());
790     for (size_t i = 0; i < groupData_.size(); i++)
791     {
792         msdPlotData_.setColumnCount(i, 1);
793     }
794     AnalysisDataHandle dh = msdPlotData_.startData({});
795     for (size_t tauIndex = 0; tauIndex < taus_.size(); tauIndex++)
796     {
797         dh.startFrame(tauIndex, taus_[tauIndex]);
798         for (size_t dataSetIndex = 0; dataSetIndex < groupData_.size(); dataSetIndex++)
799         {
800             dh.selectDataSet(dataSetIndex);
801             dh.setPoint(0, groupData_[dataSetIndex].msdSums[tauIndex]);
802         }
803         dh.finishFrame();
804     }
805     dh.finishData();
806
807     if (molSelected_)
808     {
809         AnalysisDataPlotModulePointer molPlotModule(new AnalysisDataPlotModule(plotSettings_));
810         molPlotModule->setFileName(moleculeOutput_);
811         molPlotModule->setTitle("Mean Squared Displacement / Molecule");
812         molPlotModule->setXLabel("Molecule");
813         molPlotModule->setYLabel("D(1e-5 cm^2/s)");
814         molPlotModule->setYFormat(10, 0, 'g');
815         msdMoleculePlotData_.addModule(molPlotModule);
816         msdMoleculePlotData_.setDataSetCount(1);
817         msdMoleculePlotData_.setColumnCount(0, 1);
818         AnalysisDataHandle molDh = msdMoleculePlotData_.startData({});
819         for (size_t moleculeIndex = 0; moleculeIndex < molecules_.size(); moleculeIndex++)
820         {
821             molDh.startFrame(moleculeIndex, moleculeIndex);
822             molDh.setPoint(0, molecules_[moleculeIndex].diffusionCoefficient);
823             molDh.finishFrame();
824         }
825         molDh.finishData();
826     }
827 }
828
829
830 const char                      MsdInfo::name[]             = "msd";
831 const char                      MsdInfo::shortDescription[] = "Compute mean squared displacements";
832 TrajectoryAnalysisModulePointer MsdInfo::create()
833 {
834     return TrajectoryAnalysisModulePointer(new Msd);
835 }
836
837
838 } // namespace analysismodules
839 } // namespace gmx