flekspy 0.2.3__tar.gz → 0.2.4__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.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: flekspy
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: Python utilities for processing FLEKS data
5
5
  Author: Yuxi Chen
6
6
  Author-email: yuxichen@umich.edu>, Hongyang Zhou <hyzhou@umich.edu
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "flekspy"
3
- version = "0.2.3"
3
+ version = "0.2.4"
4
4
  description = "Python utilities for processing FLEKS data"
5
5
  authors = ["Yuxi Chen <yuxichen@umich.edu>, Hongyang Zhou <hyzhou@umich.edu>"]
6
6
  readme = "README.md"
@@ -1,428 +1,466 @@
1
- from typing import List, Tuple, Dict, Union, Callable
2
-
3
- import matplotlib.pyplot as plt
4
- import os
5
- import numpy as np
6
- import glob
7
- import struct
8
-
9
-
10
- class FLEKSTP(object):
11
- r"""
12
- A class that is used to read and plot test particles. Each particle ID consists of
13
- a CPU index, a particle index on each CPU, and a location index.
14
- By default, 7 real numbers saved for each step: time + position + velocity.
15
-
16
- Args:
17
- dirs (str): the path to the test particle dataset.
18
-
19
- Examples:
20
- >>> tp = FLEKSTP("res/run1/PC/test_particles", iSpecies=1)
21
- >>> pIDs = list(tp.IDs())
22
- >>> tp.plot_trajectory(pIDs[3])
23
- >>> tp.save_trajectory_to_csv(pIDs[5])
24
- >>> ids, pData = tp.read_particles_at_time(6500.8, doSave=True)
25
- >>> f = tp.plot_loc(pData)
26
- """
27
-
28
- it_ = 0
29
- ix_ = 1
30
- iy_ = 2
31
- iz_ = 3
32
- iu_ = 4
33
- iv_ = 5
34
- iw_ = 6
35
- iBx_ = 7
36
- iBy_ = 8
37
- iBz_ = 9
38
- iEx_ = 10
39
- iEy_ = 11
40
- iEz_ = 12
41
-
42
- def __init__(
43
- self,
44
- dirs: Union[str, List[str]],
45
- iDomain: int = 0,
46
- iSpecies: int = 0,
47
- iListStart: int = 0,
48
- iListEnd: int = -1,
49
- readAllFiles: bool = False,
50
- ):
51
- if type(dirs) == str:
52
- dirs = [dirs]
53
-
54
- header = dirs[0] + "/Header"
55
- if os.path.exists(header):
56
- with open(header, "r") as f:
57
- self.nReal = int(f.readline())
58
- else:
59
- raise FileNotFoundError(f"Header file not found in {dirs[0]}")
60
-
61
- self.iSpecies = iSpecies
62
- self.plistfiles = list()
63
- self.pfiles = list()
64
-
65
- for outputDir in dirs:
66
- self.plistfiles = self.plistfiles + glob.glob(
67
- f"{outputDir}/FLEKS{iDomain}_particle_list_species_{iSpecies}_*"
68
- )
69
-
70
- self.pfiles = self.pfiles + glob.glob(
71
- f"{outputDir}/FLEKS{iDomain}_particle_species_{iSpecies}_*"
72
- )
73
-
74
- self.plistfiles.sort()
75
- self.pfiles.sort()
76
-
77
- self.list_index_to_time = []
78
- if readAllFiles:
79
- for filename in self.pfiles:
80
- record = self._read_the_first_record(filename)
81
- if record == None:
82
- continue
83
- self.list_index_to_time.append(record[FLEKSTP.it_])
84
-
85
- if iListEnd == -1:
86
- iListEnd = len(self.plistfiles)
87
- self.plistfiles = self.plistfiles[iListStart:iListEnd]
88
- self.pfiles = self.pfiles[iListStart:iListEnd]
89
-
90
- self.plists: List[Dict[Tuple[int, int], int]] = []
91
- for fileName in self.plistfiles:
92
- self.plists.append(self.read_particle_list(fileName))
93
-
94
- self.IDs = set()
95
- for plist in self.plists:
96
- self.IDs.update(plist.keys())
97
-
98
- self.file_time = []
99
- for filename in self.pfiles:
100
- record = self._read_the_first_record(filename)
101
- if record == None:
102
- continue
103
- self.file_time.append(record[FLEKSTP.it_])
104
-
105
- print(f"Particles of species {self.iSpecies} are read from {dirs}")
106
- print(f"Number of particles: {len(self.IDs)}")
107
-
108
- def getIDs(self):
109
- return list(sorted(self.IDs))
110
-
111
- def get_index_to_time(self) -> List:
112
- r"""
113
- Getter method for accessing list_index_to_time.
114
- """
115
- if len(self.list_index_to_time) == 0:
116
- print("Index to time mapping was not initialized")
117
- return self.list_index_to_time
118
-
119
- def read_particle_list(self, fileName: str) -> Dict[Tuple[int, int], int]:
120
- r"""
121
- Read and return a list of the particle IDs.
122
- """
123
- # 2 integers + 1 unsigned long long
124
- listUnitSize = 2 * 4 + 8
125
- nByte = os.path.getsize(fileName)
126
- nPart = int(nByte / listUnitSize)
127
- plist = {}
128
- with open(fileName, "rb") as f:
129
- for _ in range(nPart):
130
- binaryData = f.read(listUnitSize)
131
- (cpu, id, loc) = struct.unpack("iiQ", binaryData)
132
- plist.update({(cpu, id): loc})
133
- return plist
134
-
135
- def _read_the_first_record(self, fileName: str) -> Union[List[float], None]:
136
- r"""
137
- Get the first record stored in one file.
138
- """
139
- dataList = list()
140
- with open(fileName, "rb") as f:
141
- while True:
142
- binaryData = f.read(4 * 4)
143
-
144
- if not binaryData:
145
- break # EOF
146
-
147
- (cpu, idtmp, nRecord, weight) = struct.unpack("iiif", binaryData)
148
- if nRecord > 0:
149
- binaryData = f.read(4 * self.nReal)
150
- dataList = dataList + list(
151
- struct.unpack("f" * self.nReal, binaryData)
152
- )
153
- return dataList
154
-
155
- def read_particles_at_time(
156
- self, time: float, doSave: bool = False
157
- ) -> Tuple[np.ndarray, np.ndarray]:
158
- r"""
159
- Get the information of all the particles at a given time, and save to a csv file
160
- with the name "particles_t***.csv" in the current directory if doSave is True.
161
-
162
- Returns:
163
- ids: a numpy array of tuples contains the particle IDs.
164
- pData: a numpy real array with the particle weight, location and velocity.
165
-
166
- Examples:
167
- >>> ids, pData = pt.read_particles_at_time(3700, doSave=True)
168
- """
169
-
170
- nFile = len(self.pfiles)
171
- for iFile in range(nFile):
172
- if iFile == 0 and time < self.file_time[iFile]:
173
- raise Exception("Error: There is no particle at the given time")
174
-
175
- if iFile == nFile - 1:
176
- break
177
- if time >= self.file_time[iFile] and time < self.file_time[iFile + 1]:
178
- break
179
-
180
- fileName = self.pfiles[iFile]
181
-
182
- dataList = []
183
- idList = []
184
- with open(fileName, "rb") as f:
185
- while True:
186
- binaryData = f.read(4 * 4)
187
- if not binaryData:
188
- break # EOF
189
-
190
- (cpu, idtmp, nRecord, weight) = struct.unpack("iiif", binaryData)
191
- binaryData = f.read(4 * self.nReal * nRecord)
192
- allRecords = list(struct.unpack("f" * nRecord * self.nReal, binaryData))
193
- for i in range(nRecord):
194
- if (
195
- allRecords[self.nReal * i + FLEKSTP.it_] >= time
196
- or i == nRecord - 1
197
- ):
198
- dataList.append(
199
- allRecords[self.nReal * i : self.nReal * (i + 1)]
200
- )
201
- idList.append((cpu, idtmp))
202
- break
203
-
204
- npData = np.array(dataList)
205
- idData = np.array(idList, dtype="i,i")
206
- if doSave:
207
- fileName = f"particles_t{time}.csv"
208
- header = "cpu,iid,time,x,y,z,ux,uy,uz"
209
- if self.nReal == 10:
210
- header += ",bx,by,bz"
211
- elif self.nReal == 13:
212
- header += ",bx,by,bz,ex,ey,ez"
213
-
214
- with open(fileName, "w") as f:
215
- f.write(header + "\n")
216
- for id_row, data_row in zip(idData, npData):
217
- f.write(
218
- f"{id_row[0]},{id_row[1]},{','.join(str(x) for x in data_row)}\n"
219
- )
220
-
221
- return idData, npData
222
-
223
- def save_trajectory_to_csv(
224
- self,
225
- pID: Tuple[int, int],
226
- fileName: str = None,
227
- shiftTime: bool = False,
228
- scaleTime: bool = False,
229
- ) -> None:
230
- r"""
231
- Save the trajectory of a particle to a csv file.
232
-
233
- Args:
234
- pID: particle ID.
235
- shiftTime (bool): If set to True, set the initial time to be 0.
236
- scaleTime (bool): If set to True, scale the time into [0,1] range, only scale time if shiftTime = True.
237
-
238
- Example:
239
- >>> tp.save_trajectory_to_csv((3,15))
240
- """
241
- pData = self.read_particle_trajectory(pID)
242
- if fileName == None:
243
- fileName = "trajectory_" + str(pID[0]) + "_" + str(pID[1]) + ".csv"
244
- header = "time [s], X [R], Y [R], Z [R], U_x [km/s], U_y [km/s], U_z [km/s]"
245
- if self.nReal == 10:
246
- header += ", B_x [nT], B_y [nT], B_z [nT]"
247
- if self.nReal == 13:
248
- header += (
249
- ", B_x [nT], B_y [nT], B_z [nT], E_x [uV/m], E_y [uV/m], E_z [uV/m]"
250
- )
251
- if shiftTime:
252
- pData[:, 0] -= pData[0, 0]
253
- if scaleTime:
254
- pData[:, 0] /= pData[-1, 0]
255
- np.savetxt(fileName, pData, delimiter=",", header=header, comments="")
256
-
257
- def read_particle_trajectory(self, pID: Tuple[int, int]):
258
- r"""
259
- Read and return the trajectory of a particle.
260
-
261
- Args:
262
- pID: particle ID
263
-
264
- Examples:
265
- >>> trajectory = tp.read_particle_trajectory((66,888))
266
- """
267
- dataList = list()
268
- for fileName, plist in zip(self.pfiles, self.plists):
269
- if pID in plist:
270
- ploc = plist[pID]
271
- with open(fileName, "rb") as f:
272
- f.seek(ploc)
273
- binaryData = f.read(4 * 4)
274
- (cpu, idtmp, nRecord, weight) = struct.unpack("iiif", binaryData)
275
- binaryData = f.read(4 * self.nReal * nRecord)
276
- dataList = dataList + list(
277
- struct.unpack("f" * nRecord * self.nReal, binaryData)
278
- )
279
-
280
- nRecord = int(len(dataList) / self.nReal)
281
- return np.array(dataList).reshape(nRecord, self.nReal)
282
-
283
- def read_initial_location(self, pID):
284
- r"""
285
- Read and return the initial location of a test particle
286
- """
287
-
288
- for fileName, plist in zip(self.pfiles, self.plists):
289
- if pID in plist:
290
- ploc = plist[pID]
291
- with open(fileName, "rb") as f:
292
- f.seek(ploc)
293
- binaryData = f.read(4 * 4)
294
- (cpu, idtmp, nRecord, weight) = struct.unpack("iiif", binaryData)
295
- nRead = 1
296
- binaryData = f.read(4 * self.nReal * nRead)
297
- dataList = list(struct.unpack("f" * nRead * self.nReal, binaryData))
298
- return dataList
299
-
300
- def select_particles(self, fSelect: Callable = None) -> List[Tuple[int, int]]:
301
- r"""
302
- Select and return the particles whose initial condition satisfy the requirement
303
- set by the user defined function fSelect. The first argument of fSelect is the
304
- particle ID, and the second argument is the initial record (time, location,
305
- velocity and weight of a particle) of a particle.
306
-
307
- Examples:
308
- >>> def fselect(tp, pid):
309
- >>> pdata = tp.read_initial_loc_with_ID(pid)
310
- >>> intime = pdata[FLEKSTP.it_] < 3601
311
- >>> inregion = pdata[FLEKSTP.ix_] > 20
312
- >>> return intime and inregion
313
- >>>
314
- >>> pselected = tp.select_particles(fselect)
315
- >>> tp.plot_trajectory(list(pselected.keys())[1])
316
- """
317
-
318
- if fSelect == None:
319
-
320
- def fSelect(tp, pid):
321
- return True
322
-
323
- pselected = list(filter(lambda pid: fSelect(self, pid), self.IDs))
324
-
325
- return pselected
326
-
327
- def plot_trajectory(self, pID: Tuple[int, int]):
328
- r"""
329
- Plots the trajectory and velocities of the particle pID.
330
-
331
- Example:
332
- >>> tp.plot_trajectory((3,15))
333
- """
334
-
335
- data = self.read_particle_trajectory(pID)
336
- t = data[:, FLEKSTP.it_]
337
- tNorm = (t - t[0]) / (t[-1] - t[0])
338
-
339
- ncol = 3
340
- nrow = 3 # Default for X, V
341
- if self.nReal == 10: # additional B field
342
- nrow = 4
343
- elif self.nReal == 13: # additional B and E field
344
- nrow = 5
345
-
346
- f, axs = plt.subplots(nrow, ncol, figsize=(12, 6), constrained_layout=True)
347
-
348
- # Plot trajectories
349
- for i, ax in enumerate(axs[0, :]):
350
- x_id = FLEKSTP.ix_ if i < 2 else FLEKSTP.iy_
351
- y_id = FLEKSTP.iy_ if i == 0 else FLEKSTP.iz_
352
- ax.plot(data[:, x_id], data[:, y_id], "k")
353
- ax.scatter(
354
- data[:, x_id],
355
- data[:, y_id],
356
- c=plt.cm.winter(tNorm),
357
- edgecolor="none",
358
- marker="o",
359
- s=10,
360
- )
361
- ax.set_xlabel("x" if i < 2 else "y")
362
- ax.set_ylabel("y" if i == 0 else "z")
363
-
364
- def plot_data(dd, label, irow, icol):
365
- axs[irow, icol].plot(t, dd, label=label)
366
- axs[irow, icol].scatter(
367
- t, dd, c=plt.cm.winter(tNorm), edgecolor="none", marker="o", s=10
368
- )
369
- axs[irow, icol].set_xlabel("time")
370
- axs[irow, icol].set_ylabel(label)
371
-
372
- def plot_vector(idx, labels, irow):
373
- for i, (id, label) in enumerate(zip(idx, labels)):
374
- plot_data(data[:, id], label, irow, i)
375
-
376
- plot_vector([FLEKSTP.ix_, FLEKSTP.iy_, FLEKSTP.iz_], ["x", "y", "z"], 1)
377
- plot_vector([FLEKSTP.iu_, FLEKSTP.iv_, FLEKSTP.iw_], ["Vx", "Vy", "Vz"], 2)
378
-
379
- if self.nReal > FLEKSTP.iBx_:
380
- plot_vector(
381
- [FLEKSTP.iBx_, FLEKSTP.iBy_, FLEKSTP.iBz_], ["Bx", "By", "Bz"], 3
382
- )
383
-
384
- if self.nReal > FLEKSTP.iEx_:
385
- plot_vector(
386
- [FLEKSTP.iEx_, FLEKSTP.iEy_, FLEKSTP.iEz_], ["Ex", "Ey", "Ez"], 4
387
- )
388
-
389
- return
390
-
391
- def plot_loc(self, pData: np.ndarray):
392
- r"""
393
- Plot the location of particles pData.
394
-
395
- Examples:
396
- >>> ids, pData = tp.read_particles_at_time(3700, doSave=True)
397
- >>> f = tp.plot_loc(pData)
398
- """
399
-
400
- px = pData[:, FLEKSTP.ix_]
401
- py = pData[:, FLEKSTP.iy_]
402
- pz = pData[:, FLEKSTP.iz_]
403
-
404
- # create subplot mosaic with different keyword arguments
405
- skeys = ["A", "B", "C", "D"]
406
- f, axs = plt.subplot_mosaic(
407
- "AB;CD",
408
- per_subplot_kw={("D"): {"projection": "3d"}},
409
- gridspec_kw={"width_ratios": [1, 1], "wspace": 0.1, "hspace": 0.1},
410
- figsize=(10, 10),
411
- constrained_layout=True,
412
- )
413
-
414
- # Create 2D scatter plots
415
- for i, (x, y, labels) in enumerate(
416
- zip([px, px, py], [py, pz, pz], [("x", "y"), ("x", "z"), ("y", "z")])
417
- ):
418
- axs[skeys[i]].scatter(x, y, s=1)
419
- axs[skeys[i]].set_xlabel(labels[0])
420
- axs[skeys[i]].set_ylabel(labels[1])
421
-
422
- # Create 3D scatter plot
423
- axs[skeys[3]].scatter(px, py, pz, s=1)
424
- axs[skeys[3]].set_xlabel("x")
425
- axs[skeys[3]].set_ylabel("y")
426
- axs[skeys[3]].set_zlabel("z")
427
-
428
- return
1
+ from typing import List, Tuple, Dict, Union, Callable
2
+
3
+ import matplotlib.pyplot as plt
4
+ import os
5
+ import numpy as np
6
+ import glob
7
+ import struct
8
+
9
+
10
+ class FLEKSTP(object):
11
+ r"""
12
+ A class that is used to read and plot test particles. Each particle ID consists of
13
+ a CPU index, a particle index on each CPU, and a location index.
14
+ By default, 7 real numbers saved for each step: time + position + velocity.
15
+
16
+ Args:
17
+ dirs (str): the path to the test particle dataset.
18
+
19
+ Examples:
20
+ >>> tp = FLEKSTP("res/run1/PC/test_particles", iSpecies=1)
21
+ >>> pIDs = list(tp.IDs())
22
+ >>> tp.plot_trajectory(pIDs[3])
23
+ >>> tp.save_trajectory_to_csv(pIDs[5])
24
+ >>> ids, pData = tp.read_particles_at_time(6500.8, doSave=True)
25
+ >>> f = tp.plot_loc(pData)
26
+ """
27
+
28
+ it_ = 0
29
+ ix_ = 1
30
+ iy_ = 2
31
+ iz_ = 3
32
+ iu_ = 4
33
+ iv_ = 5
34
+ iw_ = 6
35
+ iBx_ = 7
36
+ iBy_ = 8
37
+ iBz_ = 9
38
+ iEx_ = 10
39
+ iEy_ = 11
40
+ iEz_ = 12
41
+
42
+ def __init__(
43
+ self,
44
+ dirs: Union[str, List[str]],
45
+ iDomain: int = 0,
46
+ iSpecies: int = 0,
47
+ iListStart: int = 0,
48
+ iListEnd: int = -1,
49
+ readAllFiles: bool = False,
50
+ ):
51
+ if type(dirs) == str:
52
+ dirs = [dirs]
53
+
54
+ header = dirs[0] + "/Header"
55
+ if os.path.exists(header):
56
+ with open(header, "r") as f:
57
+ self.nReal = int(f.readline())
58
+ else:
59
+ raise FileNotFoundError(f"Header file not found in {dirs[0]}")
60
+
61
+ self.iSpecies = iSpecies
62
+ self.plistfiles = list()
63
+ self.pfiles = list()
64
+
65
+ for outputDir in dirs:
66
+ self.plistfiles = self.plistfiles + glob.glob(
67
+ f"{outputDir}/FLEKS{iDomain}_particle_list_species_{iSpecies}_*"
68
+ )
69
+
70
+ self.pfiles = self.pfiles + glob.glob(
71
+ f"{outputDir}/FLEKS{iDomain}_particle_species_{iSpecies}_*"
72
+ )
73
+
74
+ self.plistfiles.sort()
75
+ self.pfiles.sort()
76
+
77
+ self.indextotime = []
78
+ if readAllFiles:
79
+ for filename in self.pfiles:
80
+ record = self._read_the_first_record(filename)
81
+ if record == None:
82
+ continue
83
+ self.indextotime.append(record[FLEKSTP.it_])
84
+
85
+ if iListEnd == -1:
86
+ iListEnd = len(self.plistfiles)
87
+ self.plistfiles = self.plistfiles[iListStart:iListEnd]
88
+ self.pfiles = self.pfiles[iListStart:iListEnd]
89
+
90
+ self.plists: List[Dict[Tuple[int, int], int]] = []
91
+ for fileName in self.plistfiles:
92
+ self.plists.append(self.read_particle_list(fileName))
93
+
94
+ self.IDs = set()
95
+ for plist in self.plists:
96
+ self.IDs.update(plist.keys())
97
+
98
+ self.filetime = []
99
+ for filename in self.pfiles:
100
+ record = self._read_the_first_record(filename)
101
+ if record == None:
102
+ continue
103
+ self.filetime.append(record[FLEKSTP.it_])
104
+
105
+ print(f"Particles of species {self.iSpecies} are read from {dirs}")
106
+ print(f"Number of particles: {len(self.IDs)}")
107
+
108
+ def getIDs(self):
109
+ return list(sorted(self.IDs))
110
+
111
+ def get_index_to_time(self) -> List:
112
+ """
113
+ Getter method for accessing indextotime.
114
+ """
115
+ if len(self.indextotime) == 0:
116
+ print("Index to time mapping was not initialized")
117
+ return self.indextotime
118
+
119
+ def read_particle_list(self, fileName: str) -> Dict[Tuple[int, int], int]:
120
+ """
121
+ Read and return a list of the particle IDs.
122
+ """
123
+ # 2 integers + 1 unsigned long long
124
+ listUnitSize = 2 * 4 + 8
125
+ nByte = os.path.getsize(fileName)
126
+ nPart = int(nByte / listUnitSize)
127
+ plist = {}
128
+ with open(fileName, "rb") as f:
129
+ for _ in range(nPart):
130
+ binaryData = f.read(listUnitSize)
131
+ (cpu, id, loc) = struct.unpack("iiQ", binaryData)
132
+ plist.update({(cpu, id): loc})
133
+ return plist
134
+
135
+ def _read_the_first_record(self, fileName: str) -> Union[List[float], None]:
136
+ """
137
+ Get the first record stored in one file.
138
+ """
139
+ dataList = list()
140
+ with open(fileName, "rb") as f:
141
+ while True:
142
+ binaryData = f.read(4 * 4)
143
+
144
+ if not binaryData:
145
+ break # EOF
146
+
147
+ (cpu, idtmp, nRecord, weight) = struct.unpack("iiif", binaryData)
148
+ if nRecord > 0:
149
+ binaryData = f.read(4 * self.nReal)
150
+ dataList = dataList + list(
151
+ struct.unpack("f" * self.nReal, binaryData)
152
+ )
153
+ return dataList
154
+
155
+ def read_particles_at_time(
156
+ self, time: float, doSave: bool = False
157
+ ) -> Tuple[np.ndarray, np.ndarray]:
158
+ r"""
159
+ Get the information of all the particles at a given time, and save to a csv file
160
+ with the name "particles_t***.csv" in the current directory if doSave is True.
161
+
162
+ Returns:
163
+ ids: a numpy array of tuples contains the particle IDs.
164
+ pData: a numpy real array with the particle weight, location and velocity.
165
+
166
+ Examples:
167
+ >>> ids, pData = pt.read_particles_at_time(3700, doSave=True)
168
+ """
169
+
170
+ nFile = len(self.pfiles)
171
+ if time < self.filetime[0] or time > self.filetime[-1]:
172
+ raise Exception(f"There are no particles at time {time}.")
173
+ iFile = 0
174
+ while iFile < nFile - 1:
175
+ if time < self.filetime[iFile + 1]:
176
+ break
177
+ iFile += 1
178
+
179
+ fileName = self.pfiles[iFile]
180
+
181
+ dataList: list[float] = []
182
+ idList: list[tuple] = []
183
+ with open(fileName, "rb") as f:
184
+ while True:
185
+ binaryData = f.read(4 * 4)
186
+ if not binaryData:
187
+ break # EOF
188
+
189
+ (cpu, idtmp, nRecord, weight) = struct.unpack("iiif", binaryData)
190
+ binaryData = f.read(4 * self.nReal * nRecord)
191
+ allRecords = list(struct.unpack("f" * nRecord * self.nReal, binaryData))
192
+ for i in range(nRecord):
193
+ if (
194
+ allRecords[self.nReal * i + FLEKSTP.it_] >= time
195
+ or i == nRecord - 1
196
+ ):
197
+ dataList.append(
198
+ allRecords[self.nReal * i : self.nReal * (i + 1)]
199
+ )
200
+ idList.append((cpu, idtmp))
201
+ break
202
+
203
+ npData = np.array(dataList)
204
+ idData = np.array(idList, dtype="i,i")
205
+ if doSave:
206
+ fileName = f"particles_t{time}.csv"
207
+ header = "cpu,iid,time,x,y,z,ux,uy,uz"
208
+ if self.nReal == 10:
209
+ header += ",bx,by,bz"
210
+ elif self.nReal == 13:
211
+ header += ",bx,by,bz,ex,ey,ez"
212
+
213
+ with open(fileName, "w") as f:
214
+ f.write(header + "\n")
215
+ for id_row, data_row in zip(idData, npData):
216
+ f.write(
217
+ f"{id_row[0]},{id_row[1]},{','.join(str(x) for x in data_row)}\n"
218
+ )
219
+
220
+ return idData, npData
221
+
222
+ def save_trajectory_to_csv(
223
+ self,
224
+ pID: Tuple[int, int],
225
+ fileName: str = None,
226
+ shiftTime: bool = False,
227
+ scaleTime: bool = False,
228
+ ) -> None:
229
+ r"""
230
+ Save the trajectory of a particle to a csv file.
231
+
232
+ Args:
233
+ pID: particle ID.
234
+ shiftTime (bool): If set to True, set the initial time to be 0.
235
+ scaleTime (bool): If set to True, scale the time into [0,1] range, only scale time if shiftTime = True.
236
+
237
+ Example:
238
+ >>> tp.save_trajectory_to_csv((3,15))
239
+ """
240
+ pData = self.read_particle_trajectory(pID)
241
+ if fileName == None:
242
+ fileName = "trajectory_" + str(pID[0]) + "_" + str(pID[1]) + ".csv"
243
+ header = "time [s], X [R], Y [R], Z [R], U_x [km/s], U_y [km/s], U_z [km/s]"
244
+ if self.nReal == 10:
245
+ header += ", B_x [nT], B_y [nT], B_z [nT]"
246
+ if self.nReal == 13:
247
+ header += (
248
+ ", B_x [nT], B_y [nT], B_z [nT], E_x [uV/m], E_y [uV/m], E_z [uV/m]"
249
+ )
250
+ if shiftTime:
251
+ pData[:, 0] -= pData[0, 0]
252
+ if scaleTime:
253
+ pData[:, 0] /= pData[-1, 0]
254
+ np.savetxt(fileName, pData, delimiter=",", header=header, comments="")
255
+
256
+ def read_particle_trajectory(self, pID: Tuple[int, int]):
257
+ r"""
258
+ Read and return the trajectory of a particle.
259
+
260
+ Args:
261
+ pID: particle ID
262
+
263
+ Examples:
264
+ >>> trajectory = tp.read_particle_trajectory((66,888))
265
+ """
266
+ dataList = list()
267
+ for fileName, plist in zip(self.pfiles, self.plists):
268
+ if pID in plist:
269
+ ploc = plist[pID]
270
+ with open(fileName, "rb") as f:
271
+ f.seek(ploc)
272
+ binaryData = f.read(4 * 4)
273
+ (cpu, idtmp, nRecord, weight) = struct.unpack("iiif", binaryData)
274
+ binaryData = f.read(4 * self.nReal * nRecord)
275
+ dataList = dataList + list(
276
+ struct.unpack("f" * nRecord * self.nReal, binaryData)
277
+ )
278
+
279
+ nRecord = int(len(dataList) / self.nReal)
280
+ return np.array(dataList).reshape(nRecord, self.nReal)
281
+
282
+ def read_initial_location(self, pID):
283
+ r"""
284
+ Read and return the initial location of a test particle
285
+ """
286
+
287
+ for fileName, plist in zip(self.pfiles, self.plists):
288
+ if pID in plist:
289
+ ploc = plist[pID]
290
+ with open(fileName, "rb") as f:
291
+ f.seek(ploc)
292
+ binaryData = f.read(4 * 4)
293
+ (cpu, idtmp, nRecord, weight) = struct.unpack("iiif", binaryData)
294
+ nRead = 1
295
+ binaryData = f.read(4 * self.nReal * nRead)
296
+ dataList = list(struct.unpack("f" * nRead * self.nReal, binaryData))
297
+ return dataList
298
+
299
+ def select_particles(self, fSelect: Callable = None) -> List[Tuple[int, int]]:
300
+ r"""
301
+ Select and return the particles whose initial condition satisfy the requirement
302
+ set by the user defined function fSelect. The first argument of fSelect is the
303
+ particle ID, and the second argument is the initial record (time, location,
304
+ velocity and weight of a particle) of a particle.
305
+
306
+ Examples:
307
+ >>> def fselect(tp, pid):
308
+ >>> pdata = tp.read_initial_loc_with_ID(pid)
309
+ >>> intime = pdata[FLEKSTP.it_] < 3601
310
+ >>> inregion = pdata[FLEKSTP.ix_] > 20
311
+ >>> return intime and inregion
312
+ >>>
313
+ >>> pselected = tp.select_particles(fselect)
314
+ >>> tp.plot_trajectory(list(pselected.keys())[1])
315
+ """
316
+
317
+ if fSelect == None:
318
+
319
+ def fSelect(tp, pid):
320
+ return True
321
+
322
+ pselected = list(filter(lambda pid: fSelect(self, pid), self.IDs))
323
+
324
+ return pselected
325
+
326
+ def get_data(self, data, name: str):
327
+ match name:
328
+ case "t":
329
+ x = data[:, FLEKSTP.it_]
330
+ case "x":
331
+ x = data[:, FLEKSTP.ix_]
332
+ case "y":
333
+ x = data[:, FLEKSTP.iy_]
334
+ case "z":
335
+ x = data[:, FLEKSTP.iz_]
336
+ case "u" | "vx" | "ux":
337
+ x = data[:, FLEKSTP.iu_]
338
+ case "v" | "vy" | "uy":
339
+ x = data[:, FLEKSTP.iv_]
340
+ case "w" | "vz" | "uz":
341
+ x = data[:, FLEKSTP.iw_]
342
+ case _:
343
+ raise Exception(f"Unknown plot variable {name}")
344
+
345
+ return x
346
+
347
+ def plot_trajectory(
348
+ self,
349
+ pID: Tuple[int, int],
350
+ *,
351
+ type="all",
352
+ xaxis="t",
353
+ yaxis="x",
354
+ ax=None,
355
+ **kwargs,
356
+ ):
357
+ r"""
358
+ Plots the trajectory and velocities of the particle pID.
359
+
360
+ Example:
361
+ >>> tp.plot_trajectory((3,15))
362
+ """
363
+
364
+ def plot_data(dd, label, irow, icol):
365
+ ax[irow, icol].plot(t, dd, label=label)
366
+ ax[irow, icol].scatter(
367
+ t, dd, c=plt.cm.winter(tNorm), edgecolor="none", marker="o", s=10
368
+ )
369
+ ax[irow, icol].set_xlabel("time")
370
+ ax[irow, icol].set_ylabel(label)
371
+
372
+ def plot_vector(idx, labels, irow):
373
+ for i, (id, label) in enumerate(zip(idx, labels)):
374
+ plot_data(data[:, id], label, irow, i, **kwargs)
375
+
376
+ data = self.read_particle_trajectory(pID)
377
+ t = data[:, FLEKSTP.it_]
378
+ tNorm = (t - t[0]) / (t[-1] - t[0])
379
+
380
+ if type == "single":
381
+ x = self.get_data(data, xaxis)
382
+ y = self.get_data(data, yaxis)
383
+
384
+ if ax == None:
385
+ f, ax = plt.subplots(1, 1, figsize=(10, 6), constrained_layout=True)
386
+
387
+ ax.plot(x, y, **kwargs)
388
+ elif type == "all":
389
+ ncol = 3
390
+ nrow = 3 # Default for X, V
391
+ if self.nReal == 10: # additional B field
392
+ nrow = 4
393
+ elif self.nReal == 13: # additional B and E field
394
+ nrow = 5
395
+
396
+ f, ax = plt.subplots(nrow, ncol, figsize=(12, 6), constrained_layout=True)
397
+
398
+ # Plot trajectories
399
+ for i, a in enumerate(ax[0, :]):
400
+ x_id = FLEKSTP.ix_ if i < 2 else FLEKSTP.iy_
401
+ y_id = FLEKSTP.iy_ if i == 0 else FLEKSTP.iz_
402
+ a.plot(data[:, x_id], data[:, y_id], "k")
403
+ a.scatter(
404
+ data[:, x_id],
405
+ data[:, y_id],
406
+ c=plt.cm.winter(tNorm),
407
+ edgecolor="none",
408
+ marker="o",
409
+ s=10,
410
+ )
411
+ a.set_xlabel("x" if i < 2 else "y")
412
+ a.set_ylabel("y" if i == 0 else "z")
413
+
414
+ plot_vector([FLEKSTP.ix_, FLEKSTP.iy_, FLEKSTP.iz_], ["x", "y", "z"], 1)
415
+ plot_vector([FLEKSTP.iu_, FLEKSTP.iv_, FLEKSTP.iw_], ["Vx", "Vy", "Vz"], 2)
416
+
417
+ if self.nReal > FLEKSTP.iBx_:
418
+ plot_vector(
419
+ [FLEKSTP.iBx_, FLEKSTP.iBy_, FLEKSTP.iBz_], ["Bx", "By", "Bz"], 3
420
+ )
421
+
422
+ if self.nReal > FLEKSTP.iEx_:
423
+ plot_vector(
424
+ [FLEKSTP.iEx_, FLEKSTP.iEy_, FLEKSTP.iEz_], ["Ex", "Ey", "Ez"], 4
425
+ )
426
+
427
+ return ax
428
+
429
+ def plot_loc(self, pData: np.ndarray):
430
+ r"""
431
+ Plot the location of particles pData.
432
+
433
+ Examples:
434
+ >>> ids, pData = tp.read_particles_at_time(3700, doSave=True)
435
+ >>> f = tp.plot_loc(pData)
436
+ """
437
+
438
+ px = pData[:, FLEKSTP.ix_]
439
+ py = pData[:, FLEKSTP.iy_]
440
+ pz = pData[:, FLEKSTP.iz_]
441
+
442
+ # create subplot mosaic with different keyword arguments
443
+ skeys = ["A", "B", "C", "D"]
444
+ f, ax = plt.subplot_mosaic(
445
+ "AB;CD",
446
+ per_subplot_kw={("D"): {"projection": "3d"}},
447
+ gridspec_kw={"width_ratios": [1, 1], "wspace": 0.1, "hspace": 0.1},
448
+ figsize=(10, 10),
449
+ constrained_layout=True,
450
+ )
451
+
452
+ # Create 2D scatter plots
453
+ for i, (x, y, labels) in enumerate(
454
+ zip([px, px, py], [py, pz, pz], [("x", "y"), ("x", "z"), ("y", "z")])
455
+ ):
456
+ ax[skeys[i]].scatter(x, y, s=1)
457
+ ax[skeys[i]].set_xlabel(labels[0])
458
+ ax[skeys[i]].set_ylabel(labels[1])
459
+
460
+ # Create 3D scatter plot
461
+ ax[skeys[3]].scatter(px, py, pz, s=1)
462
+ ax[skeys[3]].set_xlabel("x")
463
+ ax[skeys[3]].set_ylabel("y")
464
+ ax[skeys[3]].set_zlabel("z")
465
+
466
+ return ax
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes