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

XENONnT / straxen / 11867531211

16 Nov 2024 05:23AM UTC coverage: 89.785% (+0.02%) from 89.77%
11867531211

push

github

web-flow
Merge branch 'sr1_leftovers' into master (#1478)

* Update pytest.yml (#1431)

* Update pytest.yml

Specify the strax to be v1.6.5

* add install base_env

* Add force reinstall

* Update definition of the SE Score (previously the SE density) for SR1 WIMP (#1430)

* Update score definition. Modify file names.

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

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

* Modify file names in the initialization file.

* Rearrangenames. Move sr phase assignment elsewhere.

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

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

* Add url configs and modify code style.

* Modify the parameter names.

* Fix data type in url config.

* Add docstring for the eps used to prevent divide by zero.

* Reformmated with precommit.

* Add docstrings. Remove redundant code.

* Add docstring for the 2D Gaussian.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Dacheng Xu <dx2227@columbia.edu>

* Copy https://github.com/XENONnT/straxen/pull/1417 (#1438)

* Bump to v2.2.6 (#1441)

* Bump version: 2.2.4 → 2.2.6

* Update HISTORY.md

* Constraint strax version

---------

Co-authored-by: Kexin Liu <lkx21@mails.tsinghua.edu.cn>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

72 of 72 new or added lines in 4 files covered. (100.0%)

7 existing lines in 3 files now uncovered.

8614 of 9594 relevant lines covered (89.79%)

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
    colors=("gray", "blue", "green"),
37
    yscale=("linear", "linear", "linear"),
38
    log=True,
39
):
40
    """Interactive event display for XENONnT. Plots detailed main/alt S1/S2, bottom and top PMT hit
41
    pattern as well as all other peaks in a given event.
42

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

62
    example::
63

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

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

78
    :return: bokeh.plotting.figure instance.
79

80
    """
81
    st = context
2✔
82

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

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

91
        output_notebook()
2✔
92

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

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

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

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

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

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

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

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

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

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

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

169
    # Create tile:
170
    title = _make_event_title(events[0], run_id)
2✔
171

172
    # Put everything together:
173
    if bottom_pmt_array:
2✔
174
        upper_row = [fig_s1, fig_s2, fig_top, fig_bottom]
2✔
175
    else:
176
        upper_row = [fig_s1, fig_s2, fig_top]
×
177

178
    upper_row = bokeh.layouts.Row(
2✔
179
        children=upper_row,
180
        sizing_mode="scale_width",
181
    )
182

183
    plots = bokeh.layouts.gridplot(
2✔
184
        children=[upper_row, waveform],
185
        sizing_mode="scale_width",
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_width",
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
                " 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,
249
            wf * records_in_window,
250
            sizing_mode="scale_width",
251
        )
252

253
    return event_display
2✔
254

255

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

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

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

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

304

305
def plot_pmt_arrays_and_positions(
2✔
306
    top_array_keys, bottom_array_keys, signal, to_pe, labels, plot_all_pmts, log=True
307
):
308
    """Function which plots the Top and Bottom PMT array.
309

310
    :return: fig_top, fig_bottom
311

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

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

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

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

351
    return fig_top, fig_bottom
2✔
352

353

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

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

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

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

398
    waveform.x_range.start = max(-0.1 * length, (event["time"] - start) / 10**3)
2✔
399
    waveform.x_range.end = min(1.1 * length, (event["endtime"] - start) / 10**3)
2✔
400
    return waveform
2✔
401

402

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

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

423
    """
424
    if not peak.shape:
2✔
425
        peak = np.array([peak])
×
426

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

433
    p_type = peak[0]["type"]
2✔
434

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

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

442
    source = straxen.bokeh_utils.get_peaks_source(
2✔
443
        peak,
444
        relative_start=peak[0]["time"],
445
        time_scaler=time_scalar,
446
        keep_amplitude_per_sample=False,
447
    )
448

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

463
    fig.legend.location = "top_right"
2✔
464
    fig.legend.click_policy = "hide"
2✔
465

466
    if label:
2✔
467
        fig.legend.visible = True
2✔
468
    else:
469
        fig.legend.visible = False
×
470

471
    return fig, patches
2✔
472

473

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

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

485
    """
486
    if not fig:
2✔
487
        fig = straxen.bokeh_utils.default_fig(width=1600, height=400, y_axis_type=yscale)
2✔
488

489
    for i in range(0, 3):
2✔
490
        _ind = np.where(peaks["type"] == i)[0]
2✔
491
        if not len(_ind):
2✔
492
            continue
2✔
493

494
        source = straxen.bokeh_utils.get_peaks_source(
2✔
495
            peaks[_ind],
496
            relative_start=peaks[0]["time"],
497
            time_scaler=time_scalar,
498
            keep_amplitude_per_sample=False,
499
        )
500

501
        fig.patches(
2✔
502
            source=source,
503
            fill_color=colors[i],
504
            fill_alpha=0.2,
505
            line_color=colors[i],
506
            line_width=0.5,
507
            legend_label=LEGENDS[i],
508
            name=LEGENDS[i],
509
        )
510

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

517
    fig.xaxis.axis_label = "Time [µs]"
2✔
518
    fig.xaxis.axis_label_text_font_size = "12pt"
2✔
519
    fig.yaxis.axis_label = "Amplitude [pe/ns]"
2✔
520
    fig.yaxis.axis_label_text_font_size = "12pt"
2✔
521

522
    fig.legend.location = "top_left"
2✔
523
    fig.legend.click_policy = "hide"
2✔
524
    return fig
2✔
525

526

527
def plot_pmt_array(
2✔
528
    peak,
529
    array_type,
530
    to_pe,
531
    plot_all_pmts=False,
532
    log=False,
533
    fig=None,
534
    label="",
535
):
536
    """Plots top or bottom PMT array for given peak.
537

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

548
    """
549
    if peak.shape:
2✔
550
        raise ValueError("Can plot PMT array only for a single peak at a time.")
551

552
    tool_tip = [
2✔
553
        ("Plot", "$name"),
554
        ("Channel", "@pmt"),
555
        ("X-Position [cm]", "$x"),
556
        ("Y-Position [cm]", "$y"),
557
        ("area [pe]", "@area"),
558
    ]
559

560
    array = ("top", "bottom")
2✔
561
    if array_type not in array:
2✔
562
        raise ValueError('"array_type" must be either top or bottom.')
563

564
    if not fig:
2✔
565
        fig = straxen.bokeh_utils.default_fig(title=f"{array_type} array")
×
566

567
    # Creating TPC axis and title
568
    fig = _plot_tpc(fig)
2✔
569

570
    # Plotting PMTs:
571
    pmts = straxen.pmt_positions()
2✔
572
    if plot_all_pmts:
2✔
573
        mask_pmts = np.zeros(len(pmts), dtype=np.bool_)
×
574
    else:
575
        mask_pmts = to_pe == 0
2✔
576
    pmts_on = pmts[~mask_pmts]
2✔
577
    pmts_on = pmts_on[pmts_on["array"] == array_type]
2✔
578

579
    if np.any(mask_pmts):
2✔
580
        pmts_off = pmts[mask_pmts]
2✔
581
        pmts_off = pmts_off[pmts_off["array"] == array_type]
2✔
582
        fig = _plot_off_pmts(pmts_off, fig)
2✔
583

584
    area_per_channel = peak["area_per_channel"][pmts_on["i"]]
2✔
585

586
    if log:
2✔
587
        area_plot = np.log10(area_per_channel)
2✔
588
        # Manually set infs to zero since cmap cannot handle it.
589
        area_plot = np.where(area_plot == -np.inf, 0, area_plot)
2✔
590
    else:
591
        area_plot = area_per_channel
×
592

593
    mapper = bokeh.transform.linear_cmap(
2✔
594
        field_name="area_plot", palette="Viridis256", low=min(area_plot), high=max(area_plot)
595
    )
596

597
    source_on = bklt.ColumnDataSource(
2✔
598
        data={
599
            "x": pmts_on["x"],
600
            "y": pmts_on["y"],
601
            "area": area_per_channel,
602
            "area_plot": area_plot,
603
            "pmt": pmts_on["i"],
604
        }
605
    )
606

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

624

625
def _plot_tpc(fig=None):
2✔
626
    """Plots ring at TPC radius and sets xy limits + labels."""
627
    if not fig:
2✔
628
        fig = straxen.bokeh_utils.default_fig()
×
629

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

648
    return fig
2✔
649

650

651
def _plot_off_pmts(pmts, fig=None):
2✔
652
    """Plots PMTs which are switched off."""
653
    if not fig:
2✔
654
        fig = straxen.bokeh_utils.default_fig()
×
655
    fig.circle(
2✔
656
        x=pmts["x"],
657
        y=pmts["y"],
658
        fill_color="gray",
659
        line_color="black",
660
        radius=straxen.tpc_pmt_radius,
661
    )
662
    return fig
2✔
663

664

665
def plot_posS2s(peaks, label="", fig=None, s2_type_style_id=0):
2✔
666
    """Plots xy-positions of specified peaks.
667

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

675
    """
676
    if not peaks.shape:
2✔
677
        peaks = np.array([peaks])
2✔
678

679
    if not np.all(peaks["type"] == 2):
2✔
680
        raise ValueError("All peaks must be S2!")
681

682
    if not fig:
2✔
683
        fig = straxen.bokeh_utils.default_fig()
×
684

685
    source = straxen.bokeh_utils.get_peaks_source(peaks)
2✔
686

687
    if s2_type_style_id == 0:
2✔
688
        p = fig.cross(
2✔
689
            source=source, name=label, legend_label=label, color="red", line_width=2, size=12
690
        )
691

692
    if s2_type_style_id == 1:
2✔
693
        p = fig.cross(
×
694
            source=source,
695
            name=label,
696
            legend_label=label,
697
            color="orange",
698
            angle=45 / 360 * 2 * np.pi,
699
            line_width=2,
700
            size=12,
701
        )
702

703
    if s2_type_style_id == 2:
2✔
704
        p = fig.diamond_cross(source=source, name=label, legend_label=label, color="red", size=8)
×
705

706
    tt = straxen.bokeh_utils.peak_tool_tip(2)
2✔
707
    tt = [v for k, v in tt.items() if k not in ["time_dynamic", "amplitude"]]
2✔
708
    fig.add_tools(
2✔
709
        bokeh.models.HoverTool(
710
            name=label, tooltips=[("Position x [cm]", "@x"), ("Position y [cm]", "@y")] + tt
711
        )
712
    )
713
    return fig, p
2✔
714

715

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

719
    Note:
720
        To center the title I use a transparent box.
721

722
    :param event: Event which we are plotting
723
    :param run_id: run_id
724

725
    :return: Title as bokeh.models.Div instance
726

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

740
    title = bokeh.models.Div(
2✔
741
        text=text,
742
        styles={
743
            "text-align": "left",
744
        },
745
        sizing_mode="scale_width",
746
        width=width,
747
        # orientation='vertical',
748
        width_policy="fit",
749
        margin=(0, 0, -30, 50),
750
    )
751
    return title
2✔
752

753

754
def bokeh_set_x_range(plot, x_range, debug=False):
2✔
755
    """Function which adjust java script call back for x_range of a bokeh plot. Required to link
756
    bokeh and holoviews x_range.
757

758
    Note:
759
        This is somewhat voodoo + some black magic,
760
        but it works....
761

762
    """
763
    from bokeh.models import CustomJS
2✔
764

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

777

778
class DataSelectionHist:
2✔
779
    """Class for an interactive data selection plot."""
780

781
    def __init__(self, name, size=600):
2✔
782
        """Class for an interactive data selection plot.
783

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

788
        """
789
        raise NotImplementedError(
790
            "This function does not work with"
791
            " the latest bokeh version. If you are still using"
792
            " this function please let us know in tech-support"
793
            " by the 01.04.2024, else we reomve this function."
794
        )
UNCOV
795
        self.name = name
UNCOV
796
        self.selection_index = None
UNCOV
797
        self.size = size
798

UNCOV
799
        from bokeh.io import output_notebook
800

UNCOV
801
        output_notebook()
802

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

821
        Note:
822
            You can select the data either via a box select or Lasso
823
            select tool. The data can be returned by:
824

825
            ds.get_back_selected_items()
826

827
            Hold shift to select multiple regions.
828

829
        Warnings:
830
            Depending on the number of bins the Lasso selection can
831
            become relatively slow. The number of bins should not be
832
            larger than 100.
833
            The box selection performance is better.
834

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

856
        """
857
        if isinstance(bins, tuple):
×
858
            raise ValueError(
859
                "Currently only squared bins are supported. Plase change bins into an integer."
860
            )
861

862
        x_pos, y_pos = self._make_bin_positions((bins, bins), hist_range)
×
863
        weights = np.ones(len(xdata)) * weights
×
864

865
        hist, hist_inds = self._hist2d_with_index(xdata, ydata, weights, self.xedges, self.yedges)
×
866

867
        # Define times and ids for return:
868
        self.items = items
×
869
        self.hist_inds = hist_inds
×
870

871
        colors = self._get_color(
×
872
            hist,
873
            cmap_steps,
874
            log_color_scale=log_color_scale,
875
            clim=clim,
876
            undeflow_color=undeflow_color,
877
            overflow_color=overflow_color,
878
        )
879

880
        # Create Figure and add LassoTool:
881
        f = bokeh.plotting.figure(
×
882
            title="DataSelection", width=self.size, height=self.size, tools="box_select,reset,save"
883
        )
884

885
        # Add hover tool, colorbar is too complictaed:
886
        tool_tip = [("Bin Center x", "@x"), ("Bin Center y", "@y"), ("Entries", "@h")]
×
887
        f.add_tools(
×
888
            bokeh.models.LassoSelectTool(select_every_mousemove=False),
889
            bokeh.models.HoverTool(tooltips=tool_tip),
890
        )
891

892
        s1 = bokeh.plotting.ColumnDataSource(
×
893
            data=dict(x=x_pos, y=y_pos, h=hist.flatten(), color=colors)
894
        )
895
        f.square(source=s1, size=self.size / bins, color="color", nonselection_alpha=0.3)
×
896

897
        f.x_range.start = self.xedges[0]
×
898
        f.x_range.end = self.xedges[-1]
×
899
        f.y_range.start = self.yedges[0]
×
900
        f.y_range.end = self.yedges[-1]
×
901
        f.xaxis.axis_label = x_label
×
902
        f.yaxis.axis_label = y_label
×
903

904
        self.selection_index = None
×
905
        s1.selected.js_on_change(
×
906
            "indices",
907
            bokeh.models.CustomJS(
908
                args=dict(s1=s1),
909
                code=f"""
910
                var inds = cb_obj.indices;
911
                var kernel = IPython.notebook.kernel;
912
                kernel.execute("{self.name}.selection_index = " + inds);
913
                """,
914
            ),
915
        )
916
        return f
×
917

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

928
    @staticmethod
2✔
929
    @numba.njit
2✔
930
    def _hist2d_with_index(xdata, ydata, weights, x_edges, y_edges):
2✔
931
        n_x_bins = len(x_edges) - 1
×
932
        n_y_bins = len(y_edges) - 1
×
933
        res_hist_inds = np.zeros(len(xdata), dtype=np.int32)
×
934
        res_hist = np.zeros((n_x_bins, n_y_bins), dtype=np.int64)
×
935

936
        # Create bin ranges:
937
        offset = 0
×
938
        for ind, xv in enumerate(xdata):
×
939
            yv = ydata[ind]
×
940
            w = weights[ind]
×
941
            hist_ind = 0
×
942
            found = False
×
943
            for ind_xb, low_xb in enumerate(x_edges[:-1]):
×
944
                high_xb = x_edges[ind_xb + 1]
×
945

946
                if not low_xb <= xv:
×
947
                    hist_ind += n_y_bins
×
948
                    continue
×
949
                if not xv < high_xb:
×
950
                    hist_ind += n_y_bins
×
951
                    continue
×
952

953
                # Checked both bins value is in bin, so check y:
954
                for ind_yb, low_yb in enumerate(y_edges[:-1]):
×
955
                    high_yb = y_edges[ind_yb + 1]
×
956

957
                    if not low_yb <= yv:
×
958
                        hist_ind += 1
×
959
                        continue
×
960
                    if not yv < high_yb:
×
961
                        hist_ind += 1
×
962
                        continue
×
963

964
                    found = True
×
965
                    res_hist_inds[offset] = hist_ind
×
966
                    res_hist[ind_xb, ind_yb] += w
×
967
                    offset += 1
×
968

969
            # Set to -1 if not in any
970
            if not found:
×
971
                res_hist_inds[offset] = -1
×
972
                offset += 1
×
973
        return res_hist, res_hist_inds
×
974

975
    def _make_bin_positions(self, bins, bin_range):
2✔
976
        """Helper function to create center positions for "histogram" markers."""
977
        edges = []
×
978
        for b, br in zip(bins, bin_range):
×
979
            # Create x and y edges
980
            d_range = br[1] - br[0]
×
981
            edges.append(np.arange(br[0], br[1] + d_range / b, d_range / b))
×
982

983
        # Convert into marker positions:
984
        xedges = edges[0]
×
985
        yedges = edges[1]
×
986
        self.xedges = xedges
×
987
        self.yedges = yedges
×
988
        x_pos = xedges[:-1] + np.diff(xedges) / 2
×
989
        x_pos = np.repeat(x_pos, len(yedges) - 1)
×
990

991
        y_pos = yedges[:-1] + np.diff(yedges) / 2
×
992
        y_pos = np.array(list(y_pos) * (len(xedges) - 1))
×
993
        return x_pos, y_pos
×
994

995
    def _get_color(
2✔
996
        self,
997
        hist,
998
        cmap_steps,
999
        log_color_scale=False,
1000
        clim=(None, None),
1001
        undeflow_color=None,
1002
        overflow_color=None,
1003
    ):
1004
        """Helper function to create colorscale."""
1005
        hist = hist.flatten()
×
1006

1007
        if clim[0] and undeflow_color:
×
1008
            # If underflow is specified get indicies for underflow bins
1009
            inds_underflow = np.argwhere(hist < clim[0]).flatten()
×
1010

1011
        if clim[1] and overflow_color:
×
1012
            inds_overflow = np.argwhere(hist > clim[1]).flatten()
×
1013

1014
        # Clip data according to clim
1015
        if np.any(clim):
×
1016
            hist = np.clip(hist, clim[0], clim[1])
×
1017

1018
        self.clim = (np.min(hist), np.max(hist))
×
1019
        if log_color_scale:
×
1020
            color = np.log10(hist)
×
1021
            color /= np.max(color)
×
1022
            color *= cmap_steps - 1
×
1023
        else:
1024
            color = hist / np.max(hist)
×
1025
            color *= cmap_steps - 1
×
1026

1027
        cmap = np.array(bokeh.palettes.viridis(cmap_steps))
×
1028
        cmap = cmap[np.round(color).astype(np.int8)]
×
1029

1030
        if undeflow_color:
×
1031
            cmap[inds_underflow] = undeflow_color
×
1032

1033
        if overflow_color:
×
1034
            cmap[inds_overflow] = overflow_color
×
1035
        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

© 2026 Coveralls, Inc