OPALX (Object Oriented Parallel Accelerator Library for Exascal) master (dc2a29eed580)
OPALX
Loading...
Searching...
No Matches
TestBinning.cpp
Go to the documentation of this file.
1
16#include "Ippl.h"
18#include "gtest/gtest.h"
19
20// VField_t alias required by ParallelReduceTools.h (vnorm) and AdaptBins.h (LTrans)
21template <unsigned Dim>
22using Mesh_t = ippl::UniformCartesian<double, Dim>;
23
24template <unsigned Dim>
26
27template <typename T, unsigned Dim, class... ViewArgs>
28using Field = ippl::Field<T, Dim, Mesh_t<Dim>, Centering_t<Dim>, ViewArgs...>;
29
30template <typename T, unsigned Dim>
31using Vector_t = ippl::Vector<T, Dim>;
32
33template <typename T, unsigned Dim, class... ViewArgs>
34using VField_t = Field<Vector_t<T, Dim>, Dim, ViewArgs...>;
35
36// Alias needed by AdaptBins.tpp LTrans (uses unqualified Vector)
37template <typename T, unsigned Dim>
38using Vector = ippl::Vector<T, Dim>;
39
44
45using size_type = ippl::detail::size_type;
46
47#include <algorithm>
48#include <cmath>
49#include <iostream>
50#include <numeric>
51#include <random>
52#include <string>
53#include <vector>
54
55// ============================================================================
56// Minimal bunch type for testing (mirrors ParticleContainer's relevant parts)
57// ============================================================================
58constexpr unsigned TestDim = 3;
59using TestT = double;
60
61using TestMesh_t = ippl::UniformCartesian<TestT, TestDim>;
62using TestPLayout_t = ippl::ParticleSpatialLayout<TestT, TestDim, TestMesh_t>;
63using TestFL_t = ippl::FieldLayout<TestDim>;
64
71struct TestBunch : public ippl::ParticleBase<TestPLayout_t> {
72 using Base = ippl::ParticleBase<TestPLayout_t>;
73 using bin_index_type = short int; // same as ParticleContainer
74
76 typename Base::particle_position_type P;
77
79 ippl::ParticleAttrib<bin_index_type> Bin;
80
82 this->addAttribute(P);
83 this->addAttribute(Bin);
84 }
85 ~TestBunch() = default;
86};
87
88// ============================================================================
89// Common type aliases
90// ============================================================================
95using value_type = TestT; // double
96
97// ============================================================================
98// Test fixture
99// ============================================================================
100class BinningTest : public ::testing::Test {
101protected:
102 static void SetUpTestSuite() {
103 int argc = 0;
104 char** argv = nullptr;
105 ippl::initialize(argc, argv);
106 }
107
108 static void TearDownTestSuite() { ippl::finalize(); }
109
110 // Members kept alive for the duration of each test
111 std::unique_ptr<TestMesh_t> mesh;
112 std::unique_ptr<TestFL_t> fl;
113 std::unique_ptr<TestPLayout_t> playout;
114 std::shared_ptr<Container_t> bunch;
115 std::shared_ptr<AdaptBins_t> adaptBins;
116
117 // Parameters
118 static constexpr size_t gridPoints = 32;
119 static constexpr TestT domainLen = 1.0;
120
121 void SetUp() override {
122 // Build mesh + field layout (similar to GatherScatterTest)
123 ippl::Vector<TestT, TestDim> hr;
124 ippl::Vector<TestT, TestDim> origin;
125 ippl::NDIndex<TestDim> domain;
126 std::array<bool, TestDim> decomp;
127 decomp.fill(true);
128
129 for (unsigned d = 0; d < TestDim; ++d) {
130 domain[d] = ippl::Index(gridPoints);
131 hr[d] = domainLen / gridPoints;
132 origin[d] = 0.0;
133 }
134
135 mesh = std::make_unique<TestMesh_t>(domain, hr, origin);
136 fl = std::make_unique<TestFL_t>(MPI_COMM_WORLD, domain, decomp, true);
137 playout = std::make_unique<TestPLayout_t>(*fl, *mesh);
138 bunch = std::make_shared<Container_t>(*playout);
139 bunch->setParticleBC(ippl::BC::PERIODIC);
140 }
141
142 void TearDown() override {
143 adaptBins.reset();
144 bunch.reset();
145 playout.reset();
146 fl.reset();
147 mesh.reset();
148 }
149
150 // ---- helpers ----------------------------------------------------------
152 void createParticles(size_t n) {
153 bunch->create(n);
154 std::mt19937_64 eng(42 + ippl::Comm->rank());
155 std::uniform_real_distribution<TestT> unifR(0.05, 0.95);
156 std::uniform_real_distribution<TestT> unifP(0.1, 0.9);
157
158 auto R_host = bunch->R.getHostMirror();
159 auto P_host = bunch->P.getHostMirror();
160 for (size_t i = 0; i < n; ++i) {
161 ippl::Vector<TestT, TestDim> r, p;
162 for (unsigned d = 0; d < TestDim; ++d) {
163 r[d] = unifR(eng);
164 p[d] = unifP(eng);
165 }
166 R_host(i) = r;
167 P_host(i) = p;
168 }
169 Kokkos::deep_copy(bunch->R.getView(), R_host);
170 Kokkos::deep_copy(bunch->P.getView(), P_host);
171 ippl::Comm->barrier();
172 Kokkos::fence();
173 bunch->update();
174 }
175
177 void createParticlesUniformP(size_t n, int axis, TestT pMin, TestT pMax) {
178 bunch->create(n);
179 std::mt19937_64 eng(123 + ippl::Comm->rank());
180 std::uniform_real_distribution<TestT> unifR(0.05, 0.95);
181
182 auto R_host = bunch->R.getHostMirror();
183 auto P_host = bunch->P.getHostMirror();
184 for (size_t i = 0; i < n; ++i) {
185 ippl::Vector<TestT, TestDim> r, p;
186 for (unsigned d = 0; d < TestDim; ++d) {
187 r[d] = unifR(eng);
188 p[d] = 0.0;
189 }
190 // Uniform spacing along the chosen axis
191 p[axis] = pMin + (pMax - pMin) * (static_cast<TestT>(i) + 0.5) / n;
192 R_host(i) = r;
193 P_host(i) = p;
194 }
195 Kokkos::deep_copy(bunch->R.getView(), R_host);
196 Kokkos::deep_copy(bunch->P.getView(), P_host);
197 ippl::Comm->barrier();
198 Kokkos::fence();
199 bunch->update();
200 }
201
204 bin_index_type maxBins = 5, value_type alpha = 1.0, value_type beta = 1.0,
205 value_type desW = 0.1, const std::string& binningCmdName = "TEST_BINNING_CMD") {
206 Selector_t selector(2); // bin along axis 2 (z-component of P)
207 adaptBins = std::make_shared<AdaptBins_t>(
208 *bunch, selector, maxBins, alpha, beta, desW, binningCmdName);
209 }
210};
211
212// ############################################################################
213// 1. ParallelReduceTools tests
214// ############################################################################
215
216TEST_F(BinningTest, ArrayReductionDefaultInit) {
217 // ArrayReduction should initialise all elements to zero.
218 constexpr bin_index_type N = 4;
220 for (bin_index_type i = 0; i < N; ++i) {
221 EXPECT_EQ(arr.the_array[i], 0u);
222 }
223}
224
225TEST_F(BinningTest, ArrayReductionCopy) {
226 constexpr bin_index_type N = 3;
228 a.the_array[0] = 10;
229 a.the_array[1] = 20;
230 a.the_array[2] = 30;
231
233 for (bin_index_type i = 0; i < N; ++i) {
234 EXPECT_EQ(b.the_array[i], a.the_array[i]);
235 }
236}
237
238TEST_F(BinningTest, ArrayReductionPlusEquals) {
239 constexpr bin_index_type N = 3;
241 a.the_array[0] = 1;
242 a.the_array[1] = 2;
243 a.the_array[2] = 3;
244 b.the_array[0] = 4;
245 b.the_array[1] = 5;
246 b.the_array[2] = 6;
247 a += b;
248 EXPECT_EQ(a.the_array[0], 5u);
249 EXPECT_EQ(a.the_array[1], 7u);
250 EXPECT_EQ(a.the_array[2], 9u);
251}
252
253TEST_F(BinningTest, CreateReductionObjectValid) {
254 // createReductionObject should succeed for sizes 1..maxArrSize.
255 for (bin_index_type n = 1; n <= ParticleBinning::maxArrSize<bin_index_type>; ++n) {
256 auto var = ParticleBinning::createReductionObject<size_type, bin_index_type>(n);
257 // Just verify no exception and variant is valid.
258 std::visit(
259 [&](auto& reducer) {
260 EXPECT_EQ(
261 sizeof(reducer.the_array) / sizeof(reducer.the_array[0]),
262 static_cast<size_t>(n));
263 },
264 var);
265 }
266}
267
268TEST_F(BinningTest, CreateReductionObjectInvalid) {
269 // Requesting a size above maxArrSize should throw.
270 bin_index_type tooBig = ParticleBinning::maxArrSize<bin_index_type> + 1;
271 // Wrap in a lambda to avoid macro comma issue with template args.
272 EXPECT_THROW(
273 (ParticleBinning::createReductionObject<size_type, bin_index_type>(tooBig)),
274 std::out_of_range);
275}
276
277TEST_F(BinningTest, HostArrayReductionBasics) {
278 // HostArrayReduction is a host-only helper and cannot be used when
279 // OPALX_DEVICE_COMPILATION is enabled (see ParallelReduceTools.h).
280#ifdef OPALX_DEVICE_COMPILATION
281 GTEST_SKIP() << "HostArrayReduction is host-only; skipped under OPALX_DEVICE_COMPILATION.";
282#else
283 constexpr bin_index_type N = 4;
285
287 for (bin_index_type i = 0; i < N; ++i) {
288 EXPECT_EQ(a.the_array[i], 0u);
289 }
290 a.the_array[0] = 7;
291 a.the_array[3] = 3;
292
294 for (bin_index_type i = 0; i < N; ++i) {
295 EXPECT_EQ(b.the_array[i], 0u);
296 }
297
298 b.the_array[0] = 1;
299 b.the_array[3] = 2;
300
301 a += b;
302 EXPECT_EQ(a.the_array[0], 8u);
303 EXPECT_EQ(a.the_array[3], 5u);
304#endif
305}
306
307TEST_F(BinningTest, KokkosReductionIdentityArrayReduction) {
308 // The Kokkos reduction_identity should return a zero-initialized ArrayReduction.
309 constexpr bin_index_type N = 3;
310 auto identity = Kokkos::reduction_identity<
312 for (bin_index_type i = 0; i < N; ++i) {
313 EXPECT_EQ(identity.the_array[i], 0u);
314 }
315}
316
317TEST_F(BinningTest, KokkosReductionIdentityHostArrayReduction) {
318#ifdef OPALX_DEVICE_COMPILATION
319 GTEST_SKIP() << "HostArrayReduction reduction identity is host-only; skipped under "
320 "OPALX_DEVICE_COMPILATION.";
321#else
322 constexpr bin_index_type N = 4;
324 auto identity = Kokkos::reduction_identity<
326 for (bin_index_type i = 0; i < N; ++i) {
327 EXPECT_EQ(identity.the_array[i], 0u);
328 }
329#endif
330}
331
332// ############################################################################
333// 2. BinningTools tests
334// ############################################################################
335
336TEST_F(BinningTest, DetermineHistoReductionModeStandard) {
337 // Standard mode should auto-select based on bin count.
338 auto modeSmall = ParticleBinning::determineHistoReductionMode<bin_index_type>(
340 auto modeLarge = ParticleBinning::determineHistoReductionMode<bin_index_type>(
342
343 bool isHostSpace =
344 std::is_same<Kokkos::DefaultExecutionSpace, Kokkos::DefaultHostExecutionSpace>::value;
345 if (isHostSpace) {
346 // On host spaces, always HostOnly.
349 } else {
350 // On device, small bin count -> ParallelReduce, large -> TeamBased.
353 }
354}
355
356TEST_F(BinningTest, DetermineHistoReductionModeForced) {
357 // If a specific mode is forced, it should be respected where supported.
358 bool isHostSpace =
359 std::is_same<Kokkos::DefaultExecutionSpace, Kokkos::DefaultHostExecutionSpace>::value;
360
361 if (!isHostSpace) {
362#ifdef OPALX_DEVICE_COMPILATION
363 // On device builds, forcing HostOnly must throw (guard against misuse).
364 EXPECT_THROW(
365 (ParticleBinning::determineHistoReductionMode<bin_index_type>(
368#endif
369
370 // For other modes like TeamBased, the preference must be respected.
371 auto mode = ParticleBinning::determineHistoReductionMode<bin_index_type>(
374 }
375}
376
377TEST_F(BinningTest, ComputeFixSum) {
378 // Test computeFixSum with a simple host-space view.
379 constexpr size_t N = 5;
380 Kokkos::View<size_type*> input("input", N);
381 Kokkos::View<size_type*> postsum("postsum", N + 1);
382
383 // Fill input = {1,2,3,4,5}
384 auto h_in = Kokkos::create_mirror_view(input);
385 for (size_t i = 0; i < N; ++i)
386 h_in(i) = i + 1;
387 Kokkos::deep_copy(input, h_in);
388
389 ParticleBinning::computeFixSum<Kokkos::View<size_type*>>(input, postsum);
390
391 auto h_ps = Kokkos::create_mirror_view(postsum);
392 Kokkos::deep_copy(h_ps, postsum);
393
394 // Expected postsum: {0, 1, 3, 6, 10, 15}
395 EXPECT_EQ(h_ps(0), 0u);
396 EXPECT_EQ(h_ps(1), 1u);
397 EXPECT_EQ(h_ps(2), 3u);
398 EXPECT_EQ(h_ps(3), 6u);
399 EXPECT_EQ(h_ps(4), 10u);
400 EXPECT_EQ(h_ps(5), 15u);
401}
402
403TEST_F(BinningTest, ViewIsSortedTrue) {
404 constexpr size_t N = 5;
405 Kokkos::View<bin_index_type*> view("sortedView", N);
406 auto h_view = Kokkos::create_mirror_view(view);
407 for (size_t i = 0; i < N; ++i)
408 h_view(i) = static_cast<bin_index_type>(i);
409 Kokkos::deep_copy(view, h_view);
410
411 // Identity hash: indices = {0,1,2,3,4}
412 using hash_type = ippl::detail::hash_type<Kokkos::DefaultExecutionSpace::memory_space>;
413 hash_type idx("idx", N);
414 auto h_idx = Kokkos::create_mirror_view(idx);
415 for (size_t i = 0; i < N; ++i)
416 h_idx(i) = static_cast<int>(i);
417 Kokkos::deep_copy(idx, h_idx);
418
419 bool sorted = ParticleBinning::viewIsSorted<bin_index_type, size_type>(view, idx, N);
420 EXPECT_TRUE(sorted);
421}
422
423TEST_F(BinningTest, ViewIsSortedFalse) {
424 constexpr size_t N = 5;
425 Kokkos::View<bin_index_type*> view("unsortedView", N);
426 auto h_view = Kokkos::create_mirror_view(view);
427 h_view(0) = 0;
428 h_view(1) = 3;
429 h_view(2) = 1;
430 h_view(3) = 2;
431 h_view(4) = 4;
432 Kokkos::deep_copy(view, h_view);
433
434 using hash_type = ippl::detail::hash_type<Kokkos::DefaultExecutionSpace::memory_space>;
435 hash_type idx("idx", N);
436 auto h_idx = Kokkos::create_mirror_view(idx);
437 for (size_t i = 0; i < N; ++i)
438 h_idx(i) = static_cast<int>(i);
439 Kokkos::deep_copy(idx, h_idx);
440
441 bool sorted = ParticleBinning::viewIsSorted<bin_index_type, size_type>(view, idx, N);
442 EXPECT_FALSE(sorted);
443}
444
445// ############################################################################
446// 3. BinHisto (Histogram) tests
447// ############################################################################
448
449TEST_F(BinningTest, HistogramConstruction) {
450 // Construct a Histogram with DualView=false on host space.
451 using Histo_t = ParticleBinning::Histogram<
452 size_type, bin_index_type, value_type, false, Kokkos::HostSpace>;
453 Histo_t histo("testHisto", 4, 2.0, 1.0, 1.0, 0.5);
454
455 EXPECT_EQ(histo.getCurrentBinCount(), 4);
456}
457
458TEST_F(BinningTest, HistogramInitSetsWidthsAndPostSum) {
459 // After init(), widths should be uniform and postSum should be a prefix sum.
460 using Histo_t = ParticleBinning::Histogram<
461 size_type, bin_index_type, value_type, false, Kokkos::HostSpace>;
462 constexpr bin_index_type nBins = 4;
463 constexpr value_type totalWidth = 2.0;
464
465 Histo_t histo("testInit", nBins, totalWidth, 1.0, 1.0, 0.5);
466
467 // Manually set histogram counts on host: {10, 20, 30, 40}
468 auto histView = histo.getHistogram();
469 histView(0) = 10;
470 histView(1) = 20;
471 histView(2) = 30;
472 histView(3) = 40;
473
474 histo.init();
475
476 // Check bin widths are uniform: totalWidth/nBins = 0.5
477 auto widths = histo.getBinWidths();
478 for (bin_index_type i = 0; i < nBins; ++i) {
479 EXPECT_NEAR(widths(i), totalWidth / nBins, 1e-12);
480 }
481
482 // Check post-sum: {0, 10, 30, 60, 100}
483 auto ps = histo.getPostSum();
484 EXPECT_EQ(ps(0), 0u);
485 EXPECT_EQ(ps(1), 10u);
486 EXPECT_EQ(ps(2), 30u);
487 EXPECT_EQ(ps(3), 60u);
488 EXPECT_EQ(ps(4), 100u);
489}
490
491TEST_F(BinningTest, HistogramGetNPartInBin) {
492 using Histo_t = ParticleBinning::Histogram<
493 size_type, bin_index_type, value_type, false, Kokkos::HostSpace>;
494 Histo_t histo("testNPart", 3, 1.0, 1.0, 1.0, 0.3);
495 auto histView = histo.getHistogram();
496 histView(0) = 5;
497 histView(1) = 15;
498 histView(2) = 25;
499 histo.init();
500
501 EXPECT_EQ(histo.getNPartInBin(0), 5u);
502 EXPECT_EQ(histo.getNPartInBin(1), 15u);
503 EXPECT_EQ(histo.getNPartInBin(2), 25u);
504}
505
506TEST_F(BinningTest, HistogramCopyConstructor) {
507 using Histo_t = ParticleBinning::Histogram<
508 size_type, bin_index_type, value_type, false, Kokkos::HostSpace>;
509 Histo_t original("origHisto", 3, 1.5, 1.0, 1.0, 0.5);
510 auto histView = original.getHistogram();
511 histView(0) = 10;
512 histView(1) = 20;
513 histView(2) = 30;
514 original.init();
515
516 Histo_t copy(original);
517 EXPECT_EQ(copy.getCurrentBinCount(), 3);
518 EXPECT_EQ(copy.getNPartInBin(0), 10u);
519 EXPECT_EQ(copy.getNPartInBin(1), 20u);
520 EXPECT_EQ(copy.getNPartInBin(2), 30u);
521}
522
523TEST_F(BinningTest, HistogramAssignmentOperator) {
524 using Histo_t = ParticleBinning::Histogram<
525 size_type, bin_index_type, value_type, false, Kokkos::HostSpace>;
526 Histo_t original("assignOrig", 2, 1.0, 1.0, 1.0, 0.5);
527 auto histView = original.getHistogram();
528 histView(0) = 7;
529 histView(1) = 13;
530 original.init();
531
532 Histo_t assigned("dummy", 1, 0.5, 1.0, 1.0, 0.5);
533 assigned = original;
534 EXPECT_EQ(assigned.getCurrentBinCount(), 2);
535 EXPECT_EQ(assigned.getNPartInBin(0), 7u);
536 EXPECT_EQ(assigned.getNPartInBin(1), 13u);
537}
538
541 using view_type = decltype(std::declval<Histo_t>().getHistogram().view_device());
543
545
546 KOKKOS_FUNCTION
547 void operator()(const size_t) const {
548 dView(0) = 5;
549 dView(1) = 10;
550 dView(2) = 15;
551 }
552};
553
554TEST_F(BinningTest, HistogramGetBinIterationPolicy) {
555 // Use DualView=true to avoid Kokkos subview type mismatch in non-DualView path.
557 Histo_t histo("policyTest", 3, 1.0, 1.0, 1.0, 0.3);
558
559 auto dView = histo.getHistogram().view_device();
560 Kokkos::parallel_for("fillPolicyHisto1", 1, FillPolicyHistogram1(dView));
561
562 histo.modify_device();
563 histo.sync();
564 histo.init();
565
566 // postSum should be {0, 5, 15, 30}
567 auto policy0 = histo.getBinIterationPolicy(0);
568 auto policy1 = histo.getBinIterationPolicy(1);
569 auto policy2 = histo.getBinIterationPolicy(2);
570
571 // Check that the ranges encode [start, end) matching the post-sum
572 EXPECT_EQ(policy0.begin(), 0);
573 EXPECT_EQ(policy0.end(), 5);
574 EXPECT_EQ(policy1.begin(), 5);
575 EXPECT_EQ(policy1.end(), 15);
576 EXPECT_EQ(policy2.begin(), 15);
577 EXPECT_EQ(policy2.end(), 30);
578}
579
580TEST_F(BinningTest, HistogramMergeBins) {
581 // Create a histogram with many bins and verify that mergeBins reduces them.
582 using Histo_t = ParticleBinning::Histogram<
583 size_type, bin_index_type, value_type, false, Kokkos::HostSpace>;
584 constexpr bin_index_type nBins = 10;
585 constexpr value_type totalWidth = 1.0;
586 Histo_t histo("mergeTest", nBins, totalWidth, 1.0, 1.0, 0.2);
587
588 // Create a distribution with most particles in a few bins.
589 // Bins 0-2: 1 particle each (sparse tails)
590 // Bins 3-6: 100 particles each (dense center)
591 // Bins 7-9: 1 particle each (sparse tails)
592 auto histView = histo.getHistogram();
593 histView(0) = 1;
594 histView(1) = 1;
595 histView(2) = 1;
596 histView(3) = 100;
597 histView(4) = 100;
598 histView(5) = 100;
599 histView(6) = 100;
600 histView(7) = 1;
601 histView(8) = 1;
602 histView(9) = 1;
603 histo.init();
604
605 auto lookupTable = histo.mergeBins();
606
607 // After merging, the number of bins should be less than the original 10.
608 bin_index_type mergedCount = histo.getCurrentBinCount();
609 EXPECT_GT(mergedCount, 0);
610 EXPECT_LE(mergedCount, nBins);
611
612 // The lookup table maps old bin indices to new ones.
613 // It should be monotonically non-decreasing.
614 auto h_lookup = Kokkos::create_mirror_view(lookupTable);
615 Kokkos::deep_copy(h_lookup, lookupTable);
616 for (bin_index_type i = 1; i < nBins; ++i) {
617 EXPECT_GE(h_lookup(i), h_lookup(i - 1));
618 }
619
620 // All new indices should be in [0, mergedCount)
621 for (bin_index_type i = 0; i < nBins; ++i) {
622 EXPECT_GE(h_lookup(i), 0);
623 EXPECT_LT(h_lookup(i), mergedCount);
624 }
625
626 // Verify total particle count is conserved after merge.
627 size_type totalAfter = 0;
628 for (bin_index_type i = 0; i < mergedCount; ++i) {
629 totalAfter += histo.getNPartInBin(i);
630 }
631 EXPECT_EQ(totalAfter, 406u); // 3*1 + 4*100 + 3*1 = 406
632}
633
636 using view_type = decltype(std::declval<Histo_t>().getHistogram().view_device());
638
640
641 KOKKOS_FUNCTION
642 void operator()(const size_t i) const { dView(i) = (i + 1) * 10; }
643};
644
645TEST_F(BinningTest, HistogramDualViewConstruction) {
646 // Test that the DualView variant of Histogram works properly.
648 Histo_t histo("dualViewHisto", 4, 2.0, 1.0, 1.0, 0.5);
649 EXPECT_EQ(histo.getCurrentBinCount(), 4);
650
651 // Set counts on device, sync to host
652 auto dView = histo.getHistogram().view_device();
653 Kokkos::parallel_for("fillPolicyHisto2", 4, FillPolicyHistogram2(dView));
654
655 histo.modify_device();
656 histo.sync();
657 histo.init();
658
659 EXPECT_EQ(histo.getNPartInBin(0), 10u);
660 EXPECT_EQ(histo.getNPartInBin(1), 20u);
661 EXPECT_EQ(histo.getNPartInBin(2), 30u);
662 EXPECT_EQ(histo.getNPartInBin(3), 40u);
663}
664
665// ############################################################################
666// 4. CoordinateSelector tests
667// ############################################################################
668
669TEST_F(BinningTest, CoordinateSelectorReturnsNormalizedValues) {
670 // CoordinateSelector applies: v = fabs(P[axis]); return v/sqrt(1+v*v)
671 // So the output should always be in [0, 1).
672 size_t nPart = 100;
673 createParticles(nPart);
674
675 Selector_t selector(2); // z-axis
676 selector.updateDataArr(*bunch);
677
678 auto P_host = bunch->P.getHostMirror();
679 Kokkos::deep_copy(P_host, bunch->P.getView());
680
681 size_t localN = bunch->getLocalNum();
682 for (size_t i = 0; i < localN; ++i) {
683 value_type p_z = std::fabs(P_host(i)[2]);
684 value_type expected = p_z / std::sqrt(1.0 + p_z * p_z);
685
686 // Evaluate on host through a host mirror of viewdata
687 // (The selector runs on device normally; here we verify the formula.)
688 EXPECT_GE(expected, 0.0);
689 EXPECT_LT(expected, 1.0);
690 }
691}
692
693// ############################################################################
694// 5. AdaptBins: getBin static function tests
695// ############################################################################
696
697TEST_F(BinningTest, GetBinBasic) {
698 // Test the static getBin function for bin assignment.
699 value_type xMin = 0.0, xMax = 1.0;
700 bin_index_type numBins = 5;
701 value_type binWidthInv = numBins / (xMax - xMin);
702
703 // Midpoint of each bin should map to its index.
704 for (bin_index_type b = 0; b < numBins; ++b) {
705 value_type midpoint = xMin + (b + 0.5) / numBins * (xMax - xMin);
706 bin_index_type result = AdaptBins_t::getBin(midpoint, xMin, xMax, binWidthInv, numBins);
707 EXPECT_EQ(result, b);
708 }
709}
710
711TEST_F(BinningTest, GetBinClampsOutOfRange) {
712 value_type xMin = 0.0, xMax = 1.0;
713 bin_index_type numBins = 4;
714 value_type binWidthInv = numBins / (xMax - xMin);
715
716 // Value below xMin should clamp to bin 0
717 bin_index_type resultLow = AdaptBins_t::getBin(-0.5, xMin, xMax, binWidthInv, numBins);
718 EXPECT_EQ(resultLow, 0);
719
720 // Value above xMax should clamp to last bin (numBins-1)
721 bin_index_type resultHigh = AdaptBins_t::getBin(1.5, xMin, xMax, binWidthInv, numBins);
722 EXPECT_EQ(resultHigh, numBins - 1);
723}
724
725TEST_F(BinningTest, GetBinOnBoundary) {
726 value_type xMin = 0.0, xMax = 1.0;
727 bin_index_type numBins = 4;
728 value_type binWidthInv = numBins / (xMax - xMin);
729
730 // xMin should be bin 0
731 EXPECT_EQ(AdaptBins_t::getBin(0.0, xMin, xMax, binWidthInv, numBins), 0);
732
733 // xMax should be clamped to numBins-1
734 EXPECT_EQ(AdaptBins_t::getBin(1.0, xMin, xMax, binWidthInv, numBins), numBins - 1);
735}
736
737// ############################################################################
738// 6. AdaptBins: integration tests with particles
739// ############################################################################
740
741TEST_F(BinningTest, AdaptBinsConstruction) {
742 createParticles(50);
743 buildAdaptBins(5);
744 EXPECT_EQ(adaptBins->getMaxBinCount(), 5);
745 EXPECT_EQ(adaptBins->getCurrentBinCount(), 5);
746}
747
748TEST_F(BinningTest, AdaptBinsSetCurrentBinCount) {
749 createParticles(50);
750 buildAdaptBins(5);
751
752 adaptBins->setCurrentBinCount(3);
753 EXPECT_EQ(adaptBins->getCurrentBinCount(), 3);
754
755 // Setting above max should clamp to max.
756 adaptBins->setCurrentBinCount(100);
757 EXPECT_EQ(adaptBins->getCurrentBinCount(), 5);
758}
759
760TEST_F(BinningTest, AdaptBinsInitLimits) {
761 createParticles(200);
762 buildAdaptBins(5);
763
764 adaptBins->initLimits();
765
766 // After initLimits, getBinWidth should be set.
767 value_type bw = adaptBins->getBinWidth();
768 EXPECT_GT(bw, 0.0);
769}
770
771TEST_F(BinningTest, AdaptBinsDoFullRebin) {
772 // Full rebinning should complete without error and produce valid histogram.
773 size_t nPart = 200;
774 createParticles(nPart);
775 buildAdaptBins(5);
776
777 adaptBins->doFullRebin(5);
778
779 // After full rebin, every bin should sum up to the total number of local particles.
780 size_type totalInBins = 0;
781 for (bin_index_type b = 0; b < adaptBins->getCurrentBinCount(); ++b) {
782 totalInBins += adaptBins->getNPartInBin(b);
783 }
784 EXPECT_EQ(totalInBins, bunch->getLocalNum());
785}
786
787TEST_F(BinningTest, AdaptBinsGlobalHistogramSumsToTotal) {
788 // The global histogram should sum to bunch->getTotalNum().
789 size_t nPart = 300;
790 createParticles(nPart);
791 buildAdaptBins(5);
792
793 adaptBins->doFullRebin(5);
794
795 size_type totalGlobal = 0;
796 for (bin_index_type b = 0; b < adaptBins->getCurrentBinCount(); ++b) {
797 totalGlobal += adaptBins->getNPartInBin(b, /*global=*/true);
798 }
799 EXPECT_EQ(totalGlobal, bunch->getTotalNum());
800}
801
802TEST_F(BinningTest, AdaptBinsBinsAssignedInRange) {
803 // After bin assignment, every particle's Bin attribute should be in [0, currentBins).
804 size_t nPart = 200;
805 createParticles(nPart);
806 buildAdaptBins(5);
807
808 adaptBins->doFullRebin(5);
809
810 auto binHost = bunch->Bin.getHostMirror();
811 Kokkos::deep_copy(binHost, bunch->Bin.getView());
812 bin_index_type nBins = adaptBins->getCurrentBinCount();
813 for (size_t i = 0; i < bunch->getLocalNum(); ++i) {
814 EXPECT_GE(binHost(i), 0);
815 EXPECT_LT(binHost(i), nBins);
816 }
817}
818
819TEST_F(BinningTest, AdaptBinsSortContainerByBin) {
820 // After sorting, accessing particles through the hash should yield a
821 // monotonically non-decreasing sequence of bin indices.
822 size_t nPart = 400;
823 createParticles(nPart);
824 buildAdaptBins(5);
825
826 adaptBins->doFullRebin(5);
827 adaptBins->sortContainerByBin();
828
829 auto hashArr = adaptBins->getHashArray();
830 auto binView = bunch->Bin.getView();
831
832 size_t localN = bunch->getLocalNum();
833 if (localN <= 1) return;
834
835 // Copy to host for checking
836 auto h_hash = Kokkos::create_mirror_view(hashArr);
837 Kokkos::deep_copy(h_hash, hashArr);
838 auto h_bin = bunch->Bin.getHostMirror();
839 Kokkos::deep_copy(h_bin, binView);
840
841 for (size_t i = 1; i < localN; ++i) {
842 EXPECT_GE(h_bin(h_hash(i)), h_bin(h_hash(i - 1))) << "Sort violated at index " << i;
843 }
844}
845
846TEST_F(BinningTest, AdaptBinsBinIterationPolicyCoversAllParticles) {
847 // The union of all bin iteration policies should cover exactly all local particles.
848 size_t nPart = 300;
849 createParticles(nPart);
850 buildAdaptBins(5);
851
852 adaptBins->doFullRebin(5);
853 adaptBins->sortContainerByBin();
854
855 size_type totalCovered = 0;
856 for (bin_index_type b = 0; b < adaptBins->getCurrentBinCount(); ++b) {
857 auto policy = adaptBins->getBinIterationPolicy(b);
858 totalCovered += (policy.end() - policy.begin());
859 }
860 EXPECT_EQ(totalCovered, bunch->getLocalNum());
861}
862
863TEST_F(BinningTest, AdaptBinsGenAdaptiveHistogramReducesBins) {
864 // After genAdaptiveHistogram(), the number of bins should be <= maxBins.
865 // Use a larger maxBins so there are bins to merge.
866 size_t nPart = 500;
867 createParticles(nPart);
868
869 // Enough bins for a fine histogram that can be merged down.
870 bin_index_type maxBins = 5;
871 buildAdaptBins(maxBins, /*alpha=*/1.0, /*beta=*/1.0, /*desW=*/0.3);
872
873 adaptBins->doFullRebin(maxBins);
874
875 bin_index_type binsBefore = adaptBins->getCurrentBinCount();
876 EXPECT_EQ(binsBefore, maxBins);
877
878 adaptBins->genAdaptiveHistogram();
879
880 bin_index_type binsAfter = adaptBins->getCurrentBinCount();
881 EXPECT_GT(binsAfter, 0);
882 EXPECT_LE(binsAfter, binsBefore);
883
884 // Verify total particle count in local histogram is still correct.
885 size_type totalInBins = 0;
886 for (bin_index_type b = 0; b < binsAfter; ++b) {
887 totalInBins += adaptBins->getNPartInBin(b);
888 }
889 EXPECT_EQ(totalInBins, bunch->getLocalNum());
890}
891
892TEST_F(BinningTest, AdaptBinsRebinWithDifferentBinCounts) {
893 // Rebinning with different bin counts should produce valid histograms each time.
894 size_t nPart = 200;
895 createParticles(nPart);
896 buildAdaptBins(5);
897
898 for (bin_index_type nBins : {2, 3, 4, 5}) {
899 adaptBins->setCurrentBinCount(nBins);
900 adaptBins->doFullRebin(nBins);
901
902 size_type totalInBins = 0;
903 for (bin_index_type b = 0; b < adaptBins->getCurrentBinCount(); ++b) {
904 totalInBins += adaptBins->getNPartInBin(b);
905 }
906 EXPECT_EQ(totalInBins, bunch->getLocalNum()) << "Mismatch with nBins=" << nBins;
907 }
908}
909
910TEST_F(BinningTest, AdaptBinsRepeatedDoFullRebin) {
911 // Calling doFullRebin multiple times should be idempotent in the histogram total.
912 size_t nPart = 150;
913 createParticles(nPart);
914 buildAdaptBins(4);
915
916 for (int iter = 0; iter < 3; ++iter) {
917 adaptBins->doFullRebin(4);
918
919 size_type total = 0;
920 for (bin_index_type b = 0; b < adaptBins->getCurrentBinCount(); ++b) {
921 total += adaptBins->getNPartInBin(b);
922 }
923 EXPECT_EQ(total, bunch->getLocalNum()) << "Failed at iteration " << iter;
924 }
925}
926
927TEST_F(BinningTest, AdaptBinsSortAndPoliciesConsistent) {
928 // The hash + bin iteration policy should allow correct per-bin iteration.
929 size_t nPart = 300;
930 createParticles(nPart);
931 buildAdaptBins(4);
932
933 adaptBins->doFullRebin(4);
934 adaptBins->sortContainerByBin();
935
936 auto hashArr = adaptBins->getHashArray();
937 auto h_hash = Kokkos::create_mirror_view(hashArr);
938 Kokkos::deep_copy(h_hash, hashArr);
939
940 auto h_bin = bunch->Bin.getHostMirror();
941 Kokkos::deep_copy(h_bin, bunch->Bin.getView());
942
943 for (bin_index_type b = 0; b < adaptBins->getCurrentBinCount(); ++b) {
944 auto policy = adaptBins->getBinIterationPolicy(b);
945 for (auto idx = policy.begin(); idx < policy.end(); ++idx) {
946 EXPECT_EQ(h_bin(h_hash(idx)), b) << "Particle at sorted index " << idx << " has bin "
947 << h_bin(h_hash(idx)) << " but expected " << b;
948 }
949 }
950}
951
952TEST_F(BinningTest, AdaptBinsHistoReductionModes) {
953 // Test that doFullRebin works with each explicitly forced reduction mode.
954 // HostOnly reduction mode is implemented using HostArrayReduction, which is
955 // explicitly host-only in device builds (see ParallelReduceTools.h).
956#ifdef OPALX_DEVICE_COMPILATION
957 GTEST_SKIP()
958 << "HostOnly histogram reduction is host-only; skipped under OPALX_DEVICE_COMPILATION.";
959#else
960 size_t nPart = 200;
961 createParticles(nPart);
962
963 // Use maxBins within maxArrSize range for ParallelReduce to work.
964 bin_index_type maxBins = 3;
965 buildAdaptBins(maxBins);
966
967 // Test HostOnly mode
968 adaptBins->doFullRebin(maxBins, true, ParticleBinning::HistoReductionMode::HostOnly);
969 size_type totalHost = 0;
970 for (bin_index_type b = 0; b < adaptBins->getCurrentBinCount(); ++b) {
971 totalHost += adaptBins->getNPartInBin(b);
972 }
973 EXPECT_EQ(totalHost, bunch->getLocalNum());
974#endif
975}
976
977TEST_F(BinningTest, AdaptBinsUniformDistributionEvenBins) {
978 // For a truly uniform distribution of P values, bins should have roughly equal counts.
979 size_t nPart = 1000;
980 createParticlesUniformP(nPart, 2, 0.1, 0.9);
981 buildAdaptBins(5);
982
983 /*
984 Note that this test works right now for a fixed seed. If the seed is not fixed, there
985 is no guarantee that it passes. In that case, you might need to adapt the required
986 number of particles below. This is because the distribution is not "uniform" after
987 CoordinateSelector is applied.
988 */
989
990 adaptBins->doFullRebin(5);
991
992 // Check local histogram - should be roughly uniform within each rank's portion.
993 size_type localN = bunch->getLocalNum();
994 if (localN < 5) return; // skip if too few particles on this rank
995
996 for (bin_index_type b = 0; b < 5; ++b) {
997 size_type count = adaptBins->getNPartInBin(b);
998 // Verify bins are not empty for uniform distribution.
999 EXPECT_GT(count, 50) << "Bin " << b << " is too small for \"uniform\" distribution.";
1000 EXPECT_LT(count, 350) << "Bin " << b << " is too big for \"uniform\" distribution";
1001 }
1002}
1003
1004TEST_F(BinningTest, AdaptBinsFullWorkflow) {
1005 // End-to-end test of the full workflow:
1006 // create -> rebin -> adaptive merge -> sort -> iterate per bin.
1007 size_t nPart = 500;
1008 createParticles(nPart);
1009
1010 bin_index_type maxBins = 5;
1011 buildAdaptBins(maxBins, 1.0, 1.0, 0.2);
1012
1013 // 1. Full rebin
1014 adaptBins->doFullRebin(maxBins);
1015
1016 // 2. Adaptive histogram (merge)
1017 adaptBins->genAdaptiveHistogram();
1018 bin_index_type adaptiveBins = adaptBins->getCurrentBinCount();
1019 EXPECT_GT(adaptiveBins, 0);
1020
1021 // 3. Sort
1022 adaptBins->sortContainerByBin();
1023
1024 // 4. Iterate per bin and verify all particles are covered.
1025 auto hashArr = adaptBins->getHashArray();
1026 auto h_hash = Kokkos::create_mirror_view(hashArr);
1027 Kokkos::deep_copy(h_hash, hashArr);
1028 auto h_bin = bunch->Bin.getHostMirror();
1029 Kokkos::deep_copy(h_bin, bunch->Bin.getView());
1030
1031 size_type totalCovered = 0;
1032 for (bin_index_type b = 0; b < adaptiveBins; ++b) {
1033 auto policy = adaptBins->getBinIterationPolicy(b);
1034 size_type rangeSize = policy.end() - policy.begin();
1035 totalCovered += rangeSize;
1036
1037 // All particles in this range should have bin == b.
1038 for (auto idx = policy.begin(); idx < policy.end(); ++idx) {
1039 EXPECT_EQ(h_bin(h_hash(idx)), b);
1040 }
1041 }
1042 EXPECT_EQ(totalCovered, bunch->getLocalNum());
1043}
1044
1045// Use gtest_main provided by the framework, so no need to define main() here.
1046// ############################################################################
1047// main: initialize/finalize handled by the test fixture
1048// ############################################################################
1049/*
1050int main(int argc, char* argv[]) {
1051 int result = 1;
1052 ::testing::InitGoogleTest(&argc, argv);
1053 result = RUN_ALL_TESTS();
1054 return result;
1055}*/
Defines a structure to hold particles in energy bins and their associated data.
ippl::Vector< T, Dim > Vector_t
typename Mesh_t< Dim >::DefaultCentering Centering_t
ippl::Field< T, Dim, Mesh_t< Dim >, Centering_t< Dim >, ViewArgs... > Field
double T
Definition OPALTypes.h:8
constexpr unsigned Dim
Definition OPALTypes.h:7
ippl::Field< ippl::Vector< T, Dim >, Dim, ViewArgs... > VField_t
Definition PBunchDefs.h:31
ippl::UniformCartesian< double, 3 > Mesh_t
Definition PBunchDefs.h:18
ippl::detail::size_type size_type
ippl::detail::size_type size_type
TEST_F(BinningTest, ArrayReductionDefaultInit)
ippl::UniformCartesian< TestT, TestDim > TestMesh_t
TestT value_type
double TestT
ippl::FieldLayout< TestDim > TestFL_t
ippl::Vector< T, Dim > Vector
ippl::ParticleSpatialLayout< TestT, TestDim, TestMesh_t > TestPLayout_t
Container_t::bin_index_type bin_index_type
constexpr unsigned TestDim
std::shared_ptr< Container_t > bunch
std::unique_ptr< TestMesh_t > mesh
void TearDown() override
static constexpr TestT domainLen
void SetUp() override
static constexpr size_t gridPoints
static void SetUpTestSuite()
std::unique_ptr< TestPLayout_t > playout
static void TearDownTestSuite()
void buildAdaptBins(bin_index_type maxBins=5, value_type alpha=1.0, value_type beta=1.0, value_type desW=0.1, const std::string &binningCmdName="TEST_BINNING_CMD")
Build an AdaptBins with given parameters.
void createParticlesUniformP(size_t n, int axis, TestT pMin, TestT pMax)
Creates n particles where P[axis] is uniformly spread in [pMin, pMax].
std::unique_ptr< TestFL_t > fl
void createParticles(size_t n)
Creates n particles with random P values in [0.1, 0.9] and R in mesh interior.
std::shared_ptr< AdaptBins_t > adaptBins
A class that bins particles in energy bins and allows for adaptive runtime rebinning.
Definition AdaptBins.h:79
static KOKKOS_INLINE_FUNCTION bin_index_type getBin(value_type x, value_type xMin, value_type xMax, value_type binWidthInv, bin_index_type numBins)
Calculates the bin index for a given position value in a uniform histogram.
Template class providing adaptive particle histogram binning with support for Kokkos Views and DualVi...
Definition BinHisto.h:64
decltype(std::declval< Histo_t >().getHistogram().view_device()) view_type
KOKKOS_FUNCTION void operator()(const size_t) const
FillPolicyHistogram1(view_type v)
FillPolicyHistogram2(view_type v)
KOKKOS_FUNCTION void operator()(const size_t i) const
decltype(std::declval< Histo_t >().getHistogram().view_device()) view_type
A templated structure for performing array-based reductions in parallel computations.
Example struct used to access the binning variable for each particle.
void updateDataArr(bunch_type &bunch)
Updates the data array view with the latest particle data.
Host-only array reduction structure for dynamic array sizes in Kokkos::parallel_reduce.
SizeType * the_array
Pointer to the dynamically allocated array for reduction operations.
Minimal particle bunch for binning unit tests.
short int bin_index_type
~TestBunch()=default
ippl::ParticleAttrib< bin_index_type > Bin
Bin index attribute (used by AdaptBins)
TestBunch(TestPLayout_t &pl)
ippl::ParticleBase< TestPLayout_t > Base
Base::particle_position_type P
Particle momenta (used by CoordinateSelector)