-
Notifications
You must be signed in to change notification settings - Fork 389
/
Copy pathparseparticles.py
executable file
·415 lines (285 loc) · 12.1 KB
/
parseparticles.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
"""
a module for parsing MAESTRO particle files
Particle histories in MAESTRO are stored in a set of files, e.g.
timestamp_#, where the # in the file name is the number of the CPU
(actually MPI process) with particles. As particles advect, they
may jump to different CPUs, so a given particle's history may
initially be written out in one file and then switch to a different
file. This module sorts the particle data to be in proper time
order.
This module should be imported in your own python script, for example:
>>> import parseparticles
>>> myparticles = parseparticles.parseParticleFile(myParticleFiles)
where myParticleFiles is a list of the timestamp files containing
the particle data.
This returns a dictionary whose keys are internal ids (comprised
of the particle's id and its origin CPU) and values are particle
objects, which contain all of the associated information about a
particle's history
Usually we don't care about the original particle ID information, so
we just strip out the particle objects into a list:
>>> particles = myparticles.values()
Here, each item in the list is a particle object.
Each particle object contains data describing the particle
identity, as well as a history. The history itself is a list of
particleInstance objects. Each particleInstance object stores
the state of the particle at an instance in time. For example:
>>> time = particles[n].history[i].t
is the time associated with particleInstance i of particle n.
There is a unique particleInstance for particle n for every
timestep of this particle's existence.
Coordinate information is accessed as:
>>> x = particles[n].history[i].xyz[0]
for the x-coordinate (xyz[1] would be y, etc.).
State data is stored in a numpy array. The state names
corresponding to each data element are stored in the main particle
object and can be looked-up via the getVarIndex() method. For
example, to store the density of particle n at timestep i into the
dens variable, we would do:
>>> idens = particles[n].getVarIndex("density")
>>> dens = particles[n].history[i].data[idens]
Any field that was stored in the original timestamp file would be
present in the data array (and the name should be available
via getVarIndex())
The total number of particles can be found simply as len(particles),
and the number of particleInstances for a given particle can be
found as len(particles[n].history)
"""
import sys
import string
import numpy
# a global dictionary to store the particle objects. Each unique
# particle stored with its own key:value pair -- the key is the unique
# particle ID and the value is the particle object itself. The
# particle objects in turn contain many particleInstance objects in
# the history.
_particleDict = {}
# this works for < 1000 MPI processes, but should ultimately be something
# dynamic to work for all cases
_idFactor = 1000
#------------------------------------------------------------------------
class particleInstance(object):
"""
a particleInstance object represents a single instance of a
particle, and will carry the position, time, and associated
data.
"""
__slots__ = ['xyz', 't', 'data'] # memory management
def __init__(self, xyz, t, data):
self.xyz = xyz
self.t = t
self.data = data
def __str__(self):
"""
string representation of the particleInstance for printing
"""
string = "particle pos: (%g, %g, %g) \n" % \
(self.xyz[0], self.xyz[1], self.xyz[2]) + \
" time: %g \n" % (self.t)
return string
def value(self):
"""
return the value of a particleInstance for comparison
purposes. The value is simply the time.
"""
return self.t
def __cmp__(self, other):
return cmp(self.value(), other.value())
#------------------------------------------------------------------------
class particle(object):
"""
a particle object stores a history of a single particle, each
element of which is a particleInstance object.
"""
__slots__ = ["pid", "originCPU", "dim", "finalized",
"history", "dataNames", "numInstances"]
def __new__(cls, pid=None, originCPU=None, *args, **kwargs):
"""
Create a new particle instance in the global _particleDict
dictionary.
"""
# before building a new particle, make sure it isn't already
# built by checking the global _particleDict
id = pid*_idFactor + originCPU
if id not in _particleDict:
obj = object.__new__(cls)
_particleDict[id] = obj
return _particleDict[id]
def __init__(self, pid, originCPU, dim, dataNames):
"""
initialize a particle object
"""
# a MAESTRO particle is identified by 2 numbers, the pid and
# the CPU that it was created on. Together, these uniquely
# identify the particle.
self.pid = pid
self.originCPU = originCPU
# dimensionality of particle data
self.dim = dim
# finalized is 1 when we have finished adding data and sorted
# the history in time-order
self.finalized = 0
# the history list will store instances of the particle at
# different times.
self.history = []
# the dataNames will store the names of the data associated
# with each particle instance
self.dataNames = list(dataNames)
# keep track of the number of particle history instances we've
# stored
self.numInstances = 0
def addInstance(self, xyz=[-1.0,-1.0,-1.0],t=0.0,
dataValues=[]):
"""
add a particleInstance object to the particle history to
store a single instance (in time) of this particle
"""
if (self.finalized == 1):
print "ERROR: particle data finalized before addInstance call"
sys.exit(2)
xyz = numpy.array(xyz)
dataValues = numpy.array(dataValues)
# add this particle instance to the particle history
self.history.append(particleInstance(xyz,t,dataValues))
self.numInstances += 1
def finalize(self):
"""
sort the particle histories in time order (since they may
have been spread across multiple processors / files.
finalize() should only be called after all particle data
has been read in
"""
# sort the history by time -- the particleInstance objects use
# time as their value for comparison, so sort() should handle
# this nicely for us.
self.history.sort()
self.finalized = 1
def flatten(self):
"""
return numpy arrays with the coordinates and time for all
instances of this particle
"""
if (not self.finalized == 1):
print "ERROR: particle data not finalized before flatten call"
sys.exit(2)
coords = numpy.zeros((self.dim, self.numInstances), dtype=numpy.float64)
time = numpy.zeros((self.numInstances), dtype=numpy.float64)
n = 0
while (n < len(self.history)):
coords[:,n] = self.history[n].xyz[:self.dim]
time[n] = self.history[n].t
n += 1
return coords, time
def getVarIndex(self, varname):
"""
returns the index into the history[].data array corresponding
to the variable varname
"""
index = self.dataNames.index(varname)
return index
def __str__(self):
string = "particle ID: %d \n" % (self.pid) + \
"particle origin CPU: %d \n" % (self.originCPU) + \
"dimensionality: %d \n" % (self.dim) + \
"num. stored instances: %d \n" % (self.numInstances) + \
"associated data: \n"
for item in self.dataNames:
string += " %s" % (item)
string += "\n"
return string
#------------------------------------------------------------------------
def ensure_list(obj):
"""
make sure an object is a list; used, for example, when a single
file is passed into the parseParticleFile method
"""
import types
if obj is None:
return [obj]
if not isinstance(obj, types.ListType):
return [obj]
return obj
#-----------------------------------------------------------------------------
def parseParticleFile(maestroParticleFiles):
"""
read in all the particle data from a file and add each particle
instance to the _particleDict dictionary, grouping all of the
history for a unique particle (identified by the pid and
originCPU) together in a particle object in the dictionary
"""
haveHeader = 0
for maestroParticleFile in ensure_list(maestroParticleFiles):
# read the file line by line
mf = open(maestroParticleFile, "r")
for line in mf:
# skip blank lines
if line.isspace(): continue
# look for a header information
if (line.startswith("#")):
# store old header, if it exists
if (haveHeader == 1):
oldHeader = list(dataNames) # list() makes a copy
fields = string.split(line[1:])
# make sure we know what we are doing -- the first 2
# fields should be the particle ID and origin CPU
if (fields[0] == "part-ID" and fields[1] == "origin-CPU"):
ipid = 0
ioCPU = 1
else:
print "ERROR: particle file columns not in expected order"
sys.exit(2)
# the next fields should be x, y, and z, depending on the
# dimensionality
if (fields[2] == "x" and fields[3] == "y" and
fields[4] == "z"):
dim = 3
ix = 2
elif (fields[2] == "x" and fields[3] == "y"):
dim = 2
ix = 2
elif (fields[2] == "x"):
dim = 1
ix = 2
else:
print "ERROR: particle file columns not in expected order"
sys.exit(2)
# then comes time
if (fields[2 + dim] == "time"):
it = 2 + dim
else:
print "ERROR: particle file columns not in expected order"
sys.exit(2)
# everything else is associated data
if (len(fields) > 3 + dim):
idata = 3 + dim
dataNames = fields[idata:]
# did the header change?
if (haveHeader == 1):
if (not oldHeader == dataNames):
print "ERROR: header changed while parsing files"
sys.exit(2)
# done with the header
haveHeader = 1
numData = len(dataNames)
continue
# normal particle data -- first find the required particle data
fields = map(float,string.split(line))
pid = int(fields[ipid])
originCPU = int(fields[ioCPU])
id = pid*_idFactor + originCPU
xyz = fields[ix:ix+dim]
time = fields[it]
dataValues = fields[idata:]
if (not len(dataValues) == numData):
print "ERROR: number of data values not equal to number of names"
sys.exit(2)
if not id in _particleDict:
particle(pid, originCPU, dim, dataNames)
_particleDict[id].addInstance(xyz=xyz,
t=time,
dataValues=dataValues)
# finish up by making sure that each particle's history is
# in proper time-order
for id in _particleDict:
_particleDict[id].finalize()
return _particleDict