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

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

413 

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

415 

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 ) 

460 

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) 

481 

482 self.checkbox_group = CheckboxGroup( 

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

484 ) 

485 

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

487 

488 # callback for adding 

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

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

491 

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 ) 

504 

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

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

507 

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 

529 

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) 

533 

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 

546 

547 def unpack_parameters_to_fix(self): 

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

549 

550 def make_fix_command_from_parameters(self, parameters): 

551 command = "" 

552 for parameter in parameters: 

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

554 return command 

555 

556 def fit_selected(self, event): 

557 selectionIndex = self.source.selected.indices 

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

559 

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 ) 

566 

567 selected_df = self.peakipy_data.df[ 

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

569 ] 

570 

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 ) 

582 

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

591 

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

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

594 

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) 

599 

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) 

605 

606 if to_save.exists(): 

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

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

609 

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) 

615 

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 ) 

640 

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

674 

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 

684 

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 ) 

691 

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] 

696 

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

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

699 

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

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

702 

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] 

710 

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 ) 

729 

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 ) 

747 

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 ) 

765 

766 def exit_edit_peaks(self, event): 

767 sys.exit()