Skip to content

Commit 8836f4a

Browse files
roelof-groenewaldatmyersax3l
authored
Allow extra particle attributes (besides ux, uy, uz and w) to be set at particle creation in AddNParticles() (#2115)
* exposes AddRealComp to Python to allow extra particle attributes to be added at runtime; also includes a new function to grab a particle data array from the name of the component rather than the index * added functionality to AddNParticles() to allow extra particle attributes to also be set during particle creation * added function to get index of a particle component given the PID name * changed new get component index and get_particle_arrays_from_comp_name functions to take species name as argument rather than species id * changed warpx_addRealComp to accept a species name as input and only add the new component for that species * added a test of the pywarpx bridge to get particle data and add new particle attributes at runtime * changed all particle interacting functions in libwarpx to use the species name rather than id, also changed the functions to get particle array data to use the component name rather than index * updated test according to PR #2119 changes * removed unneeded BL_ASSERT(nattr == 1) statement * fixed bug in add_particles to correctly determine the number of extra attributes * fixed bug in AddNParticles if fewer attribute values are passed than the number of extra arrays for the species * use isinstance(attr, ndarray) rather than type(attr) is np.ndarray * generalize_runtime_comps_io * fix OpenPMD * fix OpenPMD * fix plot flags in WritePlotFile * fix offset and comment * changed extra pid test to not use an underscore in the pid name * switched _libwarpx.py::add_particles to use kwargs to accept the weight and extra attribute arrays * License update in test file Co-authored-by: Axel Huebl <[email protected]> * fix typo * added a test with unique_particles=False * Apply suggestions from code review Co-authored-by: Axel Huebl <[email protected]> * updated docstring and comments Co-authored-by: atmyers <[email protected]> Co-authored-by: Axel Huebl <[email protected]>
1 parent cd8cb0b commit 8836f4a

File tree

8 files changed

+257
-27
lines changed

8 files changed

+257
-27
lines changed
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
from mpi4py import MPI
2+
from pywarpx import picmi
3+
import numpy as np
4+
5+
##########################
6+
# MPI communicator setup
7+
##########################
8+
9+
# split processor 0 into separate communicator from others
10+
comm_world = MPI.COMM_WORLD
11+
rank = comm_world.Get_rank()
12+
if rank == 0:
13+
color = 0
14+
else:
15+
color = 1
16+
new_comm = comm_world.Split(color)
17+
18+
##########################
19+
# numerics parameters
20+
##########################
21+
22+
dt = 7.5e-10
23+
24+
# --- Nb time steps
25+
26+
max_steps = 10
27+
28+
# --- grid
29+
30+
nx = 64
31+
ny = 64
32+
33+
xmin = 0
34+
xmax = 0.03
35+
ymin = 0
36+
ymax = 0.03
37+
38+
39+
##########################
40+
# numerics components
41+
##########################
42+
43+
grid = picmi.Cartesian2DGrid(
44+
number_of_cells = [nx, ny],
45+
lower_bound = [xmin, ymin],
46+
upper_bound = [xmax, ymax],
47+
lower_boundary_conditions = ['dirichlet', 'periodic'],
48+
upper_boundary_conditions = ['dirichlet', 'periodic'],
49+
lower_boundary_conditions_particles = ['absorbing', 'periodic'],
50+
upper_boundary_conditions_particles = ['absorbing', 'periodic'],
51+
moving_window_velocity = None,
52+
warpx_max_grid_size = 32
53+
)
54+
55+
solver = picmi.ElectrostaticSolver(
56+
grid=grid, method='Multigrid', required_precision=1e-6,
57+
warpx_self_fields_verbosity=0
58+
)
59+
60+
##########################
61+
# physics components
62+
##########################
63+
64+
electrons = picmi.Species(
65+
particle_type='electron', name='electrons'
66+
)
67+
68+
##########################
69+
# diagnostics
70+
##########################
71+
72+
field_diag = picmi.FieldDiagnostic(
73+
name = 'diag1',
74+
grid = grid,
75+
period = 10,
76+
data_list = ['phi'],
77+
write_dir = '.',
78+
warpx_file_prefix = f'Python_particle_attr_access_plt_{color}'
79+
)
80+
81+
##########################
82+
# simulation setup
83+
##########################
84+
85+
sim = picmi.Simulation(
86+
solver = solver,
87+
time_step_size = dt,
88+
max_steps = max_steps,
89+
verbose = 1
90+
)
91+
92+
sim.add_species(
93+
electrons,
94+
layout = picmi.GriddedLayout(
95+
n_macroparticle_per_cell=[0, 0], grid=grid
96+
)
97+
)
98+
sim.add_diagnostic(field_diag)
99+
100+
sim.initialize_inputs()
101+
sim.initialize_warpx(mpi_comm=new_comm)
102+
103+
##########################
104+
# python particle data access
105+
##########################
106+
107+
from pywarpx import _libwarpx, callbacks
108+
109+
_libwarpx.add_real_comp('electrons', 'newPid')
110+
111+
def add_particles():
112+
113+
nps = 10
114+
x = np.random.rand(nps) * 0.03
115+
y = np.zeros(nps)
116+
z = np.random.random(nps) * 0.03
117+
ux = np.random.normal(loc=0, scale=1e3, size=nps)
118+
uy = np.random.normal(loc=0, scale=1e3, size=nps)
119+
uz = np.random.normal(loc=0, scale=1e3, size=nps)
120+
w = np.ones(nps) * 2.0
121+
newPid = 5.0
122+
123+
_libwarpx.add_particles(
124+
species_name='electrons', x=x, y=y, z=z, ux=ux, uy=uy, uz=uz,
125+
w=w, newPid=newPid, unique_particles=(not color)
126+
)
127+
128+
callbacks.installbeforestep(add_particles)
129+
130+
##########################
131+
# simulation run
132+
##########################
133+
134+
sim.step(max_steps - 1, mpi_comm=new_comm)
135+
136+
##########################
137+
# check that the new PIDs are properly set
138+
##########################
139+
140+
if color == 0:
141+
assert (_libwarpx.get_particle_count('electrons') == 90)
142+
else:
143+
assert (_libwarpx.get_particle_count('electrons') == 90)
144+
assert (_libwarpx.get_particle_comp_index('electrons', 'w') == 0)
145+
assert (_libwarpx.get_particle_comp_index('electrons', 'newPid') == 4)
146+
147+
new_pid_vals = _libwarpx.get_particle_arrays(
148+
'electrons', 'newPid', 0
149+
)
150+
for vals in new_pid_vals:
151+
assert np.allclose(vals, 5)
152+
153+
##########################
154+
# take the final sim step
155+
##########################
156+
157+
sim.step(1)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#! /usr/bin/env python
2+
3+
# Copyright 2021 Modern Electron
4+
#
5+
# License: BSD-3-Clause-LBNL
6+
7+
# This script just checks that the PICMI file executed successfully.
8+
# If it did there will be a plotfile for the final step.
9+
10+
import yt
11+
12+
plotfile = 'Python_particle_attr_access_plt_000010'
13+
ds = yt.load( plotfile ) # noqa
14+
15+
plotfile = 'Python_particle_attr_access_plt_100010'
16+
ds = yt.load( plotfile ) # noqa
17+
18+
assert True

Python/pywarpx/_libwarpx.py

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,15 @@ def get_nattr():
243243
# --- The -3 is because the comps include the velocites
244244
return libwarpx.warpx_nComps() - 3
245245

246+
def get_nattr_species(species_name):
247+
'''
248+
249+
Get the number of real attributes for the given species.
250+
251+
'''
252+
return libwarpx.warpx_nCompsSpecies(
253+
ctypes.c_char_p(species_name.encode('utf-8')))
254+
246255
def amrex_init(argv, mpi_comm=None):
247256
# --- Construct the ctype list of strings to pass in
248257
argc = len(argv)
@@ -361,9 +370,8 @@ def getCellSize(direction, level=0):
361370
#
362371
# libwarpx.warpx_ComputePMLFactors(lev, dt)
363372

364-
def add_particles(species_name,
365-
x=0., y=0., z=0., ux=0., uy=0., uz=0., attr=0.,
366-
unique_particles=True):
373+
def add_particles(species_name, x=0., y=0., z=0., ux=0., uy=0., uz=0.,
374+
unique_particles=True, **kwargs):
367375
'''
368376
369377
A function for adding particles to the WarpX simulation.
@@ -374,9 +382,11 @@ def add_particles(species_name,
374382
species_name : the species to add the particle to
375383
x, y, z : arrays or scalars of the particle positions (default = 0.)
376384
ux, uy, uz : arrays or scalars of the particle momenta (default = 0.)
377-
attr : a 2D numpy array or scalar with the particle attributes (default = 0.)
378385
unique_particles : whether the particles are unique or duplicated on
379386
several processes. (default = True)
387+
kwargs : dictionary containing an entry for the particle weights
388+
(with keyword 'w') and all the extra particle attribute
389+
arrays. If an attribute is not given it will be set to 0.
380390
381391
'''
382392

@@ -387,20 +397,20 @@ def add_particles(species_name,
387397
lenux = np.size(ux)
388398
lenuy = np.size(uy)
389399
lenuz = np.size(uz)
390-
lenattr = np.size(attr)
391400

392401
if (lenx == 0 or leny == 0 or lenz == 0 or lenux == 0 or
393-
lenuy == 0 or lenuz == 0 or lenattr == 0):
402+
lenuy == 0 or lenuz == 0):
394403
return
395404

396-
maxlen = max(lenx, leny, lenz, lenux, lenuy, lenuz, lenattr)
405+
maxlen = max(lenx, leny, lenz, lenux, lenuy, lenuz)
397406
assert lenx==maxlen or lenx==1, "Length of x doesn't match len of others"
398407
assert leny==maxlen or leny==1, "Length of y doesn't match len of others"
399408
assert lenz==maxlen or lenz==1, "Length of z doesn't match len of others"
400409
assert lenux==maxlen or lenux==1, "Length of ux doesn't match len of others"
401410
assert lenuy==maxlen or lenuy==1, "Length of uy doesn't match len of others"
402411
assert lenuz==maxlen or lenuz==1, "Length of uz doesn't match len of others"
403-
assert lenattr==maxlen or lenattr==1, "Length of attr doesn't match len of others"
412+
for key, val in kwargs.items():
413+
assert np.size(val)==1 or len(val)==maxlen, f"Length of {key} doesn't match len of others"
404414

405415
if lenx == 1:
406416
x = np.array(x)*np.ones(maxlen)
@@ -414,15 +424,27 @@ def add_particles(species_name,
414424
uy = np.array(uy)*np.ones(maxlen)
415425
if lenuz == 1:
416426
uz = np.array(uz)*np.ones(maxlen,'d')
417-
if lenattr == 1:
418-
nattr = get_nattr()
419-
attr = np.array(attr)*np.ones([maxlen,nattr])
427+
for key, val in kwargs.items():
428+
if np.size(val) == 1:
429+
kwargs[key] = np.array(val)*np.ones(maxlen)
430+
431+
# --- The -3 is because the comps include the velocites
432+
nattr = get_nattr_species(species_name) - 3
433+
attr = np.zeros((maxlen, nattr))
434+
435+
for key, vals in kwargs.items():
436+
if key == 'w':
437+
attr[:,0] = vals
438+
else:
439+
# --- The -3 is because components 1 to 3 are velocities
440+
attr[:,get_particle_comp_index(species_name, key)-3] = vals
420441

421442
libwarpx.warpx_addNParticles(
422443
ctypes.c_char_p(species_name.encode('utf-8')), x.size,
423-
x, y, z, ux, uy, uz, attr.shape[-1], attr, unique_particles
444+
x, y, z, ux, uy, uz, nattr, attr, unique_particles
424445
)
425446

447+
426448
def get_particle_count(species_name):
427449
'''
428450
@@ -444,6 +466,7 @@ def get_particle_count(species_name):
444466
ctypes.c_char_p(species_name.encode('utf-8'))
445467
)
446468

469+
447470
def get_particle_structs(species_name, level):
448471
'''
449472

Regression/WarpX-tests.ini

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2294,3 +2294,19 @@ compareParticles = 1
22942294
particleTypes = electrons
22952295
analysisRoutine = Examples/Modules/ParticleBoundaryProcess/analysis_absorption.py
22962296
tolerance = 1.0e-4
2297+
2298+
[Python_particle_attr_access]
2299+
buildDir = .
2300+
inputFile = Examples/Tests/ParticleDataPython/PICMI_inputs_2d.py
2301+
runtime_params =
2302+
customRunCmd = python PICMI_inputs_2d.py
2303+
dim = 2
2304+
addToCompileString = USE_PYTHON_MAIN=TRUE PYINSTALLOPTIONS="--user --prefix="
2305+
restartTest = 0
2306+
useMPI = 1
2307+
numprocs = 3
2308+
useOMP = 0
2309+
numthreads = 0
2310+
compileTest = 0
2311+
doVis = 0
2312+
analysisRoutine = Examples/Tests/ParticleDataPython/analysis.py

Source/Diagnostics/FlushFormats/FlushFormatPlotfile.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,7 @@ FlushFormatPlotfile::WriteParticles(const std::string& dir,
374374
}, true);
375375

376376
// real_names contains a list of all particle attributes.
377-
// particle_diags[i].plot_flags is 1 or 0, whether quantity is dumped or not.
377+
// real_flags & int_flags are 1 or 0, whether quantity is dumped or not.
378378
tmp.WritePlotFile(
379379
dir, particle_diags[i].getSpeciesName(),
380380
real_flags, int_flags,

Source/Particles/WarpXParticleContainer.cpp

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -139,13 +139,6 @@ WarpXParticleContainer::AddNParticles (int /*lev*/,
139139
const ParticleReal* vx, const ParticleReal* vy, const ParticleReal* vz,
140140
int nattr, const ParticleReal* attr, int uniqueparticles, amrex::Long id)
141141
{
142-
// nattr is unused below but needed in the BL_ASSERT
143-
amrex::ignore_unused(nattr);
144-
145-
BL_ASSERT(nattr == 1);
146-
147-
const ParticleReal* weight = attr;
148-
149142
int ibegin, iend;
150143
if (uniqueparticles) {
151144
ibegin = 0;
@@ -175,6 +168,9 @@ WarpXParticleContainer::AddNParticles (int /*lev*/,
175168

176169
std::size_t np = iend-ibegin;
177170

171+
// treat weight as a special attr since it will always be specified
172+
Vector<ParticleReal> weight(np);
173+
178174
#ifdef WARPX_DIM_RZ
179175
Vector<ParticleReal> theta(np);
180176
#endif
@@ -204,16 +200,15 @@ WarpXParticleContainer::AddNParticles (int /*lev*/,
204200
p.pos(1) = z[i];
205201
#endif
206202

207-
if ( (NumRuntimeRealComps()>0) || (NumRuntimeIntComps()>0) ){
208-
DefineAndReturnParticleTile(0, 0, 0);
209-
}
210-
211203
pinned_tile.push_back(p);
204+
205+
// grab weight from the attr array
206+
weight[i-ibegin] = attr[i*nattr];
212207
}
213208

214209
if (np > 0)
215210
{
216-
pinned_tile.push_back_real(PIdx::w , weight + ibegin, weight + iend);
211+
pinned_tile.push_back_real(PIdx::w , weight.data(), weight.data() + np);
217212
pinned_tile.push_back_real(PIdx::ux, vx + ibegin, vx + iend);
218213
pinned_tile.push_back_real(PIdx::uy, vy + ibegin, vy + iend);
219214
pinned_tile.push_back_real(PIdx::uz, vz + ibegin, vz + iend);
@@ -236,9 +231,20 @@ WarpXParticleContainer::AddNParticles (int /*lev*/,
236231
#endif
237232
}
238233

239-
for (int i = PIdx::nattribs; i < NumRealComps(); ++i)
234+
for (int j = PIdx::nattribs; j < NumRealComps(); ++j)
240235
{
241-
pinned_tile.push_back_real(i, 0.0);
236+
if (j - PIdx::nattribs < nattr - 1) {
237+
// get the next attribute from attr array
238+
Vector<ParticleReal> attr_vals(np);
239+
for (int i = ibegin; i < iend; ++i)
240+
{
241+
attr_vals[i-ibegin] = attr[j - PIdx::nattribs + 1 + i*nattr];
242+
}
243+
pinned_tile.push_back_real(j, attr_vals.data(), attr_vals.data() + np);
244+
}
245+
else {
246+
pinned_tile.push_back_real(j, np, 0.0);
247+
}
242248
}
243249

244250
auto old_np = particle_tile.numParticles();

Source/Python/WarpXWrappers.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,14 @@ extern "C"
132132
return PIdx::nattribs;
133133
}
134134

135+
int warpx_nCompsSpecies(const char* char_species_name)
136+
{
137+
auto & mypc = WarpX::GetInstance().GetPartContainer();
138+
const std::string species_name(char_species_name);
139+
auto & myspc = mypc.GetParticleContainerFromName(species_name);
140+
return myspc.NumRealComps();
141+
}
142+
135143
int warpx_SpaceDim()
136144
{
137145
return AMREX_SPACEDIM;

0 commit comments

Comments
 (0)