Coverage for peakipy/cli/main.py: 89%

330 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-09-15 20:42 -0400

1#!/usr/bin/env python3 

2import json 

3import shutil 

4from pathlib import Path 

5from functools import lru_cache 

6from dataclasses import dataclass, field 

7from typing import Optional, Tuple, List, Annotated 

8from multiprocessing import Pool, cpu_count 

9 

10import typer 

11import numpy as np 

12import nmrglue as ng 

13import pandas as pd 

14 

15from tqdm import tqdm 

16from rich import print 

17from skimage.filters import threshold_otsu 

18 

19from mpl_toolkits.mplot3d import axes3d 

20from matplotlib.backends.backend_pdf import PdfPages 

21from bokeh.models.widgets.tables import ScientificFormatter 

22 

23import plotly.io as pio 

24import panel as pn 

25 

26pio.templates.default = "plotly_dark" 

27 

28from peakipy.io import ( 

29 Peaklist, 

30 LoadData, 

31 Pseudo3D, 

32 StrucEl, 

33 PeaklistFormat, 

34 OutFmt, 

35 get_vclist, 

36) 

37from peakipy.utils import ( 

38 mkdir_tmp_dir, 

39 create_log_path, 

40 run_log, 

41 df_to_rich_table, 

42 write_config, 

43 update_config_file, 

44 update_args_with_values_from_config_file, 

45 update_linewidths_from_hz_to_points, 

46 update_peak_positions_from_ppm_to_points, 

47 check_data_shape_is_consistent_with_dims, 

48 check_for_include_column_and_add_if_missing, 

49 remove_excluded_peaks, 

50 warn_if_trying_to_fit_large_clusters, 

51 save_data, 

52 check_for_existing_output_file_and_backup 

53) 

54 

55from peakipy.lineshapes import ( 

56 Lineshape, 

57 calculate_lineshape_specific_height_and_fwhm, 

58 calculate_peak_centers_in_ppm, 

59 calculate_peak_linewidths_in_hz, 

60) 

61from peakipy.fitting import ( 

62 get_limits_for_axis_in_points, 

63 deal_with_peaks_on_edge_of_spectrum, 

64 select_specified_planes, 

65 exclude_specified_planes, 

66 unpack_xy_bounds, 

67 validate_plane_selection, 

68 get_fit_data_for_selected_peak_clusters, 

69 make_masks_from_plane_data, 

70 simulate_lineshapes_from_fitted_peak_parameters, 

71 simulate_pv_pv_lineshapes_from_fitted_peak_parameters, 

72 validate_fit_dataframe, 

73 fit_peak_clusters, 

74 FitPeaksInput, 

75 FitPeaksArgs, 

76) 

77 

78from peakipy.plotting import ( 

79 PlottingDataForPlane, 

80 validate_sample_count, 

81 unpack_plotting_colors, 

82 create_plotly_figure, 

83 create_residual_figure, 

84 create_matplotlib_figure, 

85) 

86from peakipy.cli.edit import BokehScript 

87 

88pn.extension("plotly") 

89pn.config.theme = "dark" 

90 

91 

92@dataclass 

93class PlotContainer: 

94 main_figure: pn.pane.Plotly 

95 residual_figure: pn.pane.Plotly 

96 

97 

98@lru_cache(maxsize=1) 

99def data_singleton_edit(): 

100 return EditData() 

101 

102 

103@lru_cache(maxsize=1) 

104def data_singleton_check(): 

105 return CheckData() 

106 

107 

108@dataclass 

109class EditData: 

110 peaklist_path: Path = Path("./test.csv") 

111 data_path: Path = Path("./test.ft2") 

112 _bs: BokehScript = field(init=False) 

113 

114 def load_data(self): 

115 self._bs = BokehScript(self.peaklist_path, self.data_path) 

116 

117 @property 

118 def bs(self): 

119 return self._bs 

120 

121 

122@dataclass 

123class CheckData: 

124 fits_path: Path = Path("./fits.csv") 

125 data_path: Path = Path("./test.ft2") 

126 config_path: Path = Path("./peakipy.config") 

127 _df: pd.DataFrame = field(init=False) 

128 

129 def load_dataframe(self): 

130 self._df = validate_fit_dataframe(pd.read_csv(self.fits_path)) 

131 

132 @property 

133 def df(self): 

134 return self._df 

135 

136 

137app = typer.Typer() 

138 

139 

140peaklist_path_help = "Path to peaklist" 

141data_path_help = "Path to 2D or pseudo3D processed NMRPipe data (e.g. .ft2 or .ft3)" 

142peaklist_format_help = "The format of your peaklist. This can be a2 for CCPN Analysis version 2 style, a3 for CCPN Analysis version 3, sparky, pipe for NMRPipe, or peakipy if you want to use a previously .csv peaklist from peakipy" 

143thres_help = "Threshold for making binary mask that is used for peak clustering. If set to None then threshold_otsu from scikit-image is used to determine threshold" 

144x_radius_ppm_help = "X radius in ppm of the elliptical fitting mask for each peak" 

145y_radius_ppm_help = "Y radius in ppm of the elliptical fitting mask for each peak" 

146dims_help = "Dimension order of your data" 

147 

148 

149@app.command(help="Read NMRPipe/Analysis peaklist into pandas dataframe") 

150def read( 

151 peaklist_path: Annotated[Path, typer.Argument(help=peaklist_path_help)], 

152 data_path: Annotated[Path, typer.Argument(help=data_path_help)], 

153 peaklist_format: Annotated[ 

154 PeaklistFormat, typer.Argument(help=peaklist_format_help) 

155 ], 

156 thres: Annotated[Optional[float], typer.Option(help=thres_help)] = None, 

157 struc_el: StrucEl = StrucEl.disk, 

158 struc_size: Tuple[int, int] = (3, None), # Tuple[int, Optional[int]] = (3, None), 

159 x_radius_ppm: Annotated[float, typer.Option(help=x_radius_ppm_help)] = 0.04, 

160 y_radius_ppm: Annotated[float, typer.Option(help=y_radius_ppm_help)] = 0.4, 

161 x_ppm_column_name: str = "Position F1", 

162 y_ppm_column_name: str = "Position F2", 

163 dims: Annotated[List[int], typer.Option(help=dims_help)] = [0, 1, 2], 

164 outfmt: OutFmt = OutFmt.csv, 

165 fuda: bool = False, 

166): 

167 """Read NMRPipe/Analysis peaklist into pandas dataframe 

168 

169 

170 Parameters 

171 ---------- 

172 peaklist_path : Path 

173 Analysis2/CCPNMRv3(assign)/Sparky/NMRPipe peak list (see below) 

174 data_path : Path 

175 2D or pseudo3D NMRPipe data 

176 peaklist_format : PeaklistFormat 

177 a2 - Analysis peaklist as input (tab delimited) 

178 a3 - CCPNMR v3 peaklist as input (tab delimited) 

179 sparky - Sparky peaklist as input 

180 pipe - NMRPipe peaklist as input 

181 peakipy - peakipy peaklist.csv or .tab (originally output from peakipy read or edit) 

182 

183 thres : Optional[float] 

184 Threshold for making binary mask that is used for peak clustering [default: None] 

185 If set to None then threshold_otsu from scikit-image is used to determine threshold 

186 struc_el : StrucEl 

187 Structuring element for binary_closing [default: disk] 

188 'square'|'disk'|'rectangle' 

189 struc_size : Tuple[int, int] 

190 Size/dimensions of structuring element [default: 3, None] 

191 For square and disk first element of tuple is used (for disk value corresponds to radius). 

192 For rectangle, tuple corresponds to (width,height). 

193 x_radius_ppm : float 

194 F2 radius in ppm for fit mask [default: 0.04] 

195 y_radius_ppm : float 

196 F1 radius in ppm for fit mask [default: 0.4] 

197 dims : Tuple[int] 

198 <planes,y,x> 

199 Order of dimensions [default: 0,1,2] 

200 posF2 : str 

201 Name of column in Analysis2 peak list containing F2 (i.e. X_PPM) 

202 peak positions [default: "Position F1"] 

203 posF1 : str 

204 Name of column in Analysis2 peak list containing F1 (i.e. Y_PPM) 

205 peak positions [default: "Position F2"] 

206 outfmt : OutFmt 

207 Format of output peaklist [default: csv] 

208 Create a parameter file for running fuda (params.fuda) 

209 

210 

211 Examples 

212 -------- 

213 peakipy read test.tab test.ft2 pipe --dims 0 --dims 1 

214 peakipy read test.a2 test.ft2 a2 --thres 1e5 --dims 0 --dims 2 --dims 1 

215 peakipy read ccpnTable.tsv test.ft2 a3 --y-radius-ppm 0.3 --x_radius-ppm 0.03 

216 peakipy read test.csv test.ft2 peakipy --dims 0 --dims 1 --dims 2 

217 

218 Description 

219 ----------- 

220 

221 NMRPipe column headers: 

222 

223 INDEX X_AXIS Y_AXIS DX DY X_PPM Y_PPM X_HZ Y_HZ XW YW XW_HZ YW_HZ X1 X3 Y1 Y3 HEIGHT DHEIGHT VOL PCHI2 TYPE ASS CLUSTID MEMCNT 

224 

225 Are mapped onto analysis peak list 

226 

227 'Number', '#', 'Position F1', 'Position F2', 'Sampled None', 

228 'Assign F1', 'Assign F2', 'Assign F3', 'Height', 'Volume', 

229 'Line Width F1 (Hz)', 'Line Width F2 (Hz)', 'Line Width F3 (Hz)', 

230 'Merit', 'Details', 'Fit Method', 'Vol. Method' 

231 

232 Or sparky peaklist 

233 

234 Assignment w1 w2 Volume Data Height lw1 (hz) lw2 (hz) 

235 

236 Clusters of peaks are selected 

237 

238 """ 

239 mkdir_tmp_dir(peaklist_path.parent) 

240 log_path = create_log_path(peaklist_path.parent) 

241 

242 clust_args = { 

243 "struc_el": struc_el, 

244 "struc_size": struc_size, 

245 } 

246 # name of output peaklist 

247 outname = peaklist_path.parent / peaklist_path.stem 

248 cluster = True 

249 

250 match peaklist_format: 

251 case peaklist_format.a2: 

252 # set X and Y ppm column names if not default (i.e. "Position F1" = "X_PPM" 

253 # "Position F2" = "Y_PPM" ) this is due to Analysis2 often having the 

254 #  dimension order flipped relative to convention 

255 peaks = Peaklist( 

256 peaklist_path, 

257 data_path, 

258 fmt=PeaklistFormat.a2, 

259 dims=dims, 

260 radii=[x_radius_ppm, y_radius_ppm], 

261 posF1=y_ppm_column_name, 

262 posF2=x_ppm_column_name, 

263 ) 

264 

265 case peaklist_format.a3: 

266 peaks = Peaklist( 

267 peaklist_path, 

268 data_path, 

269 fmt=PeaklistFormat.a3, 

270 dims=dims, 

271 radii=[x_radius_ppm, y_radius_ppm], 

272 ) 

273 

274 case peaklist_format.sparky: 

275 peaks = Peaklist( 

276 peaklist_path, 

277 data_path, 

278 fmt=PeaklistFormat.sparky, 

279 dims=dims, 

280 radii=[x_radius_ppm, y_radius_ppm], 

281 ) 

282 

283 case peaklist_format.pipe: 

284 peaks = Peaklist( 

285 peaklist_path, 

286 data_path, 

287 fmt=PeaklistFormat.pipe, 

288 dims=dims, 

289 radii=[x_radius_ppm, y_radius_ppm], 

290 ) 

291 

292 case peaklist_format.peakipy: 

293 # read in a peakipy .csv file 

294 peaks = LoadData( 

295 peaklist_path, data_path, fmt=PeaklistFormat.peakipy, dims=dims 

296 ) 

297 cluster = False 

298 # don't overwrite the old .csv file 

299 outname = outname.parent / (outname.stem + "_new") 

300 

301 case peaklist_format.csv: 

302 peaks = Peaklist( 

303 peaklist_path, 

304 data_path, 

305 fmt=PeaklistFormat.csv, 

306 dims=dims, 

307 radii=[x_radius_ppm, y_radius_ppm], 

308 ) 

309 

310 peaks.update_df() 

311 

312 data = peaks.df 

313 thres = peaks.thres 

314 

315 if cluster: 

316 if struc_el == StrucEl.mask_method: 

317 peaks.mask_method(overlap=struc_size[0]) 

318 else: 

319 peaks.clusters(thres=thres, **clust_args, l_struc=None) 

320 else: 

321 pass 

322 

323 if fuda: 

324 peaks.to_fuda() 

325 

326 match outfmt.value: 

327 case "csv": 

328 outname = outname.with_suffix(".csv") 

329 data.to_csv(check_for_existing_output_file_and_backup(outname), float_format="%.4f", index=False) 

330 case "pkl": 

331 outname = outname.with_suffix(".pkl") 

332 data.to_pickle(check_for_existing_output_file_and_backup(outname)) 

333 

334 # write config file 

335 config_path = peaklist_path.parent / Path("peakipy.config") 

336 config_kvs = [ 

337 ("dims", dims), 

338 ("data_path", str(data_path)), 

339 ("thres", float(thres)), 

340 ("y_radius_ppm", y_radius_ppm), 

341 ("x_radius_ppm", x_radius_ppm), 

342 ("fit_method", "leastsq"), 

343 ] 

344 try: 

345 update_config_file(config_path, config_kvs) 

346 

347 except json.decoder.JSONDecodeError: 

348 print( 

349 "\n" 

350 + f"[yellow]Your {config_path} may be corrupted. Making new one (old one moved to {config_path}.bak)[/yellow]" 

351 ) 

352 shutil.copy(f"{config_path}", f"{config_path}.bak") 

353 config_dic = dict(config_kvs) 

354 write_config(config_path, config_dic) 

355 

356 run_log(log_path) 

357 

358 print( 

359 f"""[green] 

360 

361 ✨✨ Finished reading and clustering peaks! ✨✨ 

362 

363 Use {outname} to run peakipy edit or fit.[/green] 

364 

365 """ 

366 ) 

367 

368 

369fix_help = "Set parameters to fix after initial lineshape fit (see docs)" 

370xy_bounds_help = ( 

371 "Restrict fitted peak centre within +/- x and y from initial picked position" 

372) 

373reference_plane_index_help = ( 

374 "Select plane(s) to use for initial estimation of lineshape parameters" 

375) 

376mp_help = "Use multiprocessing" 

377vclist_help = "Provide a vclist style file" 

378plane_help = "Select individual planes for fitting" 

379exclude_plane_help = "Exclude individual planes from fitting" 

380 

381 

382@app.command(help="Fit NMR data to lineshape models and deconvolute overlapping peaks") 

383def fit( 

384 peaklist_path: Annotated[Path, typer.Argument(help=peaklist_path_help)], 

385 data_path: Annotated[Path, typer.Argument(help=data_path_help)], 

386 output_path: Path, 

387 max_cluster_size: Optional[int] = None, 

388 lineshape: Lineshape = Lineshape.PV, 

389 fix: Annotated[List[str], typer.Option(help=fix_help)] = [ 

390 "fraction", 

391 "sigma", 

392 "center", 

393 ], 

394 xy_bounds: Annotated[Tuple[float, float], typer.Option(help=xy_bounds_help)] = ( 

395 0, 

396 0, 

397 ), 

398 vclist: Annotated[Optional[Path], typer.Option(help=vclist_help)] = None, 

399 plane: Annotated[Optional[List[int]], typer.Option(help=plane_help)] = None, 

400 exclude_plane: Annotated[ 

401 Optional[List[int]], typer.Option(help=exclude_plane_help) 

402 ] = None, 

403 reference_plane_index: Annotated[ 

404 List[int], typer.Option(help=reference_plane_index_help) 

405 ] = [], 

406 initial_fit_threshold: Optional[float] = None, 

407 jack_knife_sample_errors: bool = False, 

408 mp: Annotated[bool, typer.Option(help=mp_help)] = True, 

409 verbose: bool = False, 

410): 

411 """Fit NMR data to lineshape models and deconvolute overlapping peaks 

412 

413 Parameters 

414 ---------- 

415 peaklist_path : Path 

416 peaklist output from read_peaklist.py 

417 data_path : Path 

418 2D or pseudo3D NMRPipe data (single file) 

419 output_path : Path 

420 output peaklist "<output>.csv" will output CSV 

421 format file, "<output>.tab" will give a tab delimited output 

422 while "<output>.pkl" results in Pandas pickle of DataFrame 

423 max_cluster_size : int 

424 Maximum size of cluster to fit (i.e exclude large clusters) [default: None] 

425 lineshape : Lineshape 

426 Lineshape to fit [default: Lineshape.PV] 

427 fix : List[str] 

428 <fraction,sigma,center> 

429 Parameters to fix after initial fit on summed planes [default: fraction,sigma,center] 

430 xy_bounds : Tuple[float,float] 

431 <x_ppm,y_ppm> 

432 Bound X and Y peak centers during fit [default: (0,0) which means no bounding] 

433 This can be set like so --xy-bounds 0.1 0.5 

434 vclist : Optional[Path] 

435 Bruker style vclist [default: None] 

436 plane : Optional[List[int]] 

437 Specific plane(s) to fit [default: None] 

438 eg. [1,4,5] will use only planes 1, 4 and 5 

439 exclude_plane : Optional[List[int]] 

440 Specific plane(s) to fit [default: None] 

441 eg. [1,4,5] will exclude planes 1, 4 and 5 

442 initial_fit_threshold: Optional[float] 

443 threshold used to select planes for fitting of initial lineshape parameters. Only planes with 

444 intensities above this threshold will be included in the intial fit of summed planes. 

445 mp : bool 

446 Use multiprocessing [default: True] 

447 verb : bool 

448 Print what's going on 

449 """ 

450 tmp_path = mkdir_tmp_dir(peaklist_path.parent) 

451 log_path = create_log_path(peaklist_path.parent) 

452 # number of CPUs 

453 n_cpu = cpu_count() 

454 

455 # read NMR data 

456 args = {} 

457 config = {} 

458 data_dir = peaklist_path.parent 

459 args, config = update_args_with_values_from_config_file( 

460 args, config_path=data_dir / "peakipy.config" 

461 ) 

462 dims = config.get("dims", [0, 1, 2]) 

463 peakipy_data = LoadData(peaklist_path, data_path, dims=dims) 

464 peakipy_data = check_for_include_column_and_add_if_missing(peakipy_data) 

465 peakipy_data = remove_excluded_peaks(peakipy_data) 

466 max_cluster_size = warn_if_trying_to_fit_large_clusters( 

467 max_cluster_size, peakipy_data 

468 ) 

469 # remove peak clusters larger than max_cluster_size 

470 peakipy_data.df = peakipy_data.df[peakipy_data.df.MEMCNT <= max_cluster_size] 

471 

472 args["max_cluster_size"] = max_cluster_size 

473 args["to_fix"] = fix 

474 args["verbose"] = verbose 

475 args["mp"] = mp 

476 args["initial_fit_threshold"] = initial_fit_threshold 

477 args["reference_plane_indices"] = reference_plane_index 

478 args["jack_knife_sample_errors"] = jack_knife_sample_errors 

479 

480 args = get_vclist(vclist, args) 

481 # plot results or not 

482 log_file = open(log_path, "w") 

483 

484 uc_dics = {"f1": peakipy_data.uc_f1, "f2": peakipy_data.uc_f2} 

485 args["uc_dics"] = uc_dics 

486 

487 check_data_shape_is_consistent_with_dims(peakipy_data) 

488 plane_numbers, peakipy_data = select_specified_planes(plane, peakipy_data) 

489 plane_numbers, peakipy_data = exclude_specified_planes(exclude_plane, peakipy_data) 

490 noise = abs(threshold_otsu(peakipy_data.data)) 

491 args["noise"] = noise 

492 args["lineshape"] = lineshape 

493 xy_bounds = unpack_xy_bounds(xy_bounds, peakipy_data) 

494 args["xy_bounds"] = xy_bounds 

495 peakipy_data = update_linewidths_from_hz_to_points(peakipy_data) 

496 peakipy_data = update_peak_positions_from_ppm_to_points(peakipy_data) 

497 # prepare data for multiprocessing 

498 nclusters = peakipy_data.df.CLUSTID.nunique() 

499 npeaks = peakipy_data.df.shape[0] 

500 if (nclusters >= n_cpu) and mp: 

501 print( 

502 f"[green]Using multiprocessing to fit {npeaks} peaks in {nclusters} clusters [/green]" 

503 + "\n" 

504 ) 

505 fit_peaks_args = FitPeaksInput( 

506 FitPeaksArgs(**args), peakipy_data.data, config, plane_numbers 

507 ) 

508 with ( 

509 Pool(processes=n_cpu) as pool, 

510 tqdm( 

511 total=len(peakipy_data.df.CLUSTID.unique()), 

512 ascii="▱▰", 

513 colour="green", 

514 ) as pbar, 

515 ): 

516 result = [ 

517 pool.apply_async( 

518 fit_peak_clusters, 

519 args=( 

520 peaklist, 

521 fit_peaks_args, 

522 ), 

523 callback=lambda _: pbar.update(1), 

524 ).get() 

525 for _, peaklist in peakipy_data.df.groupby("CLUSTID") 

526 ] 

527 df = pd.concat([i.df for i in result], ignore_index=True) 

528 for num, i in enumerate(result): 

529 log_file.write(i.log + "\n") 

530 else: 

531 print("[green]Not using multiprocessing[/green]") 

532 result = fit_peak_clusters( 

533 peakipy_data.df, 

534 FitPeaksInput( 

535 FitPeaksArgs(**args), peakipy_data.data, config, plane_numbers 

536 ), 

537 ) 

538 df = result.df 

539 log_file.write(result.log) 

540 

541 # finished fitting 

542 

543 # close log file 

544 log_file.close() 

545 output = Path(output_path) 

546 df = calculate_lineshape_specific_height_and_fwhm(lineshape, df) 

547 df = calculate_peak_centers_in_ppm(df, peakipy_data) 

548 df = calculate_peak_linewidths_in_hz(df, peakipy_data) 

549 

550 save_data(df, output) 

551 

552 print( 

553 """[green] 

554 🍾 ✨ Finished! ✨ 🍾 

555 [/green] 

556 """ 

557 ) 

558 run_log(log_path) 

559 

560 

561@app.command() 

562def edit(peaklist_path: Path, data_path: Path, test: bool = False): 

563 data = data_singleton_edit() 

564 data.peaklist_path = peaklist_path 

565 data.data_path = data_path 

566 data.load_data() 

567 panel_app(test=test) 

568 

569 

570fits_help = "CSV file containing peakipy fits" 

571panel_help = "Open fits in browser with an interactive panel app" 

572individual_help = "Show individual peak fits as surfaces" 

573label_help = "Add peak assignment labels" 

574first_help = "Show only first plane" 

575plane_help = "Select planes to plot" 

576clusters_help = "Select clusters to plot" 

577colors_help = "Customize colors for data and fit lines respectively" 

578show_help = "Open interactive matplotlib window" 

579outname_help = "Name of output multipage pdf" 

580 

581 

582@app.command(help="Interactive plots for checking fits") 

583def check( 

584 fits_path: Annotated[Path, typer.Argument(help=fits_help)], 

585 data_path: Annotated[Path, typer.Argument(help=data_path_help)], 

586 panel: Annotated[bool, typer.Option(help=panel_help)] = False, 

587 clusters: Annotated[Optional[List[int]], typer.Option(help=clusters_help)] = None, 

588 plane: Annotated[Optional[List[int]], typer.Option(help=plane_help)] = None, 

589 first: Annotated[bool, typer.Option(help=first_help)] = False, 

590 show: Annotated[bool, typer.Option(help=show_help)] = False, 

591 label: Annotated[bool, typer.Option(help=label_help)] = False, 

592 individual: Annotated[bool, typer.Option(help=individual_help)] = False, 

593 outname: Annotated[Path, typer.Option(help=outname_help)] = Path("plots.pdf"), 

594 colors: Annotated[Tuple[str, str], typer.Option(help=colors_help)] = ( 

595 "#5e3c99", 

596 "#e66101", 

597 ), 

598 rcount: int = 50, 

599 ccount: int = 50, 

600 ccpn: bool = False, 

601 plotly: bool = False, 

602 test: bool = False, 

603): 

604 """Interactive plots for checking fits 

605 

606 Parameters 

607 ---------- 

608 fits : Path 

609 data_path : Path 

610 clusters : Optional[List[int]] 

611 <id1,id2,etc> 

612 Plot selected cluster based on clustid [default: None] 

613 e.g. clusters=[2,4,6,7] 

614 plane : int 

615 Plot selected plane [default: 0] 

616 e.g. --plane 2 will plot second plane only 

617 outname : Path 

618 Plot name [default: Path("plots.pdf")] 

619 first : bool 

620 Only plot first plane (overrides --plane option) 

621 show : bool 

622 Invoke plt.show() for interactive plot 

623 individual : bool 

624 Plot individual fitted peaks as surfaces with different colors 

625 label : bool 

626 Label individual peaks 

627 ccpn : bool 

628 for use in ccpnmr 

629 rcount : int 

630 row count setting for wireplot [default: 50] 

631 ccount : int 

632 column count setting for wireplot [default: 50] 

633 colors : Tuple[str,str] 

634 <data,fit> 

635 plot colors [default: #5e3c99,#e66101] 

636 verb : bool 

637 verbose mode 

638 """ 

639 log_path = create_log_path(fits_path.parent) 

640 columns_to_print = [ 

641 "assignment", 

642 "clustid", 

643 "memcnt", 

644 "plane", 

645 "amp", 

646 "height", 

647 "center_x_ppm", 

648 "center_y_ppm", 

649 "fwhm_x_hz", 

650 "fwhm_y_hz", 

651 "lineshape", 

652 ] 

653 fits = validate_fit_dataframe(pd.read_csv(fits_path)) 

654 args = {} 

655 # get dims from config file 

656 config_path = data_path.parent / "peakipy.config" 

657 args, config = update_args_with_values_from_config_file(args, config_path) 

658 dims = config.get("dims", (1, 2, 3)) 

659 

660 if panel: 

661 create_check_panel( 

662 fits_path=fits_path, data_path=data_path, config_path=config_path, test=test 

663 ) 

664 return 

665 

666 ccpn_flag = ccpn 

667 if ccpn_flag: 

668 from ccpn.ui.gui.widgets.PlotterWidget import PlotterWidget 

669 else: 

670 pass 

671 dic, data = ng.pipe.read(data_path) 

672 pseudo3D = Pseudo3D(dic, data, dims) 

673 

674 # first only overrides plane option 

675 if first: 

676 selected_planes = [0] 

677 else: 

678 selected_planes = validate_plane_selection(plane, pseudo3D) 

679 ccount = validate_sample_count(ccount) 

680 rcount = validate_sample_count(rcount) 

681 data_color, fit_color = unpack_plotting_colors(colors) 

682 fits = get_fit_data_for_selected_peak_clusters(fits, clusters) 

683 

684 peak_clusters = fits.query(f"plane in @selected_planes").groupby("clustid") 

685 

686 # make plotting meshes 

687 x = np.arange(pseudo3D.f2_size) 

688 y = np.arange(pseudo3D.f1_size) 

689 XY = np.meshgrid(x, y) 

690 X, Y = XY 

691 

692 all_plot_data = [] 

693 for _, peak_cluster in peak_clusters: 

694 table = df_to_rich_table( 

695 peak_cluster, 

696 title="", 

697 columns=columns_to_print, 

698 styles=["blue" for _ in columns_to_print], 

699 ) 

700 print(table) 

701 

702 x_radius = peak_cluster.x_radius.max() 

703 y_radius = peak_cluster.y_radius.max() 

704 max_x, min_x = get_limits_for_axis_in_points( 

705 group_axis_points=peak_cluster.center_x, mask_radius_in_points=x_radius 

706 ) 

707 max_y, min_y = get_limits_for_axis_in_points( 

708 group_axis_points=peak_cluster.center_y, mask_radius_in_points=y_radius 

709 ) 

710 max_x, min_x, max_y, min_y = deal_with_peaks_on_edge_of_spectrum( 

711 pseudo3D.data.shape, max_x, min_x, max_y, min_y 

712 ) 

713 

714 empty_mask_array = np.zeros((pseudo3D.f1_size, pseudo3D.f2_size), dtype=bool) 

715 first_plane = peak_cluster[peak_cluster.plane == selected_planes[0]] 

716 individual_masks, mask = make_masks_from_plane_data( 

717 empty_mask_array, first_plane 

718 ) 

719 

720 # generate simulated data 

721 for plane_id, plane in peak_cluster.groupby("plane"): 

722 sim_data_singles = [] 

723 sim_data = np.zeros((pseudo3D.f1_size, pseudo3D.f2_size)) 

724 try: 

725 ( 

726 sim_data, 

727 sim_data_singles, 

728 ) = simulate_pv_pv_lineshapes_from_fitted_peak_parameters( 

729 plane, XY, sim_data, sim_data_singles 

730 ) 

731 except: 

732 ( 

733 sim_data, 

734 sim_data_singles, 

735 ) = simulate_lineshapes_from_fitted_peak_parameters( 

736 plane, XY, sim_data, sim_data_singles 

737 ) 

738 

739 plot_data = PlottingDataForPlane( 

740 pseudo3D, 

741 plane_id, 

742 plane, 

743 X, 

744 Y, 

745 mask, 

746 individual_masks, 

747 sim_data, 

748 sim_data_singles, 

749 min_x, 

750 max_x, 

751 min_y, 

752 max_y, 

753 fit_color, 

754 data_color, 

755 rcount, 

756 ccount, 

757 ) 

758 all_plot_data.append(plot_data) 

759 if plotly: 

760 fig = create_plotly_figure(plot_data) 

761 residual_fig = create_residual_figure(plot_data) 

762 return fig, residual_fig 

763 if first: 

764 break 

765 

766 with PdfPages(data_path.parent / outname) as pdf: 

767 for plot_data in all_plot_data: 

768 create_matplotlib_figure( 

769 plot_data, pdf, individual, label, ccpn_flag, show, test 

770 ) 

771 

772 run_log(log_path) 

773 

774 

775def create_plotly_pane(cluster, plane): 

776 data = data_singleton_check() 

777 fig, residual_fig = check( 

778 fits_path=data.fits_path, 

779 data_path=data.data_path, 

780 clusters=[cluster], 

781 plane=[plane], 

782 # config_path=data.config_path, 

783 plotly=True, 

784 ) 

785 fig["layout"].update(height=800, width=800) 

786 residual_fig["layout"].update(width=400) 

787 fig = fig.to_dict() 

788 residual_fig = residual_fig.to_dict() 

789 return pn.Row(pn.pane.Plotly(fig), pn.pane.Plotly(residual_fig)) 

790 

791 

792def get_cluster(cluster): 

793 tabulator_stylesheet = """ 

794 .tabulator-cell { 

795 font-size: 12px; 

796 } 

797 .tabulator-headers { 

798 font-size: 12px; 

799 } 

800 """ 

801 table_formatters = { 

802 "amp": ScientificFormatter(precision=3), 

803 "height": ScientificFormatter(precision=3), 

804 } 

805 data = data_singleton_check() 

806 cluster_groups = data.df.groupby("clustid") 

807 cluster_group = cluster_groups.get_group(cluster) 

808 df_pane = pn.widgets.Tabulator( 

809 cluster_group[ 

810 [ 

811 "assignment", 

812 "clustid", 

813 "memcnt", 

814 "plane", 

815 "amp", 

816 "height", 

817 "center_x_ppm", 

818 "center_y_ppm", 

819 "fwhm_x_hz", 

820 "fwhm_y_hz", 

821 "lineshape", 

822 ] 

823 ], 

824 selectable=False, 

825 disabled=True, 

826 width=800, 

827 show_index=False, 

828 frozen_columns=["assignment","clustid","plane"], 

829 stylesheets=[tabulator_stylesheet], 

830 formatters=table_formatters, 

831 ) 

832 return df_pane 

833 

834 

835def update_peakipy_data_on_edit_of_table(event): 

836 data = data_singleton_edit() 

837 column = event.column 

838 row = event.row 

839 value = event.value 

840 data.bs.peakipy_data.df.loc[row, column] = value 

841 data.bs.update_memcnt() 

842 

843 

844def panel_app(test=False): 

845 data = data_singleton_edit() 

846 bs = data.bs 

847 bokeh_pane = pn.pane.Bokeh(bs.p) 

848 spectrum_view_settings = pn.WidgetBox( 

849 "# Contour settings", bs.pos_neg_contour_radiobutton, bs.contour_start 

850 ) 

851 save_peaklist_box = pn.WidgetBox( 

852 "# Save your peaklist", 

853 bs.savefilename, 

854 bs.button, 

855 pn.layout.Divider(), 

856 bs.exit_button, 

857 ) 

858 recluster_settings = pn.WidgetBox( 

859 "# Re-cluster your peaks", 

860 bs.clust_div, 

861 bs.struct_el, 

862 bs.struct_el_size, 

863 pn.layout.Divider(), 

864 bs.recluster_warning, 

865 bs.recluster, 

866 sizing_mode="stretch_width", 

867 ) 

868 button = pn.widgets.Button(name="Fit selected cluster(s)", button_type="primary") 

869 fit_controls = pn.WidgetBox( 

870 "# Fit controls", 

871 button, 

872 pn.layout.Divider(), 

873 bs.select_plane, 

874 bs.checkbox_group, 

875 pn.layout.Divider(), 

876 bs.select_reference_planes_help, 

877 bs.select_reference_planes, 

878 pn.layout.Divider(), 

879 bs.set_initial_fit_threshold_help, 

880 bs.set_initial_fit_threshold, 

881 pn.layout.Divider(), 

882 bs.select_fixed_parameters_help, 

883 bs.select_fixed_parameters, 

884 pn.layout.Divider(), 

885 bs.select_lineshape_radiobuttons_help, 

886 bs.select_lineshape_radiobuttons, 

887 ) 

888 

889 mask_adjustment_controls = pn.WidgetBox( 

890 "# Fitting mask adjustment", bs.slider_X_RADIUS, bs.slider_Y_RADIUS 

891 ) 

892 

893 # bs.source.on_change() 

894 def fit_peaks_button_click(event): 

895 check_app.loading = True 

896 bs.fit_selected(None) 

897 check_panel = create_check_panel(bs.TEMP_OUT_CSV, bs.data_path, edit_panel=True) 

898 check_app.objects = check_panel.objects 

899 check_app.loading = False 

900 

901 button.on_click(fit_peaks_button_click) 

902 

903 def update_source_selected_indices(event): 

904 bs.source.selected.indices = bs.tabulator_widget.selection 

905 

906 # Use on_selection_changed to immediately capture the updated selection 

907 bs.tabulator_widget.param.watch(update_source_selected_indices, 'selection') 

908 bs.tabulator_widget.on_edit(update_peakipy_data_on_edit_of_table) 

909 

910 template = pn.template.BootstrapTemplate( 

911 title="Peakipy", 

912 sidebar=[mask_adjustment_controls, fit_controls], 

913 ) 

914 spectrum = pn.Card( 

915 pn.Column( 

916 pn.Row( 

917 bokeh_pane, 

918 bs.tabulator_widget,), 

919 pn.Row( 

920 spectrum_view_settings,recluster_settings, save_peaklist_box, 

921 ), 

922 ), 

923 title="Peakipy fit", 

924 ) 

925 check_app = pn.Card(title="Peakipy check") 

926 template.main.append(pn.Column(check_app, spectrum)) 

927 if test: 

928 return 

929 else: 

930 template.show() 

931 

932 

933def create_check_panel( 

934 fits_path: Path, 

935 data_path: Path, 

936 config_path: Path = Path("./peakipy.config"), 

937 edit_panel: bool = False, 

938 test: bool = False, 

939): 

940 data = data_singleton_check() 

941 data.fits_path = fits_path 

942 data.data_path = data_path 

943 data.config_path = config_path 

944 data.load_dataframe() 

945 

946 clusters = [(row.clustid, row.memcnt) for _, row in data.df.iterrows()] 

947 

948 select_cluster = pn.widgets.Select( 

949 name="Cluster (number of peaks)", options={f"{c} ({m})": c for c, m in clusters} 

950 ) 

951 select_plane = pn.widgets.Select( 

952 name="Plane", options={f"{plane}": plane for plane in data.df.plane.unique()} 

953 ) 

954 result_table_pane = pn.bind(get_cluster, select_cluster) 

955 interactive_plotly_pane = pn.bind( 

956 create_plotly_pane, cluster=select_cluster, plane=select_plane 

957 ) 

958 check_pane = pn.Card( 

959 # info_pane, 

960 # pn.Row(select_cluster, select_plane), 

961 pn.Row( 

962 pn.Column( 

963 pn.Row(pn.Card(result_table_pane, title="Fitted parameters for cluster"), 

964 pn.Card(select_cluster, select_plane, title="Select cluster and plane")), 

965 pn.Card(interactive_plotly_pane, title="Fitted cluster"), 

966 ), 

967 ), 

968 title="Peakipy check", 

969 ) 

970 if edit_panel: 

971 return check_pane 

972 elif test: 

973 return 

974 else: 

975 check_pane.show() 

976 

977 

978if __name__ == "__main__": 

979 app()