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

1#!/usr/bin/env python3 

2""" Script for checking fits and editing fit params 

3""" 

4import os 

5import sys 

6import shutil 

7 

8from subprocess import check_output 

9from pathlib import Path 

10 

11 

12import numpy as np 

13import pandas as pd 

14from skimage.filters import threshold_otsu 

15from rich import print 

16 

17 

18import panel as pn 

19 

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 

36 

37from peakipy.io import LoadData, StrucEl 

38from peakipy.utils import update_args_with_values_from_config_file 

39 

40log_style = "overflow:scroll;" 

41log_div = """<div style=%s>%s</div>""" 

42 

43 

44 

45 

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 = "" 

71 

72 def init(self, doc): 

73 """initialise the bokeh app""" 

74 

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" 

84 

85 @property 

86 def args(self): 

87 return self._args 

88 

89 @property 

90 def path(self): 

91 return self._path 

92 

93 @property 

94 def data_path(self): 

95 return self._data_path 

96 

97 @property 

98 def peakipy_data(self): 

99 return self._peakipy_data 

100 

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) 

105 

106 self.TEMP_OUT_CSV = self.TEMP_PATH / Path("tmp_out.csv") 

107 self.TEMP_INPUT_CSV = self.TEMP_PATH / Path("tmp.csv") 

108 

109 self.TEMP_OUT_PLOT = self.TEMP_PATH / Path("plots") 

110 self.TEMP_OUT_PLOT.mkdir(parents=True, exist_ok=True) 

111 

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 

117 

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 

134 

135 @property 

136 def tabulator_non_editable_columns(self): 

137 editors = {"X_RADIUS_PPM": None, "Y_RADIUS_PPM": None} 

138 return editors 

139 

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 """ 

149 

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 

162 

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() 

167 

168 def setup_radii_sliders(self): 

169 

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 ) 

187 

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 ) 

194 

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) 

202 

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 ) 

210 

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") 

216 

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 

226 

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 

233 

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") 

239 

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 [] 

246 

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 

252 

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") 

258 

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 

265 

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 

274 

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) 

279 

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 ) 

317 

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 ) 

339 

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) 

344 

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 ) 

357 

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 ] 

384 

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 ] 

392 

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 ) 

412 

413 self.p.on_event(DoubleTap, self.peak_pick_callback) 

414 

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 ) 

459 

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) 

480 

481 self.checkbox_group = CheckboxGroup( 

482 labels=["fit current plane only"], active=[] 

483 ) 

484 

485 self.fit_button.on_event(ButtonClick, self.fit_selected) 

486 

487 # callback for adding 

488 # source.selected.on_change('indices', callback) 

489 self.source.selected.on_change("indices", self.select_callback) 

490 

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 ) 

503 

504 self.recluster = Button(label="Re-cluster", button_type="warning") 

505 self.recluster.on_event(ButtonClick, self.recluster_peaks) 

506 

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 

528 

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) 

532 

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 

545 

546 def unpack_parameters_to_fix(self): 

547 return self.select_fixed_parameters.value.strip().split(" ") 

548 

549 def make_fix_command_from_parameters(self, parameters): 

550 command = "" 

551 for parameter in parameters: 

552 command += f" --fix {parameter}" 

553 return command 

554 

555 def fit_selected(self, event): 

556 selectionIndex = self.source.selected.indices 

557 current = self.peakipy_data.df.iloc[selectionIndex] 

558 

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 ) 

565 

566 selected_df = self.peakipy_data.df[ 

567 self.peakipy_data.df.CLUSTID.isin(list(current.CLUSTID)) 

568 ] 

569 

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 ) 

581 

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}" 

590 

591 print(f"[blue]{fit_command}[/blue]") 

592 self.fit_reports += fit_command + "<br>" 

593 

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) 

598 

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) 

604 

605 if to_save.exists(): 

606 shutil.copy(f"{to_save}", f"{to_save}.bak") 

607 print(f"Making backup {to_save}.bak") 

608 

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) 

614 

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 ) 

639 

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() 

673 

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 

683 

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 ) 

690 

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] 

695 

696 def slider_callback_x(self, attrname, old, new): 

697 self.slider_callback("X", "f2") 

698 

699 def slider_callback_y(self, attrname, old, new): 

700 self.slider_callback("Y", "f1") 

701 

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] 

709 

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 ) 

728 

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 ) 

746 

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 ) 

764 

765 def exit_edit_peaks(self, event): 

766 sys.exit()