2 * \brief Code to implement the potential declared in ensemblepotential.h
4 * This file currently contains boilerplate that will not be necessary in future gmxapi releases, as
5 * well as additional code used in implementing the restrained ensemble example workflow.
7 * A simpler restraint potential would only update the calculate() function. If a callback function is
8 * not needed or desired, remove the callback() code from this file and from ensemblepotential.h
10 * \author M. Eric Irrgang <ericirrgang@gmail.com>
13 #include "ensemblepotential.h"
21 #include "gmxapi/context.h"
22 #include "gmxapi/session.h"
23 #include "gmxapi/md/mdsignals.h"
25 #include "sessionresources.h"
31 * \brief Discretize a density field on a grid.
33 * Apply a Gaussian blur when building a density grid for a list of values.
34 * Normalize such that the area under each sample is 1.0/num_samples.
40 * \brief Construct the blurring functor.
42 * \param low The coordinate value of the first grid point.
43 * \param gridSpacing Distance between grid points.
44 * \param sigma Gaussian parameter for blurring inputs onto the grid.
46 BlurToGrid(double low
,
50 binWidth_
{gridSpacing
},
56 * \brief Callable for the functor.
58 * \param samples A list of values to be blurred onto the grid.
59 * \param grid Pointer to the container into which to accumulate a blurred histogram of samples.
63 * # Acquire 3 samples to be discretized with blurring.
64 * std::vector<double> someData = {3.7, 8.1, 4.2};
66 * # Create an empty grid to store magnitudes for points 0.5, 1.0, ..., 10.0.
67 * std::vector<double> histogram(20, 0.);
69 * # Specify the above grid and a Gaussian parameter of 0.8.
70 * auto blur = BlurToGrid(0.5, 0.5, 0.8);
72 * # Collect the density grid for the samples.
73 * blur(someData, &histogram);
76 void operator()(const std::vector
<double>& samples
,
77 std::vector
<double>* grid
)
79 const auto nbins
= grid
->size();
80 const double& dx
{binWidth_
};
81 const auto num_samples
= samples
.size();
83 const double denominator
= 1.0 / (2 * sigma_
* sigma_
);
84 const double normalization
= 1.0 / (num_samples
* sqrt(2.0 * M_PI
* sigma_
* sigma_
));
85 // We aren't doing any filtering of values too far away to contribute meaningfully, which
86 // is admittedly wasteful for large sigma...
87 for (size_t i
= 0;i
< nbins
;++i
)
90 const double bin_x
{low_
+ i
* dx
};
91 for (const auto distance
: samples
)
93 const double relative_distance
{bin_x
- distance
};
94 const auto numerator
= -relative_distance
* relative_distance
;
95 bin_value
+= normalization
* exp(numerator
* denominator
);
97 grid
->at(i
) = bin_value
;
102 /// Minimum value of bin zero
106 const double binWidth_
;
112 EnsemblePotential::EnsemblePotential(size_t nbins
,
116 PairHist experimental
,
117 unsigned int nSamples
,
119 unsigned int nWindows
,
128 experimental_
{std::move(experimental
)},
131 samplePeriod_
{samplePeriod
},
132 // In actuality, we have nsamples at (samplePeriod - dt), but we don't have access to dt.
133 nextSampleTime_
{samplePeriod
},
134 distanceSamples_(nSamples
),
138 nextWindowUpdateTime_
{nSamples
* samplePeriod
},
144 EnsemblePotential::EnsemblePotential(const input_param_type
& params
) :
145 EnsemblePotential(params
.nBins
,
160 // HERE is the (optional) function that updates the state of the restraint periodically.
161 // It is called before calculate() once per timestep per simulation (on the master rank of
162 // a parallelized simulation).
165 void EnsemblePotential::callback(gmx::Vector v
,
168 const Resources
& resources
)
170 const auto rdiff
= v
- v0
;
171 const auto Rsquared
= dot(rdiff
,
173 const auto R
= sqrt(Rsquared
);
175 // Store historical data every sample_period steps
176 if (t
>= nextSampleTime_
)
178 distanceSamples_
[currentSample_
++] = R
;
179 nextSampleTime_
= (currentSample_
+ 1) * samplePeriod_
+ windowStartTime_
;
183 // 0. Drop oldest window
184 // 1. Reduce historical data for this restraint in this simulation.
185 // 2. Call out to the global reduction for this window.
186 // 3. On update, checkpoint the historical data source.
187 // 4. Update historic windows.
188 // 5. Use handles retained from previous windows to reconstruct the smoothed working histogram
189 if (t
>= nextWindowUpdateTime_
)
191 // Get next histogram array, recycling old one if available.
192 std::unique_ptr
<Matrix
<double>> new_window
= std::make_unique
<Matrix
<double>>(1,
194 std::unique_ptr
<Matrix
<double>> temp_window
;
195 if (windows_
.size() == nWindows_
)
197 // Recycle the oldest window.
198 // \todo wrap this in a helper class that manages a buffer we can shuffle through.
199 windows_
[0].swap(temp_window
);
200 windows_
.erase(windows_
.begin());
204 auto new_temp_window
= std::make_unique
<Matrix
<double>>(1,
206 assert(new_temp_window
);
207 temp_window
.swap(new_temp_window
);
210 // Reduce sampled data for this restraint in this simulation, applying a Gaussian blur to fill a grid.
211 auto blur
= BlurToGrid(0.0,
214 assert(new_window
!= nullptr);
215 assert(distanceSamples_
.size() == nSamples_
);
216 assert(currentSample_
== nSamples_
);
217 blur(distanceSamples_
,
218 new_window
->vector());
219 // We can just do the blur locally since there aren't many bins. Bundling these operations for
220 // all restraints could give us a chance at some parallelism. We should at least use some
221 // threading if we can.
223 // We request a handle each time before using resources to make error handling easier if there is a failure in
224 // one of the ensemble member processes and to give more freedom to how resources are managed from step to step.
225 auto ensemble
= resources
.getHandle();
226 // Get global reduction (sum) and checkpoint.
227 assert(temp_window
!= nullptr);
228 // Todo: in reduce function, give us a mean instead of a sum.
229 ensemble
.reduce(*new_window
,
232 // Update window list with smoothed data.
233 windows_
.emplace_back(std::move(new_window
));
235 // Get new histogram difference. Subtract the experimental distribution to get the values to use in our potential.
236 for (auto& bin
: histogram_
)
240 for (const auto& window
: windows_
)
242 for (size_t i
= 0;i
< window
->cols();++i
)
244 histogram_
.at(i
) += (window
->vector()->at(i
) - experimental_
.at(i
)) / windows_
.size();
249 // Note we do not have the integer timestep available here. Therefore, we can't guarantee that updates occur
250 // with the same number of MD steps in each interval, and the interval will effectively lose digits as the
251 // simulation progresses, so _update_period should be cleanly representable in binary. When we extract this
252 // to a facility, we can look for a part of the code with access to the current timestep.
253 windowStartTime_
= t
;
254 nextWindowUpdateTime_
= nSamples_
* samplePeriod_
+ windowStartTime_
;
255 ++currentWindow_
; // This is currently never used. I'm not sure it will be, either...
257 // Reset sample bufering.
259 // Reset sample times.
260 nextSampleTime_
= t
+ samplePeriod_
;
268 // HERE is the function that does the calculation of the restraint force.
271 gmx::PotentialPointData
EnsemblePotential::calculate(gmx::Vector v
,
275 // This is not the vector from v to v0. It is the position of a site
276 // at v, relative to the origin v0. This is a potentially confusing convention...
277 const auto rdiff
= v
- v0
;
278 const auto Rsquared
= dot(rdiff
,
280 const auto R
= sqrt(Rsquared
);
284 gmx::PotentialPointData output
;
285 // Energy not needed right now.
286 // output.energy = 0;
288 if (R
!= 0) // Direction of force is ill-defined when v == v0
295 // apply a force to reduce R
296 f
= k_
* (maxDist_
- R
);
298 else if (R
< minDist_
)
300 // apply a force to increase R
301 f
= k_
* (minDist_
- R
);
307 const size_t numBins
= histogram_
.size();
308 double normConst
= sqrt(2 * M_PI
) * sigma_
* sigma_
* sigma_
;
310 for (size_t n
= 0;n
< numBins
;n
++)
312 const double x
{n
* binWidth_
- R
};
313 const double argExp
{-0.5 * x
* x
/ (sigma_
* sigma_
)};
314 f_scal
+= histogram_
.at(n
) * exp(argExp
) * x
/ normConst
;
319 const auto magnitude
= f
/ norm(rdiff
);
320 output
.force
= rdiff
* static_cast<decltype(rdiff
[0])>(magnitude
);
325 std::unique_ptr
<ensemble_input_param_type
>
326 makeEnsembleParams(size_t nbins
,
330 const std::vector
<double>& experimental
,
331 unsigned int nSamples
,
333 unsigned int nWindows
,
337 using std::make_unique
;
338 auto params
= make_unique
<ensemble_input_param_type
>();
339 params
->nBins
= nbins
;
340 params
->binWidth
= binWidth
;
341 params
->minDist
= minDist
;
342 params
->maxDist
= maxDist
;
343 params
->experimental
= experimental
;
344 params
->nSamples
= nSamples
;
345 params
->samplePeriod
= samplePeriod
;
346 params
->nWindows
= nWindows
;
348 params
->sigma
= sigma
;
353 // Important: Explicitly instantiate a definition for the templated class declared in ensemblepotential.h.
354 // Failing to do this will cause a linker error.
356 class ::plugin::RestraintModule
<EnsembleRestraint
>;
358 } // end namespace plugin