pueoAnalysisTools
Loading...
Searching...
No Matches
waveform_plots.py
Go to the documentation of this file.
1r"""!
2@file waveform_plots.py
3@brief `main()` shows how to use
4[load_waveforms()](#waveform_plots.load_waveforms) and
5[load_waveforms_from_IceFinal()](#waveform_plots.load_waveforms_from_IceFinal).
6@note Altair is needed to make the plot.
7 ```
8 pip3 install altair[save]
9 ```
10"""
11
12# Examples are in the main function at the bottom of the script
13
14import ROOT
15import numpy as np
16import polars as pl
17from pathlib import Path
18from initialise import load_pueoEvent_Dataset
19from scipy.signal import lfilter
20
21
23
24
25
26def load_waveforms_old(dataset: ROOT.pueo.Dataset, antennas: str = None) -> pl.DataFrame():
27 r"""! Loads time domain signals
28 @ingroup lwf
29
30 @param[in] dataset The output of #initialise.load_pueoEvent_Dataset
31 @param[in] antennas (optional) **whitespace separated list** specifying
32 [AntNum](#antenna_attributes.ALL_PUEO_ANTENNA_NAMES)s.
33 @retval waveforms The desired [channels](#antenna_attributes.read_global_channel_mapping)
34 and their waveforms [Volts]
35
36@todo Currently main instrument only, since oftentimes LF is not simulated
37
38* `antenna`:
39 * Users can optionally provide a string containing a whitespace separated list of antenna
40 indices (ie [AntNum](#antenna_attributes.read_global_channel_mapping))
41 * If no antennas is specified, all MI channels will be loaded into the DataFrame.
42 * An exception is raised if the list contains invalid `AntNum`s.
43
44* Output schema:
45 ```
46 $ GlobalChan <u32>
47 $ AntNum <enum>
48 $ AntIdx <u32>
49 $ Pol <enum>
50 $ AntType <enum>
51 $ SurfNum <u8>
52 $ SurfChanNum <u8>
53 $ PhiSector <u8>
54 $ Ring <u8>
55 $ waveforms (volts) <array[f64, 1024]>
56 $ step size (nanoseconds) <f64>
57 ```
58 """
59
60 from antenna_attributes import read_global_channel_mapping, ALL_PUEO_ANTENNA_NAMES
61
62 if antennas is None:
63 channel_mapping: pl.DataFrame = read_global_channel_mapping()
64 else: # if user provides a list of antennas, filter out unused antennas
65 try: # see if the user had provided a bad antenna index
66 antennas = antennas.split()
67 pl.Series(antennas, dtype=ALL_PUEO_ANTENNA_NAMES)
68 except pl.exceptions.InvalidOperationError as exc:
69 print("InvalidOperationError:", exc)
70 print("\n\nPerhaps you included a non-existent antenna index?")
71 print("aborting operation at function: load_waveforms()")
72 exit(1)
73
74 channel_mapping = (
75 read_global_channel_mapping().filter(pl.col("AntNum").is_in(antennas))
76 )
77
78 # Filter out LF channels
79 channel_mapping = channel_mapping.filter(pl.col("AntType") != "LF")
80
81 useful = dataset.useful()
82
83 '''
84 waveforms = channel_mapping.select(
85 pl.all().exclude("Comment"),
86
87 #return list, not np.array
88 pl.col("GlobalChan").map_batches(
89 lambda s: [np.asarray(useful.volts[int(i)], dtype=float) for i in s]
90 ).alias("waveforms (volts)"),
91
92 pl.col("GlobalChan").map_batches(
93 lambda s: [float(useful.dt[int(i)]) for i in s]
94 ).alias("step size (nanoseconds)")
95 )
96 '''
97
98 waveforms = channel_mapping.select(
99 pl.all().exclude("Comment"),
100
101 pl.col("GlobalChan").map_batches(
102 lambda s: np.array([useful.volts[i] for i in s])
103 ).alias("waveforms (volts)"),
104
105 pl.col("GlobalChan").map_batches(
106 lambda s: np.array([useful.dt[i] for i in s])
107 ).alias("step size (nanoseconds)")
108 )
109
110 return waveforms
111
112def load_waveforms(dataset: ROOT.pueo.Dataset, antennas: str = None) -> pl.DataFrame:
113 from antenna_attributes import read_global_channel_mapping, ALL_PUEO_ANTENNA_NAMES
114 import numpy as np
115 import polars as pl
116
117 if antennas is None:
118 channel_mapping: pl.DataFrame = read_global_channel_mapping()
119 else:
120 try:
121 antennas = antennas.split()
122 pl.Series(antennas, dtype=ALL_PUEO_ANTENNA_NAMES)
123 except pl.exceptions.InvalidOperationError as exc:
124 print("InvalidOperationError:", exc)
125 print("\n\nPerhaps you included a non-existent antenna index?")
126 print("aborting operation at function: load_waveforms()")
127 exit(1)
128
129 channel_mapping = read_global_channel_mapping().filter(pl.col("AntNum").is_in(antennas))
130
131 channel_mapping = channel_mapping.filter(pl.col("AntType") != "LF")
132
133 useful = dataset.useful()
134
135 waveforms = channel_mapping.select(
136 pl.all().exclude("Comment"),
137
138 pl.col("GlobalChan").map_batches(
139 lambda s: pl.Series(
140 [np.asarray(list(useful.volts[int(i)]), dtype=np.float64) for i in s]
141 )
142 ).alias("waveforms (volts)"),
143
144 pl.col("GlobalChan").map_batches(
145 lambda s: pl.Series([float(useful.dt[int(i)]) for i in s])
146 ).alias("step size (nanoseconds)")
147 )
148 '''
149 waveforms = waveforms.with_columns(
150 pl.col("waveforms (volts)")
151 .map_batches(window_batches)
152 .alias("waveforms (volts)")
153 )
154 '''
155 return waveforms
156
157def upsample_waveforms(waveforms: pl.DataFrame, upsample_factor: int) -> pl.DataFrame:
158 r"""!Calls SciPy's
159 [resample()](https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.resample.html)
160 to upsample the waveforms.
161 @ingroup lwf
162 @param[in] waveforms The output of #load_waveforms
163 @param[in] upsample_factor
164 @retval upsampled_waveforms
165
166 * The following columns are required in the input:
167 -# `waveforms (volts)`
168 -# `step size (nanoseconds)`
169 * No additional columns will be attached to the output,
170 `waveforms (volts)` and `step size (nanoseconds)` will be upsampled and replaced.
171
172 """
173 from scipy.signal import resample
174 signal_length = len(waveforms["waveforms (volts)"][0])
175
176 upsampled_waveforms = waveforms.with_columns(
177 pl.col("waveforms (volts)").map_batches(
178 lambda wf: resample(wf, signal_length * upsample_factor, axis=1)
179 ),
180 pl.col("step size (nanoseconds)") / upsample_factor
181 )
182
183 return upsampled_waveforms
184
185
186
187def window_batches(wfs: pl.Series) -> np.ndarray:
188 WINDOW_front=60
189 WINDOW_back=60
190 out = []
191 for wf in wfs:
192 wf = np.asarray(wf)
193 imax = np.argmax(abs(wf))
194
195 tmp = np.zeros_like(wf)
196 lo = max(imax - WINDOW_front, 0)
197 hi = min(imax + WINDOW_back+1, wf.size)
198 tmp[lo:hi] = wf[lo:hi]
199
200 out.append(tmp)
201
202 return np.stack(out)
203
204
205def load_waveforms_from_root(filepath, ev=0):
206 from antenna_attributes import read_global_channel_mapping
207 import ROOT
208 import os
209
210 channel_mapping: pl.DataFrame = read_global_channel_mapping()
211 channel_mapping = channel_mapping.filter(pl.col("AntType") == "LF")
212
213 channel_mapping_H = channel_mapping.filter(pl.col("Pol") == "H")
214 channel_mapping_V = channel_mapping.filter(pl.col("Pol") == "V")
215
216 lib_dir = str(os.getenv('PUEO_UTIL_INSTALL_DIR'))+'/lib64'
217
218 ROOT.gSystem.Load(lib_dir+"/libPueoSim.so")
219 ROOT.gSystem.Load(lib_dir+"/libNiceMC.so")
220
221
222 rootFile = ROOT.TFile.Open(filepath)
223 passTree = rootFile.Get("passTree")
224
225 passTree.GetEntry(ev)
226
227
228 det_evt = passTree.detectorEvents[1]
229
230 def load_y_H(s: pl.Series) -> np.ndarray:
231 return np.stack([
232 np.array(det_evt.waveformsH[int(i)-192].GetY(), copy=True)
233 for i in s
234 ])
235
236 def load_y_V(s: pl.Series) -> np.ndarray:
237 return np.stack([
238 np.array(det_evt.waveformsV[int(i)-200].GetY(), copy=True)
239 for i in s
240 ])
241 waveformsH = channel_mapping_H.select(
242 pl.all().exclude("Comment"),
243
244 pl.col("GlobalChan")
245 .map_batches(load_y_H)
246 .alias("waveforms (volts)"),
247 pl.col("GlobalChan").map_batches(
248 lambda s: np.array([1/3 for i in s])
249 ).alias("step size (nanoseconds)")
250 )
251 waveformsV = channel_mapping_V.select(
252 pl.all().exclude("Comment"),
253
254 pl.col("GlobalChan")
255 .map_batches(load_y_V)
256 .alias("waveforms (volts)"),
257 pl.col("GlobalChan").map_batches(
258 lambda s: np.array([1/3 for i in s])
259 ).alias("step size (nanoseconds)")
260 )
261
262 waveforms = pl.concat([waveformsH, waveformsV], how="vertical")
263
264
265 waveforms = waveforms.with_columns(
266 pl.col("waveforms (volts)")
267 .map_batches(filter_batch)
268 .alias("waveforms (volts)")
269 )
270
271
272 waveforms = waveforms.with_columns(
273 pl.col("waveforms (volts)")
274 .map_batches(window_batches)
275 .alias("waveforms (volts)")
276 )
277
278 return waveforms
279
280
281def filter_batch(s: pl.Series) -> np.ndarray:
282 # s is shape (n_rows, 1024)
283 taps = np.array([1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
284 0, 0, 0, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
285 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
286 1, 1, 1, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2,
287 -2, -1, -1, -1, -1, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2,
288 2, 2, 1, 1, 1, 0, 0, -1, -1, -2, -2, -2, -2, -2, -2, -2, -2, -2,
289 -1, -1, 0, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 0, -1, -2,
290 -2, -2, -4, -4, -2, -2, -1, 0, 1, 2, 2, 2, 4, 2, 2, 1, 0, -1,
291 -2, -2, -2, -2, -2, -1, 1, 2, 4, 4, 2, -1, -2, -2, -2, 0, 1, 1, 1])
292 return np.array([lfilter(taps, 1.0, np.asarray(w, dtype=np.float64)) for w in s])
293
294
295
296def plot_waveforms(waveforms: pl.DataFrame, plot_name: str) -> None:
297 r"""!Uses Vega-Altair to make waveform plots.
298 @ingroup lwf
299 @param[in] waveforms The output of any other functions in @ref load_waveforms
300 @param[in] plot_name Be sure to specify the file format (eg. `my_plot.png` or `my_plot.html`)
301
302* The following columns are required in the input:
303 * `waveforms (volts)`
304 * `step size (nanoseconds)`
305 * `PhiSector`
306 * `Ring`
307 * `Pol`
308
309* Example: Plot the vpol channels of three antennas using the last event in `run0/`.\n
310 Place the figure in the `/tmp` directory.
311```python
312run_zero_data = load_pueoEvent_Dataset(pueo_mc_data=Path("/home/all_my_runs"), run_number=0)
313run_zero_data.last()
314wf = load_waveforms(dataset=run_zero_data, antennas="403 111 201")
315plot_waveforms(wf.filter(pl.col("Pol") == "V"), plot_name="/tmp/waveforms.html")
316```
317 @note The actual file name will be changed to `/tmp/Vpol_waveforms.html`.
318 Had we chosen not to filter out the horizontally polarized channels,
319 there will be another plot: `/tmp/Hpol_waveforms.html`.
320
321 \htmlonly
322 <iframe src="example_three_antenna_waveforms_from_pueoEvent.html" width="950" height="1000"></iframe>
323 \endhtmlonly
324 """
325 import altair as alt
326
327 # this forces function to error out instead of failing silently
328 waveforms.select("PhiSector", "Ring")
329
330 time = np.zeros_like(waveforms["waveforms (volts)"])
331 signal_length = time.shape[1]
332
333 for i, dt in enumerate(waveforms["step size (nanoseconds)"]):
334 time[i] = dt * np.arange(signal_length)
335
336 waveforms_list = (
337 waveforms.with_columns(pl.Series(time).alias("time (ns)"))
338 .explode("waveforms (volts)", "time (ns)")
339 .partition_by("Pol")
340 )
341
342 plot_path = Path(plot_name)
343 destination = plot_path.parent
344
345 # make the plots, let Altair take care of subplot positions automatically
346 for frame in waveforms_list:
347 pol = frame["Pol"][0]
348
349 chart = (
350 alt.Chart(frame).mark_line()
351 .encode(
352 x='time (ns):Q',
353 y=alt.Y('waveforms (volts)').title("[Volts]"),
354 )
355 .facet(
356 column=alt.Column("PhiSector:O", header=alt.Header(labelFontSize=16, titleFontSize=16)),
357 row=alt.Row("Ring:O", header=alt.Header(labelFontSize=16, titleFontSize=16))
358 )
359 .properties(title=f"{pol}Pol Waveforms")
360 .configure_title(fontSize=16)
361 )
362
363 pn = destination / f"{pol}pol_{plot_path.name}"
364 chart.save(f"{str(pn)}")
365
366
367def load_waveforms_from_IceFinal(filepath: str, event_number: [int] = 0,
368 antennas: str = None, upsample_factor: int = None,
369 want_plots: bool = False,
370 plot_name: str = None) -> tuple[pl.DataFrame(), np.array(1024)]:
371 r"""! Loads time domain signals from an IceFinal file;
372 users can optionally provide a list of antenna indices.
373 @ingroup lwf
374
375 @param[in] filepath path to `IceFinal_*_allTree.root`
376 @param[in] event_number (optional) an integer or a **list** of **integers**, defaults to event 0
377 @param[in] antennas (optional) **whitespace separated string** specifying the antenna names
378 @param[in] upsample_factor (optional)
379 @param[in] want_plots (optional) defaults to False
380 @param[in] plot_name (optional)
381 @retval waveforms_frame waveforms recorded by the antennas [Volts]
382 @retval time_mesh time mesh corresponding to the waveforms [seconds].
383
384* The function will error out if the `passTree` of the IceFinal file (see `filepath`) is empty.
385 (ie. no triggered events)
386* If no antennas are specified, all waveforms will be loaded into the DataFrame,
387 which presents them in [ascending](#antenna_attributes.ALL_PUEO_ANTENNA_NAMES) `AntNum`.
388* Example:
389 ```
390 load_waveforms_from_IceFinal("IceFinal_420_allTree.root", antennas="403 111 201", event_number=[0, 1])
391 ```
392 ```
393 ┌──────────────┬────────┬─────────────┬─────────────────────┬───────────────────────┐
394 │ event number ┆ AntNum ┆ pueoSim idx ┆ VPol waveforms (V) ┆ truth (signal origin) │
395 │ --- ┆ --- ┆ --- ┆ --- ┆ --- │
396 │ i32 ┆ enum ┆ u8 ┆ array[f64, 1024] ┆ array[f64, 3] │
397 ╞══════════════╪════════╪═════════════╪═════════════════════╪═══════════════════════╡
398 │ 0 ┆ 201 ┆ 1 ┆ […] ┆ […] │
399 │ 0 ┆ 403 ┆ 11 ┆ […] ┆ […] │
400 │ 0 ┆ 111 ┆ 40 ┆ […] ┆ […] │
401 │ 1 ┆ 201 ┆ 1 ┆ […] ┆ […] │
402 │ 1 ┆ 403 ┆ 11 ┆ […] ┆ […] │
403 │ 1 ┆ 111 ┆ 40 ┆ […] ┆ […] │
404 └──────────────┴────────┴─────────────┴─────────────────────┴───────────────────────┘
405 ```
406* The waveforms can be upsampled by @p upsample_factor.
407* `truth (signal origin)` is a unit vector that represents the true direction of the incoming
408 signal.
409 * Note that in the IceFinal files, the variable `RFdir_payload` is the direction the signal is
410 travelling **to**.
411 * Therefore, to obtain the direction that the signal is coming **from**,
412 we need the opposite vector: \n
413 `signal_origin` = - `RFdir_payload`
414 * This is useful for verifying the correctness of the [correlation maps](@ref example_corr_map).
415
416* The function also optionally creates waveform plots and saves to the working directory
417 (ie. wherever the user calls this script) via the internal function #plot_waveforms().
418 Below is an example of a three-antenna plot (actually two plots would be made, one for each event):
419 ```python
420 load_waveforms_from_IceFinal(
421 "IceFinal_420_allTree.root", antennas="403 111 201",
422 want_plots=True, event_number=[0, 1])
423 ```
424 \anchor exampleThreeAntennaPlot
425 \htmlonly
426 <iframe src="example_three_antenna_waveforms.html" width="950" height="1000"></iframe>
427 \endhtmlonly
428 @todo **currently main instrument only**
429
430 @warning Will abort if non-existent antenna indices are provided.
431 ```
432 load_waveforms_from_IceFinal("IceFinal_420_allTree.root", antennas="201 511")
433 ```
434 In this case, Antenna `511` does not exist, since there are only 4 antennas in each `PhiSector`.\n
435 Thus, you would see
436 > InvalidOperationError: conversion from `str` to `enum` failed in column '' for 1 out of 2 values: ["511"]
437 >
438 > Ensure that all values in the input column are present in the categories of the enum datatype.
439 >
440 > Perhaps you included a non-existent antenna index?
441 > aborting operation at function: load_waveforms_from_IceFinal()
442 """
443 #from antenna_attributes import generate_dataframe_from_pueodata_qrh, ALL_PUEO_ANTENNA_NAMES
444 from antenna_attributes import read_global_channel_mapping, ALL_PUEO_ANTENNA_NAMES
445
446 if antennas is None:
447 channel_mapping: pl.DataFrame = read_global_channel_mapping()
448
449 # turn int into a list[int] so that it's iterable
450 if isinstance(event_number, int):
451 event_number = [event_number]
452
453 # if user provides a list of antennas, filter out unused antennas
454 if antennas is not None:
455 # see if the user had provided a bad antenna index
456 try:
457 antennas = antennas.split()
458 pl.Series(antennas, dtype=ALL_PUEO_ANTENNA_NAMES)
459 except pl.exceptions.InvalidOperationError as exc:
460 print("InvalidOperationError:", exc)
461 print("\n\nPerhaps you included a non-existent antenna index?")
462 print("aborting operation at function: load_waveforms_from_IceFinal()")
463 exit(1)
464
465 _index_conversion = (
466 generate_dataframe_from_pueodata_qrh().filter(pl.col("AntNum").is_in(antennas))
467 )
468 else:
469 _index_conversion: pl.DataFrame = generate_dataframe_from_pueodata_qrh()
470
471 ROOT.gSystem.Load("libNiceMC.so")
472 ROOT.gSystem.Load("libPueoSim.so")
473
474 _rootFile: ROOT.TFile = ROOT.TFile.Open(filepath)
475 _passTree: ROOT.TTree = _rootFile.Get('passTree')
476
477 waveforms: [pl.DataFrame] = []
478 for ev in event_number:
479
480 if ev < 0 or ev >= _passTree.GetEntries():
481 print(f"Invalid event index \"{ev}\" specified for {filepath}")
482 print(f"There are only {_passTree.GetEntries()} events.")
483 exit(1)
484
485 _passTree.GetEvent(ev)
486 _df: pl.DataFrame = (
487 _index_conversion
488 .select(
489 pl.lit(ev).alias("event number"),
490 pl.col("AntNum", "pueoSim idx"),
491 pl.col("pueoSim idx").map_batches(
492 lambda s: np.array([
493 _passTree.detectorEvents[0].waveformsV[i].GetY()
494 for i in s
495 ])
496 ).alias("VPol waveforms (V)"),
497 pl.col("pueoSim idx").map_batches(
498 lambda s: np.array([
499 -(_passTree.detectorEvents[0].RFdir_payload)
500 for _ in s
501 ])
502 ).alias("truth (signal origin)"),
503 )
504 )
505 waveforms.append(_df)
506 print(waveforms)
507 # time mesh in seconds
508 time_mesh = np.array(_passTree.detectorEvents[0].waveformsV[0].GetX())
509
510 # turn the list of waveform dataframes into one dataframe and apply SciPy upsampling if needed
511 waveforms = pl.concat(waveforms)
512
513 # if want_plots:
514 # _plot_waveforms_frame( # plot in nanoseconds
515 # _explode_waveforms_frame(waveforms, time_mesh * 1e9), plot_name=plot_name
516 # )
517
518 if upsample_factor is not None:
519 from scipy.signal import resample
520 upsampled_waveforms = waveforms.with_columns(
521 pl.col("VPol waveforms (V)").map_batches(
522 lambda s: resample(s, len(time_mesh) * upsample_factor, axis=1)
523 )
524 )
525 _, upsampled_time_mesh = resample(waveforms["VPol waveforms (V)"][0],
526 num=len(time_mesh) * upsample_factor, t=time_mesh)
527
528 return upsampled_waveforms, upsampled_time_mesh
529
530 return waveforms, time_mesh
531
532
533
534
535if __name__ == "__main__":
536 # Below we illustrate the basic features of the Dataset class,
537 # and how to pass it to the analysis functions.
538 # For more pueo::Dataset class methods, see its header file:
539 # https://github.com/PUEOCollaboration/pueoEvent/blob/main/src/pueo/Dataset.h
540
541 # Use run zero (for example) to initialize the Dataset
542 # Warning: make sure that the run is not empty (has events),
543 # otherwise pueo::Dataset's constructor will crash
544 myDataset: ROOT.pueo.Dataset = load_pueoEvent_Dataset(pueo_mc_data=Path("/users/PAS0654/yuchieh/geometry_bug"), run_number=1)
545 print(f"The current run is {myDataset.getCurrRun()}")
546
547 # suppose we now want to use another run, maybe run one. Warning: `/tmp/run1/` has to exist!
548 #if not myDataset.loadRun(1, ROOT.pueo.Dataset.PUEO_MC_DATA):
549 # exit(1)
550 #else:
551 # print(f"The current run is {myDataset.getCurrRun()}")
552 print(f"The current run is {myDataset.getCurrRun()}")
553
554 # Suppose we want to analyze the 15th entry
555 if not myDataset.getEntry(1):
556 print("no this entry")
557 exit(1)
558 else:
559 print(f"Loaded event {myDataset.current()}")
560
561 # We could also analyze the last event
562 # myDataset.last()
563
564 # ---------------------------- Examples ---------------------------- #
565
566 # Example 1: plot the VPol waveforms of just three antennas:
567 #wf: pl.DataFrame = load_waveforms(dataset=myDataset, antennas="403 111 201")
568 #plot_waveforms(wf.filter(pl.col("Pol") == "V"), plot_name="./three_waveforms.png")
569
570 # Example 2: plot upsampled VPol and HPol waveforms of all antennas
571 #wf: pl.DataFrame = load_waveforms(dataset=myDataset)
572 for ev in range(2):
573 wf: pl.DataFrame = load_waveforms(dataset=myDataset, antennas="119 219 319 419 120 220 320 420")
574 #wf: pl.DataFrame = load_waveforms_from_root("/users/PAS0654/yuchieh/geometry_bug/run0/IceFinal_0_allTree.root", ev=ev)
575 #up: pl.DataFrame = upsample_waveforms(waveforms=wf, upsample_factor=3)
576 plot_waveforms(wf, plot_name=f"/users/PAS0654/yuchieh/geometry_bug/GeoTest_MI_Evt{ev}_update.png")