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

XENONnT / straxen / 11293456467

11 Oct 2024 01:41PM UTC coverage: 89.774% (-0.06%) from 89.83%
11293456467

push

github

web-flow
Fixed default window position (#1429)

* fixed default window position

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fixed empty input handling

* fixed empty record handling II

* changed area averaging

* fixed default window clipping

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fixed minimum_led_position help and docstring

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fixed whitespace at the end of help strings

* fixed area averaging help strings

* Added baseline to datatype

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Giovanni Volta <38431109+GiovanniVolta@users.noreply.github.com>
Co-authored-by: Dacheng Xu <dx2227@columbia.edu>
Co-authored-by: Yue Ma <3124558229@qq.com>

15 of 30 new or added lines in 1 file covered. (50.0%)

4 existing lines in 2 files now uncovered.

9156 of 10199 relevant lines covered (89.77%)

1.8 hits per line

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

66.85
/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(
2✔
182
        children=upper_row,
183
        sizing_mode="scale_width",
184
    )
185

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

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

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

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

243
        # Render plot to initialize x_range:
244
        import holoviews as hv
2✔
245
        import panel
2✔
246

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

256
    return event_display
2✔
257

258

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

262
    :param signal: Dictionary containing the peak information.
263
    :param s1_keys: S1 keys to be plotted e.g. with and without alt S1
264
    :param s2_keys: Same but for S2
265
    :param labels: Labels to be used for Peaks
266
    :param colors: Colors to be used
267
    :param yscale: Tuple with axis scale type.
268
    :return: S1 and S2 bokeh figure.
269

270
    """
271
    # First we create figure then we loop over figures and plots and
272
    # add drawings:
273
    fig_s1 = straxen.bokeh_utils.default_fig(
2✔
274
        title="Main/Alt S1",
275
        y_axis_type=yscale[0],
276
    )
277
    fig_s2 = straxen.bokeh_utils.default_fig(
2✔
278
        title="Main/Alt S2",
279
        y_axis_type=yscale[1],
280
    )
281

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

307

308
def plot_pmt_arrays_and_positions(
2✔
309
    top_array_keys, bottom_array_keys, signal, to_pe, labels, plot_all_pmts, xenon1t=False, log=True
310
):
311
    """Function which plots the Top and Bottom PMT array.
312

313
    :return: fig_top, fig_bottom
314

315
    """
316
    # Same logic as for detailed Peaks, first make figures
317
    # then loop over figures and data and populate figures with plots
318
    fig_top = straxen.bokeh_utils.default_fig(title="Top array")
2✔
319
    fig_bottom = straxen.bokeh_utils.default_fig(title="Bottom array")
2✔
320

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

332
            fig, plot, _ = plot_pmt_array(
2✔
333
                signal[k][0],
334
                pmt_array_type,
335
                to_pe,
336
                plot_all_pmts=plot_all_pmts,
337
                label=labels[k],
338
                xenon1t=xenon1t,
339
                fig=fig,
340
                log=log,
341
            )
342
            if ind:
2✔
343
                # Not main S1 or S2
344
                plot.visible = False
2✔
345

346
            if pmt_array_type == "top" and "s2" in k:
2✔
347
                # In case of the top PMT array we also have to plot the S2 positions:
348
                fig, plot = plot_posS2s(
2✔
349
                    signal[k][0], label=labels[k], fig=fig, s2_type_style_id=ind
350
                )
351
                if ind:
2✔
352
                    # Not main S2
353
                    plot.visible = False
×
354

355
    return fig_top, fig_bottom
2✔
356

357

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

361
    :param peaks: Peaks in event
362
    :param signal: Dictionary containing main/alt. S1/S2
363
    :param labels: dict with labels to be used
364
    :param event: Event to set correctly x-ranges.
365
    :param colors: Colors to be used for unknown, s1 and s2 signals.
366
    :param yscale: string of yscale type.
367
    :return: bokeh.plotting.figure instance
368

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

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

402
    waveform.x_range.start = max(-0.1 * length, (event["time"] - start) / 10**3)
2✔
403
    waveform.x_range.end = min(1.1 * length, (event["endtime"] - start) / 10**3)
2✔
404
    return waveform
2✔
405

406

407
def plot_peak_detail(
2✔
408
    peak,
409
    time_scalar=1,
410
    label="",
411
    unit="ns",
412
    colors=("gray", "blue", "green"),
413
    fig=None,
414
):
415
    """Function which makes a detailed plot for the given peak. As in the main/alt S1/S2 plots of
416
    the event display.
417

418
    :param peak: Peak to be plotted.
419
    :param time_scalar: Factor to rescale the time from ns to other scale. E.g. =1000 scales to µs.
420
    :param label: Label to be used in the plot legend.
421
    :param unit: Time unit of the plotted peak.
422
    :param colors: Colors to be used for unknown, s1 and s2 peaks.
423
    :param fig: Instance of bokeh.plotting.figure if None one will be created via
424
        straxen.bokeh.utils.default_figure().
425
    :return: Instance of bokeh.plotting.figure
426

427
    """
428
    if not peak.shape:
2✔
429
        peak = np.array([peak])
×
430

431
    if peak.shape[0] != 1:
2✔
432
        raise ValueError(
433
            "Cannot plot the peak details for more than one "
434
            "peak. Please make sure peaks has the shape (1,)!"
435
        )
436

437
    p_type = peak[0]["type"]
2✔
438

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

442
    tt = straxen.bokeh_utils.peak_tool_tip(p_type)
2✔
443
    tt = [v for k, v in tt.items() if k not in ["time_static", "center_time", "endtime"]]
2✔
444
    fig.add_tools(bokeh.models.HoverTool(name=label, tooltips=tt))
2✔
445

446
    source = straxen.bokeh_utils.get_peaks_source(
2✔
447
        peak,
448
        relative_start=peak[0]["time"],
449
        time_scaler=time_scalar,
450
        keep_amplitude_per_sample=False,
451
    )
452

453
    patches = fig.patches(
2✔
454
        source=source,
455
        legend_label=label,
456
        fill_color=colors[p_type],
457
        fill_alpha=0.2,
458
        line_color=colors[p_type],
459
        line_width=0.5,
460
        name=label,
461
    )
462
    fig.xaxis.axis_label = f"Time [{unit}]"
2✔
463
    fig.xaxis.axis_label_text_font_size = "12pt"
2✔
464
    fig.yaxis.axis_label = "Amplitude [pe/ns]"
2✔
465
    fig.yaxis.axis_label_text_font_size = "12pt"
2✔
466

467
    fig.legend.location = "top_right"
2✔
468
    fig.legend.click_policy = "hide"
2✔
469

470
    if label:
2✔
471
        fig.legend.visible = True
2✔
472
    else:
473
        fig.legend.visible = False
×
474

475
    return fig, patches
2✔
476

477

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

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

489
    """
490
    if not fig:
2✔
491
        fig = straxen.bokeh_utils.default_fig(width=1600, height=400, y_axis_type=yscale)
2✔
492

493
    for i in range(0, 3):
2✔
494
        _ind = np.where(peaks["type"] == i)[0]
2✔
495
        if not len(_ind):
2✔
496
            continue
2✔
497

498
        source = straxen.bokeh_utils.get_peaks_source(
2✔
499
            peaks[_ind],
500
            relative_start=peaks[0]["time"],
501
            time_scaler=time_scalar,
502
            keep_amplitude_per_sample=False,
503
        )
504

505
        fig.patches(
2✔
506
            source=source,
507
            fill_color=colors[i],
508
            fill_alpha=0.2,
509
            line_color=colors[i],
510
            line_width=0.5,
511
            legend_label=LEGENDS[i],
512
            name=LEGENDS[i],
513
        )
514

515
        tt = straxen.bokeh_utils.peak_tool_tip(i)
2✔
516
        tt = [v for k, v in tt.items() if k != "time_dynamic"]
2✔
517
        fig.add_tools(bokeh.models.HoverTool(name=LEGENDS[i], tooltips=tt))
2✔
518
        fig.add_tools(bokeh.models.WheelZoomTool(dimensions="width", name="wheel"))
2✔
519
        fig.toolbar.active_scroll = [t for t in fig.tools if t.name == "wheel"][0]
2✔
520

521
    fig.xaxis.axis_label = "Time [µs]"
2✔
522
    fig.xaxis.axis_label_text_font_size = "12pt"
2✔
523
    fig.yaxis.axis_label = "Amplitude [pe/ns]"
2✔
524
    fig.yaxis.axis_label_text_font_size = "12pt"
2✔
525

526
    fig.legend.location = "top_left"
2✔
527
    fig.legend.click_policy = "hide"
2✔
528
    return fig
2✔
529

530

531
def plot_pmt_array(
2✔
532
    peak,
533
    array_type,
534
    to_pe,
535
    plot_all_pmts=False,
536
    log=False,
537
    xenon1t=False,
538
    fig=None,
539
    label="",
540
):
541
    """Plots top or bottom PMT array for given peak.
542

543
    :param peak: Peak for which the hit pattern should be plotted.
544
    :param array_type: String which specifies if "top" or "bottom" PMT array should be plotted
545
    :param to_pe: PMT gains.
546
    :param log: If true use a log-scale for the color scale.
547
    :param plot_all_pmts: If True colors all PMTs instead of showing swtiched off PMTs as gray dots.
548
    :param xenon1t: If True plots 1T array.
549
    :param fig: Instance of bokeh.plotting.figure if None one will be created via
550
        straxen.bokeh.utils.default_figure().
551
    :param label: Label of the peak which should be used for the plot legend
552
    :return: Tuple containing a bokeh figure, glyph and transform instance.
553

554
    """
555
    if peak.shape:
2✔
556
        raise ValueError("Can plot PMT array only for a single peak at a time.")
557

558
    tool_tip = [
2✔
559
        ("Plot", "$name"),
560
        ("Channel", "@pmt"),
561
        ("X-Position [cm]", "$x"),
562
        ("Y-Position [cm]", "$y"),
563
        ("area [pe]", "@area"),
564
    ]
565

566
    array = ("top", "bottom")
2✔
567
    if array_type not in array:
2✔
568
        raise ValueError('"array_type" must be either top or bottom.')
569

570
    if not fig:
2✔
571
        fig = straxen.bokeh_utils.default_fig(title=f"{array_type} array")
×
572

573
    # Creating TPC axis and title
574
    fig = _plot_tpc(fig)
2✔
575

576
    # Plotting PMTs:
577
    pmts = straxen.pmt_positions(xenon1t)
2✔
578
    if plot_all_pmts:
2✔
579
        mask_pmts = np.zeros(len(pmts), dtype=np.bool_)
×
580
    else:
581
        mask_pmts = to_pe == 0
2✔
582
    pmts_on = pmts[~mask_pmts]
2✔
583
    pmts_on = pmts_on[pmts_on["array"] == array_type]
2✔
584

585
    if np.any(mask_pmts):
2✔
586
        pmts_off = pmts[mask_pmts]
2✔
587
        pmts_off = pmts_off[pmts_off["array"] == array_type]
2✔
588
        fig = _plot_off_pmts(pmts_off, fig)
2✔
589

590
    area_per_channel = peak["area_per_channel"][pmts_on["i"]]
2✔
591

592
    if log:
2✔
593
        area_plot = np.log10(area_per_channel)
2✔
594
        # Manually set infs to zero since cmap cannot handle it.
595
        area_plot = np.where(area_plot == -np.inf, 0, area_plot)
2✔
596
    else:
597
        area_plot = area_per_channel
×
598

599
    mapper = bokeh.transform.linear_cmap(
2✔
600
        field_name="area_plot", palette="Viridis256", low=min(area_plot), high=max(area_plot)
601
    )
602

603
    source_on = bklt.ColumnDataSource(
2✔
604
        data={
605
            "x": pmts_on["x"],
606
            "y": pmts_on["y"],
607
            "area": area_per_channel,
608
            "area_plot": area_plot,
609
            "pmt": pmts_on["i"],
610
        }
611
    )
612

613
    p = fig.scatter(
2✔
614
        source=source_on,
615
        radius=straxen.tpc_pmt_radius,
616
        fill_color=mapper,
617
        fill_alpha=1,
618
        line_color="black",
619
        legend_label=label,
620
        name=label + "_pmt_array",
621
    )
622
    fig.add_tools(bokeh.models.HoverTool(name=label + "_pmt_array", tooltips=tool_tip))
2✔
623
    fig.legend.location = "top_left"
2✔
624
    fig.legend.click_policy = "hide"
2✔
625
    fig.legend.orientation = "horizontal"
2✔
626
    fig.legend.padding = 0
2✔
627
    fig.toolbar_location = None
2✔
628
    return fig, p, mapper
2✔
629

630

631
def _plot_tpc(fig=None):
2✔
632
    """Plots ring at TPC radius and sets xy limits + labels."""
633
    if not fig:
2✔
634
        fig = straxen.bokeh_utils.default_fig()
×
635

636
    fig.circle(
2✔
637
        x=0,
638
        y=0,
639
        radius=straxen.tpc_r,
640
        fill_color="white",
641
        line_color="black",
642
        line_width=3,
643
        fill_alpha=0,
644
    )
645
    fig.xaxis.axis_label = "x [cm]"
2✔
646
    fig.xaxis.axis_label_text_font_size = "12pt"
2✔
647
    fig.yaxis.axis_label = "y [cm]"
2✔
648
    fig.yaxis.axis_label_text_font_size = "12pt"
2✔
649
    fig.x_range.start = -80
2✔
650
    fig.x_range.end = 80
2✔
651
    fig.y_range.start = -80
2✔
652
    fig.y_range.end = 80
2✔
653

654
    return fig
2✔
655

656

657
def _plot_off_pmts(pmts, fig=None):
2✔
658
    """Plots PMTs which are switched off."""
659
    if not fig:
2✔
660
        fig = straxen.bokeh_utils.default_fig()
×
661
    fig.circle(
2✔
662
        x=pmts["x"],
663
        y=pmts["y"],
664
        fill_color="gray",
665
        line_color="black",
666
        radius=straxen.tpc_pmt_radius,
667
    )
668
    return fig
2✔
669

670

671
def plot_posS2s(peaks, label="", fig=None, s2_type_style_id=0):
2✔
672
    """Plots xy-positions of specified peaks.
673

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

681
    """
682
    if not peaks.shape:
2✔
683
        peaks = np.array([peaks])
2✔
684

685
    if not np.all(peaks["type"] == 2):
2✔
686
        raise ValueError("All peaks must be S2!")
687

688
    if not fig:
2✔
689
        fig = straxen.bokeh_utils.default_fig()
×
690

691
    source = straxen.bokeh_utils.get_peaks_source(peaks)
2✔
692

693
    if s2_type_style_id == 0:
2✔
694
        p = fig.cross(
2✔
695
            source=source, name=label, legend_label=label, color="red", line_width=2, size=12
696
        )
697

698
    if s2_type_style_id == 1:
2✔
699
        p = fig.cross(
×
700
            source=source,
701
            name=label,
702
            legend_label=label,
703
            color="orange",
704
            angle=45 / 360 * 2 * np.pi,
705
            line_width=2,
706
            size=12,
707
        )
708

709
    if s2_type_style_id == 2:
2✔
710
        p = fig.diamond_cross(source=source, name=label, legend_label=label, color="red", size=8)
×
711

712
    tt = straxen.bokeh_utils.peak_tool_tip(2)
2✔
713
    tt = [v for k, v in tt.items() if k not in ["time_dynamic", "amplitude"]]
2✔
714
    fig.add_tools(
2✔
715
        bokeh.models.HoverTool(
716
            name=label, tooltips=[("Position x [cm]", "@x"), ("Position y [cm]", "@y")] + tt
717
        )
718
    )
719
    return fig, p
2✔
720

721

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

725
    Note:
726
        To center the title I use a transparent box.
727

728
    :param event: Event which we are plotting
729
    :param run_id: run_id
730

731
    :return: Title as bokeh.models.Div instance
732

733
    """
734
    start = event["time"]
2✔
735
    date = np.datetime_as_string(start.astype("<M8[ns]"), unit="s")
2✔
736
    start_ns = start - (start // 10**9) * 10**9
2✔
737
    end = strax.endtime(event)
2✔
738
    end_ns = end - start + start_ns
2✔
739
    event_number = event["event_number"]
2✔
740
    text = (
2✔
741
        f"<h2>Event {event_number} from run {run_id}<br>"
742
        f"Recorded at {date[:10]} {date[10:]} UTC,"
743
        f" {start_ns} ns - {end_ns} ns </h2>"
744
    )
745

746
    title = bokeh.models.Div(
2✔
747
        text=text,
748
        styles={
749
            "text-align": "left",
750
        },
751
        sizing_mode="scale_width",
752
        width=width,
753
        # orientation='vertical',
754
        width_policy="fit",
755
        margin=(0, 0, -30, 50),
756
    )
757
    return title
2✔
758

759

760
def bokeh_set_x_range(plot, x_range, debug=False):
2✔
761
    """Function which adjust java script call back for x_range of a bokeh plot. Required to link
762
    bokeh and holoviews x_range.
763

764
    Note:
765
        This is somewhat voodoo + some black magic,
766
        but it works....
767

768
    """
769
    from bokeh.models import CustomJS
2✔
770

771
    code = """\
2✔
772
    const start = cb_obj.start;
773
    const end = cb_obj.end;
774
    // Need to update the attributes at the same time.
775
    x_range.setv({start, end});
776
    """
777
    for attr in ["start", "end"]:
2✔
778
        if debug:
2✔
779
            # Prints x_range bar to check Id, as I said voodoo
780
            print(x_range)
×
781
        plot.x_range.js_on_change(attr, CustomJS(args=dict(x_range=x_range), code=code))
2✔
782

783

784
class DataSelectionHist:
2✔
785
    """Class for an interactive data selection plot."""
786

787
    def __init__(self, name, size=600):
2✔
788
        """Class for an interactive data selection plot.
789

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

794
        """
795
        raise NotImplementedError(
796
            "This function does not work with"
797
            " the latest bokeh version. If you are still using"
798
            " this function please let us know in tech-support"
799
            " by the 01.04.2024, else we reomve this function."
800
        )
801
        self.name = name
802
        self.selection_index = None
803
        self.size = size
804

805
        from bokeh.io import output_notebook
806

807
        output_notebook()
808

809
    def histogram2d(
2✔
810
        self,
811
        items,
812
        xdata,
813
        ydata,
814
        bins,
815
        hist_range,
816
        x_label="X-Data",
817
        y_label="Y-Data",
818
        log_color_scale=True,
819
        cmap_steps=256,
820
        clim=(None, None),
821
        undeflow_color=None,
822
        overflow_color=None,
823
        weights=1,
824
    ):
825
        """2d Histogram which allows to select the plotted items dynamically.
826

827
        Note:
828
            You can select the data either via a box select or Lasso
829
            select tool. The data can be returned by:
830

831
            ds.get_back_selected_items()
832

833
            Hold shift to select multiple regions.
834

835
        Warnings:
836
            Depending on the number of bins the Lasso selection can
837
            become relatively slow. The number of bins should not be
838
            larger than 100.
839
            The box selection performance is better.
840

841
        :param items: numpy.structured.array of items to be selected.
842
            e.g. peaks or events.
843
        :param xdata: numpy.array for xdata e.g. peaks['area']
844
        :param ydata: same
845
        :param bins: Integer specifying the number of bins. Currently
846
            x and y axis must share the same binning.
847
        :param hist_range: Tuple of x-range and y-range.
848
        :param x_label: Label to be used for the x-axis
849
        :param y_label: same but for y
850
        :param log_color_scale: If true (default) use log colorscale
851
        :param cmap_steps: Integer between 0 and 256 for stepped
852
            colorbar.
853
        :param clim: Tuple of color limits.
854
        :param undeflow_color: If specified colors all bins below clim
855
            with the corresponding color.
856
        :param overflow_color: Same but per limit.
857
        :param weights: If specified each bin entry is weighted by this
858
            value. Can be either a scalar e.g. a time or an array of
859
            weights which has the same length as the x/y data.
860
        :return: bokeh figure instance.
861

862
        """
863
        if isinstance(bins, tuple):
×
864
            raise ValueError(
865
                "Currently only squared bins are supported. Plase change bins into an integer."
866
            )
867

868
        x_pos, y_pos = self._make_bin_positions((bins, bins), hist_range)
×
869
        weights = np.ones(len(xdata)) * weights
×
870

871
        hist, hist_inds = self._hist2d_with_index(xdata, ydata, weights, self.xedges, self.yedges)
×
872

873
        # Define times and ids for return:
874
        self.items = items
×
875
        self.hist_inds = hist_inds
×
876

877
        colors = self._get_color(
×
878
            hist,
879
            cmap_steps,
880
            log_color_scale=log_color_scale,
881
            clim=clim,
882
            undeflow_color=undeflow_color,
883
            overflow_color=overflow_color,
884
        )
885

886
        # Create Figure and add LassoTool:
887
        f = bokeh.plotting.figure(
×
888
            title="DataSelection", width=self.size, height=self.size, tools="box_select,reset,save"
889
        )
890

891
        # Add hover tool, colorbar is too complictaed:
892
        tool_tip = [("Bin Center x", "@x"), ("Bin Center y", "@y"), ("Entries", "@h")]
×
893
        f.add_tools(
×
894
            bokeh.models.LassoSelectTool(select_every_mousemove=False),
895
            bokeh.models.HoverTool(tooltips=tool_tip),
896
        )
897

898
        s1 = bokeh.plotting.ColumnDataSource(
×
899
            data=dict(x=x_pos, y=y_pos, h=hist.flatten(), color=colors)
900
        )
901
        f.square(source=s1, size=self.size / bins, color="color", nonselection_alpha=0.3)
×
902

903
        f.x_range.start = self.xedges[0]
×
904
        f.x_range.end = self.xedges[-1]
×
905
        f.y_range.start = self.yedges[0]
×
906
        f.y_range.end = self.yedges[-1]
×
907
        f.xaxis.axis_label = x_label
×
908
        f.yaxis.axis_label = y_label
×
909

910
        self.selection_index = None
×
911
        s1.selected.js_on_change(
×
912
            "indices",
913
            bokeh.models.CustomJS(
914
                args=dict(s1=s1),
915
                code=f"""
916
                var inds = cb_obj.indices;
917
                var kernel = IPython.notebook.kernel;
918
                kernel.execute("{self.name}.selection_index = " + inds);
919
                """,
920
            ),
921
        )
922
        return f
×
923

924
    def get_back_selected_items(self):
2✔
925
        if not self.selection_index:
×
926
            raise ValueError(
927
                "No data selection found. Have you selected any data? "
928
                "If yes you most likely have not intialized the DataSelctor correctly. "
929
                'You have to callit as: my_instance_name = DataSelectionHist("my_instance_name")'
930
            )
931
        m = np.isin(self.hist_inds, self.selection_index)
×
932
        return self.items[m]
×
933

934
    @staticmethod
2✔
935
    @numba.njit
2✔
936
    def _hist2d_with_index(xdata, ydata, weights, x_edges, y_edges):
2✔
937
        n_x_bins = len(x_edges) - 1
×
938
        n_y_bins = len(y_edges) - 1
×
939
        res_hist_inds = np.zeros(len(xdata), dtype=np.int32)
×
940
        res_hist = np.zeros((n_x_bins, n_y_bins), dtype=np.int64)
×
941

942
        # Create bin ranges:
943
        offset = 0
×
944
        for ind, xv in enumerate(xdata):
×
945
            yv = ydata[ind]
×
946
            w = weights[ind]
×
947
            hist_ind = 0
×
948
            found = False
×
949
            for ind_xb, low_xb in enumerate(x_edges[:-1]):
×
950
                high_xb = x_edges[ind_xb + 1]
×
951

952
                if not low_xb <= xv:
×
953
                    hist_ind += n_y_bins
×
954
                    continue
×
955
                if not xv < high_xb:
×
956
                    hist_ind += n_y_bins
×
957
                    continue
×
958

959
                # Checked both bins value is in bin, so check y:
960
                for ind_yb, low_yb in enumerate(y_edges[:-1]):
×
961
                    high_yb = y_edges[ind_yb + 1]
×
962

963
                    if not low_yb <= yv:
×
964
                        hist_ind += 1
×
965
                        continue
×
966
                    if not yv < high_yb:
×
967
                        hist_ind += 1
×
968
                        continue
×
969

970
                    found = True
×
971
                    res_hist_inds[offset] = hist_ind
×
972
                    res_hist[ind_xb, ind_yb] += w
×
973
                    offset += 1
×
974

975
            # Set to -1 if not in any
976
            if not found:
×
977
                res_hist_inds[offset] = -1
×
978
                offset += 1
×
979
        return res_hist, res_hist_inds
×
980

981
    def _make_bin_positions(self, bins, bin_range):
2✔
982
        """Helper function to create center positions for "histogram" markers."""
983
        edges = []
×
984
        for b, br in zip(bins, bin_range):
×
985
            # Create x and y edges
986
            d_range = br[1] - br[0]
×
987
            edges.append(np.arange(br[0], br[1] + d_range / b, d_range / b))
×
988

989
        # Convert into marker positions:
990
        xedges = edges[0]
×
991
        yedges = edges[1]
×
992
        self.xedges = xedges
×
993
        self.yedges = yedges
×
994
        x_pos = xedges[:-1] + np.diff(xedges) / 2
×
995
        x_pos = np.repeat(x_pos, len(yedges) - 1)
×
996

997
        y_pos = yedges[:-1] + np.diff(yedges) / 2
×
998
        y_pos = np.array(list(y_pos) * (len(xedges) - 1))
×
999
        return x_pos, y_pos
×
1000

1001
    def _get_color(
2✔
1002
        self,
1003
        hist,
1004
        cmap_steps,
1005
        log_color_scale=False,
1006
        clim=(None, None),
1007
        undeflow_color=None,
1008
        overflow_color=None,
1009
    ):
1010
        """Helper function to create colorscale."""
1011
        hist = hist.flatten()
×
1012

1013
        if clim[0] and undeflow_color:
×
1014
            # If underflow is specified get indicies for underflow bins
1015
            inds_underflow = np.argwhere(hist < clim[0]).flatten()
×
1016

1017
        if clim[1] and overflow_color:
×
1018
            inds_overflow = np.argwhere(hist > clim[1]).flatten()
×
1019

1020
        # Clip data according to clim
1021
        if np.any(clim):
×
1022
            hist = np.clip(hist, clim[0], clim[1])
×
1023

1024
        self.clim = (np.min(hist), np.max(hist))
×
1025
        if log_color_scale:
×
1026
            color = np.log10(hist)
×
1027
            color /= np.max(color)
×
1028
            color *= cmap_steps - 1
×
1029
        else:
1030
            color = hist / np.max(hist)
×
1031
            color *= cmap_steps - 1
×
1032

1033
        cmap = np.array(bokeh.palettes.viridis(cmap_steps))
×
1034
        cmap = cmap[np.round(color).astype(np.int8)]
×
1035

1036
        if undeflow_color:
×
1037
            cmap[inds_underflow] = undeflow_color
×
1038

1039
        if overflow_color:
×
1040
            cmap[inds_overflow] = overflow_color
×
1041
        return cmap
×
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

© 2025 Coveralls, Inc