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.
8 pip3 install altair[save]
17from pathlib
import Path
18from initialise
import load_pueoEvent_Dataset
19from scipy.signal
import lfilter
23sys.path.append(
"../pueoTALON")
24import pueoPythonAnalyzer.waveform
as pueoTalonwaveform
31def load_waveforms_old(dataset: ROOT.pueo.Dataset, antennas: str =
None) -> pl.DataFrame():
32 r"""! Loads time domain signals
35 @param[in] dataset The output of #initialise.load_pueoEvent_Dataset
36 @param[in] antennas (optional) **whitespace separated list** specifying
37 [AntNum](#antenna_attributes.ALL_PUEO_ANTENNA_NAMES)s.
38 @retval waveforms The desired [channels](#antenna_attributes.read_global_channel_mapping)
39 and their waveforms [Volts]
41@todo Currently main instrument only, since oftentimes LF is not simulated
44 * Users can optionally provide a string containing a whitespace separated list of antenna
45 indices (ie [AntNum](#antenna_attributes.read_global_channel_mapping))
46 * If no antennas is specified, all MI channels will be loaded into the DataFrame.
47 * An exception is raised if the list contains invalid `AntNum`s.
60 $ waveforms (volts) <array[f64, 1024]>
61 $ step size (nanoseconds) <f64>
65 from antenna_attributes
import read_global_channel_mapping, ALL_PUEO_ANTENNA_NAMES
68 channel_mapping: pl.DataFrame = read_global_channel_mapping()
71 antennas = antennas.split()
72 pl.Series(antennas, dtype=ALL_PUEO_ANTENNA_NAMES)
73 except pl.exceptions.InvalidOperationError
as exc:
74 print(
"InvalidOperationError:", exc)
75 print(
"\n\nPerhaps you included a non-existent antenna index?")
76 print(
"aborting operation at function: load_waveforms()")
80 read_global_channel_mapping().filter(pl.col(
"AntNum").is_in(antennas))
84 channel_mapping = channel_mapping.filter(pl.col(
"AntType") !=
"LF")
86 useful = dataset.useful()
89 waveforms = channel_mapping.select(
90 pl.all().exclude("Comment"),
92 #return list, not np.array
93 pl.col("GlobalChan").map_batches(
94 lambda s: [np.asarray(useful.volts[int(i)], dtype=float) for i in s]
95 ).alias("waveforms (volts)"),
97 pl.col("GlobalChan").map_batches(
98 lambda s: [float(useful.dt[int(i)]) for i in s]
99 ).alias("step size (nanoseconds)")
103 waveforms = channel_mapping.select(
104 pl.all().exclude(
"Comment"),
106 pl.col(
"GlobalChan").map_batches(
107 lambda s: np.array([useful.volts[i]
for i
in s])
108 ).alias(
"waveforms (volts)"),
110 pl.col(
"GlobalChan").map_batches(
111 lambda s: np.array([useful.dt[i]
for i
in s])
112 ).alias(
"step size (nanoseconds)")
117def load_waveforms(run:int, entry:int=
None, eventNumber:int=
None, antennas: str =
None, anttype=
'MI', upsample_factor=1) -> pl.DataFrame:
119 r"""! Loads time domain signals
122 @param[in] int run number
123 @param[in] event event, this is entry number in the data file
124 @param[in] eventID This is evnet number, you can specify either entry or eventnumber
125 @param[in] antennas [AntNum](#antenna_attributes.ALL_PUEO_ANTENNA_NAMES)s.
126 @param[in] anttype Either MI or LF
127 @paran[in] upsample_factor
128 @retval waveforms The desired [channels](#antenna_attributes.read_global_channel_mapping)
129 and their waveforms [Volts]
133 * Users can optionally provide a string containing a whitespace separated list of antenna
134 indices (ie [AntNum](#antenna_attributes.read_global_channel_mapping))
135 * If no antennas is specified, all MI channels will be loaded into the DataFrame.
136 * An exception is raised if the list contains invalid `AntNum`s.
149 $ waveforms (volts) <array[f64, 1024]>
150 $ step size (nanoseconds) <f64>
154 from antenna_attributes
import read_global_channel_mapping, ALL_PUEO_ANTENNA_NAMES
159 channel_mapping: pl.DataFrame = read_global_channel_mapping()
162 antennas = antennas.split()
163 pl.Series(antennas, dtype=ALL_PUEO_ANTENNA_NAMES)
164 except pl.exceptions.InvalidOperationError
as exc:
165 print(
"InvalidOperationError:", exc)
166 print(
"\n\nPerhaps you included a non-existent antenna index?")
167 print(
"aborting operation at function: load_waveforms()")
170 channel_mapping = read_global_channel_mapping().filter(pl.col(
"AntNum").is_in(antennas))
173 channel_mapping = channel_mapping.filter(pl.col(
"AntType") !=
"LF")
175 channel_mapping = channel_mapping.filter(pl.col(
"AntType") ==
"LF")
179 if entry
is not None:
180 processor = pueoTalonwaveform.Channels(run=run, entry=entry, geometry=
'jan26')
181 elif eventNumber
is not None:
182 processor = pueoTalonwaveform.Channels(run=run, eventNumber=eventNumber, geometry=
'jan26')
188 for ch
in channel_mapping[
"GlobalChan"]:
190 sig, new_dt = processor.condition_lfsignal(int(ch), zero_pad_factor = 1, upsample_factor = upsample_factor, dt = 1/3)
192 sig, new_dt = processor.condition_signal(int(ch), zero_pad_factor = 1, upsample_factor = upsample_factor, dt = 1/3)
193 waveform_list.append(sig[0])
194 dt_list.append(new_dt)
195 chan_list.append(int(ch))
198 wf_df = pl.DataFrame({
199 "GlobalChan": chan_list,
200 "waveforms (volts)": waveform_list,
201 "step size (nanoseconds)": dt_list
204 waveforms = channel_mapping.join(wf_df, on=
"GlobalChan")
207 useful = dataset.useful()
209 waveforms = channel_mapping.select(
210 pl.all().exclude("Comment"),
212 pl.col("GlobalChan").map_elements(
213 lambda i: np.asarray(list(useful.volts[int(i)]), dtype=np.float64),
214 return_dtype=pl.Array(pl.Float64, 1024)
215 ).alias("waveforms (volts)"),
217 pl.lit(1/3).alias("step size (nanoseconds)")
220 # ToDo: make use of the conditionings in pueoTALON
222 waveforms = waveforms.with_columns(
223 pl.col("waveforms (volts)")
224 .map_batches(filter_batch, return_dtype=pl.Array(pl.Float64, 1024))
225 .alias("waveforms (volts)")
228 waveforms = waveforms.with_columns(
229 pl.col("waveforms (volts)")
230 .map_batches(window_batches, return_dtype=pl.Array(pl.Float64, 1024))
231 .alias("waveforms (volts)")
236def upsample_waveforms(waveforms: pl.DataFrame, upsample_factor: int) -> pl.DataFrame:
238 [resample()](https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.resample.html)
239 to upsample the waveforms.
241 @param[in] waveforms The output of #load_waveforms
242 @param[in] upsample_factor
243 @retval upsampled_waveforms
245 * The following columns are required in the input:
246 -# `waveforms (volts)`
247 -# `step size (nanoseconds)`
248 * No additional columns will be attached to the output,
249 `waveforms (volts)` and `step size (nanoseconds)` will be upsampled and replaced.
252 from scipy.signal
import resample
253 signal_length = len(waveforms[
"waveforms (volts)"][0])
255 upsampled_waveforms = waveforms.with_columns(
256 pl.col(
"waveforms (volts)").map_batches(
257 lambda wf: resample(wf, signal_length * upsample_factor, axis=1)
258 ,return_dtype=pl.Array(pl.Float64, 1024*upsample_factor)
260 pl.col(
"step size (nanoseconds)") / upsample_factor
263 return upsampled_waveforms
267def window_batches(wfs: pl.Series) -> pl.Series:
277 imax = np.argmax(np.abs(wf))
279 tmp = np.zeros(N, dtype=np.float64)
281 lo = max(imax - WINDOW_front, 0)
282 hi = min(imax + WINDOW_back + 1, N)
284 tmp[lo:hi] = wf[lo:hi]
288 return pl.Series(out)
290def load_waveforms_from_root(filepath, ev=0):
291 from antenna_attributes
import read_global_channel_mapping
295 channel_mapping: pl.DataFrame = read_global_channel_mapping()
296 channel_mapping = channel_mapping.filter(pl.col(
"AntType") ==
"LF")
298 channel_mapping_H = channel_mapping.filter(pl.col(
"Pol") ==
"H")
299 channel_mapping_V = channel_mapping.filter(pl.col(
"Pol") ==
"V")
301 lib_dir = str(os.getenv(
'PUEO_UTIL_INSTALL_DIR'))+
'/lib'
303 ROOT.gSystem.Load(lib_dir+
"/libPueoSim.so")
304 ROOT.gSystem.Load(lib_dir+
"/libNiceMC.so")
307 rootFile = ROOT.TFile.Open(filepath)
308 passTree = rootFile.Get(
"passTree")
310 passTree.GetEntry(ev)
313 det_evt = passTree.detectorEvents[1]
315 def load_y_H(s: pl.Series) -> np.ndarray:
317 np.array(det_evt.waveformsH[int(i)-192].GetY(), copy=
True)
321 def load_y_V(s: pl.Series) -> np.ndarray:
323 np.array(det_evt.waveformsV[int(i)-200].GetY(), copy=
True)
326 waveformsH = channel_mapping_H.select(
327 pl.all().exclude(
"Comment"),
330 .map_batches(load_y_H, return_dtype=pl.Array(pl.Float64, 1024))
331 .alias(
"waveforms (volts)"),
334 .alias(
"step size (nanoseconds)")
336 waveformsV = channel_mapping_V.select(
337 pl.all().exclude(
"Comment"),
340 .map_batches(load_y_V, return_dtype=pl.Array(pl.Float64, 1024))
341 .alias(
"waveforms (volts)"),
344 .alias(
"step size (nanoseconds)")
347 waveforms = pl.concat([waveformsH, waveformsV], how=
"vertical")
350 waveforms = waveforms.with_columns(
351 pl.col(
"waveforms (volts)")
352 .map_batches(filter_batch)
353 .alias(
"waveforms (volts)")
357 waveforms = waveforms.with_columns(
358 pl.col(
"waveforms (volts)")
359 .map_batches(window_batches)
360 .alias(
"waveforms (volts)")
366def filter_batch(s: pl.Series) -> np.ndarray:
368 taps = np.array([1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
369 0, 0, 0, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
370 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
371 1, 1, 1, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2,
372 -2, -1, -1, -1, -1, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2,
373 2, 2, 1, 1, 1, 0, 0, -1, -1, -2, -2, -2, -2, -2, -2, -2, -2, -2,
374 -1, -1, 0, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 0, -1, -2,
375 -2, -2, -4, -4, -2, -2, -1, 0, 1, 2, 2, 2, 4, 2, 2, 1, 0, -1,
376 -2, -2, -2, -2, -2, -1, 1, 2, 4, 4, 2, -1, -2, -2, -2, 0, 1, 1, 1])
377 return np.array([lfilter(taps, 1.0, np.asarray(w, dtype=np.float64))
for w
in s])
381def plot_waveforms(waveforms: pl.DataFrame, plot_name: str) ->
None:
382 r"""!Uses Vega-Altair to make waveform plots.
384 @param[in] waveforms The output of any other functions in @ref load_waveforms
385 @param[in] plot_name Be sure to specify the file format (eg. `my_plot.png` or `my_plot.html`)
387* The following columns are required in the input:
388 * `waveforms (volts)`
389 * `step size (nanoseconds)`
394* Example: Plot the vpol channels of three antennas using the last event in `run0/`.\n
395 Place the figure in the `/tmp` directory.
397run_zero_data = load_pueoEvent_Dataset(pueo_mc_data=Path("/home/all_my_runs"), run_number=0)
399wf = load_waveforms(dataset=run_zero_data, antennas="403 111 201")
400plot_waveforms(wf.filter(pl.col("Pol") == "V"), plot_name="/tmp/waveforms.html")
402 @note The actual file name will be changed to `/tmp/Vpol_waveforms.html`.
403 Had we chosen not to filter out the horizontally polarized channels,
404 there will be another plot: `/tmp/Hpol_waveforms.html`.
407 <iframe src="example_three_antenna_waveforms_from_pueoEvent.html" width="950" height="1000"></iframe>
413 waveforms.select(
"PhiSector",
"Ring")
414 wf_array = np.stack(waveforms[
"waveforms (volts)"].to_list())
415 time = np.zeros_like(wf_array)
416 signal_length = time.shape[1]
418 for i, dt
in enumerate(waveforms[
"step size (nanoseconds)"]):
419 time[i] = dt * np.arange(signal_length)
422 waveforms.with_columns(pl.Series(time).alias(
"time (ns)"))
423 .explode(
"waveforms (volts)",
"time (ns)")
427 plot_path = Path(plot_name)
428 destination = plot_path.parent
431 for frame
in waveforms_list:
432 pol = frame[
"Pol"][0]
435 alt.Chart(frame).mark_line()
438 y=alt.Y(
'waveforms (volts)').title(
"[Volts]"),
441 column=alt.Column(
"PhiSector:O", header=alt.Header(labelFontSize=16, titleFontSize=16)),
442 row=alt.Row(
"Ring:O", header=alt.Header(labelFontSize=16, titleFontSize=16))
444 .properties(title=f
"{pol}Pol Waveforms")
445 .configure_title(fontSize=16)
448 pn = destination / f
"{pol}pol_{plot_path.name}"
449 chart.save(f
"{str(pn)}")
452def load_waveforms_from_IceFinal(filepath: str, event_number: [int] = 0,
453 antennas: str =
None, upsample_factor: int =
None,
454 want_plots: bool =
False,
455 plot_name: str =
None) -> tuple[pl.DataFrame(), np.array(1024)]:
456 r"""! Loads time domain signals from an IceFinal file;
457 users can optionally provide a list of antenna indices.
460 @param[in] filepath path to `IceFinal_*_allTree.root`
461 @param[in] event_number (optional) an integer or a **list** of **integers**, defaults to event 0
462 @param[in] antennas (optional) **whitespace separated string** specifying the antenna names
463 @param[in] upsample_factor (optional)
464 @param[in] want_plots (optional) defaults to False
465 @param[in] plot_name (optional)
466 @retval waveforms_frame waveforms recorded by the antennas [Volts]
467 @retval time_mesh time mesh corresponding to the waveforms [seconds].
469* The function will error out if the `passTree` of the IceFinal file (see `filepath`) is empty.
470 (ie. no triggered events)
471* If no antennas are specified, all waveforms will be loaded into the DataFrame,
472 which presents them in [ascending](#antenna_attributes.ALL_PUEO_ANTENNA_NAMES) `AntNum`.
475 load_waveforms_from_IceFinal("IceFinal_420_allTree.root", antennas="403 111 201", event_number=[0, 1])
478 ┌──────────────┬────────┬─────────────┬─────────────────────┬───────────────────────┐
479 │ event number ┆ AntNum ┆ pueoSim idx ┆ VPol waveforms (V) ┆ truth (signal origin) │
480 │ --- ┆ --- ┆ --- ┆ --- ┆ --- │
481 │ i32 ┆ enum ┆ u8 ┆ array[f64, 1024] ┆ array[f64, 3] │
482 ╞══════════════╪════════╪═════════════╪═════════════════════╪═══════════════════════╡
483 │ 0 ┆ 201 ┆ 1 ┆ […] ┆ […] │
484 │ 0 ┆ 403 ┆ 11 ┆ […] ┆ […] │
485 │ 0 ┆ 111 ┆ 40 ┆ […] ┆ […] │
486 │ 1 ┆ 201 ┆ 1 ┆ […] ┆ […] │
487 │ 1 ┆ 403 ┆ 11 ┆ […] ┆ […] │
488 │ 1 ┆ 111 ┆ 40 ┆ […] ┆ […] │
489 └──────────────┴────────┴─────────────┴─────────────────────┴───────────────────────┘
491* The waveforms can be upsampled by @p upsample_factor.
492* `truth (signal origin)` is a unit vector that represents the true direction of the incoming
494 * Note that in the IceFinal files, the variable `RFdir_payload` is the direction the signal is
496 * Therefore, to obtain the direction that the signal is coming **from**,
497 we need the opposite vector: \n
498 `signal_origin` = - `RFdir_payload`
499 * This is useful for verifying the correctness of the [correlation maps](@ref example_corr_map).
501* The function also optionally creates waveform plots and saves to the working directory
502 (ie. wherever the user calls this script) via the internal function #plot_waveforms().
503 Below is an example of a three-antenna plot (actually two plots would be made, one for each event):
505 load_waveforms_from_IceFinal(
506 "IceFinal_420_allTree.root", antennas="403 111 201",
507 want_plots=True, event_number=[0, 1])
509 \anchor exampleThreeAntennaPlot
511 <iframe src="example_three_antenna_waveforms.html" width="950" height="1000"></iframe>
513 @todo **currently main instrument only**
515 @warning Will abort if non-existent antenna indices are provided.
517 load_waveforms_from_IceFinal("IceFinal_420_allTree.root", antennas="201 511")
519 In this case, Antenna `511` does not exist, since there are only 4 antennas in each `PhiSector`.\n
521 > InvalidOperationError: conversion from `str` to `enum` failed in column '' for 1 out of 2 values: ["511"]
523 > Ensure that all values in the input column are present in the categories of the enum datatype.
525 > Perhaps you included a non-existent antenna index?
526 > aborting operation at function: load_waveforms_from_IceFinal()
529 from antenna_attributes
import read_global_channel_mapping, ALL_PUEO_ANTENNA_NAMES
532 channel_mapping: pl.DataFrame = read_global_channel_mapping()
535 if isinstance(event_number, int):
536 event_number = [event_number]
539 if antennas
is not None:
542 antennas = antennas.split()
543 pl.Series(antennas, dtype=ALL_PUEO_ANTENNA_NAMES)
544 except pl.exceptions.InvalidOperationError
as exc:
545 print(
"InvalidOperationError:", exc)
546 print(
"\n\nPerhaps you included a non-existent antenna index?")
547 print(
"aborting operation at function: load_waveforms_from_IceFinal()")
550 _index_conversion = (
551 generate_dataframe_from_pueodata_qrh().filter(pl.col(
"AntNum").is_in(antennas))
554 _index_conversion: pl.DataFrame = generate_dataframe_from_pueodata_qrh()
556 ROOT.gSystem.Load(
"libNiceMC.so")
557 ROOT.gSystem.Load(
"libPueoSim.so")
559 _rootFile: ROOT.TFile = ROOT.TFile.Open(filepath)
560 _passTree: ROOT.TTree = _rootFile.Get(
'passTree')
562 waveforms: [pl.DataFrame] = []
563 for ev
in event_number:
565 if ev < 0
or ev >= _passTree.GetEntries():
566 print(f
"Invalid event index \"{ev}\" specified for {filepath}")
567 print(f
"There are only {_passTree.GetEntries()} events.")
570 _passTree.GetEvent(ev)
571 _df: pl.DataFrame = (
574 pl.lit(ev).alias(
"event number"),
575 pl.col(
"AntNum",
"pueoSim idx"),
576 pl.col(
"pueoSim idx").map_batches(
578 _passTree.detectorEvents[0].waveformsV[i].GetY()
581 ).alias(
"VPol waveforms (V)"),
582 pl.col(
"pueoSim idx").map_batches(
584 -(_passTree.detectorEvents[0].RFdir_payload)
587 ).alias(
"truth (signal origin)"),
590 waveforms.append(_df)
593 time_mesh = np.array(_passTree.detectorEvents[0].waveformsV[0].GetX())
596 waveforms = pl.concat(waveforms)
603 if upsample_factor
is not None:
604 from scipy.signal
import resample
605 upsampled_waveforms = waveforms.with_columns(
606 pl.col(
"VPol waveforms (V)").map_batches(
607 lambda s: resample(s, len(time_mesh) * upsample_factor, axis=1)
610 _, upsampled_time_mesh = resample(waveforms[
"VPol waveforms (V)"][0],
611 num=len(time_mesh) * upsample_factor, t=time_mesh)
613 return upsampled_waveforms, upsampled_time_mesh
615 return waveforms, time_mesh
620if __name__ ==
"__main__":
660 wf: pl.DataFrame = load_waveforms(run, eventNumber=eventNumber, anttype=anttype, upsample_factor=3)
664 plot_waveforms(wf, plot_name=f
"test{eventNumber}.png")