Skip to content

Commit defb663

Browse files
authored
Doc: Compute Pure SoA Layout (#249)
* Doc: Compute Pure SoA Layout Add an explicit and GPU/CPU portable example to compute on the new, pure SoA particle layout. * `ParticleInitData` for PureSoA
1 parent ded4955 commit defb663

File tree

5 files changed

+92
-22
lines changed

5 files changed

+92
-22
lines changed

docs/source/usage/compute.rst

+18-6
Original file line numberDiff line numberDiff line change
@@ -61,15 +61,27 @@ AMReX `Particles <https://amrex-codes.github.io/amrex/docs_html/Particle_Chapter
6161
There are a few small differences to the `iteration over a ParticleContainer <https://amrex-codes.github.io/amrex/docs_html/Particle.html#iterating-over-particles>`__ compared to a ``MultiFab``:
6262

6363
* ``ParticleContainer`` is aware of mesh-refinement levels,
64-
* AMReX supports a variety of data layouts for particles (AoS and SoA + runtime SoA attributes), which requires a few more calls to access.
64+
* AMReX supports a variety of data layouts for particles, the modern pure SoA + runtime attribute layout and the legacy AoS + SoA + runtime SoA attributes layout.
6565

6666
Here is the general structure for computing on particles:
6767

68-
.. literalinclude:: ../../../tests/test_particleContainer.py
69-
:language: python3
70-
:dedent: 4
71-
:start-after: # Manual: Compute PC START
72-
:end-before: # Manual: Compute PC END
68+
.. tab-set::
69+
70+
.. tab-item:: Modern (pure SoA) Layout
71+
72+
.. literalinclude:: ../../../tests/test_particleContainer.py
73+
:language: python3
74+
:dedent: 4
75+
:start-after: # Manual: Pure SoA Compute PC START
76+
:end-before: # Manual: Pure SoA Compute PC END
77+
78+
.. tab-item:: Legacy (AoS + SoA) Layout
79+
80+
.. literalinclude:: ../../../tests/test_particleContainer.py
81+
:language: python3
82+
:dedent: 4
83+
:start-after: # Manual: Legacy Compute PC START
84+
:end-before: # Manual: Legacy Compute PC END
7385

7486
For many small CPU and GPU examples on how to compute on particles, see the following test cases:
7587

src/Particle/ParticleContainer.H

+8-7
Original file line numberDiff line numberDiff line change
@@ -380,13 +380,17 @@ void make_ParticleContainer_and_Iterators (py::module &m, std::string allocstr)
380380
// }
381381
;
382382

383+
py_pc
384+
.def("InitRandom", py::overload_cast<Long, ULong, const ParticleInitData&, bool, RealBox>(&ParticleContainerType::InitRandom))
385+
;
386+
383387
// TODO for pure SoA
384388
// depends on https://github.com/AMReX-Codes/amrex/pull/3280
385389
if constexpr (!T_ParticleType::is_soa_particle) {
386390
py_pc
387-
.def("InitRandom", py::overload_cast<Long, ULong, const ParticleInitData&, bool, RealBox>(&ParticleContainerType::InitRandom)) // TODO pure SoA
388-
.def("InitRandomPerBox", py::overload_cast<Long, ULong, const ParticleInitData&>(&ParticleContainerType::InitRandomPerBox)) // TODO pure SoA
389-
.def("InitOnePerCell", &ParticleContainerType::InitOnePerCell);
391+
.def("InitRandomPerBox", py::overload_cast<Long, ULong, const ParticleInitData&>(&ParticleContainerType::InitRandomPerBox))
392+
.def("InitOnePerCell", &ParticleContainerType::InitOnePerCell)
393+
;
390394
}
391395

392396
using iterator = amrex::ParIter_impl<ParticleType, T_NArrayReal, T_NArrayInt, Allocator>;
@@ -408,10 +412,7 @@ void make_ParticleContainer_and_Iterators (py::module &m, std::string allocstr)
408412
template <typename T_ParticleType, int T_NArrayReal=0, int T_NArrayInt=0>
409413
void make_ParticleContainer_and_Iterators (py::module &m)
410414
{
411-
// TODO for pure SoA
412-
// depends on https://github.com/AMReX-Codes/amrex/pull/3280
413-
if constexpr (!T_ParticleType::is_soa_particle)
414-
make_ParticleInitData<T_ParticleType, T_NArrayReal, T_NArrayInt>(m);
415+
make_ParticleInitData<T_ParticleType, T_NArrayReal, T_NArrayInt>(m);
415416

416417
// first, because used as copy target in methods in containers with other allocators
417418
make_ParticleContainer_and_Iterators<T_ParticleType, T_NArrayReal, T_NArrayInt,

src/Particle/ParticleContainer_ImpactX.cpp

-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,5 @@ void init_ParticleContainer_ImpactX(py::module& m) {
1414

1515
// TODO: we might need to move all or most of the defines in here into a
1616
// test/example submodule, so they do not collide with downstream projects
17-
make_ParticleContainer_and_Iterators<Particle<0, 0>, 5, 0>(m); // ImpactX 22.07 - 24.02
1817
make_ParticleContainer_and_Iterators<SoAParticle<8, 0>, 8, 0>(m); // ImpactX 24.03+
1918
}

src/Particle/ParticleContainer_WarpX.cpp

-3
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,6 @@ void init_ParticleContainer_WarpX(py::module& m) {
1313

1414
// TODO: we might need to move all or most of the defines in here into a
1515
// test/example submodule, so they do not collide with downstream projects
16-
make_ParticleContainer_and_Iterators<Particle<0, 0>, 4, 0>(m); // WarpX 22.07 - 24.02 1D-3D
17-
//make_ParticleContainer_and_Iterators<Particle<0, 0>, 5, 0> (m); // WarpX 22.07 - 24.02 RZ
18-
1916
#if AMREX_SPACEDIM == 1
2017
make_ParticleContainer_and_Iterators<SoAParticle<5, 0>, 5, 0>(m); // WarpX 24.03+ 1D
2118
#elif AMREX_SPACEDIM == 2

tests/test_particleContainer.py

+66-5
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,18 @@ def particle_container(Npart, std_geometry, distmap, boxarr, std_real_box):
4343
return pc
4444

4545

46+
@pytest.fixture(scope="function")
47+
def soa_particle_container(Npart, std_geometry, distmap, boxarr, std_real_box):
48+
pc = amr.ParticleContainer_pureSoA_8_0_default(std_geometry, distmap, boxarr)
49+
myt = amr.ParticleInitType_pureSoA_8_0()
50+
myt.real_array_data = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8]
51+
myt.int_array_data = []
52+
53+
iseed = 1
54+
pc.InitRandom(Npart, iseed, myt, False, std_real_box)
55+
return pc
56+
57+
4658
def test_particleInitType():
4759
myt = amr.ParticleInitType_1_1_2_1()
4860
print(myt.real_struct_data)
@@ -276,24 +288,73 @@ def test_per_cell(empty_particle_container, std_geometry, std_particle):
276288
assert ncells * std_particle.real_array_data[1] == sum_1
277289

278290

291+
def test_soa_pc_numpy(soa_particle_container, Npart):
292+
"""Used in docs/source/usage/compute.rst"""
293+
pc = soa_particle_container
294+
295+
class Config:
296+
have_gpu = False
297+
298+
# Manual: Pure SoA Compute PC START
299+
# code-specific getter function, e.g.:
300+
# pc = sim.get_particles()
301+
# Config = sim.extension.Config
302+
303+
# iterate over mesh-refinement level (no MR: lev=0)
304+
for lvl in range(pc.finest_level + 1):
305+
# get every local chunk of particles
306+
for pti in pc.iterator(pc, level=lvl):
307+
# compile-time and runtime attributes in SoA format
308+
soa = pti.soa().to_cupy() if Config.have_gpu else pti.soa().to_numpy()
309+
310+
# notes:
311+
# Only the next lines are the "HOT LOOP" of the computation.
312+
# For speed, use array operations.
313+
314+
# write to all particles in the chunk
315+
# note: careful, if you change particle positions, you might need to
316+
# redistribute particles before continuing the simulation step
317+
print(soa.real)
318+
soa.real[0][()] = 0.30 # x
319+
soa.real[1][()] = 0.35 # y
320+
soa.real[2][()] = 0.40 # z
321+
322+
# all other real attributes
323+
for soa_real in soa.real[3:]:
324+
soa_real[()] = 42.0
325+
326+
# all int attributes
327+
for soa_int in soa.int:
328+
soa_int[()] = 12
329+
# Manual: Pure SoA Compute PC END
330+
331+
279332
def test_pc_numpy(particle_container, Npart):
280333
"""Used in docs/source/usage/compute.rst"""
281334
pc = particle_container
282335

283-
# Manual: Compute PC START
336+
class Config:
337+
have_gpu = False
338+
339+
# Manual: Legacy Compute PC START
284340
# code-specific getter function, e.g.:
285341
# pc = sim.get_particles()
342+
# Config = sim.extension.Config
286343

287-
# iterate over every mesh-refinement levels (no MR: lev=0)
344+
# iterate over mesh-refinement level (no MR: lev=0)
288345
for lvl in range(pc.finest_level + 1):
289346
# get every local chunk of particles
290347
for pti in pc.iterator(pc, level=lvl):
291348
# default layout: AoS with positions and cpuid
292349
# note: not part of the new PureSoA particle container layout
293-
aos = pti.aos().to_numpy()
350+
aos = (
351+
pti.aos().to_numpy(copy=True)
352+
if Config.have_gpu
353+
else pti.aos().to_numpy()
354+
)
294355

295356
# additional compile-time and runtime attributes in SoA format
296-
soa = pti.soa().to_numpy()
357+
soa = pti.soa().to_cupy() if Config.have_gpu else pti.soa().to_numpy()
297358

298359
# notes:
299360
# Only the next lines are the "HOT LOOP" of the computation.
@@ -313,7 +374,7 @@ def test_pc_numpy(particle_container, Npart):
313374

314375
for soa_int in soa.int:
315376
soa_int[()] = 12
316-
# Manual: Compute PC END
377+
# Manual: Legacy Compute PC END
317378

318379

319380
@pytest.mark.skipif(

0 commit comments

Comments
 (0)