Coverage for peakipy/cli/edit.py: 53%

316 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-09-22 23:15 -0400

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 

44class BokehScript: 

45 def __init__(self, peaklist_path: Path, data_path: Path): 

46 self._path = peaklist_path 

47 self._data_path = data_path 

48 args, config = update_args_with_values_from_config_file({}) 

49 self._dims = config.get("dims", [0, 1, 2]) 

50 self.thres = config.get("thres", 1e6) 

51 self._peakipy_data = LoadData( 

52 self._path, self._data_path, dims=self._dims, verbose=True 

53 ) 

54 # check dataframe is usable 

55 self.peakipy_data.check_data_frame() 

56 # make temporary paths 

57 self.make_temp_files() 

58 self.make_data_source() 

59 self.make_tabulator_widget() 

60 self.setup_radii_sliders() 

61 self.setup_save_buttons() 

62 self.setup_set_fixed_parameters() 

63 self.setup_xybounds() 

64 self.setup_set_reference_planes() 

65 self.setup_initial_fit_threshold() 

66 self.setup_quit_button() 

67 self.setup_plot() 

68 self.check_pane = "" 

69 

70 def init(self, doc): 

71 """initialise the bokeh app""" 

72 

73 doc.add_root( 

74 column( 

75 self.intro_div, 

76 row(column(self.p, self.doc_link), column(self.data_table, self.tabs)), 

77 sizing_mode="stretch_both", 

78 ) 

79 ) 

80 doc.title = "peakipy: Edit Fits" 

81 # doc.theme = "dark_minimal" 

82 

83 @property 

84 def args(self): 

85 return self._args 

86 

87 @property 

88 def path(self): 

89 return self._path 

90 

91 @property 

92 def data_path(self): 

93 return self._data_path 

94 

95 @property 

96 def peakipy_data(self): 

97 return self._peakipy_data 

98 

99 def make_temp_files(self): 

100 # Temp files 

101 self.TEMP_PATH = self.path.parent / Path("tmp") 

102 self.TEMP_PATH.mkdir(parents=True, exist_ok=True) 

103 

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

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

106 

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

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

109 

110 def make_data_source(self): 

111 # make datasource 

112 self.source = ColumnDataSource() 

113 self.source.data = ColumnDataSource.from_df(self.peakipy_data.df) 

114 return self.source 

115 

116 @property 

117 def tabulator_columns(self): 

118 columns = [ 

119 "ASS", 

120 "CLUSTID", 

121 "X_PPM", 

122 "Y_PPM", 

123 "X_RADIUS_PPM", 

124 "Y_RADIUS_PPM", 

125 "XW_HZ", 

126 "YW_HZ", 

127 "VOL", 

128 "include", 

129 "MEMCNT", 

130 ] 

131 return columns 

132 

133 @property 

134 def tabulator_non_editable_columns(self): 

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

136 return editors 

137 

138 def make_tabulator_widget(self): 

139 tabulator_stylesheet = """ 

140 .tabulator-cell { 

141 font-size: 12px; 

142 } 

143 .tabulator-headers { 

144 font-size: 12px; 

145 } 

146 """ 

147 self.tabulator_widget = pn.widgets.Tabulator( 

148 self.peakipy_data.df[self.tabulator_columns], 

149 editors=self.tabulator_non_editable_columns, 

150 height=500, 

151 width=800, 

152 show_index=False, 

153 frozen_columns=["ASS","CLUSTID"], 

154 stylesheets=[tabulator_stylesheet], 

155 selectable="checkbox", 

156 selection=[], 

157 ) 

158 return self.tabulator_widget 

159 

160 def select_callback(self, attrname, old, new): 

161 for col in self.peakipy_data.df.columns: 

162 self.peakipy_data.df.loc[:, col] = self.source.data[col] 

163 self.update_memcnt() 

164 

165 def setup_radii_sliders(self): 

166 # configure sliders for setting radii 

167 self.slider_X_RADIUS = Slider( 

168 title="X_RADIUS - ppm", 

169 start=0.001, 

170 end=0.200, 

171 value=0.040, 

172 step=0.001, 

173 format="0[.]000", 

174 ) 

175 self.slider_Y_RADIUS = Slider( 

176 title="Y_RADIUS - ppm", 

177 start=0.010, 

178 end=2.000, 

179 value=0.400, 

180 step=0.001, 

181 format="0[.]000", 

182 ) 

183 

184 self.slider_X_RADIUS.on_change( 

185 "value", lambda attr, old, new: self.slider_callback_x(attr, old, new) 

186 ) 

187 self.slider_Y_RADIUS.on_change( 

188 "value", lambda attr, old, new: self.slider_callback_y(attr, old, new) 

189 ) 

190 

191 def setup_save_buttons(self): 

192 # save file 

193 self.savefilename = TextInput( 

194 title="Save file as (.csv)", placeholder="edited_peaks.csv" 

195 ) 

196 self.button = Button(label="Save", button_type="success") 

197 self.button.on_event(ButtonClick, self.save_peaks) 

198 

199 def setup_set_fixed_parameters(self): 

200 self.select_fixed_parameters_help = Div( 

201 text="Select parameters to fix after initial lineshape parameters have been fitted" 

202 ) 

203 self.select_fixed_parameters = TextInput( 

204 value="fraction sigma center", width=200 

205 ) 

206 

207 def setup_xybounds(self): 

208 self.set_xybounds_help = Div( 

209 text="If floating the peak centers you can bound the fits in the x and y dimensions. Units of ppm." 

210 ) 

211 self.set_xybounds = TextInput(placeholder="e.g. 0.01 0.1") 

212 

213 def get_xybounds(self): 

214 try: 

215 x_bound, y_bound = self.set_xybounds.value.split(" ") 

216 x_bound = float(x_bound) 

217 y_bound = float(y_bound) 

218 xy_bounds = x_bound, y_bound 

219 except: 

220 xy_bounds = None, None 

221 return xy_bounds 

222 

223 def make_xybound_command(self, x_bound, y_bound): 

224 if (x_bound != None) and (y_bound != None): 

225 xy_bounds_command = f" --xy-bounds {x_bound} {y_bound}" 

226 else: 

227 xy_bounds_command = "" 

228 return xy_bounds_command 

229 

230 def setup_set_reference_planes(self): 

231 self.select_reference_planes_help = Div( 

232 text="Select reference planes (index starts at 0)" 

233 ) 

234 self.select_reference_planes = TextInput(placeholder="0 1 2 3") 

235 

236 def get_reference_planes(self): 

237 if self.select_reference_planes.value: 

238 print("You have selected1") 

239 return self.select_reference_planes.value.split(" ") 

240 else: 

241 return [] 

242 

243 def make_reference_planes_command(self, reference_plane_list): 

244 reference_plane_command = "" 

245 for plane in reference_plane_list: 

246 reference_plane_command += f" --reference-plane-index {plane}" 

247 return reference_plane_command 

248 

249 def setup_initial_fit_threshold(self): 

250 self.set_initial_fit_threshold_help = Div( 

251 text="Set an intensity threshold for selection of planes for initial estimation of lineshape parameters" 

252 ) 

253 self.set_initial_fit_threshold = TextInput(placeholder="e.g. 1e7") 

254 

255 def get_initial_fit_threshold(self): 

256 try: 

257 initial_fit_threshold = float(self.set_initial_fit_threshold.value) 

258 except ValueError: 

259 initial_fit_threshold = None 

260 return initial_fit_threshold 

261 

262 def make_initial_fit_threshold_command(self, initial_fit_threshold): 

263 if initial_fit_threshold is not None: 

264 initial_fit_threshold_command = ( 

265 f" --initial-fit-threshold {initial_fit_threshold}" 

266 ) 

267 else: 

268 initial_fit_threshold_command = "" 

269 return initial_fit_threshold_command 

270 

271 def setup_quit_button(self): 

272 # Quit button 

273 self.exit_button = Button(label="Quit", button_type="warning") 

274 self.exit_button.on_event(ButtonClick, self.exit_edit_peaks) 

275 

276 def setup_plot(self): 

277 """ " code to setup the bokeh plots""" 

278 #  make bokeh figure 

279 tools = [ 

280 "tap", 

281 "box_zoom", 

282 "lasso_select", 

283 "box_select", 

284 "wheel_zoom", 

285 "pan", 

286 "reset", 

287 ] 

288 self.p = figure( 

289 x_range=(self.peakipy_data.f2_ppm_0, self.peakipy_data.f2_ppm_1), 

290 y_range=(self.peakipy_data.f1_ppm_0, self.peakipy_data.f1_ppm_1), 

291 x_axis_label=f"{self.peakipy_data.f2_label} - ppm", 

292 y_axis_label=f"{self.peakipy_data.f1_label} - ppm", 

293 tools=tools, 

294 active_drag="pan", 

295 active_scroll="wheel_zoom", 

296 active_tap=None, 

297 ) 

298 if not self.thres: 

299 self.thres = threshold_otsu(self.peakipy_data.data[0]) 

300 self.contour_start = self.thres # contour level start value 

301 self.contour_num = 20 # number of contour levels 

302 self.contour_factor = 1.20 # scaling factor between contour levels 

303 cl = self.contour_start * self.contour_factor ** np.arange(self.contour_num) 

304 if len(cl) > 1 and np.min(np.diff(cl)) <= 0.0: 

305 print(f"Setting contour levels to np.abs({cl})") 

306 cl = np.abs(cl) 

307 self.extent = ( 

308 self.peakipy_data.f2_ppm_0, 

309 self.peakipy_data.f2_ppm_1, 

310 self.peakipy_data.f1_ppm_0, 

311 self.peakipy_data.f1_ppm_1, 

312 ) 

313 

314 self.x_ppm_mesh, self.y_ppm_mesh = np.meshgrid( 

315 self.peakipy_data.f2_ppm_scale, self.peakipy_data.f1_ppm_scale 

316 ) 

317 self.positive_contour_renderer = self.p.contour( 

318 self.x_ppm_mesh, 

319 self.y_ppm_mesh, 

320 self.peakipy_data.data[0], 

321 cl, 

322 fill_color=YlOrRd9, 

323 line_color="black", 

324 line_width=0.25, 

325 ) 

326 self.negative_contour_renderer = self.p.contour( 

327 self.x_ppm_mesh, 

328 self.y_ppm_mesh, 

329 self.peakipy_data.data[0] * -1.0, 

330 cl, 

331 fill_color=Reds256, 

332 line_color="black", 

333 line_width=0.25, 

334 ) 

335 

336 self.contour_start = TextInput( 

337 value="%.2e" % self.thres, title="Contour level:", width=100 

338 ) 

339 self.contour_start.on_change("value", self.update_contour) 

340 

341 #  plot mask outlines 

342 el = self.p.ellipse( 

343 x="X_PPM", 

344 y="Y_PPM", 

345 width="X_DIAMETER_PPM", 

346 height="Y_DIAMETER_PPM", 

347 source=self.source, 

348 fill_color="color", 

349 fill_alpha=0.25, 

350 line_dash="dotted", 

351 line_color="red", 

352 ) 

353 

354 self.p.add_tools( 

355 HoverTool( 

356 tooltips=[ 

357 ("Index", "$index"), 

358 ("Assignment", "@ASS"), 

359 ("CLUSTID", "@CLUSTID"), 

360 ("RADII", "@X_RADIUS_PPM{0.000}, @Y_RADIUS_PPM{0.000}"), 

361 ( 

362 f"{self.peakipy_data.f2_label},{self.peakipy_data.f1_label}", 

363 "$x{0.000} ppm, $y{0.000} ppm", 

364 ), 

365 ], 

366 mode="mouse", 

367 # add renderers 

368 renderers=[el], 

369 ) 

370 ) 

371 # p.toolbar.active_scroll = "auto" 

372 # draw border around spectrum area 

373 spec_border_x = [ 

374 self.peakipy_data.f2_ppm_min, 

375 self.peakipy_data.f2_ppm_min, 

376 self.peakipy_data.f2_ppm_max, 

377 self.peakipy_data.f2_ppm_max, 

378 self.peakipy_data.f2_ppm_min, 

379 ] 

380 

381 spec_border_y = [ 

382 self.peakipy_data.f1_ppm_min, 

383 self.peakipy_data.f1_ppm_max, 

384 self.peakipy_data.f1_ppm_max, 

385 self.peakipy_data.f1_ppm_min, 

386 self.peakipy_data.f1_ppm_min, 

387 ] 

388 

389 self.p.line( 

390 spec_border_x, 

391 spec_border_y, 

392 line_width=2, 

393 line_color="red", 

394 line_dash="dotted", 

395 line_alpha=0.5, 

396 ) 

397 self.p.circle(x="X_PPM", y="Y_PPM", source=self.source, color="color") 

398 # plot cluster numbers 

399 self.p.text( 

400 x="X_PPM", 

401 y="Y_PPM", 

402 text="CLUSTID", 

403 text_color="color", 

404 source=self.source, 

405 text_font_size="8pt", 

406 text_font_style="bold", 

407 ) 

408 

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

410 

411 self.pos_neg_contour_dic = {0: "pos/neg", 1: "pos", 2: "neg"} 

412 self.pos_neg_contour_radiobutton = RadioButtonGroup( 

413 labels=[ 

414 self.pos_neg_contour_dic[i] for i in self.pos_neg_contour_dic.keys() 

415 ], 

416 active=0, 

417 ) 

418 self.pos_neg_contour_radiobutton.on_change("active", self.update_contour) 

419 # call fit_peaks 

420 self.fit_button = Button(label="Fit selected cluster", button_type="primary") 

421 # lineshape selection 

422 self.lineshapes = { 

423 0: "PV", 

424 1: "V", 

425 2: "G", 

426 3: "L", 

427 4: "PV_PV", 

428 # 5: "PV_L", 

429 # 6: "PV_G", 

430 # 7: "G_L", 

431 } 

432 self.select_lineshape_radiobuttons = RadioButtonGroup( 

433 labels=[self.lineshapes[i] for i in self.lineshapes.keys()], active=0 

434 ) 

435 self.select_lineshape_radiobuttons_help = Div( 

436 text="""Choose lineshape you wish to fit. This can be Voigt (V), pseudo-Voigt (PV), Gaussian (G), Lorentzian (L). 

437 PV_PV fits a PV lineshape with independent "fraction" parameters for the direct and indirect dimensions""", 

438 ) 

439 self.clust_div = Div( 

440 text="""If you want to adjust how the peaks are automatically clustered then try changing the 

441 width/diameter/height (integer values) of the structuring element used during the binary dilation step. 

442 Increasing the size of the structuring element will cause 

443 peaks to be more readily incorporated into clusters. The mask_method scales the fitting masks based on 

444 the provided floating point value and considers any overlapping masks to be part of a cluster.""", 

445 ) 

446 self.recluster_warning = Div( 

447 text=""" 

448 Be sure to save your peak list before reclustering as 

449 any manual edits to clusters will be lost.""", 

450 ) 

451 self.intro_div = Div( 

452 text="""<h2>peakipy - interactive fit adjustment </h2> 

453 """ 

454 ) 

455 

456 self.doc_link = Div( 

457 text="<h3><a href='https://j-brady.github.io/peakipy/', target='_blank'> ℹ️ click here for documentation</a></h3>" 

458 ) 

459 self.fit_reports = "" 

460 self.fit_reports_div = Div(text="", height=400, styles={"overflow": "scroll"}) 

461 # Plane selection 

462 self.select_planes_list = [ 

463 f"{i}" 

464 for i in range(self.peakipy_data.data.shape[self.peakipy_data.planes]) 

465 ] 

466 self.select_plane = Select( 

467 title="Select plane:", 

468 value=self.select_planes_list[0], 

469 options=self.select_planes_list, 

470 ) 

471 self.select_planes_dic = { 

472 f"{i}": i 

473 for i in range(self.peakipy_data.data.shape[self.peakipy_data.planes]) 

474 } 

475 self.select_plane.on_change("value", self.update_contour) 

476 

477 self.checkbox_group = CheckboxGroup( 

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

479 ) 

480 

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

482 

483 # callback for adding 

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

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

486 

487 # reclustering tab 

488 self.struct_el = Select( 

489 title="Structuring element:", 

490 value=StrucEl.disk.value, 

491 options=[i.value for i in StrucEl], 

492 width=100, 

493 ) 

494 self.struct_el_size = TextInput( 

495 value="3", 

496 title="Size(width/radius or width,height for rectangle):", 

497 width=100, 

498 ) 

499 

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

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

502 

503 def recluster_peaks(self, event): 

504 if self.struct_el.value == "mask_method": 

505 self.struc_size = tuple( 

506 [float(i) for i in self.struct_el_size.value.split(",")] 

507 ) 

508 print(self.struc_size) 

509 self.peakipy_data.mask_method(overlap=self.struc_size[0]) 

510 else: 

511 self.struc_size = tuple( 

512 [int(i) for i in self.struct_el_size.value.split(",")] 

513 ) 

514 print(self.struc_size) 

515 self.peakipy_data.clusters( 

516 thres=eval(self.contour_start.value), 

517 struc_el=StrucEl(self.struct_el.value), 

518 struc_size=self.struc_size, 

519 ) 

520 # update data source 

521 self.source.data = ColumnDataSource.from_df(self.peakipy_data.df) 

522 self.tabulator_widget.value = self.peakipy_data.df[self.tabulator_columns] 

523 return self.peakipy_data.df 

524 

525 def update_memcnt(self): 

526 for ind, group in self.peakipy_data.df.groupby("CLUSTID"): 

527 self.peakipy_data.df.loc[group.index, "MEMCNT"] = len(group) 

528 

529 # set cluster colors (set to black if singlet peaks) 

530 self.peakipy_data.df["color"] = self.peakipy_data.df.apply( 

531 lambda x: Category20[20][int(x.CLUSTID) % 20] if x.MEMCNT > 1 else "black", 

532 axis=1, 

533 ) 

534 # change color of excluded peaks 

535 include_no = self.peakipy_data.df.include == "no" 

536 self.peakipy_data.df.loc[include_no, "color"] = "ghostwhite" 

537 # update source data 

538 self.source.data = ColumnDataSource.from_df(self.peakipy_data.df) 

539 self.tabulator_widget.value = self.peakipy_data.df[self.tabulator_columns] 

540 return self.peakipy_data.df 

541 

542 def unpack_parameters_to_fix(self): 

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

544 

545 def make_fix_command_from_parameters(self, parameters): 

546 command = "" 

547 for parameter in parameters: 

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

549 return command 

550 

551 def fit_selected(self, event): 

552 selectionIndex = self.source.selected.indices 

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

554 

555 self.peakipy_data.df.loc[selectionIndex, "X_DIAMETER_PPM"] = ( 

556 current["X_RADIUS_PPM"] * 2.0 

557 ) 

558 self.peakipy_data.df.loc[selectionIndex, "Y_DIAMETER_PPM"] = ( 

559 current["Y_RADIUS_PPM"] * 2.0 

560 ) 

561 

562 selected_df = self.peakipy_data.df[ 

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

564 ] 

565 

566 selected_df.to_csv(self.TEMP_INPUT_CSV) 

567 fix_command = self.make_fix_command_from_parameters( 

568 self.unpack_parameters_to_fix() 

569 ) 

570 xy_bounds_command = self.make_xybound_command(*self.get_xybounds()) 

571 reference_planes_command = self.make_reference_planes_command( 

572 self.get_reference_planes() 

573 ) 

574 initial_fit_threshold_command = self.make_initial_fit_threshold_command( 

575 self.get_initial_fit_threshold() 

576 ) 

577 

578 lineshape = self.lineshapes[self.select_lineshape_radiobuttons.active] 

579 print(f"[yellow]Using LS = {lineshape}[/yellow]") 

580 if self.checkbox_group.active == []: 

581 fit_command = f"peakipy fit {self.TEMP_INPUT_CSV} {self.data_path} {self.TEMP_OUT_CSV} --lineshape {lineshape}{fix_command}{reference_planes_command}{initial_fit_threshold_command}{xy_bounds_command}" 

582 else: 

583 plane_index = self.select_plane.value 

584 print(f"[yellow]Only fitting plane {plane_index}[/yellow]") 

585 fit_command = f"peakipy fit {self.TEMP_INPUT_CSV} {self.data_path} {self.TEMP_OUT_CSV} --lineshape {lineshape} --plane {plane_index}{fix_command}{reference_planes_command}{initial_fit_threshold_command}{xy_bounds_command}" 

586 

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

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

589 

590 stdout = check_output(fit_command.split(" ")) 

591 self.fit_reports += stdout.decode() + "<br><hr><br>" 

592 self.fit_reports = self.fit_reports.replace("\n", "<br>") 

593 self.fit_reports_div.text = log_div % (log_style, self.fit_reports) 

594 

595 def save_peaks(self, event): 

596 if self.savefilename.value: 

597 to_save = Path(self.savefilename.value) 

598 else: 

599 to_save = Path(self.savefilename.placeholder) 

600 

601 if to_save.exists(): 

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

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

604 

605 print(f"[green]Saving peaks to {to_save}[/green]") 

606 if to_save.suffix == ".csv": 

607 self.peakipy_data.df.to_csv(to_save, float_format="%.4f", index=False) 

608 else: 

609 self.peakipy_data.df.to_pickle(to_save) 

610 

611 def peak_pick_callback(self, event): 

612 # global so that df is updated globally 

613 x_radius_ppm = 0.035 

614 y_radius_ppm = 0.35 

615 x_radius = x_radius_ppm * self.peakipy_data.pt_per_ppm_f2 

616 y_radius = y_radius_ppm * self.peakipy_data.pt_per_ppm_f1 

617 x_diameter_ppm = x_radius_ppm * 2.0 

618 y_diameter_ppm = y_radius_ppm * 2.0 

619 clustid = self.peakipy_data.df.CLUSTID.max() + 1 

620 index = self.peakipy_data.df.INDEX.max() + 1 

621 x_ppm = event.x 

622 y_ppm = event.y 

623 x_axis = self.peakipy_data.uc_f2.f(x_ppm, "ppm") 

624 y_axis = self.peakipy_data.uc_f1.f(y_ppm, "ppm") 

625 xw_hz = 20.0 

626 yw_hz = 20.0 

627 xw = xw_hz * self.peakipy_data.pt_per_hz_f2 

628 yw = yw_hz * self.peakipy_data.pt_per_hz_f1 

629 assignment = f"test_peak_{index}_{clustid}" 

630 height = self.peakipy_data.data[0][int(y_axis), int(x_axis)] 

631 volume = height 

632 print( 

633 f"""[blue]Adding peak at {assignment}: {event.x:.3f},{event.y:.3f}[/blue]""" 

634 ) 

635 

636 new_peak = { 

637 "INDEX": index, 

638 "X_PPM": x_ppm, 

639 "Y_PPM": y_ppm, 

640 "HEIGHT": height, 

641 "VOL": volume, 

642 "XW_HZ": xw_hz, 

643 "YW_HZ": yw_hz, 

644 "X_AXIS": int(np.floor(x_axis)), # integers 

645 "Y_AXIS": int(np.floor(y_axis)), # integers 

646 "X_AXISf": x_axis, 

647 "Y_AXISf": y_axis, 

648 "XW": xw, 

649 "YW": yw, 

650 "ASS": assignment, 

651 "X_RADIUS_PPM": x_radius_ppm, 

652 "Y_RADIUS_PPM": y_radius_ppm, 

653 "X_RADIUS": x_radius, 

654 "Y_RADIUS": y_radius, 

655 "CLUSTID": clustid, 

656 "MEMCNT": 1, 

657 "X_DIAMETER_PPM": x_diameter_ppm, 

658 "Y_DIAMETER_PPM": y_diameter_ppm, 

659 "Edited": True, 

660 "include": "yes", 

661 "color": "black", 

662 } 

663 new_peak = {k: [v] for k, v in new_peak.items()} 

664 new_peak = pd.DataFrame(new_peak) 

665 self.peakipy_data.df = pd.concat( 

666 [self.peakipy_data.df, new_peak], ignore_index=True 

667 ) 

668 self.update_memcnt() 

669 

670 def slider_callback(self, dim, channel): 

671 selectionIndex = self.source.selected.indices 

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

673 self.peakipy_data.df.loc[selectionIndex, f"{dim}_RADIUS"] = getattr( 

674 self, f"slider_{dim}_RADIUS" 

675 ).value * getattr(self.peakipy_data, f"pt_per_ppm_{channel}") 

676 self.peakipy_data.df.loc[selectionIndex, f"{dim}_RADIUS_PPM"] = getattr( 

677 self, f"slider_{dim}_RADIUS" 

678 ).value 

679 

680 self.peakipy_data.df.loc[selectionIndex, f"{dim}_DIAMETER_PPM"] = ( 

681 current[f"{dim}_RADIUS_PPM"] * 2.0 

682 ) 

683 self.peakipy_data.df.loc[selectionIndex, f"{dim}_DIAMETER"] = ( 

684 current[f"{dim}_RADIUS"] * 2.0 

685 ) 

686 

687 # set edited rows to True 

688 self.peakipy_data.df.loc[selectionIndex, "Edited"] = True 

689 self.source.data = ColumnDataSource.from_df(self.peakipy_data.df) 

690 self.tabulator_widget.value = self.peakipy_data.df[self.tabulator_columns] 

691 

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

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

694 

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

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

697 

698 def update_contour(self, attrname, old, new): 

699 new_cs = eval(self.contour_start.value) 

700 cl = new_cs * self.contour_factor ** np.arange(self.contour_num) 

701 if len(cl) > 1 and np.min(np.diff(cl)) <= 0.0: 

702 print(f"Setting contour levels to np.abs({cl})") 

703 cl = np.abs(cl) 

704 plane_index = self.select_planes_dic[self.select_plane.value] 

705 

706 pos_neg = self.pos_neg_contour_dic[self.pos_neg_contour_radiobutton.active] 

707 if pos_neg == "pos/neg": 

708 self.positive_contour_renderer.set_data( 

709 contour_data( 

710 self.x_ppm_mesh, 

711 self.y_ppm_mesh, 

712 self.peakipy_data.data[plane_index], 

713 cl, 

714 ) 

715 ) 

716 self.negative_contour_renderer.set_data( 

717 contour_data( 

718 self.x_ppm_mesh, 

719 self.y_ppm_mesh, 

720 self.peakipy_data.data[plane_index] * -1.0, 

721 cl, 

722 ) 

723 ) 

724 

725 elif pos_neg == "pos": 

726 self.positive_contour_renderer.set_data( 

727 contour_data( 

728 self.x_ppm_mesh, 

729 self.y_ppm_mesh, 

730 self.peakipy_data.data[plane_index], 

731 cl, 

732 ) 

733 ) 

734 self.negative_contour_renderer.set_data( 

735 contour_data( 

736 self.x_ppm_mesh, 

737 self.y_ppm_mesh, 

738 self.peakipy_data.data[plane_index] * 0, 

739 cl, 

740 ) 

741 ) 

742 

743 elif pos_neg == "neg": 

744 self.positive_contour_renderer.set_data( 

745 contour_data( 

746 self.x_ppm_mesh, 

747 self.y_ppm_mesh, 

748 self.peakipy_data.data[plane_index] * 0.0, 

749 cl, 

750 ) 

751 ) 

752 self.negative_contour_renderer.set_data( 

753 contour_data( 

754 self.x_ppm_mesh, 

755 self.y_ppm_mesh, 

756 self.peakipy_data.data[plane_index] * -1.0, 

757 cl, 

758 ) 

759 ) 

760 

761 def exit_edit_peaks(self, event): 

762 sys.exit()