Coverage for peakipy/cli/edit.py: 53%
315 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-15 20:42 -0400
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-15 20:42 -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 self.p.circle(x="X_PPM", y="Y_PPM", source=self.source, color="color")
402 # plot cluster numbers
403 self.p.text(
404 x="X_PPM",
405 y="Y_PPM",
406 text="CLUSTID",
407 text_color="color",
408 source=self.source,
409 text_font_size="8pt",
410 text_font_style="bold",
411 )
413 self.p.on_event(DoubleTap, self.peak_pick_callback)
415 self.pos_neg_contour_dic = {0: "pos/neg", 1: "pos", 2: "neg"}
416 self.pos_neg_contour_radiobutton = RadioButtonGroup(
417 labels=[
418 self.pos_neg_contour_dic[i] for i in self.pos_neg_contour_dic.keys()
419 ],
420 active=0,
421 )
422 self.pos_neg_contour_radiobutton.on_change("active", self.update_contour)
423 # call fit_peaks
424 self.fit_button = Button(label="Fit selected cluster", button_type="primary")
425 # lineshape selection
426 self.lineshapes = {
427 0: "PV",
428 1: "V",
429 2: "G",
430 3: "L",
431 4: "PV_PV",
432 # 5: "PV_L",
433 # 6: "PV_G",
434 # 7: "G_L",
435 }
436 self.select_lineshape_radiobuttons = RadioButtonGroup(
437 labels=[self.lineshapes[i] for i in self.lineshapes.keys()], active=0
438 )
439 self.select_lineshape_radiobuttons_help = Div(
440 text="""Choose lineshape you wish to fit. This can be Voigt (V), pseudo-Voigt (PV), Gaussian (G), Lorentzian (L).
441 PV_PV fits a PV lineshape with independent "fraction" parameters for the direct and indirect dimensions""",
442 )
443 self.clust_div = Div(
444 text="""If you want to adjust how the peaks are automatically clustered then try changing the
445 width/diameter/height (integer values) of the structuring element used during the binary dilation step.
446 Increasing the size of the structuring element will cause
447 peaks to be more readily incorporated into clusters. The mask_method scales the fitting masks based on
448 the provided floating point value and considers any overlapping masks to be part of a cluster.""",
449 )
450 self.recluster_warning = Div(
451 text="""
452 Be sure to save your peak list before reclustering as
453 any manual edits to clusters will be lost.""",
454 )
455 self.intro_div = Div(
456 text="""<h2>peakipy - interactive fit adjustment </h2>
457 """
458 )
460 self.doc_link = Div(
461 text="<h3><a href='https://j-brady.github.io/peakipy/', target='_blank'> ℹ️ click here for documentation</a></h3>"
462 )
463 self.fit_reports = ""
464 self.fit_reports_div = Div(text="", height=400, styles={"overflow": "scroll"})
465 # Plane selection
466 self.select_planes_list = [
467 f"{i}"
468 for i in range(self.peakipy_data.data.shape[self.peakipy_data.planes])
469 ]
470 self.select_plane = Select(
471 title="Select plane:",
472 value=self.select_planes_list[0],
473 options=self.select_planes_list,
474 )
475 self.select_planes_dic = {
476 f"{i}": i
477 for i in range(self.peakipy_data.data.shape[self.peakipy_data.planes])
478 }
479 self.select_plane.on_change("value", self.update_contour)
481 self.checkbox_group = CheckboxGroup(
482 labels=["fit current plane only"], active=[]
483 )
485 self.fit_button.on_event(ButtonClick, self.fit_selected)
487 # callback for adding
488 # source.selected.on_change('indices', callback)
489 self.source.selected.on_change("indices", self.select_callback)
491 # reclustering tab
492 self.struct_el = Select(
493 title="Structuring element:",
494 value=StrucEl.disk.value,
495 options=[i.value for i in StrucEl],
496 width=100,
497 )
498 self.struct_el_size = TextInput(
499 value="3",
500 title="Size(width/radius or width,height for rectangle):",
501 width=100,
502 )
504 self.recluster = Button(label="Re-cluster", button_type="warning")
505 self.recluster.on_event(ButtonClick, self.recluster_peaks)
507 def recluster_peaks(self, event):
508 if self.struct_el.value == "mask_method":
509 self.struc_size = tuple(
510 [float(i) for i in self.struct_el_size.value.split(",")]
511 )
512 print(self.struc_size)
513 self.peakipy_data.mask_method(overlap=self.struc_size[0])
514 else:
515 self.struc_size = tuple(
516 [int(i) for i in self.struct_el_size.value.split(",")]
517 )
518 print(self.struc_size)
519 self.peakipy_data.clusters(
520 thres=eval(self.contour_start.value),
521 struc_el=StrucEl(self.struct_el.value),
522 struc_size=self.struc_size,
523 )
524 # update data source
525 self.source.data = ColumnDataSource.from_df(self.peakipy_data.df)
526 self.tabulator_widget.value = self.peakipy_data.df[self.tabulator_columns]
527 return self.peakipy_data.df
529 def update_memcnt(self):
530 for ind, group in self.peakipy_data.df.groupby("CLUSTID"):
531 self.peakipy_data.df.loc[group.index, "MEMCNT"] = len(group)
533 # set cluster colors (set to black if singlet peaks)
534 self.peakipy_data.df["color"] = self.peakipy_data.df.apply(
535 lambda x: Category20[20][int(x.CLUSTID) % 20] if x.MEMCNT > 1 else "black",
536 axis=1,
537 )
538 # change color of excluded peaks
539 include_no = self.peakipy_data.df.include == "no"
540 self.peakipy_data.df.loc[include_no, "color"] = "ghostwhite"
541 # update source data
542 self.source.data = ColumnDataSource.from_df(self.peakipy_data.df)
543 self.tabulator_widget.value = self.peakipy_data.df[self.tabulator_columns]
544 return self.peakipy_data.df
546 def unpack_parameters_to_fix(self):
547 return self.select_fixed_parameters.value.strip().split(" ")
549 def make_fix_command_from_parameters(self, parameters):
550 command = ""
551 for parameter in parameters:
552 command += f" --fix {parameter}"
553 return command
555 def fit_selected(self, event):
556 selectionIndex = self.source.selected.indices
557 current = self.peakipy_data.df.iloc[selectionIndex]
559 self.peakipy_data.df.loc[selectionIndex, "X_DIAMETER_PPM"] = (
560 current["X_RADIUS_PPM"] * 2.0
561 )
562 self.peakipy_data.df.loc[selectionIndex, "Y_DIAMETER_PPM"] = (
563 current["Y_RADIUS_PPM"] * 2.0
564 )
566 selected_df = self.peakipy_data.df[
567 self.peakipy_data.df.CLUSTID.isin(list(current.CLUSTID))
568 ]
570 selected_df.to_csv(self.TEMP_INPUT_CSV)
571 fix_command = self.make_fix_command_from_parameters(
572 self.unpack_parameters_to_fix()
573 )
574 xy_bounds_command = self.make_xybound_command(*self.get_xybounds())
575 reference_planes_command = self.make_reference_planes_command(
576 self.get_reference_planes()
577 )
578 initial_fit_threshold_command = self.make_initial_fit_threshold_command(
579 self.get_initial_fit_threshold()
580 )
582 lineshape = self.lineshapes[self.select_lineshape_radiobuttons.active]
583 print(f"[yellow]Using LS = {lineshape}[/yellow]")
584 if self.checkbox_group.active == []:
585 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}"
586 else:
587 plane_index = self.select_plane.value
588 print(f"[yellow]Only fitting plane {plane_index}[/yellow]")
589 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}"
591 print(f"[blue]{fit_command}[/blue]")
592 self.fit_reports += fit_command + "<br>"
594 stdout = check_output(fit_command.split(" "))
595 self.fit_reports += stdout.decode() + "<br><hr><br>"
596 self.fit_reports = self.fit_reports.replace("\n", "<br>")
597 self.fit_reports_div.text = log_div % (log_style, self.fit_reports)
599 def save_peaks(self, event):
600 if self.savefilename.value:
601 to_save = Path(self.savefilename.value)
602 else:
603 to_save = Path(self.savefilename.placeholder)
605 if to_save.exists():
606 shutil.copy(f"{to_save}", f"{to_save}.bak")
607 print(f"Making backup {to_save}.bak")
609 print(f"[green]Saving peaks to {to_save}[/green]")
610 if to_save.suffix == ".csv":
611 self.peakipy_data.df.to_csv(to_save, float_format="%.4f", index=False)
612 else:
613 self.peakipy_data.df.to_pickle(to_save)
615 def peak_pick_callback(self, event):
616 # global so that df is updated globally
617 x_radius_ppm = 0.035
618 y_radius_ppm = 0.35
619 x_radius = x_radius_ppm * self.peakipy_data.pt_per_ppm_f2
620 y_radius = y_radius_ppm * self.peakipy_data.pt_per_ppm_f1
621 x_diameter_ppm = x_radius_ppm * 2.0
622 y_diameter_ppm = y_radius_ppm * 2.0
623 clustid = self.peakipy_data.df.CLUSTID.max() + 1
624 index = self.peakipy_data.df.INDEX.max() + 1
625 x_ppm = event.x
626 y_ppm = event.y
627 x_axis = self.peakipy_data.uc_f2.f(x_ppm, "ppm")
628 y_axis = self.peakipy_data.uc_f1.f(y_ppm, "ppm")
629 xw_hz = 20.0
630 yw_hz = 20.0
631 xw = xw_hz * self.peakipy_data.pt_per_hz_f2
632 yw = yw_hz * self.peakipy_data.pt_per_hz_f1
633 assignment = f"test_peak_{index}_{clustid}"
634 height = self.peakipy_data.data[0][int(y_axis), int(x_axis)]
635 volume = height
636 print(
637 f"""[blue]Adding peak at {assignment}: {event.x:.3f},{event.y:.3f}[/blue]"""
638 )
640 new_peak = {
641 "INDEX": index,
642 "X_PPM": x_ppm,
643 "Y_PPM": y_ppm,
644 "HEIGHT": height,
645 "VOL": volume,
646 "XW_HZ": xw_hz,
647 "YW_HZ": yw_hz,
648 "X_AXIS": int(np.floor(x_axis)), # integers
649 "Y_AXIS": int(np.floor(y_axis)), # integers
650 "X_AXISf": x_axis,
651 "Y_AXISf": y_axis,
652 "XW": xw,
653 "YW": yw,
654 "ASS": assignment,
655 "X_RADIUS_PPM": x_radius_ppm,
656 "Y_RADIUS_PPM": y_radius_ppm,
657 "X_RADIUS": x_radius,
658 "Y_RADIUS": y_radius,
659 "CLUSTID": clustid,
660 "MEMCNT": 1,
661 "X_DIAMETER_PPM": x_diameter_ppm,
662 "Y_DIAMETER_PPM": y_diameter_ppm,
663 "Edited": True,
664 "include": "yes",
665 "color": "black",
666 }
667 new_peak = {k: [v] for k, v in new_peak.items()}
668 new_peak = pd.DataFrame(new_peak)
669 self.peakipy_data.df = pd.concat(
670 [self.peakipy_data.df, new_peak], ignore_index=True
671 )
672 self.update_memcnt()
674 def slider_callback(self, dim, channel):
675 selectionIndex = self.source.selected.indices
676 current = self.peakipy_data.df.iloc[selectionIndex]
677 self.peakipy_data.df.loc[selectionIndex, f"{dim}_RADIUS"] = getattr(
678 self, f"slider_{dim}_RADIUS"
679 ).value * getattr(self.peakipy_data, f"pt_per_ppm_{channel}")
680 self.peakipy_data.df.loc[selectionIndex, f"{dim}_RADIUS_PPM"] = getattr(
681 self, f"slider_{dim}_RADIUS"
682 ).value
684 self.peakipy_data.df.loc[selectionIndex, f"{dim}_DIAMETER_PPM"] = (
685 current[f"{dim}_RADIUS_PPM"] * 2.0
686 )
687 self.peakipy_data.df.loc[selectionIndex, f"{dim}_DIAMETER"] = (
688 current[f"{dim}_RADIUS"] * 2.0
689 )
691 # set edited rows to True
692 self.peakipy_data.df.loc[selectionIndex, "Edited"] = True
693 self.source.data = ColumnDataSource.from_df(self.peakipy_data.df)
694 self.tabulator_widget.value = self.peakipy_data.df[self.tabulator_columns]
696 def slider_callback_x(self, attrname, old, new):
697 self.slider_callback("X", "f2")
699 def slider_callback_y(self, attrname, old, new):
700 self.slider_callback("Y", "f1")
702 def update_contour(self, attrname, old, new):
703 new_cs = eval(self.contour_start.value)
704 cl = new_cs * self.contour_factor ** np.arange(self.contour_num)
705 if len(cl) > 1 and np.min(np.diff(cl)) <= 0.0:
706 print(f"Setting contour levels to np.abs({cl})")
707 cl = np.abs(cl)
708 plane_index = self.select_planes_dic[self.select_plane.value]
710 pos_neg = self.pos_neg_contour_dic[self.pos_neg_contour_radiobutton.active]
711 if pos_neg == "pos/neg":
712 self.positive_contour_renderer.set_data(
713 contour_data(
714 self.x_ppm_mesh,
715 self.y_ppm_mesh,
716 self.peakipy_data.data[plane_index],
717 cl,
718 )
719 )
720 self.negative_contour_renderer.set_data(
721 contour_data(
722 self.x_ppm_mesh,
723 self.y_ppm_mesh,
724 self.peakipy_data.data[plane_index] * -1.0,
725 cl,
726 )
727 )
729 elif pos_neg == "pos":
730 self.positive_contour_renderer.set_data(
731 contour_data(
732 self.x_ppm_mesh,
733 self.y_ppm_mesh,
734 self.peakipy_data.data[plane_index],
735 cl,
736 )
737 )
738 self.negative_contour_renderer.set_data(
739 contour_data(
740 self.x_ppm_mesh,
741 self.y_ppm_mesh,
742 self.peakipy_data.data[plane_index] * 0,
743 cl,
744 )
745 )
747 elif pos_neg == "neg":
748 self.positive_contour_renderer.set_data(
749 contour_data(
750 self.x_ppm_mesh,
751 self.y_ppm_mesh,
752 self.peakipy_data.data[plane_index] * 0.0,
753 cl,
754 )
755 )
756 self.negative_contour_renderer.set_data(
757 contour_data(
758 self.x_ppm_mesh,
759 self.y_ppm_mesh,
760 self.peakipy_data.data[plane_index] * -1.0,
761 cl,
762 )
763 )
765 def exit_edit_peaks(self, event):
766 sys.exit()