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
« 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
10import typer
11import numpy as np
12import nmrglue as ng
13import pandas as pd
15from tqdm import tqdm
16from rich import print
17from skimage.filters import threshold_otsu
19from mpl_toolkits.mplot3d import axes3d
20from matplotlib.backends.backend_pdf import PdfPages
21from bokeh.models.widgets.tables import ScientificFormatter
23import plotly.io as pio
24import panel as pn
26pio.templates.default = "plotly_dark"
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)
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)
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
88pn.extension("plotly")
89pn.config.theme = "dark"
92@dataclass
93class PlotContainer:
94 main_figure: pn.pane.Plotly
95 residual_figure: pn.pane.Plotly
98@lru_cache(maxsize=1)
99def data_singleton_edit():
100 return EditData()
103@lru_cache(maxsize=1)
104def data_singleton_check():
105 return CheckData()
108@dataclass
109class EditData:
110 peaklist_path: Path = Path("./test.csv")
111 data_path: Path = Path("./test.ft2")
112 _bs: BokehScript = field(init=False)
114 def load_data(self):
115 self._bs = BokehScript(self.peaklist_path, self.data_path)
117 @property
118 def bs(self):
119 return self._bs
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)
129 def load_dataframe(self):
130 self._df = validate_fit_dataframe(pd.read_csv(self.fits_path))
132 @property
133 def df(self):
134 return self._df
137app = typer.Typer()
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"
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
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)
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)
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
218 Description
219 -----------
221 NMRPipe column headers:
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
225 Are mapped onto analysis peak list
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'
232 Or sparky peaklist
234 Assignment w1 w2 Volume Data Height lw1 (hz) lw2 (hz)
236 Clusters of peaks are selected
238 """
239 mkdir_tmp_dir(peaklist_path.parent)
240 log_path = create_log_path(peaklist_path.parent)
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
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 )
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 )
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 )
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 )
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")
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 )
310 peaks.update_df()
312 data = peaks.df
313 thres = peaks.thres
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
323 if fuda:
324 peaks.to_fuda()
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))
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)
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)
356 run_log(log_path)
358 print(
359 f"""[green]
361 ✨✨ Finished reading and clustering peaks! ✨✨
363 Use {outname} to run peakipy edit or fit.[/green]
365 """
366 )
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"
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
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()
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]
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
480 args = get_vclist(vclist, args)
481 # plot results or not
482 log_file = open(log_path, "w")
484 uc_dics = {"f1": peakipy_data.uc_f1, "f2": peakipy_data.uc_f2}
485 args["uc_dics"] = uc_dics
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)
541 # finished fitting
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)
550 save_data(df, output)
552 print(
553 """[green]
554 🍾 ✨ Finished! ✨ 🍾
555 [/green]
556 """
557 )
558 run_log(log_path)
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)
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"
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
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))
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
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)
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)
684 peak_clusters = fits.query(f"plane in @selected_planes").groupby("clustid")
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
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)
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 )
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 )
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 )
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
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 )
772 run_log(log_path)
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))
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
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()
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 )
889 mask_adjustment_controls = pn.WidgetBox(
890 "# Fitting mask adjustment", bs.slider_X_RADIUS, bs.slider_Y_RADIUS
891 )
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
901 button.on_click(fit_peaks_button_click)
903 def update_source_selected_indices(event):
904 bs.source.selected.indices = bs.tabulator_widget.selection
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)
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()
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()
946 clusters = [(row.clustid, row.memcnt) for _, row in data.df.iterrows()]
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()
978if __name__ == "__main__":
979 app()