peclet-core 1.2.0__tar.gz → 1.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. {peclet_core-1.2.0 → peclet_core-1.3.0}/CITATION.cff +2 -2
  2. {peclet_core-1.2.0 → peclet_core-1.3.0}/CLAUDE.md +10 -2
  3. {peclet_core-1.2.0 → peclet_core-1.3.0}/PKG-INFO +1 -1
  4. {peclet_core-1.2.0 → peclet_core-1.3.0}/docs/Doxyfile +1 -1
  5. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/halo/particle_halo_topology.hpp +63 -14
  6. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/halo/particle_migrator.hpp +54 -7
  7. {peclet_core-1.2.0 → peclet_core-1.3.0}/packaging/pyproject-core.toml +1 -1
  8. {peclet_core-1.2.0 → peclet_core-1.3.0}/pyproject.toml +1 -1
  9. {peclet_core-1.2.0 → peclet_core-1.3.0}/tests/CMakeLists.txt +8 -0
  10. {peclet_core-1.2.0 → peclet_core-1.3.0}/tests/test_particle_halo_exchange.cpp +70 -45
  11. peclet_core-1.3.0/tests/test_particle_halo_images_mpi.cpp +374 -0
  12. {peclet_core-1.2.0 → peclet_core-1.3.0}/.clang-format +0 -0
  13. {peclet_core-1.2.0 → peclet_core-1.3.0}/.clang-tidy +0 -0
  14. {peclet_core-1.2.0 → peclet_core-1.3.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  15. {peclet_core-1.2.0 → peclet_core-1.3.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  16. {peclet_core-1.2.0 → peclet_core-1.3.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  17. {peclet_core-1.2.0 → peclet_core-1.3.0}/.github/dependabot.yml +0 -0
  18. {peclet_core-1.2.0 → peclet_core-1.3.0}/.github/workflows/ci.yml +0 -0
  19. {peclet_core-1.2.0 → peclet_core-1.3.0}/.github/workflows/docs.yml +0 -0
  20. {peclet_core-1.2.0 → peclet_core-1.3.0}/.github/workflows/quality.yml +0 -0
  21. {peclet_core-1.2.0 → peclet_core-1.3.0}/.github/workflows/release.yml +0 -0
  22. {peclet_core-1.2.0 → peclet_core-1.3.0}/.gitignore +0 -0
  23. {peclet_core-1.2.0 → peclet_core-1.3.0}/CMakeLists.txt +0 -0
  24. {peclet_core-1.2.0 → peclet_core-1.3.0}/LICENSE +0 -0
  25. {peclet_core-1.2.0 → peclet_core-1.3.0}/README.md +0 -0
  26. {peclet_core-1.2.0 → peclet_core-1.3.0}/benchmarks/CMakeLists.txt +0 -0
  27. {peclet_core-1.2.0 → peclet_core-1.3.0}/benchmarks/bench_halo.cpp +0 -0
  28. {peclet_core-1.2.0 → peclet_core-1.3.0}/cmake/PecletCorePinMpiexec.cmake +0 -0
  29. {peclet_core-1.2.0 → peclet_core-1.3.0}/cmake/PecletCoreTest.cmake +0 -0
  30. {peclet_core-1.2.0 → peclet_core-1.3.0}/cmake/SuiteNanobind.cmake +0 -0
  31. {peclet_core-1.2.0 → peclet_core-1.3.0}/cmake/peclet-coreConfig.cmake.in +0 -0
  32. {peclet_core-1.2.0 → peclet_core-1.3.0}/docs/archive/README.md +0 -0
  33. {peclet_core-1.2.0 → peclet_core-1.3.0}/docs/archive/cuda-aware-mpi.md +0 -0
  34. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/common/cpu_budget.hpp +0 -0
  35. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/common/host_parallel.hpp +0 -0
  36. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/common/mpi.hpp +0 -0
  37. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/common/mpi_stub.hpp +0 -0
  38. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/common/portable.hpp +0 -0
  39. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/common/types.hpp +0 -0
  40. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/common/view.hpp +0 -0
  41. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/decomp/block_decomposer.hpp +0 -0
  42. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/decomp/block_indexer.hpp +0 -0
  43. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/decomp/gather_by_global_id.hpp +0 -0
  44. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/decomp/grid_redistribute.hpp +0 -0
  45. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/decomp/morton_indexer.hpp +0 -0
  46. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/decomp/redistribute_topology.hpp +0 -0
  47. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/decomp/stage_comm.hpp +0 -0
  48. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/decomp/stage_target.hpp +0 -0
  49. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/domain.hpp +0 -0
  50. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/field/field_set.hpp +0 -0
  51. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/geom/body_properties.hpp +0 -0
  52. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/geom/device_scene.hpp +0 -0
  53. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/geom/grid_sdf.hpp +0 -0
  54. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/geom/primitives.hpp +0 -0
  55. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/geom/quadrature.hpp +0 -0
  56. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/geom/scene.hpp +0 -0
  57. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/geom/scene_builder.hpp +0 -0
  58. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/geom/scene_query.hpp +0 -0
  59. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/geom/sdf.hpp +0 -0
  60. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/geom/vti_io.hpp +0 -0
  61. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/halo/grid_halo.hpp +0 -0
  62. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/halo/grid_halo_topology.hpp +0 -0
  63. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/halo/nbx.hpp +0 -0
  64. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/halo/particle_halo.hpp +0 -0
  65. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/halo/particle_migrator_view.hpp +0 -0
  66. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/halo/particle_rebalance.hpp +0 -0
  67. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/interp/particle_grid.hpp +0 -0
  68. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/python/kokkos_teardown.hpp +0 -0
  69. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/python/ndarray_interop.hpp +0 -0
  70. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/scheme/cut_cell_closure.hpp +0 -0
  71. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/scheme/ghost_closure.hpp +0 -0
  72. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/solver/coloring.hpp +0 -0
  73. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/solver/csr_bicgstab.hpp +0 -0
  74. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/solver/csr_operator.hpp +0 -0
  75. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/solver/face_csr.hpp +0 -0
  76. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/solver/graph_amg.hpp +0 -0
  77. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/solver/graph_amg_device.hpp +0 -0
  78. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/solver/vector_ops.hpp +0 -0
  79. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/vof/curvature.hpp +0 -0
  80. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/vof/cutcell.hpp +0 -0
  81. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/vof/plic.hpp +0 -0
  82. {peclet_core-1.2.0 → peclet_core-1.3.0}/include/peclet/core/vof/wetting.hpp +0 -0
  83. {peclet_core-1.2.0 → peclet_core-1.3.0}/packaging/shell/core_geom.py +0 -0
  84. {peclet_core-1.2.0 → peclet_core-1.3.0}/packaging/shell/core_init.py +0 -0
  85. {peclet_core-1.2.0 → peclet_core-1.3.0}/packaging/shell/core_mpi.py +0 -0
  86. {peclet_core-1.2.0 → peclet_core-1.3.0}/python/CMakeLists.txt +0 -0
  87. {peclet_core-1.2.0 → peclet_core-1.3.0}/python/halo_bindings.cpp +0 -0
  88. {peclet_core-1.2.0 → peclet_core-1.3.0}/python/packaging/_halo.pyi +0 -0
  89. {peclet_core-1.2.0 → peclet_core-1.3.0}/python/packaging/halo_init.py +0 -0
  90. {peclet_core-1.2.0 → peclet_core-1.3.0}/python/state_hash.py +0 -0
  91. {peclet_core-1.2.0 → peclet_core-1.3.0}/python/state_hash_reference.json +0 -0
  92. {peclet_core-1.2.0 → peclet_core-1.3.0}/python/test_mpi.py +0 -0
  93. {peclet_core-1.2.0 → peclet_core-1.3.0}/tests/data/rcp_pack_seed3_unit.txt +0 -0
  94. {peclet_core-1.2.0 → peclet_core-1.3.0}/tests/python/CMakeLists.txt +0 -0
  95. {peclet_core-1.2.0 → peclet_core-1.3.0}/tests/python/interop_test_module.cpp +0 -0
  96. {peclet_core-1.2.0 → peclet_core-1.3.0}/tests/python/test_ndarray_interop.py +0 -0
  97. {peclet_core-1.2.0 → peclet_core-1.3.0}/tests/test_aligned_weighted.cpp +0 -0
  98. {peclet_core-1.2.0 → peclet_core-1.3.0}/tests/test_aligned_weighted_mpi.cpp +0 -0
  99. {peclet_core-1.2.0 → peclet_core-1.3.0}/tests/test_cpu_budget.cpp +0 -0
  100. {peclet_core-1.2.0 → peclet_core-1.3.0}/tests/test_csr_solver.cpp +0 -0
  101. {peclet_core-1.2.0 → peclet_core-1.3.0}/tests/test_cut_cell_closure.cpp +0 -0
  102. {peclet_core-1.2.0 → peclet_core-1.3.0}/tests/test_decomposition.cpp +0 -0
  103. {peclet_core-1.2.0 → peclet_core-1.3.0}/tests/test_diffusion.cpp +0 -0
  104. {peclet_core-1.2.0 → peclet_core-1.3.0}/tests/test_diffusion_sdf.cpp +0 -0
  105. {peclet_core-1.2.0 → peclet_core-1.3.0}/tests/test_domain.cpp +0 -0
  106. {peclet_core-1.2.0 → peclet_core-1.3.0}/tests/test_field_set.cpp +0 -0
  107. {peclet_core-1.2.0 → peclet_core-1.3.0}/tests/test_geom_batch_device.cpp +0 -0
  108. {peclet_core-1.2.0 → peclet_core-1.3.0}/tests/test_geom_body.cpp +0 -0
  109. {peclet_core-1.2.0 → peclet_core-1.3.0}/tests/test_geom_owner.cpp +0 -0
  110. {peclet_core-1.2.0 → peclet_core-1.3.0}/tests/test_geom_primitives.cpp +0 -0
  111. {peclet_core-1.2.0 → peclet_core-1.3.0}/tests/test_geom_properties.cpp +0 -0
  112. {peclet_core-1.2.0 → peclet_core-1.3.0}/tests/test_geom_quadrature.cpp +0 -0
  113. {peclet_core-1.2.0 → peclet_core-1.3.0}/tests/test_geom_scene.cpp +0 -0
  114. {peclet_core-1.2.0 → peclet_core-1.3.0}/tests/test_geom_scene_builder.cpp +0 -0
  115. {peclet_core-1.2.0 → peclet_core-1.3.0}/tests/test_geom_scene_query.cpp +0 -0
  116. {peclet_core-1.2.0 → peclet_core-1.3.0}/tests/test_geom_tree_grad.cpp +0 -0
  117. {peclet_core-1.2.0 → peclet_core-1.3.0}/tests/test_ghost_particles_mpi.cpp +0 -0
  118. {peclet_core-1.2.0 → peclet_core-1.3.0}/tests/test_graph_amg.cpp +0 -0
  119. {peclet_core-1.2.0 → peclet_core-1.3.0}/tests/test_graph_amg_device.cpp +0 -0
  120. {peclet_core-1.2.0 → peclet_core-1.3.0}/tests/test_grid_halo.cpp +0 -0
  121. {peclet_core-1.2.0 → peclet_core-1.3.0}/tests/test_grid_halo_exchange.cpp +0 -0
  122. {peclet_core-1.2.0 → peclet_core-1.3.0}/tests/test_grid_redistribute_mpi.cpp +0 -0
  123. {peclet_core-1.2.0 → peclet_core-1.3.0}/tests/test_host_parallel.cpp +0 -0
  124. {peclet_core-1.2.0 → peclet_core-1.3.0}/tests/test_morton_indexer.cpp +0 -0
  125. {peclet_core-1.2.0 → peclet_core-1.3.0}/tests/test_nbx_rounds.cpp +0 -0
  126. {peclet_core-1.2.0 → peclet_core-1.3.0}/tests/test_particle_grid.cpp +0 -0
  127. {peclet_core-1.2.0 → peclet_core-1.3.0}/tests/test_particle_halo_mpi.cpp +0 -0
  128. {peclet_core-1.2.0 → peclet_core-1.3.0}/tests/test_particle_migration.cpp +0 -0
  129. {peclet_core-1.2.0 → peclet_core-1.3.0}/tests/test_particle_migrator_view_mpi.cpp +0 -0
  130. {peclet_core-1.2.0 → peclet_core-1.3.0}/tests/test_particle_rebalance.cpp +0 -0
  131. {peclet_core-1.2.0 → peclet_core-1.3.0}/tests/test_sdf.cpp +0 -0
  132. {peclet_core-1.2.0 → peclet_core-1.3.0}/tests/test_skip_mpi.hpp +0 -0
  133. {peclet_core-1.2.0 → peclet_core-1.3.0}/tests/test_stage_redistribute_mpi.cpp +0 -0
  134. {peclet_core-1.2.0 → peclet_core-1.3.0}/tests/test_stage_repartition_mpi.cpp +0 -0
  135. {peclet_core-1.2.0 → peclet_core-1.3.0}/tests/test_stage_target.cpp +0 -0
  136. {peclet_core-1.2.0 → peclet_core-1.3.0}/tests/test_util.hpp +0 -0
  137. {peclet_core-1.2.0 → peclet_core-1.3.0}/tests/test_vti.cpp +0 -0
  138. {peclet_core-1.2.0 → peclet_core-1.3.0}/tools/cuda_aware_mpi_check.cpp +0 -0
  139. {peclet_core-1.2.0 → peclet_core-1.3.0}/tools/cudampi-env.sh +0 -0
@@ -16,8 +16,8 @@ authors:
16
16
  orcid: "https://orcid.org/0000-0001-6099-3583"
17
17
  email: e.a.j.f.peters@tue.nl
18
18
  affiliation: "Eindhoven University of Technology"
19
- version: 1.2.0
20
- date-released: 2026-09-24
19
+ version: 1.3.0
20
+ date-released: 2026-09-26
21
21
  license: MIT
22
22
  doi: 10.5281/zenodo.21132435
23
23
  identifiers:
@@ -31,13 +31,13 @@ judgement call in the moment.
31
31
  ```bash
32
32
  # CPU library + tests (no device dependency):
33
33
  cmake -S . -B build -DCMAKE_BUILD_TYPE=Release && cmake --build build -j
34
- ctest --test-dir build --output-on-failure -LE bench # 72 ctests (73 with `bench`): decomposition, MPI halo, particle migration, MG stages, diffusion, geometry
34
+ ctest --test-dir build --output-on-failure -LE bench # 76 ctests (77 with `bench`): decomposition, MPI halo, particle migration, MG stages, diffusion, geometry
35
35
 
36
36
  # Portable Kokkos device halo (CUDA / HIP / OpenMP) -- opt-in, find_package(Kokkos):
37
37
  export PATH=/usr/local/cuda-13.2/bin:$PATH # if the Kokkos install targets the CUDA backend
38
38
  cmake -S . -B build_kokkos -DPECLET_CORE_ENABLE_KOKKOS=ON \
39
39
  -DCMAKE_PREFIX_PATH=../extern/install/nvidia-cuda
40
- cmake --build build_kokkos -j && ctest --test-dir build_kokkos --output-on-failure -LE bench # 87 ctests (88 with `bench`): + device halo / geometry / solver, np=1,2,4,8
40
+ cmake --build build_kokkos -j && ctest --test-dir build_kokkos --output-on-failure -LE bench # 91 ctests (92 with `bench`): + device halo / geometry / solver, np=1,2,4,8
41
41
  mpirun -np 4 ./build/benchmarks/bench_halo 48 1 300
42
42
 
43
43
  # Python modules + their ctests (test_mpi.py np=1,2,4,8; state_hash; ndarray interop). Release is
@@ -158,6 +158,14 @@ Header-only under `include/peclet/core/`:
158
158
  particle proximity; `forward` (owner→ghost), `reverse` (ghost→owner, accumulate) and
159
159
  `forwardPositions` (periodic image shift) are the cheap per-step exchanges. The standard distributed
160
160
  particle schemes (frozen/replicate, Newton-on, force-accumulate) are compositions of these.
161
+ `build(pos, rcut, includePeriodicSelf = false, allImages = false)`: by default each particle goes
162
+ to each other rank ONCE, at the image nearest that rank's block — on a periodic axis the ORB left
163
+ undecomposed that is always the unshifted image, so a wrapped image inside `rcut` is dropped and a
164
+ pair crossing a rank face while wrapping that axis is seen by neither owner. `allImages = true`
165
+ (opt-in, 1.3.0; dem's distributed step, `../dem/docs/contact_solve_framework.md` §5.3)
166
+ sends every image within `rcut`, one send entry per image, so an owned index may repeat in one
167
+ rank's `sendIdx` and a ghost is identified by (owner id, shift); forward/reverse work per entry.
168
+ Off is byte-identical to the one-image rule. Gate: `test_particle_halo_images_mpi` (np 1–8).
161
169
  - `halo/particle_halo.hpp` — `ParticleHalo<Dim>`: the Kokkos GPU-resident driver for
162
170
  `ParticleHaloTopology` (on-device forward gather + reverse atomic-accumulate; host-staged or
163
171
  GPU-aware MPI). Built from `ParticleHaloTopology::flatten()`; consumed by dem's distributed step.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: peclet-core
3
- Version: 1.2.0
3
+ Version: 1.3.0
4
4
  Summary: peclet.core — compatibility shell: peclet.core.geom is now peclet.geom, peclet.core.mpi is now peclet.halo
5
5
  Project-URL: Homepage, https://github.com/computational-chemical-engineering/peclet
6
6
  Project-URL: Documentation, https://computational-chemical-engineering.github.io/peclet/
@@ -5,7 +5,7 @@
5
5
 
6
6
  DOXYFILE_ENCODING = UTF-8
7
7
  PROJECT_NAME = "core"
8
- PROJECT_NUMBER = 1.2.0
8
+ PROJECT_NUMBER = 1.3.0
9
9
  PROJECT_BRIEF = "Shared MPI block decomposition, asynchronous ghost-layer exchange, SDF geometry, load balancing (header-only C++20)"
10
10
  OUTPUT_DIRECTORY = docs
11
11
  HTML_OUTPUT = html
@@ -21,6 +21,7 @@
21
21
  #ifndef PECLET_CORE_HALO_PARTICLE_HALO_TOPOLOGY_HPP
22
22
  #define PECLET_CORE_HALO_PARTICLE_HALO_TOPOLOGY_HPP
23
23
 
24
+ #include <algorithm>
24
25
  #include <cstring>
25
26
  #include <map>
26
27
  #include <vector>
@@ -49,7 +50,20 @@ class ParticleHaloTopology {
49
50
  /// byte-identical to the cross-rank-only behaviour (no self-ghosts, no MPI self-messages).
50
51
  /// Self-ghosts occupy the ghost slots AFTER the received ones ([numReceived, numGhost)) and are
51
52
  /// filled locally (no MPI).
52
- void build(const std::vector<Vec<Dim>>& pos, double rcut, bool includePeriodicSelf = false) {
53
+ ///
54
+ /// `allImages` changes what the CROSS-RANK exchange sends. Off (the default), each owned particle
55
+ /// goes to each other rank at most once, at the single periodic image nearest that rank's block
56
+ /// (`withinRcutOfBlock`). On a periodic axis the ORB leaves undecomposed, that nearest image is
57
+ /// always the unshifted one (its gap on the axis is 0), so a wrapped image inside `rcut` is never
58
+ /// sent. On, EVERY periodic image of the particle within `rcut` of rank r's block is sent
59
+ /// (`imagesWithinRcutOfBlock` with the identity allowed), one send entry per image: the same
60
+ /// owned index may then appear several times in one rank's send list, each with its own shift,
61
+ /// and a receiver identifies a ghost by (owner id, shift). Every exchange works per entry, so
62
+ /// forwardPositions places each image, and reverse sums every image's contribution onto the
63
+ /// owner. The shift of an allImages entry is exactly 0 or +-L per axis. Off => byte-identical to
64
+ /// the one-image behaviour.
65
+ void build(const std::vector<Vec<Dim>>& pos, double rcut, bool includePeriodicSelf = false,
66
+ bool allImages = false) {
53
67
  numOwned_ = pos.size();
54
68
  int nranks = 0;
55
69
  MPI_Comm_size(mig_->comm(), &nranks);
@@ -59,10 +73,35 @@ class ParticleHaloTopology {
59
73
  std::map<int, std::vector<Index>> sendMap;
60
74
  std::map<int, std::vector<Vec<Dim>>> shiftMap;
61
75
  Vec<Dim> img;
76
+ std::vector<Vec<Dim>> imgShifts;
77
+ // Candidate ranks (ascending): those whose block comes within rcut of the bounding box of my
78
+ // particles, images included (boxWithinRcutOfBlock). A superset of every rank any particle can
79
+ // qualify for, so the send lists -- entries and order -- are those of the all-ranks loop; the
80
+ // per-particle work drops from O(numRanks) to O(neighbours).
81
+ std::vector<int> cand;
82
+ if (!pos.empty()) {
83
+ Vec<Dim> bmin = pos[0], bmax = pos[0];
84
+ for (const auto& x : pos)
85
+ for (int d = 0; d < Dim; ++d) {
86
+ bmin[d] = std::min(bmin[d], x[d]);
87
+ bmax[d] = std::max(bmax[d], x[d]);
88
+ }
89
+ for (int r = 0; r < nranks; ++r)
90
+ if (r != me && mig_->boxWithinRcutOfBlock(bmin, bmax, r, rcut))
91
+ cand.push_back(r);
92
+ }
62
93
  for (std::size_t i = 0; i < pos.size(); ++i) {
63
- for (int r = 0; r < nranks; ++r) {
64
- if (r == me)
94
+ for (const int r : cand) {
95
+ if (allImages) {
96
+ // Every qualifying image, one send entry each (the identity included).
97
+ imgShifts.clear();
98
+ mig_->imagesWithinRcutOfBlock(pos[i], r, rcut, /*allowIdentity=*/true, imgShifts);
99
+ for (const auto& sh : imgShifts) {
100
+ sendMap[r].push_back(static_cast<Index>(i));
101
+ shiftMap[r].push_back(sh);
102
+ }
65
103
  continue;
104
+ }
66
105
  if (!mig_->withinRcutOfBlock(pos[i], r, rcut, img))
67
106
  continue;
68
107
  Vec<Dim> shift;
@@ -74,11 +113,13 @@ class ParticleHaloTopology {
74
113
  }
75
114
  sendRanks_.clear();
76
115
  sendIdx_.clear();
116
+ sendShift_.clear();
77
117
  sendRankPos_.clear();
78
118
  for (auto& [r, idx] : sendMap) {
79
119
  sendRankPos_[r] = sendRanks_.size();
80
120
  sendRanks_.push_back(r);
81
121
  sendIdx_.push_back(std::move(idx));
122
+ sendShift_.push_back(shiftMap[r]); // the image each entry is sent as (parallel to sendIdx_)
82
123
  }
83
124
 
84
125
  // Exchange the shift vectors (one NBX round) to size the ghost array + matched recv lists.
@@ -242,26 +283,33 @@ class ParticleHaloTopology {
242
283
  // pack). recvOffsets index the contiguous [0,numGhost) ghost array (in recvRanks order); shift is
243
284
  // per-ghost (for position forwards). Rebuild after each build().
244
285
  struct FlatTopo {
245
- std::vector<int> sendRanks; // neighbour ranks I send owned copies to
246
- std::vector<Index> sendIdx; // concatenated owned indices to send (all ranks)
247
- std::vector<int> sendCounts; // per send rank
248
- std::vector<int> sendOffsets; // prefix sum into sendIdx (size sendRanks+1)
249
- std::vector<int> recvRanks; // neighbour ranks I receive ghosts from
250
- std::vector<int> recvCounts; // per recv rank
251
- std::vector<Index> recvOffsets; // per recv rank: start in the [0,numReceived) ghost array
252
- std::vector<Vec<Dim>> shift; // per ghost: periodic image offset (add to forwarded position)
253
- std::vector<Index> selfIdx; // owned index of each LOCAL periodic self-ghost (size numSelf)
254
- Index numReceived = 0; // ghost slots [0,numReceived) are cross-rank (MPI), the rest
286
+ std::vector<int> sendRanks; // neighbour ranks I send owned copies to
287
+ std::vector<Index> sendIdx; // concatenated owned indices to send (all ranks); with
288
+ // allImages an index may repeat within one rank's slice
289
+ // (one entry per periodic image)
290
+ std::vector<Vec<Dim>> sendShift; // parallel to sendIdx: the periodic image offset the entry is
291
+ // sent with (the receiver's ghost shift for that entry)
292
+ std::vector<int> sendCounts; // per send rank
293
+ std::vector<int> sendOffsets; // prefix sum into sendIdx (size sendRanks+1)
294
+ std::vector<int> recvRanks; // neighbour ranks I receive ghosts from
295
+ std::vector<int> recvCounts; // per recv rank
296
+ std::vector<Index> recvOffsets; // per recv rank: start in the [0,numReceived) ghost array
297
+ std::vector<Vec<Dim>> shift; // per ghost: periodic image offset (add to forwarded position)
298
+ std::vector<Index> selfIdx; // owned index of each LOCAL periodic self-ghost (size numSelf)
299
+ Index numReceived = 0; // ghost slots [0,numReceived) are cross-rank (MPI), the rest
255
300
  // [numReceived, numGhost) are local self-ghosts gathered from selfIdx
256
301
  };
257
302
  FlatTopo flatten() const {
258
303
  FlatTopo t;
259
304
  t.sendRanks = sendRanks_;
260
305
  t.sendOffsets.push_back(0);
261
- for (const auto& idx : sendIdx_) {
306
+ for (std::size_t k = 0; k < sendIdx_.size(); ++k) {
307
+ const auto& idx = sendIdx_[k];
262
308
  t.sendCounts.push_back(static_cast<int>(idx.size()));
263
309
  for (Index id : idx)
264
310
  t.sendIdx.push_back(id);
311
+ for (const auto& sh : sendShift_[k])
312
+ t.sendShift.push_back(sh);
265
313
  t.sendOffsets.push_back(static_cast<int>(t.sendIdx.size()));
266
314
  }
267
315
  t.recvRanks = recvRanks_;
@@ -281,6 +329,7 @@ class ParticleHaloTopology {
281
329
  // Owner side: particles I send as ghosts to each neighbour rank.
282
330
  std::vector<int> sendRanks_;
283
331
  std::vector<std::vector<Index>> sendIdx_;
332
+ std::vector<std::vector<Vec<Dim>>> sendShift_; // parallel to sendIdx_: each entry's image offset
284
333
  std::map<int, std::size_t> sendRankPos_;
285
334
 
286
335
  // Ghost side: ghosts I receive from each neighbour rank (contiguous slots, in recvRanks_ order).
@@ -199,6 +199,32 @@ class ParticleMigrator {
199
199
  /// periodic face (an undecomposed periodic axis, or np=1). `allowIdentity=false` drops the
200
200
  /// un-shifted image, so r==self yields only the periodic self-wrap copies (a particle is never
201
201
  /// its own ghost).
202
+ /// True iff some point of the box [bmin, bmax] (a periodic image of it included) lies within
203
+ /// `rcut` of rank r's block. A point of the box has, per axis and per image, a gap no smaller
204
+ /// than the box's, so false proves that NO point of the box qualifies for r -- a candidate filter
205
+ /// that is a superset of every per-particle test (withinRcutOfBlock, imagesWithinRcutOfBlock).
206
+ bool boxWithinRcutOfBlock(const Vec<Dim>& bmin, const Vec<Dim>& bmax, int r, double rcut) const {
207
+ const auto& o = dec_->origins()[r];
208
+ const auto& s = dec_->sizes()[r];
209
+ const IVec<Dim>& gsize = dec_->globalSize();
210
+ double d2 = 0.0;
211
+ for (int d = 0; d < Dim; ++d) {
212
+ const double lo = map_.origin[d] + o[d] * map_.cellSize[d];
213
+ const double hi = map_.origin[d] + (o[d] + s[d]) * map_.cellSize[d];
214
+ auto gap = [&](double shift) {
215
+ const double a = bmin[d] + shift, b = bmax[d] + shift;
216
+ return std::max(0.0, std::max(lo - b, a - hi));
217
+ };
218
+ double g = gap(0.0);
219
+ if (map_.periodic[d]) {
220
+ const double L = map_.cellSize[d] * static_cast<double>(gsize[d]);
221
+ g = std::min(g, std::min(gap(-L), gap(L)));
222
+ }
223
+ d2 += g * g;
224
+ }
225
+ return d2 < rcut * rcut;
226
+ }
227
+
202
228
  void imagesWithinRcutOfBlock(const Vec<Dim>& x, int r, double rcut, bool allowIdentity,
203
229
  std::vector<Vec<Dim>>& outShifts) const {
204
230
  const auto& o = dec_->origins()[r];
@@ -218,24 +244,45 @@ class ParticleMigrator {
218
244
  nc[d] = 3;
219
245
  }
220
246
  }
247
+ // Per-axis prefilter (exact): an image qualifies only if its total squared gap is below rc2,
248
+ // so every one of its per-axis gaps is -- keep, per axis and in candidate order, the shifts
249
+ // whose own gap passes, then enumerate their product in the same nesting (axis 0 fastest) as
250
+ // the full 3^Dim walk. Same set, same order, typically one candidate per axis instead of 27
251
+ // combinations (this runs for every owned particle x every rank at every topology build).
252
+ const double rc2 = rcut * rcut;
253
+ int keep[Dim][3];
254
+ double keepGap2[Dim][3];
255
+ int nk[Dim];
256
+ for (int d = 0; d < Dim; ++d) {
257
+ nk[d] = 0;
258
+ for (int k = 0; k < nc[d]; ++k) {
259
+ const double p = x[d] + cand[d][k];
260
+ const double gap = (p < lo[d]) ? (lo[d] - p) : (p > hi[d]) ? (p - hi[d]) : 0.0;
261
+ if (gap * gap < rc2) {
262
+ keep[d][nk[d]] = k;
263
+ keepGap2[d][nk[d]] = gap * gap;
264
+ ++nk[d];
265
+ }
266
+ }
267
+ if (nk[d] == 0)
268
+ return;
269
+ }
221
270
  int total = 1;
222
271
  for (int d = 0; d < Dim; ++d)
223
- total *= nc[d];
224
- const double rc2 = rcut * rcut;
272
+ total *= nk[d];
225
273
  for (int idx = 0; idx < total; ++idx) {
226
274
  Vec<Dim> shift{};
227
275
  int t = idx;
228
276
  bool identity = true;
229
277
  double d2 = 0.0;
230
278
  for (int d = 0; d < Dim; ++d) {
231
- const int k = t % nc[d];
232
- t /= nc[d];
279
+ const int j = t % nk[d];
280
+ t /= nk[d];
281
+ const int k = keep[d][j];
233
282
  shift[d] = cand[d][k];
234
283
  if (k != 0)
235
284
  identity = false;
236
- const double p = x[d] + shift[d];
237
- const double gap = (p < lo[d]) ? (lo[d] - p) : (p > hi[d]) ? (p - hi[d]) : 0.0;
238
- d2 += gap * gap;
285
+ d2 += keepGap2[d][j];
239
286
  }
240
287
  if (d2 < rc2 && (allowIdentity || !identity))
241
288
  outShifts.push_back(shift);
@@ -19,7 +19,7 @@ build-backend = "hatchling.build"
19
19
 
20
20
  [project]
21
21
  name = "peclet-core"
22
- version = "1.2.0"
22
+ version = "1.3.0"
23
23
  description = "peclet.core — compatibility shell: peclet.core.geom is now peclet.geom, peclet.core.mpi is now peclet.halo"
24
24
  readme = "README.md"
25
25
  requires-python = ">=3.10"
@@ -19,7 +19,7 @@ build-backend = "hatchling.build"
19
19
 
20
20
  [project]
21
21
  name = "peclet-core"
22
- version = "1.2.0"
22
+ version = "1.3.0"
23
23
  description = "peclet.core — compatibility shell: peclet.core.geom is now peclet.geom, peclet.core.mpi is now peclet.halo"
24
24
  readme = "README.md"
25
25
  requires-python = ">=3.10"
@@ -227,6 +227,13 @@ if(MPI_FOUND)
227
227
  target_link_libraries(test_particle_halo_mpi PRIVATE peclet_halo)
228
228
  target_include_directories(test_particle_halo_mpi PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
229
229
 
230
+ # ParticleHaloTopology::build(..., allImages = true) on a slab whose periodic z axis the ORB never
231
+ # splits (np 1..8): every periodic image within rcut is sent, keyed (id, image), against a
232
+ # brute-force image-count oracle; reverse sums every image; the default stays the one-image rule.
233
+ add_executable(test_particle_halo_images_mpi test_particle_halo_images_mpi.cpp)
234
+ target_link_libraries(test_particle_halo_images_mpi PRIVATE peclet_halo)
235
+ target_include_directories(test_particle_halo_images_mpi PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
236
+
230
237
  # The device particle-migrator view: needs Kokkos + MPI. peclet_halo carries both when
231
238
  # PECLET_CORE_ENABLE_KOKKOS is on (same target the Kokkos grid-halo test links).
232
239
  if(PECLET_CORE_ENABLE_KOKKOS)
@@ -250,6 +257,7 @@ if(MPI_FOUND)
250
257
  peclet_core_add_mpi_test(diffusion_sdf_np${np} test_diffusion_sdf ${np})
251
258
  peclet_core_add_mpi_test(ghost_particles_np${np} test_ghost_particles_mpi ${np})
252
259
  peclet_core_add_mpi_test(particle_halo_np${np} test_particle_halo_mpi ${np})
260
+ peclet_core_add_mpi_test(particle_halo_images_np${np} test_particle_halo_images_mpi ${np})
253
261
  endforeach()
254
262
 
255
263
  # Portable GPU-resident halo : device result must match the CPU path, on whatever backend
@@ -5,8 +5,12 @@
5
5
  // to match the CPU result bit-for-bit:
6
6
  // forward(id) -> each ghost carries its owner's id,
7
7
  // reverse(ones, sum) -> each owned particle accumulates a count of how many ranks ghost it.
8
+ // Run twice: the default topology, and build(..., allImages = true), where one owned particle may
9
+ // sit several times in one rank's send list (one entry per periodic image) and the device reverse
10
+ // must still sum every image's contribution (atomic add over repeated sendIdx entries).
8
11
  #include <mpi.h>
9
12
 
13
+ #include <algorithm>
10
14
  #include <cstdint>
11
15
  #include <cstdio>
12
16
  #include <cstring>
@@ -78,56 +82,77 @@ int main(int argc, char** argv) {
78
82
  ownId[i] = (double)id;
79
83
  }
80
84
 
81
- ParticleHaloTopology<3> halo;
82
- halo.init(mig);
83
- halo.build(pos, rcut);
84
- const std::size_t G = halo.numGhost();
85
+ long long repeated = 0;
86
+ auto run = [&](bool allImages) {
87
+ ParticleHaloTopology<3> halo;
88
+ halo.init(mig);
89
+ halo.build(pos, rcut, /*includePeriodicSelf=*/allImages, allImages);
90
+ const std::size_t G = halo.numGhost();
91
+ if (allImages) { // count repeated owned indices within one rank's send slice
92
+ const auto t = halo.flatten();
93
+ for (std::size_t k = 0; k < t.sendRanks.size(); ++k) {
94
+ std::vector<Index> sl(t.sendIdx.begin() + t.sendOffsets[k],
95
+ t.sendIdx.begin() + t.sendOffsets[k + 1]);
96
+ std::sort(sl.begin(), sl.end());
97
+ for (std::size_t j = 1; j < sl.size(); ++j)
98
+ repeated += (sl[j] == sl[j - 1]) ? 1 : 0;
99
+ }
100
+ }
85
101
 
86
- // --- CPU reference exchanges ---
87
- std::vector<double> ghCpu(G), ownCntCpu(Nown, 0.0), ones(G, 1.0);
88
- halo.forward(ownId.data(), ghCpu.data());
89
- halo.reverse(ones.data(), ownCntCpu.data());
102
+ // --- CPU reference exchanges ---
103
+ std::vector<double> ghCpu(G), ownCntCpu(Nown, 0.0), ones(G, 1.0);
104
+ halo.forward(ownId.data(), ghCpu.data());
105
+ halo.reverse(ones.data(), ownCntCpu.data());
90
106
 
91
- // --- device exchanges ---
92
- ParticleHalo<3> dhalo;
93
- dhalo.init(halo);
107
+ // --- device exchanges ---
108
+ ParticleHalo<3> dhalo;
109
+ dhalo.init(halo);
94
110
 
95
- View<double> dOwn(Kokkos::view_alloc("own", Kokkos::WithoutInitializing), Nown);
96
- View<double> dGhost(Kokkos::view_alloc("ghost", Kokkos::WithoutInitializing), G);
97
- {
98
- auto hOwn = Kokkos::create_mirror_view(dOwn);
99
- for (std::size_t i = 0; i < Nown; ++i)
100
- hOwn(i) = ownId[i];
101
- Kokkos::deep_copy(dOwn, hOwn);
102
- }
103
- dhalo.forward(dOwn, dGhost);
104
- std::vector<double> ghDev(G);
105
- {
106
- auto hGhost = Kokkos::create_mirror_view(dGhost);
107
- Kokkos::deep_copy(hGhost, dGhost);
108
- for (std::size_t i = 0; i < G; ++i)
109
- ghDev[i] = hGhost(i);
110
- }
111
+ View<double> dOwn(Kokkos::view_alloc("own", Kokkos::WithoutInitializing), Nown);
112
+ View<double> dGhost(Kokkos::view_alloc("ghost", Kokkos::WithoutInitializing), G);
113
+ {
114
+ auto hOwn = Kokkos::create_mirror_view(dOwn);
115
+ for (std::size_t i = 0; i < Nown; ++i)
116
+ hOwn(i) = ownId[i];
117
+ Kokkos::deep_copy(dOwn, hOwn);
118
+ }
119
+ dhalo.forward(dOwn, dGhost);
120
+ std::vector<double> ghDev(G);
121
+ {
122
+ auto hGhost = Kokkos::create_mirror_view(dGhost);
123
+ Kokkos::deep_copy(hGhost, dGhost);
124
+ for (std::size_t i = 0; i < G; ++i)
125
+ ghDev[i] = hGhost(i);
126
+ }
111
127
 
112
- View<double> dGones(Kokkos::view_alloc("gones", Kokkos::WithoutInitializing), G);
113
- View<double> dOwnCnt("owncnt", Nown); // zero-initialised
114
- Kokkos::deep_copy(dGones, 1.0);
115
- dhalo.reverse(dGones, dOwnCnt);
116
- std::vector<double> ownCntDev(Nown);
117
- {
118
- auto hCnt = Kokkos::create_mirror_view(dOwnCnt);
119
- Kokkos::deep_copy(hCnt, dOwnCnt);
120
- for (std::size_t i = 0; i < Nown; ++i)
121
- ownCntDev[i] = hCnt(i);
122
- }
128
+ View<double> dGones(Kokkos::view_alloc("gones", Kokkos::WithoutInitializing), G);
129
+ View<double> dOwnCnt("owncnt", Nown); // zero-initialised
130
+ Kokkos::deep_copy(dGones, 1.0);
131
+ dhalo.reverse(dGones, dOwnCnt);
132
+ std::vector<double> ownCntDev(Nown);
133
+ {
134
+ auto hCnt = Kokkos::create_mirror_view(dOwnCnt);
135
+ Kokkos::deep_copy(hCnt, dOwnCnt);
136
+ for (std::size_t i = 0; i < Nown; ++i)
137
+ ownCntDev[i] = hCnt(i);
138
+ }
123
139
 
124
- // --- device must match CPU bit-for-bit ---
125
- for (std::size_t i = 0; i < G; ++i)
126
- if (ghDev[i] != ghCpu[i])
127
- ++fail;
128
- for (std::size_t i = 0; i < Nown; ++i)
129
- if (ownCntDev[i] != ownCntCpu[i])
130
- ++fail;
140
+ // --- device must match CPU bit-for-bit ---
141
+ for (std::size_t i = 0; i < G; ++i)
142
+ if (ghDev[i] != ghCpu[i])
143
+ ++fail;
144
+ for (std::size_t i = 0; i < Nown; ++i)
145
+ if (ownCntDev[i] != ownCntCpu[i])
146
+ ++fail;
147
+ };
148
+ run(false);
149
+ run(true);
150
+ // The allImages pass must actually exercise repeated send entries where a periodic axis is
151
+ // undecomposed (np 2 and 4 of this ORB; np 1 has no cross-rank exchange).
152
+ long long repeatedAll = 0;
153
+ MPI_Allreduce(&repeated, &repeatedAll, 1, MPI_LONG_LONG, MPI_SUM, MPI_COMM_WORLD);
154
+ if (size >= 2 && repeatedAll == 0)
155
+ ++fail;
131
156
  }
132
157
 
133
158
  int total = 0;