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