Coverage for peakipy/cli/edit.py: 53%
316 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-09-22 23:15 -0400
« prev ^ index » next coverage.py v7.4.4, created at 2024-09-22 23:15 -0400
1#!/usr/bin/env python3
2""" Script for checking fits and editing fit params
3"""
4import os
5import sys
6import shutil
8from subprocess import check_output
9from pathlib import Path
12import numpy as np
13import pandas as pd
14from skimage.filters import threshold_otsu
15from rich import print
18import panel as pn
20from bokeh.events import ButtonClick, DoubleTap
21from bokeh.layouts import row, column
22from bokeh.models import ColumnDataSource
23from bokeh.models.tools import HoverTool
24from bokeh.models.widgets import (
25 Slider,
26 Select,
27 Button,
28 TextInput,
29 RadioButtonGroup,
30 CheckboxGroup,
31 Div,
32)
33from bokeh.plotting import figure
34from bokeh.plotting.contour import contour_data
35from bokeh.palettes import PuBuGn9, Category20, Viridis256, RdGy11, Reds256, YlOrRd9
37from peakipy.io import LoadData, StrucEl
38from peakipy.utils import update_args_with_values_from_config_file
40log_style = "overflow:scroll;"
41log_div = """<div style=%s>%s</div>"""
44class BokehScript:
45 def __init__(self, peaklist_path: Path, data_path: Path):
46 self._path = peaklist_path
47 self._data_path = data_path
48 args, config = update_args_with_values_from_config_file({})
49 self._dims = config.get("dims", [0, 1, 2])
50 self.thres = config.get("thres", 1e6)
51 self._peakipy_data = LoadData(
52 self._path, self._data_path, dims=self._dims, verbose=True
53 )
54 # check dataframe is usable
55 self.peakipy_data.check_data_frame()
56 # make temporary paths
57 self.make_temp_files()
58 self.make_data_source()
59 self.make_tabulator_widget()
60 self.setup_radii_sliders()
61 self.setup_save_buttons()
62 self.setup_set_fixed_parameters()
63 self.setup_xybounds()
64 self.setup_set_reference_planes()
65 self.setup_initial_fit_threshold()
66 self.setup_quit_button()
67 self.setup_plot()
68 self.check_pane = ""
70 def init(self, doc):
71 """initialise the bokeh app"""
73 doc.add_root(
74 column(
75 self.intro_div,
76 row(column(self.p, self.doc_link), column(self.data_table, self.tabs)),
77 sizing_mode="stretch_both",
78 )
79 )
80 doc.title = "peakipy: Edit Fits"
81 # doc.theme = "dark_minimal"
83 @property
84 def args(self):
85 return self._args
87 @property
88 def path(self):
89 return self._path
91 @property
92 def data_path(self):
93 return self._data_path
95 @property
96 def peakipy_data(self):
97 return self._peakipy_data
99 def make_temp_files(self):
100 # Temp files
101 self.TEMP_PATH = self.path.parent / Path("tmp")
102 self.TEMP_PATH.mkdir(parents=True, exist_ok=True)
104 self.TEMP_OUT_CSV = self.TEMP_PATH / Path("tmp_out.csv")
105 self.TEMP_INPUT_CSV = self.TEMP_PATH / Path("tmp.csv")
107 self.TEMP_OUT_PLOT = self.TEMP_PATH / Path("plots")
108 self.TEMP_OUT_PLOT.mkdir(parents=True, exist_ok=True)
110 def make_data_source(self):
111 # make datasource
112 self.source = ColumnDataSource()
113 self.source.data = ColumnDataSource.from_df(self.peakipy_data.df)
114 return self.source
116 @property
117 def tabulator_columns(self):
118 columns = [
119 "ASS",
120 "CLUSTID",
121 "X_PPM",
122 "Y_PPM",
123 "X_RADIUS_PPM",
124 "Y_RADIUS_PPM",
125 "XW_HZ",
126 "YW_HZ",
127 "VOL",
128 "include",
129 "MEMCNT",
130 ]
131 return columns
133 @property
134 def tabulator_non_editable_columns(self):
135 editors = {"X_RADIUS_PPM": None, "Y_RADIUS_PPM": None}
136 return editors
138 def make_tabulator_widget(self):
139 tabulator_stylesheet = """
140 .tabulator-cell {
141 font-size: 12px;
142 }
143 .tabulator-headers {
144 font-size: 12px;
145 }
146 """
147 self.tabulator_widget = pn.widgets.Tabulator(
148 self.peakipy_data.df[self.tabulator_columns],
149 editors=self.tabulator_non_editable_columns,
150 height=500,
151 width=800,
152 show_index=False,
153 frozen_columns=["ASS","CLUSTID"],
154 stylesheets=[tabulator_stylesheet],
155 selectable="checkbox",
156 selection=[],
157 )
158 return self.tabulator_widget
160 def select_callback(self, attrname, old, new):
161 for col in self.peakipy_data.df.columns:
162 self.peakipy_data.df.loc[:, col] = self.source.data[col]
163 self.update_memcnt()
165 def setup_radii_sliders(self):
166 # configure sliders for setting radii
167 self.slider_X_RADIUS = Slider(
168 title="X_RADIUS - ppm",
169 start=0.001,
170 end=0.200,
171 value=0.040,
172 step=0.001,
173 format="0[.]000",
174 )
175 self.slider_Y_RADIUS = Slider(
176 title="Y_RADIUS - ppm",
177 start=0.010,
178 end=2.000,
179 value=0.400,
180 step=0.001,
181 format="0[.]000",
182 )
184 self.slider_X_RADIUS.on_change(
185 "value", lambda attr, old, new: self.slider_callback_x(attr, old, new)
186 )
187 self.slider_Y_RADIUS.on_change(
188 "value", lambda attr, old, new: self.slider_callback_y(attr, old, new)
189 )
191 def setup_save_buttons(self):
192 # save file
193 self.savefilename = TextInput(
194 title="Save file as (.csv)", placeholder="edited_peaks.csv"
195 )
196 self.button = Button(label="Save", button_type="success")
197 self.button.on_event(ButtonClick, self.save_peaks)
199 def setup_set_fixed_parameters(self):
200 self.select_fixed_parameters_help = Div(
201 text="Select parameters to fix after initial lineshape parameters have been fitted"
202 )
203 self.select_fixed_parameters = TextInput(
204 value="fraction sigma center", width=200
205 )
207 def setup_xybounds(self):
208 self.set_xybounds_help = Div(
209 text="If floating the peak centers you can bound the fits in the x and y dimensions. Units of ppm."
210 )
211 self.set_xybounds = TextInput(placeholder="e.g. 0.01 0.1")
213 def get_xybounds(self):
214 try:
215 x_bound, y_bound = self.set_xybounds.value.split(" ")
216 x_bound = float(x_bound)
217 y_bound = float(y_bound)
218 xy_bounds = x_bound, y_bound
219 except:
220 xy_bounds = None, None
221 return xy_bounds
223 def make_xybound_command(self, x_bound, y_bound):
224 if (x_bound != None) and (y_bound != None):
225 xy_bounds_command = f" --xy-bounds {x_bound} {y_bound}"
226 else:
227 xy_bounds_command = ""
228 return xy_bounds_command
230 def setup_set_reference_planes(self):
231 self.select_reference_planes_help = Div(
232 text="Select reference planes (index starts at 0)"
233 )
234 self.select_reference_planes = TextInput(placeholder="0 1 2 3")
236 def get_reference_planes(self):
237 if self.select_reference_planes.value:
238 print("You have selected1")
239 return self.select_reference_planes.value.split(" ")
240 else:
241 return []
243 def make_reference_planes_command(self, reference_plane_list):
244 reference_plane_command = ""
245 for plane in reference_plane_list:
246 reference_plane_command += f" --reference-plane-index {plane}"
247 return reference_plane_command
249 def setup_initial_fit_threshold(self):
250 self.set_initial_fit_threshold_help = Div(
251 text="Set an intensity threshold for selection of planes for initial estimation of lineshape parameters"
252 )
253 self.set_initial_fit_threshold = TextInput(placeholder="e.g. 1e7")
255 def get_initial_fit_threshold(self):
256 try:
257 initial_fit_threshold = float(self.set_initial_fit_threshold.value)
258 except ValueError:
259 initial_fit_threshold = None
260 return initial_fit_threshold
262 def make_initial_fit_threshold_command(self, initial_fit_threshold):
263 if initial_fit_threshold is not None:
264 initial_fit_threshold_command = (
265 f" --initial-fit-threshold {initial_fit_threshold}"
266 )
267 else:
268 initial_fit_threshold_command = ""
269 return initial_fit_threshold_command
271 def setup_quit_button(self):
272 # Quit button
273 self.exit_button = Button(label="Quit", button_type="warning")
274 self.exit_button.on_event(ButtonClick, self.exit_edit_peaks)
276 def setup_plot(self):
277 """ " code to setup the bokeh plots"""
278 # make bokeh figure
279 tools = [
280 "tap",
281 "box_zoom",
282 "lasso_select",
283 "box_select",
284 "wheel_zoom",
285 "pan",
286 "reset",
287 ]
288 self.p = figure(
289 x_range=(self.peakipy_data.f2_ppm_0, self.peakipy_data.f2_ppm_1),
290 y_range=(self.peakipy_data.f1_ppm_0, self.peakipy_data.f1_ppm_1),
291 x_axis_label=f"{self.peakipy_data.f2_label} - ppm",
292 y_axis_label=f"{self.peakipy_data.f1_label} - ppm",
293 tools=tools,
294 active_drag="pan",
295 active_scroll="wheel_zoom",
296 active_tap=None,
297 )
298 if not self.thres:
299 self.thres = threshold_otsu(self.peakipy_data.data[0])
300 self.contour_start = self.thres # contour level start value
301 self.contour_num = 20 # number of contour levels
302 self.contour_factor = 1.20 # scaling factor between contour levels
303 cl = self.contour_start * self.contour_factor ** np.arange(self.contour_num)
304 if len(cl) > 1 and np.min(np.diff(cl)) <= 0.0:
305 print(f"Setting contour levels to np.abs({cl})")
306 cl = np.abs(cl)
307 self.extent = (
308 self.peakipy_data.f2_ppm_0,
309 self.peakipy_data.f2_ppm_1,
310 self.peakipy_data.f1_ppm_0,
311 self.peakipy_data.f1_ppm_1,
312 )
314 self.x_ppm_mesh, self.y_ppm_mesh = np.meshgrid(
315 self.peakipy_data.f2_ppm_scale, self.peakipy_data.f1_ppm_scale
316 )
317 self.positive_contour_renderer = self.p.contour(
318 self.x_ppm_mesh,
319 self.y_ppm_mesh,
320 self.peakipy_data.data[0],
321 cl,
322 fill_color=YlOrRd9,
323 line_color="black",
324 line_width=0.25,
325 )
326 self.negative_contour_renderer = self.p.contour(
327 self.x_ppm_mesh,
328 self.y_ppm_mesh,
329 self.peakipy_data.data[0] * -1.0,
330 cl,
331 fill_color=Reds256,
332 line_color="black",
333 line_width=0.25,
334 )
336 self.contour_start = TextInput(
337 value="%.2e" % self.thres, title="Contour level:", width=100
338 )
339 self.contour_start.on_change("value", self.update_contour)
341 # plot mask outlines
342 el = self.p.ellipse(
343 x="X_PPM",
344 y="Y_PPM",
345 width="X_DIAMETER_PPM",
346 height="Y_DIAMETER_PPM",
347 source=self.source,
348 fill_color="color",
349 fill_alpha=0.25,
350 line_dash="dotted",
351 line_color="red",
352 )
354 self.p.add_tools(
355 HoverTool(
356 tooltips=[
357 ("Index", "$index"),
358 ("Assignment", "@ASS"),
359 ("CLUSTID", "@CLUSTID"),
360 ("RADII", "@X_RADIUS_PPM{0.000}, @Y_RADIUS_PPM{0.000}"),
361 (
362 f"{self.peakipy_data.f2_label},{self.peakipy_data.f1_label}",
363 "$x{0.000} ppm, $y{0.000} ppm",
364 ),
365 ],
366 mode="mouse",
367 # add renderers
368 renderers=[el],
369 )
370 )
371 # p.toolbar.active_scroll = "auto"
372 # draw border around spectrum area
373 spec_border_x = [
374 self.peakipy_data.f2_ppm_min,
375 self.peakipy_data.f2_ppm_min,
376 self.peakipy_data.f2_ppm_max,
377 self.peakipy_data.f2_ppm_max,
378 self.peakipy_data.f2_ppm_min,
379 ]
381 spec_border_y = [
382 self.peakipy_data.f1_ppm_min,
383 self.peakipy_data.f1_ppm_max,
384 self.peakipy_data.f1_ppm_max,
385 self.peakipy_data.f1_ppm_min,
386 self.peakipy_data.f1_ppm_min,
387 ]
389 self.p.line(
390 spec_border_x,
391 spec_border_y,
392 line_width=2,
393 line_color="red",
394 line_dash="dotted",
395 line_alpha=0.5,
396 )
397 self.p.circle(x="X_PPM", y="Y_PPM", source=self.source, color="color")
398 # plot cluster numbers
399 self.p.text(
400 x="X_PPM",
401 y="Y_PPM",
402 text="CLUSTID",
403 text_color="color",
404 source=self.source,
405 text_font_size="8pt",
406 text_font_style="bold",
407 )
409 self.p.on_event(DoubleTap, self.peak_pick_callback)
411 self.pos_neg_contour_dic = {0: "pos/neg", 1: "pos", 2: "neg"}
412 self.pos_neg_contour_radiobutton = RadioButtonGroup(
413 labels=[
414 self.pos_neg_contour_dic[i] for i in self.pos_neg_contour_dic.keys()
415 ],
416 active=0,
417 )
418 self.pos_neg_contour_radiobutton.on_change("active", self.update_contour)
419 # call fit_peaks
420 self.fit_button = Button(label="Fit selected cluster", button_type="primary")
421 # lineshape selection
422 self.lineshapes = {
423 0: "PV",
424 1: "V",
425 2: "G",
426 3: "L",
427 4: "PV_PV",
428 # 5: "PV_L",
429 # 6: "PV_G",
430 # 7: "G_L",
431 }
432 self.select_lineshape_radiobuttons = RadioButtonGroup(
433 labels=[self.lineshapes[i] for i in self.lineshapes.keys()], active=0
434 )
435 self.select_lineshape_radiobuttons_help = Div(
436 text="""Choose lineshape you wish to fit. This can be Voigt (V), pseudo-Voigt (PV), Gaussian (G), Lorentzian (L).
437 PV_PV fits a PV lineshape with independent "fraction" parameters for the direct and indirect dimensions""",
438 )
439 self.clust_div = Div(
440 text="""If you want to adjust how the peaks are automatically clustered then try changing the
441 width/diameter/height (integer values) of the structuring element used during the binary dilation step.
442 Increasing the size of the structuring element will cause
443 peaks to be more readily incorporated into clusters. The mask_method scales the fitting masks based on
444 the provided floating point value and considers any overlapping masks to be part of a cluster.""",
445 )
446 self.recluster_warning = Div(
447 text="""
448 Be sure to save your peak list before reclustering as
449 any manual edits to clusters will be lost.""",
450 )
451 self.intro_div = Div(
452 text="""<h2>peakipy - interactive fit adjustment </h2>
453 """
454 )
456 self.doc_link = Div(
457 text="<h3><a href='https://j-brady.github.io/peakipy/', target='_blank'> ℹ️ click here for documentation</a></h3>"
458 )
459 self.fit_reports = ""
460 self.fit_reports_div = Div(text="", height=400, styles={"overflow": "scroll"})
461 # Plane selection
462 self.select_planes_list = [
463 f"{i}"
464 for i in range(self.peakipy_data.data.shape[self.peakipy_data.planes])
465 ]
466 self.select_plane = Select(
467 title="Select plane:",
468 value=self.select_planes_list[0],
469 options=self.select_planes_list,
470 )
471 self.select_planes_dic = {
472 f"{i}": i
473 for i in range(self.peakipy_data.data.shape[self.peakipy_data.planes])
474 }
475 self.select_plane.on_change("value", self.update_contour)
477 self.checkbox_group = CheckboxGroup(
478 labels=["fit current plane only"], active=[]
479 )
481 self.fit_button.on_event(ButtonClick, self.fit_selected)
483 # callback for adding
484 # source.selected.on_change('indices', callback)
485 self.source.selected.on_change("indices", self.select_callback)
487 # reclustering tab
488 self.struct_el = Select(
489 title="Structuring element:",
490 value=StrucEl.disk.value,
491 options=[i.value for i in StrucEl],
492 width=100,
493 )
494 self.struct_el_size = TextInput(
495 value="3",
496 title="Size(width/radius or width,height for rectangle):",
497 width=100,
498 )
500 self.recluster = Button(label="Re-cluster", button_type="warning")
501 self.recluster.on_event(ButtonClick, self.recluster_peaks)
503 def recluster_peaks(self, event):
504 if self.struct_el.value == "mask_method":
505 self.struc_size = tuple(
506 [float(i) for i in self.struct_el_size.value.split(",")]
507 )
508 print(self.struc_size)
509 self.peakipy_data.mask_method(overlap=self.struc_size[0])
510 else:
511 self.struc_size = tuple(
512 [int(i) for i in self.struct_el_size.value.split(",")]
513 )
514 print(self.struc_size)
515 self.peakipy_data.clusters(
516 thres=eval(self.contour_start.value),
517 struc_el=StrucEl(self.struct_el.value),
518 struc_size=self.struc_size,
519 )
520 # update data source
521 self.source.data = ColumnDataSource.from_df(self.peakipy_data.df)
522 self.tabulator_widget.value = self.peakipy_data.df[self.tabulator_columns]
523 return self.peakipy_data.df
525 def update_memcnt(self):
526 for ind, group in self.peakipy_data.df.groupby("CLUSTID"):
527 self.peakipy_data.df.loc[group.index, "MEMCNT"] = len(group)
529 # set cluster colors (set to black if singlet peaks)
530 self.peakipy_data.df["color"] = self.peakipy_data.df.apply(
531 lambda x: Category20[20][int(x.CLUSTID) % 20] if x.MEMCNT > 1 else "black",
532 axis=1,
533 )
534 # change color of excluded peaks
535 include_no = self.peakipy_data.df.include == "no"
536 self.peakipy_data.df.loc[include_no, "color"] = "ghostwhite"
537 # update source data
538 self.source.data = ColumnDataSource.from_df(self.peakipy_data.df)
539 self.tabulator_widget.value = self.peakipy_data.df[self.tabulator_columns]
540 return self.peakipy_data.df
542 def unpack_parameters_to_fix(self):
543 return self.select_fixed_parameters.value.strip().split(" ")
545 def make_fix_command_from_parameters(self, parameters):
546 command = ""
547 for parameter in parameters:
548 command += f" --fix {parameter}"
549 return command
551 def fit_selected(self, event):
552 selectionIndex = self.source.selected.indices
553 current = self.peakipy_data.df.iloc[selectionIndex]
555 self.peakipy_data.df.loc[selectionIndex, "X_DIAMETER_PPM"] = (
556 current["X_RADIUS_PPM"] * 2.0
557 )
558 self.peakipy_data.df.loc[selectionIndex, "Y_DIAMETER_PPM"] = (
559 current["Y_RADIUS_PPM"] * 2.0
560 )
562 selected_df = self.peakipy_data.df[
563 self.peakipy_data.df.CLUSTID.isin(list(current.CLUSTID))
564 ]
566 selected_df.to_csv(self.TEMP_INPUT_CSV)
567 fix_command = self.make_fix_command_from_parameters(
568 self.unpack_parameters_to_fix()
569 )
570 xy_bounds_command = self.make_xybound_command(*self.get_xybounds())
571 reference_planes_command = self.make_reference_planes_command(
572 self.get_reference_planes()
573 )
574 initial_fit_threshold_command = self.make_initial_fit_threshold_command(
575 self.get_initial_fit_threshold()
576 )
578 lineshape = self.lineshapes[self.select_lineshape_radiobuttons.active]
579 print(f"[yellow]Using LS = {lineshape}[/yellow]")
580 if self.checkbox_group.active == []:
581 fit_command = f"peakipy fit {self.TEMP_INPUT_CSV} {self.data_path} {self.TEMP_OUT_CSV} --lineshape {lineshape}{fix_command}{reference_planes_command}{initial_fit_threshold_command}{xy_bounds_command}"
582 else:
583 plane_index = self.select_plane.value
584 print(f"[yellow]Only fitting plane {plane_index}[/yellow]")
585 fit_command = f"peakipy fit {self.TEMP_INPUT_CSV} {self.data_path} {self.TEMP_OUT_CSV} --lineshape {lineshape} --plane {plane_index}{fix_command}{reference_planes_command}{initial_fit_threshold_command}{xy_bounds_command}"
587 print(f"[blue]{fit_command}[/blue]")
588 self.fit_reports += fit_command + "<br>"
590 stdout = check_output(fit_command.split(" "))
591 self.fit_reports += stdout.decode() + "<br><hr><br>"
592 self.fit_reports = self.fit_reports.replace("\n", "<br>")
593 self.fit_reports_div.text = log_div % (log_style, self.fit_reports)
595 def save_peaks(self, event):
596 if self.savefilename.value:
597 to_save = Path(self.savefilename.value)
598 else:
599 to_save = Path(self.savefilename.placeholder)
601 if to_save.exists():
602 shutil.copy(f"{to_save}", f"{to_save}.bak")
603 print(f"Making backup {to_save}.bak")
605 print(f"[green]Saving peaks to {to_save}[/green]")
606 if to_save.suffix == ".csv":
607 self.peakipy_data.df.to_csv(to_save, float_format="%.4f", index=False)
608 else:
609 self.peakipy_data.df.to_pickle(to_save)
611 def peak_pick_callback(self, event):
612 # global so that df is updated globally
613 x_radius_ppm = 0.035
614 y_radius_ppm = 0.35
615 x_radius = x_radius_ppm * self.peakipy_data.pt_per_ppm_f2
616 y_radius = y_radius_ppm * self.peakipy_data.pt_per_ppm_f1
617 x_diameter_ppm = x_radius_ppm * 2.0
618 y_diameter_ppm = y_radius_ppm * 2.0
619 clustid = self.peakipy_data.df.CLUSTID.max() + 1
620 index = self.peakipy_data.df.INDEX.max() + 1
621 x_ppm = event.x
622 y_ppm = event.y
623 x_axis = self.peakipy_data.uc_f2.f(x_ppm, "ppm")
624 y_axis = self.peakipy_data.uc_f1.f(y_ppm, "ppm")
625 xw_hz = 20.0
626 yw_hz = 20.0
627 xw = xw_hz * self.peakipy_data.pt_per_hz_f2
628 yw = yw_hz * self.peakipy_data.pt_per_hz_f1
629 assignment = f"test_peak_{index}_{clustid}"
630 height = self.peakipy_data.data[0][int(y_axis), int(x_axis)]
631 volume = height
632 print(
633 f"""[blue]Adding peak at {assignment}: {event.x:.3f},{event.y:.3f}[/blue]"""
634 )
636 new_peak = {
637 "INDEX": index,
638 "X_PPM": x_ppm,
639 "Y_PPM": y_ppm,
640 "HEIGHT": height,
641 "VOL": volume,
642 "XW_HZ": xw_hz,
643 "YW_HZ": yw_hz,
644 "X_AXIS": int(np.floor(x_axis)), # integers
645 "Y_AXIS": int(np.floor(y_axis)), # integers
646 "X_AXISf": x_axis,
647 "Y_AXISf": y_axis,
648 "XW": xw,
649 "YW": yw,
650 "ASS": assignment,
651 "X_RADIUS_PPM": x_radius_ppm,
652 "Y_RADIUS_PPM": y_radius_ppm,
653 "X_RADIUS": x_radius,
654 "Y_RADIUS": y_radius,
655 "CLUSTID": clustid,
656 "MEMCNT": 1,
657 "X_DIAMETER_PPM": x_diameter_ppm,
658 "Y_DIAMETER_PPM": y_diameter_ppm,
659 "Edited": True,
660 "include": "yes",
661 "color": "black",
662 }
663 new_peak = {k: [v] for k, v in new_peak.items()}
664 new_peak = pd.DataFrame(new_peak)
665 self.peakipy_data.df = pd.concat(
666 [self.peakipy_data.df, new_peak], ignore_index=True
667 )
668 self.update_memcnt()
670 def slider_callback(self, dim, channel):
671 selectionIndex = self.source.selected.indices
672 current = self.peakipy_data.df.iloc[selectionIndex]
673 self.peakipy_data.df.loc[selectionIndex, f"{dim}_RADIUS"] = getattr(
674 self, f"slider_{dim}_RADIUS"
675 ).value * getattr(self.peakipy_data, f"pt_per_ppm_{channel}")
676 self.peakipy_data.df.loc[selectionIndex, f"{dim}_RADIUS_PPM"] = getattr(
677 self, f"slider_{dim}_RADIUS"
678 ).value
680 self.peakipy_data.df.loc[selectionIndex, f"{dim}_DIAMETER_PPM"] = (
681 current[f"{dim}_RADIUS_PPM"] * 2.0
682 )
683 self.peakipy_data.df.loc[selectionIndex, f"{dim}_DIAMETER"] = (
684 current[f"{dim}_RADIUS"] * 2.0
685 )
687 # set edited rows to True
688 self.peakipy_data.df.loc[selectionIndex, "Edited"] = True
689 self.source.data = ColumnDataSource.from_df(self.peakipy_data.df)
690 self.tabulator_widget.value = self.peakipy_data.df[self.tabulator_columns]
692 def slider_callback_x(self, attrname, old, new):
693 self.slider_callback("X", "f2")
695 def slider_callback_y(self, attrname, old, new):
696 self.slider_callback("Y", "f1")
698 def update_contour(self, attrname, old, new):
699 new_cs = eval(self.contour_start.value)
700 cl = new_cs * self.contour_factor ** np.arange(self.contour_num)
701 if len(cl) > 1 and np.min(np.diff(cl)) <= 0.0:
702 print(f"Setting contour levels to np.abs({cl})")
703 cl = np.abs(cl)
704 plane_index = self.select_planes_dic[self.select_plane.value]
706 pos_neg = self.pos_neg_contour_dic[self.pos_neg_contour_radiobutton.active]
707 if pos_neg == "pos/neg":
708 self.positive_contour_renderer.set_data(
709 contour_data(
710 self.x_ppm_mesh,
711 self.y_ppm_mesh,
712 self.peakipy_data.data[plane_index],
713 cl,
714 )
715 )
716 self.negative_contour_renderer.set_data(
717 contour_data(
718 self.x_ppm_mesh,
719 self.y_ppm_mesh,
720 self.peakipy_data.data[plane_index] * -1.0,
721 cl,
722 )
723 )
725 elif pos_neg == "pos":
726 self.positive_contour_renderer.set_data(
727 contour_data(
728 self.x_ppm_mesh,
729 self.y_ppm_mesh,
730 self.peakipy_data.data[plane_index],
731 cl,
732 )
733 )
734 self.negative_contour_renderer.set_data(
735 contour_data(
736 self.x_ppm_mesh,
737 self.y_ppm_mesh,
738 self.peakipy_data.data[plane_index] * 0,
739 cl,
740 )
741 )
743 elif pos_neg == "neg":
744 self.positive_contour_renderer.set_data(
745 contour_data(
746 self.x_ppm_mesh,
747 self.y_ppm_mesh,
748 self.peakipy_data.data[plane_index] * 0.0,
749 cl,
750 )
751 )
752 self.negative_contour_renderer.set_data(
753 contour_data(
754 self.x_ppm_mesh,
755 self.y_ppm_mesh,
756 self.peakipy_data.data[plane_index] * -1.0,
757 cl,
758 )
759 )
761 def exit_edit_peaks(self, event):
762 sys.exit()