• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

XENONnT / straxen / 6353497555

29 Sep 2023 03:10PM UTC coverage: 93.724% (+0.05%) from 93.673%
6353497555

Pull #1240

github

web-flow
Merge 6854cb9dc into 9bf6192f0
Pull Request #1240: Proposal to use pre-commit for continuous integration

2930 of 2930 new or added lines in 106 files covered. (100.0%)

8751 of 9337 relevant lines covered (93.72%)

1.87 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.3
/straxen/analyses/bokeh_waveform_plot.py
1
import warnings
2✔
2

3
import bokeh
2✔
4
import bokeh.plotting as bklt
2✔
5
import numba
2✔
6
import numpy as np
2✔
7
import strax
2✔
8
import straxen
2✔
9
from straxen.analyses.holoviews_waveform_display import (
2✔
10
    _hvdisp_plot_records_2d,
11
    hook,
12
    plot_record_polygons,
13
    get_records_matrix_in_window,
14
)  # noqa
15

16
# Default legend, unknow, S1 and S2
17
LEGENDS = ("Unknown", "S1", "S2")
2✔
18
straxen._BOKEH_CONFIGURED_NOTEBOOK = False
2✔
19

20

21
@straxen.mini_analysis(
2✔
22
    requires=("event_basics", "peaks", "peak_basics", "peak_positions"), warn_beyond_sec=0.05
23
)
24
def event_display_interactive(
2✔
25
    events,
26
    peaks,
27
    to_pe,
28
    run_id,
29
    context,
30
    bottom_pmt_array=True,
31
    only_main_peaks=False,
32
    only_peak_detail_in_wf=False,
33
    plot_all_pmts=False,
34
    plot_record_matrix=False,
35
    plot_records_threshold=10,
36
    xenon1t=False,
37
    colors=("gray", "blue", "green"),
38
    yscale=("linear", "linear", "linear"),
39
    log=True,
40
):
41
    """Interactive event display for XENONnT. Plots detailed main/alt S1/S2, bottom and top PMT hit
42
    pattern as well as all other peaks in a given event.
43

44
    :param bottom_pmt_array: If true plots bottom PMT array hit-pattern.
45
    :param only_main_peaks: If true plots only main peaks into detail
46
        plots as well as PMT arrays.
47
    :param only_peak_detail_in_wf: Only plots main/alt S1/S2 into
48
        waveform. Only plot main peaks if only_main_peaks is true.
49
    :param plot_all_pmts: Bool if True, colors switched off PMTs instead
50
        of showing them in gray, useful for graphs shown in talks.
51
    :param plot_record_matrix: If true record matrix is plotted below.
52
        waveform.
53
    :param plot_records_threshold: Threshold at which zoom level to display
54
        record matrix as polygons. Larger values may lead to longer
55
        render times since more polygons are shown.
56
    :param xenon1t: Flag to use event display with 1T data.
57
    :param colors: Colors to be used for peaks. Order is as peak types,
58
        0 = Unknown, 1 = S1, 2 = S2. Can be any colors accepted by bokeh.
59
    :param yscale: Defines scale for main/alt S1 == 0, main/alt S2 == 1,
60
        waveform plot == 2. Please note, that the log scale can lead to funny
61
        glyph renders for small values.
62
    :param log: If true color sclae is used for hitpattern plots.
63

64
    example::
65

66
        from IPython.core.display import display, HTML
67
        display(HTML("<style>.container { width:80% !important; }</style>"))
68
        import bokeh.plotting as bklt
69
        fig = st.event_display_interactive(
70
                         run_id,
71
                         time_range=(event['time'],
72
                                     event['endtime'])
73
                         )
74
        bklt.show(fig)
75

76
    :raises:
77
        Raises an error if the user queries a time range which contains
78
        more than a single event.
79

80
    :return: bokeh.plotting.figure instance.
81

82
    """
83
    st = context
2✔
84

85
    if len(yscale) != 3:
2✔
86
        raise ValueError(f'"yscale" needs three entries, but you passed {len(yscale)}.')
87

88
    if not hasattr(st, "_BOKEH_CONFIGURED_NOTEBOOK"):
2✔
89
        st._BOKEH_CONFIGURED_NOTEBOOK = True
2✔
90
        # Configure show to show notebook:
91
        from bokeh.io import output_notebook
2✔
92

93
        output_notebook()
2✔
94

95
    if len(events) != 1:
2✔
96
        raise ValueError(
97
            "The time range you specified contains more or"
98
            " less than a single event. The event display "
99
            " only works with individual events for now."
100
        )
101

102
    if peaks.shape[0] == 0:
2✔
103
        raise ValueError("Found an event without peaks this should not had have happened.")
104

105
    # Select main/alt S1/S2s based on time and endtime in event:
106
    m_other_peaks = np.ones(len(peaks), dtype=np.bool_)  # To select non-event peaks
2✔
107
    endtime = strax.endtime(peaks)
2✔
108

109
    signal = {}
2✔
110
    if only_main_peaks:
2✔
111
        s1_keys = ["s1"]
2✔
112
        s2_keys = ["s2"]
2✔
113
        labels = {"s1": "S1", "s2": "S2"}
2✔
114
    else:
115
        s1_keys = ["s1", "alt_s1"]
2✔
116
        s2_keys = ["s2", "alt_s2"]
2✔
117
        labels = {"s1": "MS1", "alt_s1": "AS1", "s2": "MS2", "alt_s2": "AS2"}
2✔
118

119
    for s_x in labels.keys():
2✔
120
        # Loop over Main/Alt Sx and get store S1/S2 Main/Alt in signals,
121
        # store information about other peaks as "m_other_peaks"
122
        m = (peaks["time"] == events[f"{s_x}_time"]) & (endtime == events[f"{s_x}_endtime"])
2✔
123
        signal[s_x] = peaks[m]
2✔
124
        m_other_peaks &= ~m
2✔
125

126
    # Detail plots for main/alt S1/S2:
127
    fig_s1, fig_s2 = plot_detail_plot_s1_s2(
2✔
128
        signal,
129
        s1_keys,
130
        s2_keys,
131
        labels,
132
        colors,
133
        yscale[:2],
134
    )
135

136
    # PMT arrays:
137
    if not only_main_peaks:
2✔
138
        # Plot all keys into both arrays:
139
        top_array_keys = s2_keys + s1_keys
2✔
140
        bottom_array_keys = s1_keys + s2_keys
2✔
141
    else:
142
        top_array_keys = s2_keys
2✔
143
        bottom_array_keys = s1_keys
2✔
144

145
    fig_top, fig_bottom = plot_pmt_arrays_and_positions(
2✔
146
        top_array_keys,
147
        bottom_array_keys,
148
        signal,
149
        to_pe,
150
        labels,
151
        plot_all_pmts,
152
        xenon1t=xenon1t,
153
        log=log,
154
    )
155

156
    m_other_s2 = m_other_peaks & (peaks["type"] == 2)
2✔
157
    if np.any(m_other_s2) and not only_main_peaks:
2✔
158
        # Now we have to add the positions of all the other S2 to the top pmt array
159
        # if not only main peaks.
160
        fig_top, plot = plot_posS2s(
×
161
            peaks[m_other_s2], label="OS2s", fig=fig_top, s2_type_style_id=2
162
        )
163
        plot.visible = False
×
164

165
    # Main waveform plot:
166
    if only_peak_detail_in_wf:
2✔
167
        # If specified by the user only plot main/alt S1/S2
168
        peaks = peaks[~m_other_peaks]
×
169

170
    waveform = plot_event(peaks, signal, labels, events[0], colors, yscale[-1])
2✔
171

172
    # Create tile:
173
    title = _make_event_title(events[0], run_id)
2✔
174

175
    # Put everything together:
176
    if bottom_pmt_array:
2✔
177
        upper_row = [fig_s1, fig_s2, fig_top, fig_bottom]
2✔
178
    else:
179
        upper_row = [fig_s1, fig_s2, fig_top]
×
180

181
    upper_row = bokeh.layouts.Row(children=upper_row)
2✔
182

183
    plots = bokeh.layouts.gridplot(
2✔
184
        children=[upper_row, waveform],
185
        sizing_mode="scale_both",
186
        ncols=1,
187
        merge_tools=True,
188
        toolbar_location="above",
189
    )
190
    event_display = bokeh.layouts.Column(
2✔
191
        children=[title, plots],
192
        sizing_mode="scale_both",
193
        max_width=1600,
194
    )
195

196
    # Add record matrix if asked:
197
    if plot_record_matrix:
2✔
198
        if st.is_stored(run_id, "records"):
2✔
199
            # Check if records can be found and load:
200
            r = st.get_array(
2✔
201
                run_id, "records", time_range=(events[0]["time"], events[0]["endtime"])
202
            )
203
        elif st.is_stored(run_id, "raw_records"):
×
204
            warnings.warn(
×
205
                f"Cannot find records for {run_id}, making them from raw_records instead."
206
            )
207
            p = st.get_single_plugin(run_id, "records")
×
208
            r = st.get_array(
×
209
                run_id, "raw_records", time_range=(events[0]["time"], events[0]["endtime"])
210
            )
211
            r = p.compute(r, events[0]["time"], events[0]["endtime"])["records"]
×
212
        else:
213
            warnings.warn(
×
214
                f"Can neither find records nor raw_records for run {run_id}, proceed without record "
215
                f"matrix."
216
            )
217
            plot_record_matrix = False
×
218

219
    if plot_record_matrix:
2✔
220
        straxen._BOKEH_X_RANGE = None
2✔
221
        # First get hook to for x_range:
222
        x_range_hook = lambda plot, element: hook(plot, x_range=straxen._BOKEH_X_RANGE, debug=False)
2✔
223

224
        # Create datashader plot:
225
        wf, record_points, time_stream = _hvdisp_plot_records_2d(
2✔
226
            records=r,
227
            to_pe=to_pe,
228
            t_reference=peaks[0]["time"],
229
            event_range=(waveform.x_range.start, waveform.x_range.end),
230
            config=st.config,
231
            hooks=[x_range_hook],
232
            tools=[],
233
        )
234
        # Create record polygons:
235
        polys = plot_record_polygons(record_points)
2✔
236
        records_in_window = polys.apply(
2✔
237
            get_records_matrix_in_window, streams=[time_stream], time_slice=plot_records_threshold
238
        )
239

240
        # Render plot to initialize x_range:
241
        import holoviews as hv
2✔
242
        import panel
2✔
243

244
        _ = hv.render(wf)
2✔
245
        # Set x-range of event plot:
246
        bokeh_set_x_range(waveform, straxen._BOKEH_X_RANGE, debug=False)
2✔
247
        event_display = panel.Column(
2✔
248
            event_display, wf * records_in_window, sizing_mode="scale_width"
249
        )
250

251
    return event_display
2✔
252

253

254
def plot_detail_plot_s1_s2(signal, s1_keys, s2_keys, labels, colors, yscale=("linear", "linear")):
2✔
255
    """Function to plot the main/alt S1/S2 peak details.
256

257
    :param signal: Dictionary containing the peak information. :param s1_keys: S1 keys to be plotted
258
    e.g. with and without alt S1 :param s2_keys: Same but for S2 :param labels: Labels to be used
259
    for Peaks :param colors: Colors to be used :param yscale: Tuple with axis scale type. :return:
260
    S1 and S2 bokeh figure.
261

262
    """
263
    # First we create figure then we loop over figures and plots and
264
    # add drawings:
265
    fig_s1 = straxen.bokeh_utils.default_fig(
2✔
266
        title="Main/Alt S1",
267
        y_axis_type=yscale[0],
268
    )
269
    fig_s2 = straxen.bokeh_utils.default_fig(
2✔
270
        title="Main/Alt S2",
271
        y_axis_type=yscale[1],
272
    )
273

274
    for fig, peak_types in zip([fig_s1, fig_s2], (s1_keys, s2_keys)):
2✔
275
        # Loop over fig and corresponding peak keys
276
        for peak_type in peak_types:
2✔
277
            if "s2" in peak_type:
2✔
278
                # If S2 use µs as units
279
                time_scalar = 1000  # ns
2✔
280
                unit = "µs"
2✔
281
            else:
282
                time_scalar = 1  # ns
2✔
283
                unit = "ns"
2✔
284
            if signal[peak_type].shape[0]:
2✔
285
                # If signal exists, plot:
286
                fig, plot = plot_peak_detail(
2✔
287
                    signal[peak_type],
288
                    time_scalar=time_scalar,
289
                    label=labels[peak_type],
290
                    unit=unit,
291
                    fig=fig,
292
                    colors=colors,
293
                )
294
                if "alt" in peak_type:
2✔
295
                    # Not main S1/S2, so make peak invisible
296
                    plot.visible = False
×
297
    return fig_s1, fig_s2
2✔
298

299

300
def plot_pmt_arrays_and_positions(
2✔
301
    top_array_keys, bottom_array_keys, signal, to_pe, labels, plot_all_pmts, xenon1t=False, log=True
302
):
303
    """Function which plots the Top and Bottom PMT array.
304

305
    :returns: fig_top, fig_bottom
306

307
    """
308
    # Same logic as for detailed Peaks, first make figures
309
    # then loop over figures and data and populate figures with plots
310
    fig_top = straxen.bokeh_utils.default_fig(title="Top array")
2✔
311
    fig_bottom = straxen.bokeh_utils.default_fig(title="Bottom array")
2✔
312

313
    for pmt_array_type, fig, peak_types in zip(
2✔
314
        ["top", "bottom"], [fig_top, fig_bottom], [top_array_keys, bottom_array_keys]
315
    ):
316
        for ind, k in enumerate(peak_types):
2✔
317
            # Loop over peaks enumerate them since we plot all Peaks
318
            # Main/ALt S1/S2 into the PMT array, but only the first one
319
            # Should be visible.
320
            if not signal[k].shape[0]:
2✔
321
                # alt S1/S2 does not exist so go to next.
322
                continue
2✔
323

324
            fig, plot, _ = plot_pmt_array(
2✔
325
                signal[k][0],
326
                pmt_array_type,
327
                to_pe,
328
                plot_all_pmts=plot_all_pmts,
329
                label=labels[k],
330
                xenon1t=xenon1t,
331
                fig=fig,
332
                log=log,
333
            )
334
            if ind:
2✔
335
                # Not main S1 or S2
336
                plot.visible = False
2✔
337

338
            if pmt_array_type == "top" and "s2" in k:
2✔
339
                # In case of the top PMT array we also have to plot the S2 positions:
340
                fig, plot = plot_posS2s(
2✔
341
                    signal[k][0], label=labels[k], fig=fig, s2_type_style_id=ind
342
                )
343
                if ind:
2✔
344
                    # Not main S2
345
                    plot.visible = False
×
346

347
    return fig_top, fig_bottom
2✔
348

349

350
def plot_event(peaks, signal, labels, event, colors, yscale="linear"):
2✔
351
    """Wrapper for plot peaks to highlight main/alt. S1/S2.
352

353
    :param peaks: Peaks in event :param signal: Dictionary containing main/alt. S1/S2 :param labels:
354
    dict with labels to be used :param event: Event to set correctly x-ranges. :param colors: Colors
355
    to be used for unknown, s1 and s2 signals. :param yscale: string of yscale type. :return:
356
    bokeh.plotting.figure instance
357

358
    """
359
    waveform = plot_peaks(peaks, time_scalar=1000, colors=colors, yscale=yscale)
2✔
360
    # Highlight main and alternate S1/S2:
361
    start = peaks[0]["time"]
2✔
362
    end = strax.endtime(peaks)[-1]
2✔
363
    # Workaround did not manage to scale via pixels...
364
    ymax = np.max((peaks["data"].T / peaks["dt"]).T)
2✔
365
    ymax -= 0.1 * ymax
2✔
366
    for s, p in signal.items():
2✔
367
        if p.shape[0]:
2✔
368
            pos = (p[0]["center_time"] - start) / 1000
2✔
369
            main = bokeh.models.Span(
2✔
370
                location=pos,
371
                dimension="height",
372
                line_alpha=0.6,
373
            )
374
            vline_label = bokeh.models.Label(
2✔
375
                x=pos,
376
                y=ymax,
377
                angle=np.pi / 2,
378
                text=labels[s],
379
            )
380
            if "alt" in s:
2✔
381
                main.line_dash = "dotted"
×
382
            else:
383
                main.line_dash = "dashed"
2✔
384
            waveform.add_layout(main)
2✔
385
            waveform.add_layout(vline_label)
2✔
386

387
    # Get some meaningful x-range limit to 10% left and right extending
388
    # beyond first last peak, clip at event boundary.
389
    length = (end - start) / 10**3
2✔
390

391
    waveform.x_range.start = max(-0.1 * length, (event["time"] - start) / 10**3)
2✔
392
    waveform.x_range.end = min(1.1 * length, (event["endtime"] - start) / 10**3)
2✔
393
    return waveform
2✔
394

395

396
def plot_peak_detail(
2✔
397
    peak,
398
    time_scalar=1,
399
    label="",
400
    unit="ns",
401
    colors=("gray", "blue", "green"),
402
    fig=None,
403
):
404
    """Function which makes a detailed plot for the given peak. As in the main/alt S1/S2 plots of
405
    the event display.
406

407
    :param peak: Peak to be plotted. :param time_scalar: Factor to rescale the time from ns to other
408
    scale. E.g. =1000 scales to µs. :param label: Label to be used in the plot legend. :param unit:
409
    Time unit of the plotted peak. :param colors: Colors to be used for unknown, s1 and s2 peaks.
410
    :param fig: Instance of bokeh.plotting.figure if None one will be     created via
411
    straxen.bokeh.utils.default_figure(). :return: Instance of bokeh.plotting.figure
412

413
    """
414
    if not peak.shape:
2✔
415
        peak = np.array([peak])
×
416

417
    if peak.shape[0] != 1:
2✔
418
        raise ValueError(
419
            "Cannot plot the peak details for more than one "
420
            "peak. Please make sure peaks has the shape (1,)!"
421
        )
422

423
    p_type = peak[0]["type"]
2✔
424

425
    if not fig:
2✔
426
        fig = straxen.bokeh_utils.default_fig(title=f"Main/Alt S{p_type}")
×
427

428
    tt = straxen.bokeh_utils.peak_tool_tip(p_type)
2✔
429
    tt = [v for k, v in tt.items() if k not in ["time_static", "center_time", "endtime"]]
2✔
430
    fig.add_tools(bokeh.models.HoverTool(names=[label], tooltips=tt))
2✔
431

432
    source = straxen.bokeh_utils.get_peaks_source(
2✔
433
        peak,
434
        relative_start=peak[0]["time"],
435
        time_scaler=time_scalar,
436
        keep_amplitude_per_sample=False,
437
    )
438

439
    patches = fig.patches(
2✔
440
        source=source,
441
        legend_label=label,
442
        fill_color=colors[p_type],
443
        fill_alpha=0.2,
444
        line_color=colors[p_type],
445
        line_width=0.5,
446
        name=label,
447
    )
448
    fig.xaxis.axis_label = f"Time [{unit}]"
2✔
449
    fig.xaxis.axis_label_text_font_size = "12pt"
2✔
450
    fig.yaxis.axis_label = "Amplitude [pe/ns]"
2✔
451
    fig.yaxis.axis_label_text_font_size = "12pt"
2✔
452

453
    fig.legend.location = "top_right"
2✔
454
    fig.legend.click_policy = "hide"
2✔
455

456
    if label:
2✔
457
        fig.legend.visible = True
2✔
458
    else:
459
        fig.legend.visible = False
×
460

461
    return fig, patches
2✔
462

463

464
def plot_peaks(peaks, time_scalar=1, fig=None, colors=("gray", "blue", "green"), yscale="linear"):
2✔
465
    """Function which plots a list/array of peaks relative to the first one.
466

467
    :param peaks: Peaks to be plotted. :param time_scalar: Factor to rescale the time from ns to
468
    other     scale. E.g. =1000 scales to µs. :param colors: Colors to be used for unknown, s1 and
469
    s2 signals :param yscale: yscale type can be "linear" or "log" :param fig: Instance of
470
    bokeh.plotting.figure if None one will be     created via straxen.bokeh.utils.default_figure().
471
    :return: bokeh.plotting.figure instance.
472

473
    """
474
    if not fig:
2✔
475
        fig = straxen.bokeh_utils.default_fig(width=1600, height=400, y_axis_type=yscale)
2✔
476

477
    for i in range(0, 3):
2✔
478
        _ind = np.where(peaks["type"] == i)[0]
2✔
479
        if not len(_ind):
2✔
480
            continue
2✔
481

482
        source = straxen.bokeh_utils.get_peaks_source(
2✔
483
            peaks[_ind],
484
            relative_start=peaks[0]["time"],
485
            time_scaler=time_scalar,
486
            keep_amplitude_per_sample=False,
487
        )
488

489
        fig.patches(
2✔
490
            source=source,
491
            fill_color=colors[i],
492
            fill_alpha=0.2,
493
            line_color=colors[i],
494
            line_width=0.5,
495
            legend_label=LEGENDS[i],
496
            name=LEGENDS[i],
497
        )
498

499
        tt = straxen.bokeh_utils.peak_tool_tip(i)
2✔
500
        tt = [v for k, v in tt.items() if k != "time_dynamic"]
2✔
501
        fig.add_tools(bokeh.models.HoverTool(names=[LEGENDS[i]], tooltips=tt))
2✔
502
        fig.add_tools(bokeh.models.WheelZoomTool(dimensions="width", name="wheel"))
2✔
503
        fig.toolbar.active_scroll = [t for t in fig.tools if t.name == "wheel"][0]
2✔
504

505
    fig.xaxis.axis_label = "Time [µs]"
2✔
506
    fig.xaxis.axis_label_text_font_size = "12pt"
2✔
507
    fig.yaxis.axis_label = "Amplitude [pe/ns]"
2✔
508
    fig.yaxis.axis_label_text_font_size = "12pt"
2✔
509

510
    fig.legend.location = "top_left"
2✔
511
    fig.legend.click_policy = "hide"
2✔
512
    return fig
2✔
513

514

515
def plot_pmt_array(
2✔
516
    peak,
517
    array_type,
518
    to_pe,
519
    plot_all_pmts=False,
520
    log=False,
521
    xenon1t=False,
522
    fig=None,
523
    label="",
524
):
525
    """Plots top or bottom PMT array for given peak.
526

527
    :param peak: Peak for which the hit pattern should be plotted. :param array_type: String which
528
    specifies if "top" or "bottom" PMT     array should be plotted :param to_pe: PMT gains. :param
529
    log: If true use a log-scale for the color scale. :param plot_all_pmts: If True colors all PMTs
530
    instead of showing     swtiched off PMTs as gray dots. :param xenon1t: If True plots 1T array.
531
    :param fig: Instance of bokeh.plotting.figure if None one will be     created via
532
    straxen.bokeh.utils.default_figure(). :param label: Label of the peak which should be used for
533
    the plot     legend :returns: Tuple containing a bokeh figure, glyph and transform     instance.
534

535
    """
536
    if peak.shape:
2✔
537
        raise ValueError("Can plot PMT array only for a single peak at a time.")
538

539
    tool_tip = [
2✔
540
        ("Plot", "$name"),
541
        ("Channel", "@pmt"),
542
        ("X-Position [cm]", "$x"),
543
        ("Y-Position [cm]", "$y"),
544
        ("area [pe]", "@area"),
545
    ]
546

547
    array = ("top", "bottom")
2✔
548
    if array_type not in array:
2✔
549
        raise ValueError('"array_type" must be either top or bottom.')
550

551
    if not fig:
2✔
552
        fig = straxen.bokeh_utils.default_fig(title=f"{array_type} array")
×
553

554
    # Creating TPC axis and title
555
    fig = _plot_tpc(fig)
2✔
556

557
    # Plotting PMTs:
558
    pmts = straxen.pmt_positions(xenon1t)
2✔
559
    if plot_all_pmts:
2✔
560
        mask_pmts = np.zeros(len(pmts), dtype=np.bool_)
×
561
    else:
562
        mask_pmts = to_pe == 0
2✔
563
    pmts_on = pmts[~mask_pmts]
2✔
564
    pmts_on = pmts_on[pmts_on["array"] == array_type]
2✔
565

566
    if np.any(mask_pmts):
2✔
567
        pmts_off = pmts[mask_pmts]
2✔
568
        pmts_off = pmts_off[pmts_off["array"] == array_type]
2✔
569
        fig = _plot_off_pmts(pmts_off, fig)
2✔
570

571
    area_per_channel = peak["area_per_channel"][pmts_on["i"]]
2✔
572

573
    if log:
2✔
574
        area_plot = np.log10(area_per_channel)
2✔
575
        # Manually set infs to zero since cmap cannot handle it.
576
        area_plot = np.where(area_plot == -np.inf, 0, area_plot)
2✔
577
    else:
578
        area_plot = area_per_channel
×
579

580
    mapper = bokeh.transform.linear_cmap(
2✔
581
        field_name="area_plot", palette="Viridis256", low=min(area_plot), high=max(area_plot)
582
    )
583

584
    source_on = bklt.ColumnDataSource(
2✔
585
        data={
586
            "x": pmts_on["x"],
587
            "y": pmts_on["y"],
588
            "area": area_per_channel,
589
            "area_plot": area_plot,
590
            "pmt": pmts_on["i"],
591
        }
592
    )
593

594
    p = fig.scatter(
2✔
595
        source=source_on,
596
        radius=straxen.tpc_pmt_radius,
597
        fill_color=mapper,
598
        fill_alpha=1,
599
        line_color="black",
600
        legend_label=label,
601
        name=label + "_pmt_array",
602
    )
603
    fig.add_tools(bokeh.models.HoverTool(names=[label + "_pmt_array"], tooltips=tool_tip))
2✔
604
    fig.legend.location = "top_left"
2✔
605
    fig.legend.click_policy = "hide"
2✔
606
    fig.legend.orientation = "horizontal"
2✔
607
    fig.legend.padding = 0
2✔
608
    fig.toolbar_location = None
2✔
609
    return fig, p, mapper
2✔
610

611

612
def _plot_tpc(fig=None):
2✔
613
    """Plots ring at TPC radius and sets xy limits + labels."""
614
    if not fig:
2✔
615
        fig = straxen.bokeh_utils.default_fig()
×
616

617
    fig.circle(
2✔
618
        x=0,
619
        y=0,
620
        radius=straxen.tpc_r,
621
        fill_color="white",
622
        line_color="black",
623
        line_width=3,
624
        fill_alpha=0,
625
    )
626
    fig.xaxis.axis_label = "x [cm]"
2✔
627
    fig.xaxis.axis_label_text_font_size = "12pt"
2✔
628
    fig.yaxis.axis_label = "y [cm]"
2✔
629
    fig.yaxis.axis_label_text_font_size = "12pt"
2✔
630
    fig.x_range.start = -80
2✔
631
    fig.x_range.end = 80
2✔
632
    fig.y_range.start = -80
2✔
633
    fig.y_range.end = 80
2✔
634

635
    return fig
2✔
636

637

638
def _plot_off_pmts(pmts, fig=None):
2✔
639
    """Plots PMTs which are switched off."""
640
    if not fig:
2✔
641
        fig = straxen.bokeh_utils.default_fig()
×
642
    fig.circle(
2✔
643
        x=pmts["x"],
644
        y=pmts["y"],
645
        fill_color="gray",
646
        line_color="black",
647
        radius=straxen.tpc_pmt_radius,
648
    )
649
    return fig
2✔
650

651

652
def plot_posS2s(peaks, label="", fig=None, s2_type_style_id=0):
2✔
653
    """Plots xy-positions of specified peaks.
654

655
    :param peaks: Peaks for which the position should be plotted. :param label: Legend label and
656
    plot name (name serves as     idenitfier). :param fig: bokeh.plotting.figure instance the plot
657
    should be     plotted into. If None creates new instance. :param s2_type_style_id: 0 plots main
658
    S2 style, 1 for alt S2 and 2     for other S2s (e.g. single electrons).
659

660
    """
661
    if not peaks.shape:
2✔
662
        peaks = np.array([peaks])
2✔
663

664
    if not np.all(peaks["type"] == 2):
2✔
665
        raise ValueError("All peaks must be S2!")
666

667
    if not fig:
2✔
668
        fig = straxen.bokeh_utils.default_fig()
×
669

670
    source = straxen.bokeh_utils.get_peaks_source(peaks)
2✔
671

672
    if s2_type_style_id == 0:
2✔
673
        p = fig.cross(
2✔
674
            source=source, name=label, legend_label=label, color="red", line_width=2, size=12
675
        )
676

677
    if s2_type_style_id == 1:
2✔
678
        p = fig.cross(
×
679
            source=source,
680
            name=label,
681
            legend_label=label,
682
            color="orange",
683
            angle=45 / 360 * 2 * np.pi,
684
            line_width=2,
685
            size=12,
686
        )
687

688
    if s2_type_style_id == 2:
2✔
689
        p = fig.diamond_cross(source=source, name=label, legend_label=label, color="red", size=8)
×
690

691
    tt = straxen.bokeh_utils.peak_tool_tip(2)
2✔
692
    tt = [v for k, v in tt.items() if k not in ["time_dynamic", "amplitude"]]
2✔
693
    fig.add_tools(
2✔
694
        bokeh.models.HoverTool(
695
            names=[label], tooltips=[("Position x [cm]", "@x"), ("Position y [cm]", "@y")] + tt
696
        )
697
    )
698
    return fig, p
2✔
699

700

701
def _make_event_title(event, run_id, width=1600):
2✔
702
    """Function which makes the title of the plot for the specified event.
703

704
    Note:
705
        To center the title I use a transparent box.
706

707
    :param event: Event which we are plotting
708
    :param run_id: run_id
709

710
    :returns: Title as bokeh.models.Div instance
711

712
    """
713
    start = event["time"]
2✔
714
    date = np.datetime_as_string(start.astype("<M8[ns]"), unit="s")
2✔
715
    start_ns = start - (start // 10**9) * 10**9
2✔
716
    end = strax.endtime(event)
2✔
717
    end_ns = end - start + start_ns
2✔
718
    event_number = event["event_number"]
2✔
719
    text = (
2✔
720
        f"<h2>Event {event_number} from run {run_id}<br>"
721
        f"Recorded at {date[:10]} {date[10:]} UTC,"
722
        f" {start_ns} ns - {end_ns} ns </h2>"
723
    )
724

725
    title = bokeh.models.Div(
2✔
726
        text=text,
727
        style={
728
            "text-align": "left",
729
        },
730
        sizing_mode="scale_both",
731
        width=width,
732
        default_size=width,
733
        # orientation='vertical',
734
        width_policy="fit",
735
        margin=(0, 0, -30, 50),
736
    )
737
    return title
2✔
738

739

740
def bokeh_set_x_range(plot, x_range, debug=False):
2✔
741
    """Function which adjust java script call back for x_range of a bokeh plot. Required to link
742
    bokeh and holoviews x_range.
743

744
    Note:
745
        This is somewhat voodoo + some black magic,
746
        but it works....
747

748
    """
749
    from bokeh.models import CustomJS
2✔
750

751
    code = """\
2✔
752
    const start = cb_obj.start;
753
    const end = cb_obj.end;
754
    // Need to update the attributes at the same time.
755
    x_range.setv({start, end});
756
    """
757
    for attr in ["start", "end"]:
2✔
758
        if debug:
2✔
759
            # Prints x_range bar to check Id, as I said voodoo
760
            print(x_range)
×
761
        plot.x_range.js_on_change(attr, CustomJS(args=dict(x_range=x_range), code=code))
2✔
762

763

764
class DataSelectionHist:
2✔
765
    """Class for an interactive data selection plot."""
766

767
    def __init__(self, name, size=600):
2✔
768
        """Class for an interactive data selection plot.
769

770
        :param name: Name of the class object instance. Needed for     dynamic return, e.g. ds =
771
        DataSelectionHist("ds") :param size: Edge size of the figure in pixel.
772

773
        """
774
        self.name = name
2✔
775
        self.selection_index = None
2✔
776
        self.size = size
2✔
777

778
        from bokeh.io import output_notebook
2✔
779

780
        output_notebook()
2✔
781

782
    def histogram2d(
2✔
783
        self,
784
        items,
785
        xdata,
786
        ydata,
787
        bins,
788
        hist_range,
789
        x_label="X-Data",
790
        y_label="Y-Data",
791
        log_color_scale=True,
792
        cmap_steps=256,
793
        clim=(None, None),
794
        undeflow_color=None,
795
        overflow_color=None,
796
        weights=1,
797
    ):
798
        """2d Histogram which allows to select the plotted items dynamically.
799

800
        Note:
801
            You can select the data either via a box select or Lasso
802
            select tool. The data can be returned by:
803

804
            ds.get_back_selected_items()
805

806
            Hold shift to select multiple regions.
807

808
        Warnings:
809
            Depending on the number of bins the Lasso selection can
810
            become relatively slow. The number of bins should not be
811
            larger than 100.
812
            The box selection performance is better.
813

814
        :param items: numpy.structured.array of items to be selected.
815
            e.g. peaks or events.
816
        :param xdata: numpy.array for xdata e.g. peaks['area']
817
        :param ydata: same
818
        :param bins: Integer specifying the number of bins. Currently
819
            x and y axis must share the same binning.
820
        :param hist_range: Tuple of x-range and y-range.
821
        :param x_label: Label to be used for the x-axis
822
        :param y_label: same but for y
823
        :param log_color_scale: If true (default) use log colorscale
824
        :param cmap_steps: Integer between 0 and 256 for stepped
825
            colorbar.
826
        :param clim: Tuple of color limits.
827
        :param undeflow_color: If specified colors all bins below clim
828
            with the corresponding color.
829
        :param overflow_color: Same but per limit.
830
        :param weights: If specified each bin entry is weighted by this
831
            value. Can be either a scalar e.g. a time or an array of
832
            weights which has the same length as the x/y data.
833
        :return: bokeh figure instance.
834

835
        """
836
        if isinstance(bins, tuple):
2✔
837
            raise ValueError(
838
                "Currently only squared bins are supported. Plase change bins into an integer."
839
            )
840

841
        x_pos, y_pos = self._make_bin_positions((bins, bins), hist_range)
2✔
842
        weights = np.ones(len(xdata)) * weights
2✔
843

844
        hist, hist_inds = self._hist2d_with_index(xdata, ydata, weights, self.xedges, self.yedges)
2✔
845

846
        # Define times and ids for return:
847
        self.items = items
2✔
848
        self.hist_inds = hist_inds
2✔
849

850
        colors = self._get_color(
2✔
851
            hist,
852
            cmap_steps,
853
            log_color_scale=log_color_scale,
854
            clim=clim,
855
            undeflow_color=undeflow_color,
856
            overflow_color=overflow_color,
857
        )
858

859
        # Create Figure and add LassoTool:
860
        f = bokeh.plotting.figure(
2✔
861
            title="DataSelection", width=self.size, height=self.size, tools="box_select,reset,save"
862
        )
863

864
        # Add hover tool, colorbar is too complictaed:
865
        tool_tip = [("Bin Center x", "@x"), ("Bin Center y", "@y"), ("Entries", "@h")]
2✔
866
        f.add_tools(
2✔
867
            bokeh.models.LassoSelectTool(select_every_mousemove=False),
868
            bokeh.models.HoverTool(tooltips=tool_tip),
869
        )
870

871
        s1 = bokeh.plotting.ColumnDataSource(
2✔
872
            data=dict(x=x_pos, y=y_pos, h=hist.flatten(), color=colors)
873
        )
874
        f.square(source=s1, size=self.size / bins, color="color", nonselection_alpha=0.3)
2✔
875

876
        f.x_range.start = self.xedges[0]
2✔
877
        f.x_range.end = self.xedges[-1]
2✔
878
        f.y_range.start = self.yedges[0]
2✔
879
        f.y_range.end = self.yedges[-1]
2✔
880
        f.xaxis.axis_label = x_label
2✔
881
        f.yaxis.axis_label = y_label
2✔
882

883
        self.selection_index = None
2✔
884
        s1.selected.js_on_change(
2✔
885
            "indices",
886
            bokeh.models.CustomJS(
887
                args=dict(s1=s1),
888
                code=f"""
889
                var inds = cb_obj.indices;
890
                var kernel = IPython.notebook.kernel;
891
                kernel.execute("{self.name}.selection_index = " + inds);
892
                """,
893
            ),
894
        )
895
        return f
2✔
896

897
    def get_back_selected_items(self):
2✔
898
        if not self.selection_index:
×
899
            raise ValueError(
900
                "No data selection found. Have you selected any data? "
901
                "If yes you most likely have not intialized the DataSelctor correctly. "
902
                'You have to callit as: my_instance_name = DataSelectionHist("my_instance_name")'
903
            )
904
        m = np.isin(self.hist_inds, self.selection_index)
×
905
        return self.items[m]
×
906

907
    @staticmethod
2✔
908
    @numba.njit
2✔
909
    def _hist2d_with_index(xdata, ydata, weights, x_edges, y_edges):
2✔
910
        n_x_bins = len(x_edges) - 1
2✔
911
        n_y_bins = len(y_edges) - 1
2✔
912
        res_hist_inds = np.zeros(len(xdata), dtype=np.int32)
2✔
913
        res_hist = np.zeros((n_x_bins, n_y_bins), dtype=np.int64)
2✔
914

915
        # Create bin ranges:
916
        offset = 0
2✔
917
        for ind, xv in enumerate(xdata):
2✔
918
            yv = ydata[ind]
2✔
919
            w = weights[ind]
2✔
920
            hist_ind = 0
2✔
921
            found = False
2✔
922
            for ind_xb, low_xb in enumerate(x_edges[:-1]):
2✔
923
                high_xb = x_edges[ind_xb + 1]
2✔
924

925
                if not low_xb <= xv:
2✔
926
                    hist_ind += n_y_bins
2✔
927
                    continue
2✔
928
                if not xv < high_xb:
2✔
929
                    hist_ind += n_y_bins
2✔
930
                    continue
2✔
931

932
                # Checked both bins value is in bin, so check y:
933
                for ind_yb, low_yb in enumerate(y_edges[:-1]):
2✔
934
                    high_yb = y_edges[ind_yb + 1]
2✔
935

936
                    if not low_yb <= yv:
2✔
937
                        hist_ind += 1
2✔
938
                        continue
2✔
939
                    if not yv < high_yb:
2✔
940
                        hist_ind += 1
×
941
                        continue
×
942

943
                    found = True
2✔
944
                    res_hist_inds[offset] = hist_ind
2✔
945
                    res_hist[ind_xb, ind_yb] += w
2✔
946
                    offset += 1
2✔
947

948
            # Set to -1 if not in any
949
            if not found:
2✔
950
                res_hist_inds[offset] = -1
2✔
951
                offset += 1
2✔
952
        return res_hist, res_hist_inds
2✔
953

954
    def _make_bin_positions(self, bins, bin_range):
2✔
955
        """Helper function to create center positions for "histogram" markers."""
956
        edges = []
2✔
957
        for b, br in zip(bins, bin_range):
2✔
958
            # Create x and y edges
959
            d_range = br[1] - br[0]
2✔
960
            edges.append(np.arange(br[0], br[1] + d_range / b, d_range / b))
2✔
961

962
        # Convert into marker positions:
963
        xedges = edges[0]
2✔
964
        yedges = edges[1]
2✔
965
        self.xedges = xedges
2✔
966
        self.yedges = yedges
2✔
967
        x_pos = xedges[:-1] + np.diff(xedges) / 2
2✔
968
        x_pos = np.repeat(x_pos, len(yedges) - 1)
2✔
969

970
        y_pos = yedges[:-1] + np.diff(yedges) / 2
2✔
971
        y_pos = np.array(list(y_pos) * (len(xedges) - 1))
2✔
972
        return x_pos, y_pos
2✔
973

974
    def _get_color(
2✔
975
        self,
976
        hist,
977
        cmap_steps,
978
        log_color_scale=False,
979
        clim=(None, None),
980
        undeflow_color=None,
981
        overflow_color=None,
982
    ):
983
        """Helper function to create colorscale."""
984
        hist = hist.flatten()
2✔
985

986
        if clim[0] and undeflow_color:
2✔
987
            # If underflow is specified get indicies for underflow bins
988
            inds_underflow = np.argwhere(hist < clim[0]).flatten()
2✔
989

990
        if clim[1] and overflow_color:
2✔
991
            inds_overflow = np.argwhere(hist > clim[1]).flatten()
×
992

993
        # Clip data according to clim
994
        if np.any(clim):
2✔
995
            hist = np.clip(hist, clim[0], clim[1])
2✔
996

997
        self.clim = (np.min(hist), np.max(hist))
2✔
998
        if log_color_scale:
2✔
999
            color = np.log10(hist)
2✔
1000
            color /= np.max(color)
2✔
1001
            color *= cmap_steps - 1
2✔
1002
        else:
1003
            color = hist / np.max(hist)
×
1004
            color *= cmap_steps - 1
×
1005

1006
        cmap = np.array(bokeh.palettes.viridis(cmap_steps))
2✔
1007
        cmap = cmap[np.round(color).astype(np.int8)]
2✔
1008

1009
        if undeflow_color:
2✔
1010
            cmap[inds_underflow] = undeflow_color
2✔
1011

1012
        if overflow_color:
2✔
1013
            cmap[inds_overflow] = overflow_color
×
1014
        return cmap
2✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc