flekspy 0.2.2__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.2
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.2"
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,441 +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
- Parameters
17
- -----------
18
- dirs: String
19
- The path to the test particle dataset.
20
-
21
- Examples
22
- ----------
23
- >>> tp = FLEKSTP("res/run1/PC/test_particles", iSpecies=1)
24
- >>> pIDs = list(tp.IDs())
25
- >>> tp.plot_trajectory(pIDs[3])
26
- >>> tp.save_trajectory_to_csv(pIDs[5])
27
- >>> ids, pData = tp.read_particles_at_time(6500.8, doSave=True)
28
- >>> f = tp.plot_loc(pData)
29
- """
30
-
31
- it_ = 0
32
- ix_ = 1
33
- iy_ = 2
34
- iz_ = 3
35
- iu_ = 4
36
- iv_ = 5
37
- iw_ = 6
38
- iBx_ = 7
39
- iBy_ = 8
40
- iBz_ = 9
41
- iEx_ = 10
42
- iEy_ = 11
43
- iEz_ = 12
44
-
45
- def __init__(
46
- self,
47
- dirs: Union[str, List[str]],
48
- iDomain: int = 0,
49
- iSpecies: int = 0,
50
- iListStart: int = 0,
51
- iListEnd: int = -1,
52
- readAllFiles: bool = False,
53
- ):
54
- if type(dirs) == str:
55
- dirs = [dirs]
56
-
57
- header = dirs[0] + "/Header"
58
- if os.path.exists(header):
59
- with open(header, "r") as f:
60
- self.nReal = int(f.readline())
61
- else:
62
- raise FileNotFoundError(f"Header file not found in {dirs[0]}")
63
-
64
- self.iSpecies = iSpecies
65
- self.plistfiles = list()
66
- self.pfiles = list()
67
-
68
- for outputDir in dirs:
69
- self.plistfiles = self.plistfiles + glob.glob(
70
- f"{outputDir}/FLEKS{iDomain}_particle_list_species_{iSpecies}_*"
71
- )
72
-
73
- self.pfiles = self.pfiles + glob.glob(
74
- f"{outputDir}/FLEKS{iDomain}_particle_species_{iSpecies}_*"
75
- )
76
-
77
- self.plistfiles.sort()
78
- self.pfiles.sort()
79
-
80
- self.list_index_to_time = []
81
- if readAllFiles:
82
- for filename in self.pfiles:
83
- record = self._read_the_first_record(filename)
84
- if record == None:
85
- continue
86
- self.list_index_to_time.append(record[FLEKSTP.it_])
87
-
88
- if iListEnd == -1:
89
- iListEnd = len(self.plistfiles)
90
- self.plistfiles = self.plistfiles[iListStart:iListEnd]
91
- self.pfiles = self.pfiles[iListStart:iListEnd]
92
-
93
- self.plists: List[Dict[Tuple[int, int], int]] = []
94
- for fileName in self.plistfiles:
95
- self.plists.append(self.read_particle_list(fileName))
96
-
97
- self.IDs = set()
98
- for plist in self.plists:
99
- self.IDs.update(plist.keys())
100
-
101
- self.file_time = []
102
- for filename in self.pfiles:
103
- record = self._read_the_first_record(filename)
104
- if record == None:
105
- continue
106
- self.file_time.append(record[FLEKSTP.it_])
107
-
108
- print(f"Particles of species {self.iSpecies} are read from {dirs}")
109
- print(f"Number of particles: {len(self.IDs)}")
110
-
111
- def getIDs(self):
112
- return list(sorted(self.IDs))
113
-
114
- def get_index_to_time(self) -> List:
115
- r"""
116
- Getter method for accessing list_index_to_time.
117
- """
118
- if len(self.list_index_to_time) == 0:
119
- print("Index to time mapping was not initialized")
120
- return self.list_index_to_time
121
-
122
- def read_particle_list(self, fileName: str) -> Dict[Tuple[int, int], int]:
123
- r"""
124
- Read and return a list of the particle IDs.
125
- """
126
- # 2 integers + 1 unsigned long long
127
- listUnitSize = 2 * 4 + 8
128
- nByte = os.path.getsize(fileName)
129
- nPart = int(nByte / listUnitSize)
130
- plist = {}
131
- with open(fileName, "rb") as f:
132
- for _ in range(nPart):
133
- binaryData = f.read(listUnitSize)
134
- (cpu, id, loc) = struct.unpack("iiQ", binaryData)
135
- plist.update({(cpu, id): loc})
136
- return plist
137
-
138
- def _read_the_first_record(self, fileName: str) -> Union[List[float], None]:
139
- r"""
140
- Get the first record stored in one file.
141
- """
142
- dataList = list()
143
- with open(fileName, "rb") as f:
144
- while True:
145
- binaryData = f.read(4 * 4)
146
-
147
- if not binaryData:
148
- break # EOF
149
-
150
- (cpu, idtmp, nRecord, weight) = struct.unpack("iiif", binaryData)
151
- if nRecord > 0:
152
- binaryData = f.read(4 * self.nReal)
153
- dataList = dataList + list(
154
- struct.unpack("f" * self.nReal, binaryData)
155
- )
156
- return dataList
157
-
158
- def read_particles_at_time(
159
- self, time: float, doSave: bool = False
160
- ) -> Tuple[np.ndarray, np.ndarray]:
161
- r"""
162
- Get the information of all the particles at a given time, and save to a csv file
163
- with the name "particles_t***.csv" in the current directory if doSave is True.
164
-
165
- Returns
166
- -----------
167
- ids: a numpy array of tuples contains the particle IDs.
168
- pData: a numpy real array with the particle weight, location and velocity.
169
-
170
- Examples
171
- ----------
172
- >>> ids, pData = pt.read_particles_at_time(3700, doSave=True)
173
- """
174
-
175
- nFile = len(self.pfiles)
176
- for iFile in range(nFile):
177
- if iFile == 0 and time < self.file_time[iFile]:
178
- raise Exception("Error: There is no particle at the given time")
179
-
180
- if iFile == nFile - 1:
181
- break
182
- if time >= self.file_time[iFile] and time < self.file_time[iFile + 1]:
183
- break
184
-
185
- fileName = self.pfiles[iFile]
186
-
187
- dataList = []
188
- idList = []
189
- with open(fileName, "rb") as f:
190
- while True:
191
- binaryData = f.read(4 * 4)
192
- if not binaryData:
193
- break # EOF
194
-
195
- (cpu, idtmp, nRecord, weight) = struct.unpack("iiif", binaryData)
196
- binaryData = f.read(4 * self.nReal * nRecord)
197
- allRecords = list(struct.unpack("f" * nRecord * self.nReal, binaryData))
198
- for i in range(nRecord):
199
- if (
200
- allRecords[self.nReal * i + FLEKSTP.it_] >= time
201
- or i == nRecord - 1
202
- ):
203
- dataList.append(
204
- allRecords[self.nReal * i : self.nReal * (i + 1)]
205
- )
206
- idList.append((cpu, idtmp))
207
- break
208
-
209
- npData = np.array(dataList)
210
- idData = np.array(idList, dtype="i,i")
211
- if doSave:
212
- fileName = f"particles_t{time}.csv"
213
- header = "cpu,iid,time,x,y,z,ux,uy,uz"
214
- if self.nReal == 10:
215
- header += ",bx,by,bz"
216
- elif self.nReal == 13:
217
- header += ",bx,by,bz,ex,ey,ez"
218
-
219
- with open(fileName, "w") as f:
220
- f.write(header + "\n")
221
- for id_row, data_row in zip(idData, npData):
222
- f.write(
223
- f"{id_row[0]},{id_row[1]},{','.join(str(x) for x in data_row)}\n"
224
- )
225
-
226
- return idData, npData
227
-
228
- def save_trajectory_to_csv(
229
- self,
230
- pID: Tuple[int, int],
231
- fileName: str = None,
232
- shiftTime: bool = False,
233
- scaleTime: bool = False,
234
- ) -> None:
235
- r"""
236
- Save the trajectory of a particle to a csv file.
237
-
238
- Parameters
239
- ----------
240
- pID: particle ID.
241
- shiftTime: If set to True, set the initial time to be 0.
242
- scaleTime: If set to True, scale the time into [0,1] range, only scale time if
243
- shiftTime = True.
244
-
245
- Example
246
- -----------------
247
- >>> tp.save_trajectory_to_csv((3,15))
248
- """
249
- pData = self.read_particle_trajectory(pID)
250
- if fileName == None:
251
- fileName = "trajectory_" + str(pID[0]) + "_" + str(pID[1]) + ".csv"
252
- header = "time [s], X [R], Y [R], Z [R], U_x [km/s], U_y [km/s], U_z [km/s]"
253
- if self.nReal == 10:
254
- header += ", B_x [nT], B_y [nT], B_z [nT]"
255
- if self.nReal == 13:
256
- header += (
257
- ", B_x [nT], B_y [nT], B_z [nT], E_x [uV/m], E_y [uV/m], E_z [uV/m]"
258
- )
259
- if shiftTime:
260
- pData[:, 0] -= pData[0, 0]
261
- if scaleTime:
262
- pData[:, 0] /= pData[-1, 0]
263
- np.savetxt(fileName, pData, delimiter=",", header=header, comments="")
264
-
265
- def read_particle_trajectory(self, pID: Tuple[int, int]):
266
- r"""
267
- Read and return the trajectory of a particle.
268
-
269
- Parameters
270
- ----------
271
- pID: particle ID
272
-
273
- Examples
274
- ----------
275
- >>> trajectory = tp.read_particle_trajectory((66,888))
276
- """
277
- dataList = list()
278
- for fileName, plist in zip(self.pfiles, self.plists):
279
- if pID in plist:
280
- ploc = plist[pID]
281
- with open(fileName, "rb") as f:
282
- f.seek(ploc)
283
- binaryData = f.read(4 * 4)
284
- (cpu, idtmp, nRecord, weight) = struct.unpack("iiif", binaryData)
285
- binaryData = f.read(4 * self.nReal * nRecord)
286
- dataList = dataList + list(
287
- struct.unpack("f" * nRecord * self.nReal, binaryData)
288
- )
289
-
290
- nRecord = int(len(dataList) / self.nReal)
291
- return np.array(dataList).reshape(nRecord, self.nReal)
292
-
293
- def read_initial_location(self, pID):
294
- r"""
295
- Read and return the initial location of a test particle
296
- """
297
-
298
- for fileName, plist in zip(self.pfiles, self.plists):
299
- if pID in plist:
300
- ploc = plist[pID]
301
- with open(fileName, "rb") as f:
302
- f.seek(ploc)
303
- binaryData = f.read(4 * 4)
304
- (cpu, idtmp, nRecord, weight) = struct.unpack("iiif", binaryData)
305
- nRead = 1
306
- binaryData = f.read(4 * self.nReal * nRead)
307
- dataList = list(struct.unpack("f" * nRead * self.nReal, binaryData))
308
- return dataList
309
-
310
- def select_particles(self, fSelect: Callable = None) -> List[Tuple[int, int]]:
311
- r"""
312
- Select and return the particles whose initial condition satisfy the requirement
313
- set by the user defined function fSelect. The first argument of fSelect is the
314
- particle ID, and the second argument is the initial record (time, location,
315
- velocity and weight of a particle) of a particle.
316
-
317
- Examples
318
- ----------
319
- >>> def fselect(tp, pid):
320
- >>> pdata = tp.read_initial_loc_with_ID(pid)
321
- >>> intime = pdata[FLEKSTP.it_] < 3601
322
- >>> inregion = pdata[FLEKSTP.ix_] > 20
323
- >>> return intime and inregion
324
- >>>
325
- >>> pselected = tp.select_particles(fselect)
326
- >>> tp.plot_trajectory(list(pselected.keys())[1])
327
- """
328
-
329
- if fSelect == None:
330
-
331
- def fSelect(tp, pid):
332
- return True
333
-
334
- pselected = list(filter(lambda pid: fSelect(self, pid), self.IDs))
335
-
336
- return pselected
337
-
338
- def plot_trajectory(self, pID: Tuple[int, int]):
339
- r"""
340
- Plots the trajectory and velocities of the particle pID.
341
-
342
- Example
343
- -----------------
344
- >>> tp.plot_trajectory((3,15))
345
- """
346
-
347
- data = self.read_particle_trajectory(pID)
348
- t = data[:, FLEKSTP.it_]
349
- tNorm = (t - t[0]) / (t[-1] - t[0])
350
-
351
- ncol = 3
352
- nrow = 3 # Default for X, V
353
- if self.nReal == 10: # additional B field
354
- nrow = 4
355
- elif self.nReal == 13: # additional B and E field
356
- nrow = 5
357
-
358
- f, axs = plt.subplots(nrow, ncol, figsize=(12, 6), constrained_layout=True)
359
-
360
- # Plot trajectories
361
- for i, ax in enumerate(axs[0, :]):
362
- x_id = FLEKSTP.ix_ if i < 2 else FLEKSTP.iy_
363
- y_id = FLEKSTP.iy_ if i == 0 else FLEKSTP.iz_
364
- ax.plot(data[:, x_id], data[:, y_id], "k")
365
- ax.scatter(
366
- data[:, x_id],
367
- data[:, y_id],
368
- c=plt.cm.winter(tNorm),
369
- edgecolor="none",
370
- marker="o",
371
- s=10,
372
- )
373
- ax.set_xlabel("x" if i < 2 else "y")
374
- ax.set_ylabel("y" if i == 0 else "z")
375
-
376
- def plot_data(dd, label, irow, icol):
377
- axs[irow, icol].plot(t, dd, label=label)
378
- axs[irow, icol].scatter(
379
- t, dd, c=plt.cm.winter(tNorm), edgecolor="none", marker="o", s=10
380
- )
381
- axs[irow, icol].set_xlabel("time")
382
- axs[irow, icol].set_ylabel(label)
383
-
384
- def plot_vector(idx, labels, irow):
385
- for i, (id, label) in enumerate(zip(idx, labels)):
386
- plot_data(data[:, id], label, irow, i)
387
-
388
- plot_vector([FLEKSTP.ix_, FLEKSTP.iy_, FLEKSTP.iz_], ["x", "y", "z"], 1)
389
- plot_vector([FLEKSTP.iu_, FLEKSTP.iv_, FLEKSTP.iw_], ["Vx", "Vy", "Vz"], 2)
390
-
391
- if self.nReal > FLEKSTP.iBx_:
392
- plot_vector(
393
- [FLEKSTP.iBx_, FLEKSTP.iBy_, FLEKSTP.iBz_], ["Bx", "By", "Bz"], 3
394
- )
395
-
396
- if self.nReal > FLEKSTP.iEx_:
397
- plot_vector(
398
- [FLEKSTP.iEx_, FLEKSTP.iEy_, FLEKSTP.iEz_], ["Ex", "Ey", "Ez"], 4
399
- )
400
-
401
- return
402
-
403
- def plot_loc(self, pData: np.ndarray):
404
- r"""
405
- Plot the location of particles pData.
406
-
407
- Examples
408
- ----------
409
- >>> ids, pData = tp.read_particles_at_time(3700, doSave=True)
410
- >>> f = tp.plot_loc(pData)
411
- """
412
-
413
- px = pData[:, FLEKSTP.ix_]
414
- py = pData[:, FLEKSTP.iy_]
415
- pz = pData[:, FLEKSTP.iz_]
416
-
417
- # create subplot mosaic with different keyword arguments
418
- skeys = ["A", "B", "C", "D"]
419
- f, axs = plt.subplot_mosaic(
420
- "AB;CD",
421
- per_subplot_kw={("D"): {"projection": "3d"}},
422
- gridspec_kw={"width_ratios": [1, 1], "wspace": 0.1, "hspace": 0.1},
423
- figsize=(10, 10),
424
- constrained_layout=True,
425
- )
426
-
427
- # Create 2D scatter plots
428
- for i, (x, y, labels) in enumerate(
429
- zip([px, px, py], [py, pz, pz], [("x", "y"), ("x", "z"), ("y", "z")])
430
- ):
431
- axs[skeys[i]].scatter(x, y, s=1)
432
- axs[skeys[i]].set_xlabel(labels[0])
433
- axs[skeys[i]].set_ylabel(labels[1])
434
-
435
- # Create 3D scatter plot
436
- axs[skeys[3]].scatter(px, py, pz, s=1)
437
- axs[skeys[3]].set_xlabel("x")
438
- axs[skeys[3]].set_ylabel("y")
439
- axs[skeys[3]].set_zlabel("z")
440
-
441
- 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
@@ -79,12 +79,12 @@ class DataContainer(object):
79
79
  if verbose > 0:
80
80
  f.text(0.01, 0.01, s)
81
81
 
82
- def analyze_variable_string(self, var):
82
+ def analyze_variable_string(self, var: str):
83
83
  r"""
84
84
  Parses the input string and return the plot variable and plot range.
85
85
 
86
86
  Args:
87
- var: str
87
+ var (str): string of variables to be parsed
88
88
  Example: var = "{bb}<(-10)>(-9.8)"
89
89
 
90
90
  Return: a tuple of the variable name, variable min and max.
@@ -116,7 +116,7 @@ class DataContainer(object):
116
116
  Evaluates the variable expression and return the result of an YTArray.
117
117
 
118
118
  Args:
119
- expression: str
119
+ expression (str): Python codes to be executed
120
120
  Example: expression = "np.log({rhos0}+{rhos1})"
121
121
  """
122
122
 
@@ -138,8 +138,7 @@ class DataContainer(object):
138
138
  Adds a variable to the dataset.
139
139
 
140
140
  Args:
141
- name: str
142
- The name of the variable to be added into self.data
141
+ name (str): The name of the variable to be added.
143
142
 
144
143
  val: array-like structure
145
144
  Values of the variable stored in an array
@@ -157,8 +156,7 @@ class DataContainer(object):
157
156
  Return raw variables or calculate derived variables.
158
157
 
159
158
  Args:
160
- var: str
161
- variable name
159
+ var (str): variable name
162
160
 
163
161
  Return: YTArray
164
162
  """
@@ -217,8 +215,7 @@ class DataContainer3D(DataContainer):
217
215
  ):
218
216
  r"""
219
217
  Args:
220
- dataSets: dictonary
221
- The key is the variable name, and the dictionary value is usually a YTArray.
218
+ dataSets (dict): The keys are variable names, and the dictionary values are YTArray.
222
219
 
223
220
  x/y/z: A 1D YTArray
224
221
  """
@@ -230,11 +227,9 @@ class DataContainer3D(DataContainer):
230
227
  """Get a 2D slice from the 3D box data.
231
228
 
232
229
  Args:
233
- norm: str
234
- The normal direction of the slice from "x", "y" or "z"
230
+ norm (str): The normal direction of the slice from "x", "y" or "z"
235
231
 
236
- cur_loc: float
237
- The position of slicing.
232
+ cur_loc (float): The position of slicing.
238
233
 
239
234
  Return: DataContainer2D
240
235
  """
@@ -293,18 +288,15 @@ class DataContainer2D(DataContainer):
293
288
  ):
294
289
  r"""
295
290
  Args:
296
- dataSets: dictonary
297
- The keys are variable names, and the values are YTArrays.
291
+ dataSets (dict): The keys are variable names, and the values are YTArrays.
298
292
 
299
293
  x/y: 1D YTArray
300
294
 
301
- xlabel/ylabel: str
295
+ xlabel/ylabel (str): x, y dimension labels
302
296
 
303
- cut_norm: str
304
- "x", "y" or "z"
297
+ cut_norm (str): "x", "y" or "z"
305
298
 
306
- cut_loc: float
307
- cut_norm and cut_loc are used to record the position of slice if this 2D data set is obtained from a 3D box.
299
+ cut_loc (float): cut_norm and cut_loc are used to record the position of slice if this 2D data set is obtained from a 3D box.
308
300
  """
309
301
 
310
302
  zlabel = None
@@ -327,15 +319,15 @@ class DataContainer2D(DataContainer):
327
319
  vars,
328
320
  xlim=None,
329
321
  ylim=None,
330
- unit="planet",
331
- nlevels=200,
332
- cmap="turbo",
322
+ unit: str = "planet",
323
+ nlevels: int = 200,
324
+ cmap: str = "turbo",
333
325
  figsize=(10, 6),
334
326
  pcolor=False,
335
327
  logscale=False,
336
328
  addgrid=False,
337
329
  bottomline=10,
338
- showcolorbar=True,
330
+ showcolorbar: bool = True,
339
331
  *args,
340
332
  **kwargs,
341
333
  ):
@@ -343,25 +335,20 @@ class DataContainer2D(DataContainer):
343
335
  2D plots.
344
336
 
345
337
  Args:
346
- vars: str
347
- Ploting variables and ploting range.
338
+ vars (str): ploting variables and ploting range.
348
339
  Example: vars = "Bx<(50)>(-50) By (np.log(2*{rhos0}))>(-5)"
349
340
 
350
341
  xlim/ylim: A list/tuple contains the x- y-axis range
351
342
 
352
- unit: str
353
- "planet" or "si"
343
+ unit (str): "planet" or "si"
354
344
 
355
- nlevels: int
356
- Number of the contour levels
345
+ nlevels (int): Number of the contour levels. Default 200.
357
346
 
358
- cmap: str
359
- Color map type
347
+ cmap (str): color map type from Matplotlib
360
348
 
361
- figsize: tuple
349
+ figsize (tuple): size of figure. Default (10, 6).
362
350
 
363
- logscale: Bool
364
- True to scale the variable in log.
351
+ logscale (bool): True to scale the variable in log.
365
352
 
366
353
  Examples:
367
354
  >>> f, axes = dc.contour("Bx<(50)>(-50) By (np.log(2*{rhos0}))>(-5)", xlim=[-40,-5])
@@ -469,10 +456,9 @@ class DataContainer2D(DataContainer):
469
456
  r"""Adding contour lines to an axis.
470
457
 
471
458
  Args:
472
- ax: matplotlib axis
473
- The axis to plot contour lines.
459
+ ax (matplotlib axis): the axis to plot contour lines.
474
460
 
475
- var: string
461
+ var (str): variable of contours.
476
462
 
477
463
  Examples:
478
464
  >>> f, axes = dc.plot("Bx<(50)>(-50) By (np.log(2*{rhos0}))>(-5)", xlim=[-40,-5])
@@ -521,14 +507,11 @@ class DataContainer2D(DataContainer):
521
507
  Adding streamlines to an axis.
522
508
 
523
509
  Args:
524
- ax: matplotlib axis
525
- The axis to add streamlines
510
+ ax (matplotlib axis): the axis to add streamlines.
526
511
 
527
- var1/var2: str
528
- Streamline variable names
512
+ var1/var2 (str): streamline variable names.
529
513
 
530
- density: int
531
- It controls the number of streamlines.
514
+ density (float): it controls the number of streamlines.
532
515
 
533
516
  Examples:
534
517
  >>> f, axes = dc.plot("Bx<(50)>(-50) By (np.log(2*{rhos0}))>(-5)", xlim=[-40,-5])
@@ -589,12 +572,11 @@ class DataContainer1D(DataContainer):
589
572
  def __init__(self, dataSets, x, xlabel, *args, **kwargs):
590
573
  r"""
591
574
  Args:
592
- dataSets: dictonary
593
- The key is the variable name, and the dictionary value is usually a YTArray.
575
+ dataSets (dict): the keys are variable names, and the values are YTArrays.
594
576
 
595
577
  x: 1D YTArray
596
578
 
597
- xlabel: str
579
+ xlabel (str): x axis label.
598
580
  """
599
581
 
600
582
  ylabel = None
@@ -76,7 +76,16 @@ plot_unit_si = {
76
76
  }
77
77
 
78
78
 
79
- def get_unit(var, unit_type="planet"):
79
+ def get_unit(var: str, unit_type="planet") -> str:
80
+ """Return the unit of variable.
81
+
82
+ Args:
83
+ var (str): variable name.
84
+ unit_type (str, optional): unit system. Defaults to "planet".
85
+
86
+ Returns:
87
+ str: unit in the specified unit system.
88
+ """
80
89
  if var[-1].isdigit():
81
90
  # Example: pxxs0 -> pxx
82
91
  var = var[0:-2]
@@ -111,21 +120,24 @@ def get_ticks(vmin, vmax):
111
120
  nticks = int((tickMax - tickMin) / dv) + 1
112
121
  return np.linspace(tickMin, tickMax, nticks)
113
122
 
123
+
114
124
  def unit_one(field, data):
115
125
  """Utility function for setting equal weights for macroparticles.
116
126
  TBD: add units.
117
127
  """
118
128
  return np.ones_like(data[("particles", "p_w")])
119
129
 
120
- def download_testfile(url, target_path="."):
130
+
131
+ def download_testfile(url: str, target_path="."):
121
132
  """
122
133
  Downloads a tar.gz file from a URL and extracts its contents.
123
134
 
124
135
  Args:
125
- url: The URL of the tar.gz file.
126
- target_path: The directory to extract the files to.
127
- Defaults to the current directory.
136
+ url (str): the URL of the tar.gz file.
137
+ target_path (str): the directory to extract the files to. Defaults to the current directory.
128
138
  """
139
+ import os
140
+
129
141
  try:
130
142
  response = requests.get(url, stream=True)
131
143
  response.raise_for_status() # Raise an exception for bad status codes
@@ -134,6 +146,9 @@ def download_testfile(url, target_path="."):
134
146
  for chunk in response.iter_content(chunk_size=8192):
135
147
  f.write(chunk)
136
148
 
149
+ if not os.path.exists(target_path):
150
+ os.makedirs(target_path)
151
+
137
152
  with tarfile.open("temp.tar.gz", "r:gz") as tar:
138
153
  tar.extractall(target_path)
139
154
 
@@ -142,7 +157,5 @@ def download_testfile(url, target_path="."):
142
157
  except tarfile.TarError as e:
143
158
  print(f"Error extracting tar file: {e}")
144
159
  finally:
145
- import os
146
-
147
160
  if os.path.exists("temp.tar.gz"):
148
161
  os.remove("temp.tar.gz") # Clean up temporary file
@@ -120,8 +120,7 @@ class FLEKSData(BoxlibDataset):
120
120
  Read and plot the AMReX format output from FLEKS.
121
121
 
122
122
  Args:
123
- output_dir : String
124
- The path to the data.
123
+ output_dir (str): the path to the data.
125
124
 
126
125
  Examples:
127
126
  >>> import flekspy
File without changes
File without changes
File without changes
File without changes
File without changes