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