f621949245d4ec06c1ed2f05ef441710d3962b0f
[alexxy/gromacs.git] / src / gromacs / mdtypes / tests / observablesreducer.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  * Tests for ObservablesReducer.
38  *
39  * \ingroup module_mdtypes
40  */
41 #include "gmxpre.h"
42
43 #include "gromacs/mdtypes/observablesreducer.h"
44
45 #include <numeric>
46 #include <optional>
47 #include <tuple>
48
49 #include <gmock/gmock.h>
50 #include <gtest/gtest.h>
51
52 #include "gromacs/utility/arrayref.h"
53 #include "gromacs/utility/basedefinitions.h"
54 #include "gromacs/utility/stringutil.h"
55
56 namespace gmx::test
57 {
58 namespace
59 {
60
61 TEST(ObservablesReducerTest, CanMoveAssign)
62 {
63     ObservablesReducerBuilder builder;
64     ObservablesReducer        observablesReducer = builder.build();
65     EXPECT_TRUE(observablesReducer.communicationBuffer().empty())
66             << "no buffer available when no subscribers requested reduction";
67 }
68
69 TEST(ObservablesReducerTest, CanMoveConstruct)
70 {
71     ObservablesReducerBuilder builder;
72     ObservablesReducer        observablesReducerOriginal = builder.build();
73     ObservablesReducer        observablesReducer(std::move(observablesReducerOriginal));
74     EXPECT_TRUE(observablesReducer.communicationBuffer().empty())
75             << "no buffer available when no subscribers requested reduction";
76 }
77
78 TEST(ObservablesReducerTest, CanBuildAndUseWithNoSubscribers)
79 {
80     ObservablesReducerBuilder builder;
81
82     ObservablesReducer observablesReducer = builder.build();
83     EXPECT_TRUE(observablesReducer.communicationBuffer().empty())
84             << "no buffer available when no subscribers requested reduction";
85     observablesReducer.reductionComplete(0);
86
87     EXPECT_TRUE(observablesReducer.communicationBuffer().empty())
88             << "no buffer available after reductionComplete()";
89     observablesReducer.stepComplete();
90 }
91
92 TEST(ObservablesReducerTest, CanBuildAndUseWithOneSubscriber)
93 {
94     ObservablesReducerBuilder builder;
95
96     // This test implements the caller, the builder and the
97     // ObservablesReducer all in the one scope, which likely does not
98     // resemble any actual use case. More realistic test cases are
99     // found below.
100
101     std::optional<int>                                    stepUponWhichReductionOccured;
102     ObservablesReducerBuilder::CallbackToRequireReduction callbackToRequireReduction;
103     ArrayRef<double>                                      bufferView;
104     ObservablesReducerBuilder::CallbackFromBuilder        callbackFromBuilder =
105             [&](ObservablesReducerBuilder::CallbackToRequireReduction&& c, ArrayRef<double> b) {
106                 callbackToRequireReduction = std::move(c);
107                 bufferView                 = b;
108             };
109
110     ObservablesReducerBuilder::CallbackAfterReduction callbackAfterReduction =
111             [&stepUponWhichReductionOccured](Step step) { stepUponWhichReductionOccured = step; };
112     const int requiredBufferSize = 2;
113     builder.addSubscriber(
114             requiredBufferSize, std::move(callbackFromBuilder), std::move(callbackAfterReduction));
115
116     ObservablesReducer observablesReducer = builder.build();
117     EXPECT_TRUE(observablesReducer.communicationBuffer().empty())
118             << "no buffer available when no subscribers requested reduction";
119     ASSERT_EQ(requiredBufferSize, bufferView.size());
120     ASSERT_NE(callbackToRequireReduction, nullptr)
121             << "must have valid callback supplied by the builder";
122     EXPECT_FALSE(stepUponWhichReductionOccured.has_value())
123             << "no callbacks until reductionComplete() is called";
124
125     // Fill some dummy data, so we can check the zeroing later
126     bufferView[0] = 3.0;
127     bufferView[1] = 4.0;
128
129     {
130         SCOPED_TRACE("Test that ReductionRequirement::Eventually doesn't trigger behavior");
131
132         EXPECT_EQ(callbackToRequireReduction(ReductionRequirement::Eventually),
133                   ObservablesReducerStatus::ReadyToReduce);
134         EXPECT_TRUE(observablesReducer.communicationBuffer().empty())
135                 << "no buffer available when the only subscribers requested reduction eventually";
136         EXPECT_FALSE(stepUponWhichReductionOccured.has_value())
137                 << "no callbacks until reductionComplete() is called";
138
139         // Note that there's nothing else to check here, because the
140         // empty buffer means that no reduction should take place.
141         observablesReducer.stepComplete();
142     }
143     {
144         SCOPED_TRACE("Test that ReductionRequirement::Soon does trigger behavior");
145
146         EXPECT_EQ(callbackToRequireReduction(ReductionRequirement::Soon),
147                   ObservablesReducerStatus::ReadyToReduce);
148         EXPECT_EQ(observablesReducer.communicationBuffer().size(), requiredBufferSize)
149                 << "buffer available when a subscriber requested reduction soon";
150         EXPECT_FALSE(stepUponWhichReductionOccured.has_value())
151                 << "no callbacks until reductionComplete() is called";
152
153         // In the intended use case, some external component must do
154         // the actual reduction across ranks using the buffer at this
155         // point. Here, we just pretend it happened.
156
157         int step = 2;
158         observablesReducer.reductionComplete(step);
159         ASSERT_TRUE(stepUponWhichReductionOccured.has_value()) << "reduction callbacks took place";
160         EXPECT_EQ(stepUponWhichReductionOccured.value(), step)
161                 << "reduction step is passed through correctly";
162         EXPECT_THAT(bufferView, testing::AllOf(testing::SizeIs(requiredBufferSize), testing::Each(0.0)))
163                 << "buffer is zeroed after reduction";
164         observablesReducer.stepComplete();
165     }
166 }
167
168 // Integration tests of ObservablesReducer, builder, and fake
169 // subscriber(s). These will model multiple ranks each with multiple
170 // subscribers. Building tests that used actual MPI would be extra
171 // complexity that is not needed at this time.
172
173 //! Helper class that models an MD module that needs to make a subscription to \c ObservablesReducer
174 class Subscriber
175 {
176 public:
177     //! Ensure that each subscriber sends an interesting amount of data
178     static constexpr int s_subscriberBufferMinimumSize = 3;
179     /*! \brief Base value used to ensure the data reduced by each
180      * subscriber is distinct, to help diagnose bugs.
181      *
182      * Also contributes to ensuring that the reduced total is
183      * never zero.
184      *
185      * Note that in a real use case, the subscribers will generally be
186      * located in multiple modules. */
187     static constexpr double s_subscriberOffset = 1000;
188     //! Constructor
189     Subscriber(int subscriberIndex, int numRanks) :
190         // Ensure each subscriber sends a different amount of data, to expose bugs
191         sizeRequired_(s_subscriberBufferMinimumSize + subscriberIndex),
192         // Ensure each subscriber sends a distinct range of data, to expose bugs
193         valueOffset_(s_subscriberOffset * (subscriberIndex + 1)),
194         numRanks_(numRanks),
195         subscriberIndex_(subscriberIndex)
196     {
197     }
198
199     //! Make the subscription via the \c observablesReducerBuilder
200     void makeSubscription(ObservablesReducerBuilder* observablesReducerBuilder)
201     {
202         observablesReducerBuilder->addSubscriber(
203                 sizeRequired_,
204                 [this](ObservablesReducerBuilder::CallbackToRequireReduction callback,
205                        ArrayRef<double>                                      bufferView) {
206                     this->callbackWhenBufferAvailable(std::move(callback), bufferView);
207                 },
208                 [this](Step step) { this->callbackAfterReduction(step); });
209     }
210
211     //! Callback to receive data from the builder
212     void callbackWhenBufferAvailable(ObservablesReducerBuilder::CallbackToRequireReduction&& callbackToRequireReduction,
213                                      ArrayRef<double> bufferView)
214     {
215         SCOPED_TRACE("In callback from builder");
216
217         callbackToRequireReduction_ = std::move(callbackToRequireReduction);
218         communicationBuffer_        = bufferView;
219         EXPECT_THAT(communicationBuffer_, testing::AllOf(testing::SizeIs(sizeRequired_), testing::Each(0.0)))
220                 << "size of buffer did not match request";
221     }
222
223     //! Pretend to do some simulation work characteristic of \c step
224     void doSimulationWork(Step step, ReductionRequirement reductionRequirement) const
225     {
226         // In a real case, MD simulation work for this PP rank and
227         // step would go here.
228         // ...
229         // Then we put values that model its intermediate output into
230         // the communication buffer. Those values vary with the step,
231         // so that we can test for correctness over multiple reduction
232         // events.
233         std::iota(communicationBuffer_.begin(), communicationBuffer_.end(), valueOffset_ + double(step));
234         // Then we require reduction.
235         EXPECT_EQ(callbackToRequireReduction_(reductionRequirement), ObservablesReducerStatus::ReadyToReduce);
236     }
237
238     //! After the reduction, check the values for this subscriber are as expected
239     void callbackAfterReduction(Step step)
240     {
241         SCOPED_TRACE(formatString("In callback after reduction for subscriber %d", subscriberIndex_));
242
243         // Expected values are different for each subscriber, and
244         // vary with step and number of ranks.
245         std::vector<double> expectedResult(communicationBuffer_.size());
246         std::iota(expectedResult.begin(), expectedResult.end(), valueOffset_ + double(step));
247         std::for_each(expectedResult.begin(), expectedResult.end(), [this](auto& v) {
248             v *= this->numRanks_;
249         });
250         EXPECT_THAT(communicationBuffer_, testing::Pointwise(testing::Eq(), expectedResult))
251                 << "wrong values were reduced";
252         // Ensuring that zero is never computed by a reduction helps
253         // test that the zeroing of the communication buffer is
254         // working correctly, as we will only observe zero after
255         // zeroing and no subsequent activity.
256         EXPECT_THAT(communicationBuffer_, testing::Not(testing::Each(0)))
257                 << "zero may not be the result of an reduction during testing";
258     }
259
260     //! The number of doubles required to reduce
261     int sizeRequired_;
262     //! The callback used to require reduction
263     ObservablesReducerBuilder::CallbackToRequireReduction callbackToRequireReduction_;
264     //! The buffer used for communication, supplied by an \c ObservablesReducer
265     ArrayRef<double> communicationBuffer_;
266     //! Offset that differentiates the values reduced by each subscriber
267     double valueOffset_;
268     //! Number of ranks, used in constructing test expectations
269     int numRanks_;
270     //! Index within the group of subscribers
271     int subscriberIndex_;
272 };
273
274 //! Test fixture class
275 class ObservablesReducerIntegrationTest : public testing::TestWithParam<std::tuple<int, int>>
276 {
277 public:
278     //! Helper struct to model data on a single MPI rank
279     struct RankData
280     {
281         /*! \brief Builder of \c observablesReducer for this "rank,"
282          * valid until after its build() method has been called. */
283         std::optional<ObservablesReducerBuilder> builder = ObservablesReducerBuilder{};
284         //! Subscribers to \c observablesReducer
285         std::vector<Subscriber> subscribers;
286         /*! \brief Manages reduction of observables on behalf of this
287          * "rank", valid only after the ObserbalesReducerBuilder
288          * builds it. */
289         std::optional<ObservablesReducer> observablesReducer;
290     };
291
292     //! Constructor
293     ObservablesReducerIntegrationTest() : numSubscribers_(std::get<0>(GetParam()))
294     {
295         int numRanks(std::get<1>(GetParam()));
296
297         rankData_.resize(numRanks);
298         for (auto& rankData : rankData_)
299         {
300             for (int i = 0; i < numSubscribers_; ++i)
301             {
302                 // Ensure each subscriber sends a different (but small) amount of data
303                 rankData.subscribers.emplace_back(Subscriber(i, numRanks));
304             }
305             // Now that the addresses of the subscribers are
306             // stable, set up the build-time callback.
307             for (auto& subscriber : rankData.subscribers)
308             {
309                 subscriber.makeSubscription(&rankData.builder.value());
310             }
311         }
312     }
313
314     /*! \brief Performs the equivalent of MPI_Allreduce on the
315      * communication buffer over \c rankData_ */
316     void fakeMpiAllReduce()
317     {
318         std::vector<double> reducedValues(
319                 rankData_[0].observablesReducer.value().communicationBuffer().size(), 0.0);
320         // Reduce the values across "ranks"
321         for (auto& rankData : rankData_)
322         {
323             for (size_t i = 0; i != reducedValues.size(); ++i)
324             {
325                 reducedValues[i] += rankData.observablesReducer.value().communicationBuffer()[i];
326             }
327         }
328         // Copy the reduced values to all "ranks"
329         for (auto& rankData : rankData_)
330         {
331             auto buffer = rankData.observablesReducer.value().communicationBuffer();
332             std::copy(reducedValues.begin(), reducedValues.end(), buffer.begin());
333         }
334     }
335
336     //! The number of subscribers
337     int numSubscribers_;
338     //! Models data distributed over MPI ranks
339     std::vector<RankData> rankData_;
340 };
341
342 TEST_P(ObservablesReducerIntegrationTest, CanBuildAndUseSimply)
343 {
344     for (auto& rankData : rankData_)
345     {
346         rankData.observablesReducer = rankData.builder.value().build();
347         rankData.builder.reset();
348         EXPECT_TRUE(rankData.observablesReducer.value().communicationBuffer().empty())
349                 << "no buffer available when no subscribers requested reduction";
350     }
351
352     Step step = 0;
353     for (auto& rankData : rankData_)
354     {
355         for (auto& subscriber : rankData.subscribers)
356         {
357             subscriber.doSimulationWork(step, ReductionRequirement::Soon);
358         }
359         EXPECT_EQ(numSubscribers_ == 0, rankData.observablesReducer.value().communicationBuffer().empty())
360                 << "buffer should be available only when there are active subscribers";
361     }
362
363     // This does reduction work, and calls the callbacks that check
364     // the buffer contents.
365     fakeMpiAllReduce();
366
367     for (auto& rankData : rankData_)
368     {
369         rankData.observablesReducer.value().reductionComplete(step);
370         EXPECT_TRUE(rankData.observablesReducer.value().communicationBuffer().empty())
371                 << "no buffer available after reductionComplete()";
372         rankData.observablesReducer.value().stepComplete();
373     }
374 }
375
376 TEST_P(ObservablesReducerIntegrationTest, CanBuildAndUseOverMultipleSteps)
377 {
378     for (auto& rankData : rankData_)
379     {
380         rankData.observablesReducer = rankData.builder.value().build();
381         rankData.builder.reset();
382         EXPECT_TRUE(rankData.observablesReducer.value().communicationBuffer().empty())
383                 << "no buffer available when no subscribers requested reduction";
384     }
385
386     for (Step step = 0; step < 20; step += 10)
387     {
388         for (auto& rankData : rankData_)
389         {
390             for (auto& subscriber : rankData.subscribers)
391             {
392                 subscriber.doSimulationWork(step, ReductionRequirement::Soon);
393             }
394             EXPECT_EQ(numSubscribers_ == 0,
395                       rankData.observablesReducer.value().communicationBuffer().empty())
396                     << "buffer should be available only when there are subscribers";
397         }
398
399         // This does reduction work, and calls the callbacks that
400         // check the buffer contents.
401         fakeMpiAllReduce();
402
403         for (auto& rankData : rankData_)
404         {
405             rankData.observablesReducer.value().reductionComplete(step);
406             EXPECT_TRUE(rankData.observablesReducer.value().communicationBuffer().empty())
407                     << "no buffer available after reductionComplete()";
408             rankData.observablesReducer.value().stepComplete();
409         }
410     }
411 }
412
413 TEST_P(ObservablesReducerIntegrationTest, CanBuildAndUseWithoutAllNeedingReduction)
414 {
415     if (numSubscribers_ == 0)
416     {
417         // Test is meaningless with no subscribers
418         return;
419     }
420
421     for (auto& rankData : rankData_)
422     {
423         rankData.observablesReducer = rankData.builder.value().build();
424         rankData.builder.reset();
425         EXPECT_TRUE(rankData.observablesReducer.value().communicationBuffer().empty())
426                 << "no buffer available when no subscribers requested reduction";
427     }
428
429     // Only one subscriber does work leading to reduction
430     size_t subscriberNeedingReduction = 0;
431     Step   step                       = 0;
432     for (auto& rankData : rankData_)
433     {
434         auto& subscriber = rankData.subscribers[subscriberNeedingReduction];
435         subscriber.doSimulationWork(step, ReductionRequirement::Soon);
436         EXPECT_FALSE(rankData.observablesReducer.value().communicationBuffer().empty())
437                 << "buffer should be available when there is an active subscriber";
438     }
439
440     // This does reduction work, and calls the callbacks that check
441     // the buffer contents.
442     fakeMpiAllReduce();
443
444     // Check that other subscribers didn't reduce anything
445     for (auto& rankData : rankData_)
446     {
447         for (size_t r = 0; r != rankData.subscribers.size(); ++r)
448         {
449             if (r == subscriberNeedingReduction)
450             {
451                 continue;
452             }
453             EXPECT_THAT(rankData.subscribers[r].communicationBuffer_, testing::Each(0.0))
454                     << "buffer for non-subscribers should be zero";
455         }
456     }
457
458     for (auto& rankData : rankData_)
459     {
460         rankData.observablesReducer.value().reductionComplete(step);
461         EXPECT_TRUE(rankData.observablesReducer.value().communicationBuffer().empty())
462                 << "no buffer available after reductionComplete()";
463         rankData.observablesReducer.value().stepComplete();
464     }
465 }
466
467 TEST_P(ObservablesReducerIntegrationTest, CanBuildAndUseWhenASubscriberUsesEventually)
468 {
469     if (numSubscribers_ < 2)
470     {
471         // Test is meaningful only with multiple subscribers
472         return;
473     }
474
475     for (auto& rankData : rankData_)
476     {
477         rankData.observablesReducer = rankData.builder.value().build();
478         rankData.builder.reset();
479         EXPECT_TRUE(rankData.observablesReducer.value().communicationBuffer().empty())
480                 << "no buffer available when no subscribers requested reduction";
481     }
482
483     // Only one subscriber does work leading to reduction
484     size_t subscriberUsingEventually = 1;
485     Step   step                      = 1;
486     for (auto& rankData : rankData_)
487     {
488         auto& subscriber = rankData.subscribers[subscriberUsingEventually];
489         subscriber.doSimulationWork(step, ReductionRequirement::Eventually);
490         EXPECT_TRUE(rankData.observablesReducer.value().communicationBuffer().empty())
491                 << "buffer should not be available when the only active subscriber used "
492                    "ReductionRequirement::Eventually";
493     }
494
495     // This will do nothing, as all the communication buffers are
496     // empty, but we can't directly test that nothing
497     // occured. Instead, we will later do some
498     // ReductionRequirement::Soon work and observe that result is
499     // consistent with exactly one reduction.
500     fakeMpiAllReduce();
501
502     for (auto& rankData : rankData_)
503     {
504         for (size_t i = 0; i != rankData.subscribers.size(); ++i)
505         {
506             if (i == subscriberUsingEventually)
507             {
508                 continue;
509             }
510             rankData.subscribers[i].doSimulationWork(step, ReductionRequirement::Soon);
511         }
512         EXPECT_FALSE(rankData.observablesReducer.value().communicationBuffer().empty())
513                 << "buffer should be available since there are subscribers";
514     }
515
516     // This does reduction work, and calls the callbacks that check
517     // the buffer contents.
518     fakeMpiAllReduce();
519
520     for (auto& rankData : rankData_)
521     {
522         rankData.observablesReducer.value().reductionComplete(step);
523         EXPECT_TRUE(rankData.observablesReducer.value().communicationBuffer().empty())
524                 << "no buffer available after reductionComplete()";
525         rankData.observablesReducer.value().stepComplete();
526     }
527 }
528
529 //! Help GoogleTest name our test cases
530 std::string namesOfTests(const testing::TestParamInfo<ObservablesReducerIntegrationTest::ParamType>& info)
531 {
532     // NB alphanumeric characters only
533     return formatString("numSubscribers%dnumRanks%d", std::get<0>(info.param), std::get<1>(info.param));
534 }
535 INSTANTIATE_TEST_SUITE_P(WithVariousSubscriberCounts,
536                          ObservablesReducerIntegrationTest,
537                          testing::Combine(testing::Values(0, 1, 2, 3), // subscriber counts
538                                           testing::Values(1, 2, 3)),   // rank counts
539                          namesOfTests);
540
541 } // namespace
542 } // namespace gmx::test