Coverage for peakipy/cli/main.py: 89%
327 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-09-22 23:15 -0400
« prev ^ index » next coverage.py v7.4.4, created at 2024-09-22 23:15 -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
22import plotly.io as pio
23import panel as pn
25pio.templates.default = "plotly_dark"
27from peakipy.io import (
28 Peaklist,
29 LoadData,
30 Pseudo3D,
31 StrucEl,
32 PeaklistFormat,
33 OutFmt,
34 get_vclist,
35)
36from peakipy.utils import (
37 mkdir_tmp_dir,
38 create_log_path,
39 run_log,
40 df_to_rich_table,
41 write_config,
42 update_config_file,
43 update_args_with_values_from_config_file,
44 update_linewidths_from_hz_to_points,
45 update_peak_positions_from_ppm_to_points,
46 check_data_shape_is_consistent_with_dims,
47 check_for_include_column_and_add_if_missing,
48 remove_excluded_peaks,
49 warn_if_trying_to_fit_large_clusters,
50 save_data,
51)
53from peakipy.lineshapes import (
54 Lineshape,
55 calculate_lineshape_specific_height_and_fwhm,
56 calculate_peak_centers_in_ppm,
57 calculate_peak_linewidths_in_hz,
58)
59from peakipy.fitting import (
60 get_limits_for_axis_in_points,
61 deal_with_peaks_on_edge_of_spectrum,
62 select_specified_planes,
63 exclude_specified_planes,
64 unpack_xy_bounds,
65 validate_plane_selection,
66 get_fit_data_for_selected_peak_clusters,
67 make_masks_from_plane_data,
68 simulate_lineshapes_from_fitted_peak_parameters,
69 simulate_pv_pv_lineshapes_from_fitted_peak_parameters,
70 validate_fit_dataframe,
71 fit_peak_clusters,
72 FitPeaksInput,
73 FitPeaksArgs,
74)
76from peakipy.plotting import (
77 PlottingDataForPlane,
78 validate_sample_count,
79 unpack_plotting_colors,
80 create_plotly_figure,
81 create_residual_figure,
82 create_matplotlib_figure,
83)
84from peakipy.cli.edit import BokehScript
86pn.extension("plotly")
87pn.config.theme = "dark"
90@dataclass
91class PlotContainer:
92 main_figure: pn.pane.Plotly
93 residual_figure: pn.pane.Plotly
96@lru_cache(maxsize=1)
97def data_singleton_edit():
98 return EditData()
101@lru_cache(maxsize=1)
102def data_singleton_check():
103 return CheckData()
106@dataclass
107class EditData:
108 peaklist_path: Path = Path("./test.csv")
109 data_path: Path = Path("./test.ft2")
110 _bs: BokehScript = field(init=False)
112 def load_data(self):
113 self._bs = BokehScript(self.peaklist_path, self.data_path)
115 @property
116 def bs(self):
117 return self._bs
120@dataclass
121class CheckData:
122 fits_path: Path = Path("./fits.csv")
123 data_path: Path = Path("./test.ft2")
124 config_path: Path = Path("./peakipy.config")
125 _df: pd.DataFrame = field(init=False)
127 def load_dataframe(self):
128 self._df = validate_fit_dataframe(pd.read_csv(self.fits_path))
129 print("Here")
131 @property
132 def df(self):
133 return self._df
136app = typer.Typer()
139peaklist_path_help = "Path to peaklist"
140data_path_help = "Path to 2D or pseudo3D processed NMRPipe data (e.g. .ft2 or .ft3)"
141peaklist_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"
142thres_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"
143x_radius_ppm_help = "X radius in ppm of the elliptical fitting mask for each peak"
144y_radius_ppm_help = "Y radius in ppm of the elliptical fitting mask for each peak"
145dims_help = "Dimension order of your data"
148@app.command(help="Read NMRPipe/Analysis peaklist into pandas dataframe")
149def read(
150 peaklist_path: Annotated[Path, typer.Argument(help=peaklist_path_help)],
151 data_path: Annotated[Path, typer.Argument(help=data_path_help)],
152 peaklist_format: Annotated[
153 PeaklistFormat, typer.Argument(help=peaklist_format_help)
154 ],
155 thres: Annotated[Optional[float], typer.Option(help=thres_help)] = None,
156 struc_el: StrucEl = StrucEl.disk,
157 struc_size: Tuple[int, int] = (3, None), # Tuple[int, Optional[int]] = (3, None),
158 x_radius_ppm: Annotated[float, typer.Option(help=x_radius_ppm_help)] = 0.04,
159 y_radius_ppm: Annotated[float, typer.Option(help=y_radius_ppm_help)] = 0.4,
160 x_ppm_column_name: str = "Position F1",
161 y_ppm_column_name: str = "Position F2",
162 dims: Annotated[List[int], typer.Option(help=dims_help)] = [0, 1, 2],
163 outfmt: OutFmt = OutFmt.csv,
164 fuda: bool = False,
165):
166 """Read NMRPipe/Analysis peaklist into pandas dataframe
169 Parameters
170 ----------
171 peaklist_path : Path
172 Analysis2/CCPNMRv3(assign)/Sparky/NMRPipe peak list (see below)
173 data_path : Path
174 2D or pseudo3D NMRPipe data
175 peaklist_format : PeaklistFormat
176 a2 - Analysis peaklist as input (tab delimited)
177 a3 - CCPNMR v3 peaklist as input (tab delimited)
178 sparky - Sparky peaklist as input
179 pipe - NMRPipe peaklist as input
180 peakipy - peakipy peaklist.csv or .tab (originally output from peakipy read or edit)
182 thres : Optional[float]
183 Threshold for making binary mask that is used for peak clustering [default: None]
184 If set to None then threshold_otsu from scikit-image is used to determine threshold
185 struc_el : StrucEl
186 Structuring element for binary_closing [default: disk]
187 'square'|'disk'|'rectangle'
188 struc_size : Tuple[int, int]
189 Size/dimensions of structuring element [default: 3, None]
190 For square and disk first element of tuple is used (for disk value corresponds to radius).
191 For rectangle, tuple corresponds to (width,height).
192 x_radius_ppm : float
193 F2 radius in ppm for fit mask [default: 0.04]
194 y_radius_ppm : float
195 F1 radius in ppm for fit mask [default: 0.4]
196 dims : Tuple[int]
197 <planes,y,x>
198 Order of dimensions [default: 0,1,2]
199 posF2 : str
200 Name of column in Analysis2 peak list containing F2 (i.e. X_PPM)
201 peak positions [default: "Position F1"]
202 posF1 : str
203 Name of column in Analysis2 peak list containing F1 (i.e. Y_PPM)
204 peak positions [default: "Position F2"]
205 outfmt : OutFmt
206 Format of output peaklist [default: csv]
207 Create a parameter file for running fuda (params.fuda)
210 Examples
211 --------
212 peakipy read test.tab test.ft2 pipe --dims 0 --dims 1
213 peakipy read test.a2 test.ft2 a2 --thres 1e5 --dims 0 --dims 2 --dims 1
214 peakipy read ccpnTable.tsv test.ft2 a3 --y_radius 0.3 --x_radius 0.03
215 peakipy read test.csv test.ft2 peakipy --dims 0 1 2
217 Description
218 -----------
220 NMRPipe column headers:
222 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 Are mapped onto analysis peak list
226 'Number', '#', 'Position F1', 'Position F2', 'Sampled None',
227 'Assign F1', 'Assign F2', 'Assign F3', 'Height', 'Volume',
228 'Line Width F1 (Hz)', 'Line Width F2 (Hz)', 'Line Width F3 (Hz)',
229 'Merit', 'Details', 'Fit Method', 'Vol. Method'
231 Or sparky peaklist
233 Assignment w1 w2 Volume Data Height lw1 (hz) lw2 (hz)
235 Clusters of peaks are selected
237 """
238 mkdir_tmp_dir(peaklist_path.parent)
239 log_path = create_log_path(peaklist_path.parent)
241 clust_args = {
242 "struc_el": struc_el,
243 "struc_size": struc_size,
244 }
245 # name of output peaklist
246 outname = peaklist_path.parent / peaklist_path.stem
247 cluster = True
249 match peaklist_format:
250 case peaklist_format.a2:
251 # set X and Y ppm column names if not default (i.e. "Position F1" = "X_PPM"
252 # "Position F2" = "Y_PPM" ) this is due to Analysis2 often having the
253 # dimension order flipped relative to convention
254 peaks = Peaklist(
255 peaklist_path,
256 data_path,
257 fmt=PeaklistFormat.a2,
258 dims=dims,
259 radii=[x_radius_ppm, y_radius_ppm],
260 posF1=y_ppm_column_name,
261 posF2=x_ppm_column_name,
262 )
264 case peaklist_format.a3:
265 peaks = Peaklist(
266 peaklist_path,
267 data_path,
268 fmt=PeaklistFormat.a3,
269 dims=dims,
270 radii=[x_radius_ppm, y_radius_ppm],
271 )
273 case peaklist_format.sparky:
274 peaks = Peaklist(
275 peaklist_path,
276 data_path,
277 fmt=PeaklistFormat.sparky,
278 dims=dims,
279 radii=[x_radius_ppm, y_radius_ppm],
280 )
282 case peaklist_format.pipe:
283 peaks = Peaklist(
284 peaklist_path,
285 data_path,
286 fmt=PeaklistFormat.pipe,
287 dims=dims,
288 radii=[x_radius_ppm, y_radius_ppm],
289 )
291 case peaklist_format.peakipy:
292 # read in a peakipy .csv file
293 peaks = LoadData(
294 peaklist_path, data_path, fmt=PeaklistFormat.peakipy, dims=dims
295 )
296 cluster = False
297 # don't overwrite the old .csv file
298 outname = outname.parent / (outname.stem + "_new")
300 peaks.update_df()
302 data = peaks.df
303 thres = peaks.thres
305 if cluster:
306 if struc_el == StrucEl.mask_method:
307 peaks.mask_method(overlap=struc_size[0])
308 else:
309 peaks.clusters(thres=thres, **clust_args, l_struc=None)
310 else:
311 pass
313 if fuda:
314 peaks.to_fuda()
316 match outfmt.value:
317 case "csv":
318 outname = outname.with_suffix(".csv")
319 data.to_csv(outname, float_format="%.4f", index=False)
320 case "pkl":
321 outname = outname.with_suffix(".pkl")
322 data.to_pickle(outname)
324 # write config file
325 config_path = peaklist_path.parent / Path("peakipy.config")
326 config_kvs = [
327 ("dims", dims),
328 ("data_path", str(data_path)),
329 ("thres", float(thres)),
330 ("y_radius_ppm", y_radius_ppm),
331 ("x_radius_ppm", x_radius_ppm),
332 ("fit_method", "leastsq"),
333 ]
334 try:
335 update_config_file(config_path, config_kvs)
337 except json.decoder.JSONDecodeError:
338 print(
339 "\n"
340 + f"[yellow]Your {config_path} may be corrupted. Making new one (old one moved to {config_path}.bak)[/yellow]"
341 )
342 shutil.copy(f"{config_path}", f"{config_path}.bak")
343 config_dic = dict(config_kvs)
344 write_config(config_path, config_dic)
346 run_log(log_path)
348 print(
349 f"""[green]
351 ✨✨ Finished reading and clustering peaks! ✨✨
353 Use {outname} to run peakipy edit or fit.[/green]
355 """
356 )
359fix_help = "Set parameters to fix after initial lineshape fit (see docs)"
360xy_bounds_help = (
361 "Restrict fitted peak centre within +/- x and y from initial picked position"
362)
363reference_plane_index_help = (
364 "Select plane(s) to use for initial estimation of lineshape parameters"
365)
366mp_help = "Use multiprocessing"
367vclist_help = "Provide a vclist style file"
368plane_help = "Select individual planes for fitting"
369exclude_plane_help = "Exclude individual planes from fitting"
372@app.command(help="Fit NMR data to lineshape models and deconvolute overlapping peaks")
373def fit(
374 peaklist_path: Annotated[Path, typer.Argument(help=peaklist_path_help)],
375 data_path: Annotated[Path, typer.Argument(help=data_path_help)],
376 output_path: Path,
377 max_cluster_size: Optional[int] = None,
378 lineshape: Lineshape = Lineshape.PV,
379 fix: Annotated[List[str], typer.Option(help=fix_help)] = [
380 "fraction",
381 "sigma",
382 "center",
383 ],
384 xy_bounds: Annotated[Tuple[float, float], typer.Option(help=xy_bounds_help)] = (
385 0,
386 0,
387 ),
388 vclist: Annotated[Optional[Path], typer.Option(help=vclist_help)] = None,
389 plane: Annotated[Optional[List[int]], typer.Option(help=plane_help)] = None,
390 exclude_plane: Annotated[
391 Optional[List[int]], typer.Option(help=exclude_plane_help)
392 ] = None,
393 reference_plane_index: Annotated[
394 List[int], typer.Option(help=reference_plane_index_help)
395 ] = [],
396 initial_fit_threshold: Optional[float] = None,
397 jack_knife_sample_errors: bool = False,
398 mp: Annotated[bool, typer.Option(help=mp_help)] = True,
399 verbose: bool = False,
400):
401 """Fit NMR data to lineshape models and deconvolute overlapping peaks
403 Parameters
404 ----------
405 peaklist_path : Path
406 peaklist output from read_peaklist.py
407 data_path : Path
408 2D or pseudo3D NMRPipe data (single file)
409 output_path : Path
410 output peaklist "<output>.csv" will output CSV
411 format file, "<output>.tab" will give a tab delimited output
412 while "<output>.pkl" results in Pandas pickle of DataFrame
413 max_cluster_size : int
414 Maximum size of cluster to fit (i.e exclude large clusters) [default: None]
415 lineshape : Lineshape
416 Lineshape to fit [default: Lineshape.PV]
417 fix : List[str]
418 <fraction,sigma,center>
419 Parameters to fix after initial fit on summed planes [default: fraction,sigma,center]
420 xy_bounds : Tuple[float,float]
421 <x_ppm,y_ppm>
422 Bound X and Y peak centers during fit [default: (0,0) which means no bounding]
423 This can be set like so --xy-bounds 0.1 0.5
424 vclist : Optional[Path]
425 Bruker style vclist [default: None]
426 plane : Optional[List[int]]
427 Specific plane(s) to fit [default: None]
428 eg. [1,4,5] will use only planes 1, 4 and 5
429 exclude_plane : Optional[List[int]]
430 Specific plane(s) to fit [default: None]
431 eg. [1,4,5] will exclude planes 1, 4 and 5
432 initial_fit_threshold: Optional[float]
433 threshold used to select planes for fitting of initial lineshape parameters. Only planes with
434 intensities above this threshold will be included in the intial fit of summed planes.
435 mp : bool
436 Use multiprocessing [default: True]
437 verb : bool
438 Print what's going on
439 """
440 tmp_path = mkdir_tmp_dir(peaklist_path.parent)
441 log_path = create_log_path(peaklist_path.parent)
442 # number of CPUs
443 n_cpu = cpu_count()
445 # read NMR data
446 args = {}
447 config = {}
448 data_dir = peaklist_path.parent
449 args, config = update_args_with_values_from_config_file(
450 args, config_path=data_dir / "peakipy.config"
451 )
452 dims = config.get("dims", [0, 1, 2])
453 peakipy_data = LoadData(peaklist_path, data_path, dims=dims)
454 peakipy_data = check_for_include_column_and_add_if_missing(peakipy_data)
455 peakipy_data = remove_excluded_peaks(peakipy_data)
456 max_cluster_size = warn_if_trying_to_fit_large_clusters(
457 max_cluster_size, peakipy_data
458 )
459 # remove peak clusters larger than max_cluster_size
460 peakipy_data.df = peakipy_data.df[peakipy_data.df.MEMCNT <= max_cluster_size]
462 args["max_cluster_size"] = max_cluster_size
463 args["to_fix"] = fix
464 args["verbose"] = verbose
465 args["mp"] = mp
466 args["initial_fit_threshold"] = initial_fit_threshold
467 args["reference_plane_indices"] = reference_plane_index
468 args["jack_knife_sample_errors"] = jack_knife_sample_errors
470 args = get_vclist(vclist, args)
471 # plot results or not
472 log_file = open(log_path, "w")
474 uc_dics = {"f1": peakipy_data.uc_f1, "f2": peakipy_data.uc_f2}
475 args["uc_dics"] = uc_dics
477 check_data_shape_is_consistent_with_dims(peakipy_data)
478 plane_numbers, peakipy_data = select_specified_planes(plane, peakipy_data)
479 plane_numbers, peakipy_data = exclude_specified_planes(exclude_plane, peakipy_data)
480 noise = abs(threshold_otsu(peakipy_data.data))
481 args["noise"] = noise
482 args["lineshape"] = lineshape
483 xy_bounds = unpack_xy_bounds(xy_bounds, peakipy_data)
484 args["xy_bounds"] = xy_bounds
485 peakipy_data = update_linewidths_from_hz_to_points(peakipy_data)
486 peakipy_data = update_peak_positions_from_ppm_to_points(peakipy_data)
487 # prepare data for multiprocessing
488 nclusters = peakipy_data.df.CLUSTID.nunique()
489 npeaks = peakipy_data.df.shape[0]
490 if (nclusters >= n_cpu) and mp:
491 print(
492 f"[green]Using multiprocessing to fit {npeaks} peaks in {nclusters} clusters [/green]"
493 + "\n"
494 )
495 fit_peaks_args = FitPeaksInput(
496 FitPeaksArgs(**args), peakipy_data.data, config, plane_numbers
497 )
498 with (
499 Pool(processes=n_cpu) as pool,
500 tqdm(
501 total=len(peakipy_data.df.CLUSTID.unique()),
502 ascii="▱▰",
503 colour="green",
504 ) as pbar,
505 ):
506 result = [
507 pool.apply_async(
508 fit_peak_clusters,
509 args=(
510 peaklist,
511 fit_peaks_args,
512 ),
513 callback=lambda _: pbar.update(1),
514 ).get()
515 for _, peaklist in peakipy_data.df.groupby("CLUSTID")
516 ]
517 df = pd.concat([i.df for i in result], ignore_index=True)
518 for num, i in enumerate(result):
519 log_file.write(i.log + "\n")
520 else:
521 print("[green]Not using multiprocessing[/green]")
522 result = fit_peak_clusters(
523 peakipy_data.df,
524 FitPeaksInput(
525 FitPeaksArgs(**args), peakipy_data.data, config, plane_numbers
526 ),
527 )
528 df = result.df
529 log_file.write(result.log)
531 # finished fitting
533 # close log file
534 log_file.close()
535 output = Path(output_path)
536 df = calculate_lineshape_specific_height_and_fwhm(lineshape, df)
537 df = calculate_peak_centers_in_ppm(df, peakipy_data)
538 df = calculate_peak_linewidths_in_hz(df, peakipy_data)
540 save_data(df, output)
542 print(
543 """[green]
544 🍾 ✨ Finished! ✨ 🍾
545 [/green]
546 """
547 )
548 run_log(log_path)
551@app.command()
552def edit(peaklist_path: Path, data_path: Path, test: bool = False):
553 data = data_singleton_edit()
554 data.peaklist_path = peaklist_path
555 data.data_path = data_path
556 data.load_data()
557 panel_app(test=test)
560fits_help = "CSV file containing peakipy fits"
561panel_help = "Open fits in browser with an interactive panel app"
562individual_help = "Show individual peak fits as surfaces"
563label_help = "Add peak assignment labels"
564first_help = "Show only first plane"
565plane_help = "Select planes to plot"
566clusters_help = "Select clusters to plot"
567colors_help = "Customize colors for data and fit lines respectively"
568show_help = "Open interactive matplotlib window"
569outname_help = "Name of output multipage pdf"
572@app.command(help="Interactive plots for checking fits")
573def check(
574 fits_path: Annotated[Path, typer.Argument(help=fits_help)],
575 data_path: Annotated[Path, typer.Argument(help=data_path_help)],
576 panel: Annotated[bool, typer.Option(help=panel_help)] = False,
577 clusters: Annotated[Optional[List[int]], typer.Option(help=clusters_help)] = None,
578 plane: Annotated[Optional[List[int]], typer.Option(help=plane_help)] = None,
579 first: Annotated[bool, typer.Option(help=first_help)] = False,
580 show: Annotated[bool, typer.Option(help=show_help)] = False,
581 label: Annotated[bool, typer.Option(help=label_help)] = False,
582 individual: Annotated[bool, typer.Option(help=individual_help)] = False,
583 outname: Annotated[Path, typer.Option(help=outname_help)] = Path("plots.pdf"),
584 colors: Annotated[Tuple[str, str], typer.Option(help=colors_help)] = (
585 "#5e3c99",
586 "#e66101",
587 ),
588 rcount: int = 50,
589 ccount: int = 50,
590 ccpn: bool = False,
591 plotly: bool = False,
592 test: bool = False,
593):
594 """Interactive plots for checking fits
596 Parameters
597 ----------
598 fits : Path
599 data_path : Path
600 clusters : Optional[List[int]]
601 <id1,id2,etc>
602 Plot selected cluster based on clustid [default: None]
603 e.g. clusters=[2,4,6,7]
604 plane : int
605 Plot selected plane [default: 0]
606 e.g. --plane 2 will plot second plane only
607 outname : Path
608 Plot name [default: Path("plots.pdf")]
609 first : bool
610 Only plot first plane (overrides --plane option)
611 show : bool
612 Invoke plt.show() for interactive plot
613 individual : bool
614 Plot individual fitted peaks as surfaces with different colors
615 label : bool
616 Label individual peaks
617 ccpn : bool
618 for use in ccpnmr
619 rcount : int
620 row count setting for wireplot [default: 50]
621 ccount : int
622 column count setting for wireplot [default: 50]
623 colors : Tuple[str,str]
624 <data,fit>
625 plot colors [default: #5e3c99,#e66101]
626 verb : bool
627 verbose mode
628 """
629 log_path = create_log_path(fits_path.parent)
630 columns_to_print = [
631 "assignment",
632 "clustid",
633 "memcnt",
634 "plane",
635 "amp",
636 "height",
637 "center_x_ppm",
638 "center_y_ppm",
639 "fwhm_x_hz",
640 "fwhm_y_hz",
641 "lineshape",
642 ]
643 fits = validate_fit_dataframe(pd.read_csv(fits_path))
644 args = {}
645 # get dims from config file
646 config_path = data_path.parent / "peakipy.config"
647 args, config = update_args_with_values_from_config_file(args, config_path)
648 dims = config.get("dims", (1, 2, 3))
650 if panel:
651 create_check_panel(
652 fits_path=fits_path, data_path=data_path, config_path=config_path, test=test
653 )
654 return
656 ccpn_flag = ccpn
657 if ccpn_flag:
658 from ccpn.ui.gui.widgets.PlotterWidget import PlotterWidget
659 else:
660 pass
661 dic, data = ng.pipe.read(data_path)
662 pseudo3D = Pseudo3D(dic, data, dims)
664 # first only overrides plane option
665 if first:
666 selected_planes = [0]
667 else:
668 selected_planes = validate_plane_selection(plane, pseudo3D)
669 ccount = validate_sample_count(ccount)
670 rcount = validate_sample_count(rcount)
671 data_color, fit_color = unpack_plotting_colors(colors)
672 fits = get_fit_data_for_selected_peak_clusters(fits, clusters)
674 peak_clusters = fits.query(f"plane in @selected_planes").groupby("clustid")
676 # make plotting meshes
677 x = np.arange(pseudo3D.f2_size)
678 y = np.arange(pseudo3D.f1_size)
679 XY = np.meshgrid(x, y)
680 X, Y = XY
682 all_plot_data = []
683 for _, peak_cluster in peak_clusters:
684 table = df_to_rich_table(
685 peak_cluster,
686 title="",
687 columns=columns_to_print,
688 styles=["blue" for _ in columns_to_print],
689 )
690 print(table)
692 x_radius = peak_cluster.x_radius.max()
693 y_radius = peak_cluster.y_radius.max()
694 max_x, min_x = get_limits_for_axis_in_points(
695 group_axis_points=peak_cluster.center_x, mask_radius_in_points=x_radius
696 )
697 max_y, min_y = get_limits_for_axis_in_points(
698 group_axis_points=peak_cluster.center_y, mask_radius_in_points=y_radius
699 )
700 max_x, min_x, max_y, min_y = deal_with_peaks_on_edge_of_spectrum(
701 pseudo3D.data.shape, max_x, min_x, max_y, min_y
702 )
704 empty_mask_array = np.zeros((pseudo3D.f1_size, pseudo3D.f2_size), dtype=bool)
705 first_plane = peak_cluster[peak_cluster.plane == selected_planes[0]]
706 individual_masks, mask = make_masks_from_plane_data(
707 empty_mask_array, first_plane
708 )
710 # generate simulated data
711 for plane_id, plane in peak_cluster.groupby("plane"):
712 sim_data_singles = []
713 sim_data = np.zeros((pseudo3D.f1_size, pseudo3D.f2_size))
714 try:
715 (
716 sim_data,
717 sim_data_singles,
718 ) = simulate_pv_pv_lineshapes_from_fitted_peak_parameters(
719 plane, XY, sim_data, sim_data_singles
720 )
721 except:
722 (
723 sim_data,
724 sim_data_singles,
725 ) = simulate_lineshapes_from_fitted_peak_parameters(
726 plane, XY, sim_data, sim_data_singles
727 )
729 plot_data = PlottingDataForPlane(
730 pseudo3D,
731 plane_id,
732 plane,
733 X,
734 Y,
735 mask,
736 individual_masks,
737 sim_data,
738 sim_data_singles,
739 min_x,
740 max_x,
741 min_y,
742 max_y,
743 fit_color,
744 data_color,
745 rcount,
746 ccount,
747 )
748 all_plot_data.append(plot_data)
749 if plotly:
750 fig = create_plotly_figure(plot_data)
751 residual_fig = create_residual_figure(plot_data)
752 return fig, residual_fig
753 if first:
754 break
756 with PdfPages(data_path.parent / outname) as pdf:
757 for plot_data in all_plot_data:
758 create_matplotlib_figure(
759 plot_data, pdf, individual, label, ccpn_flag, show, test
760 )
762 run_log(log_path)
765def create_plotly_pane(cluster, plane):
766 data = data_singleton_check()
767 fig, residual_fig = check(
768 fits_path=data.fits_path,
769 data_path=data.data_path,
770 clusters=[cluster],
771 plane=[plane],
772 # config_path=data.config_path,
773 plotly=True,
774 )
775 fig["layout"].update(height=800, width=800)
776 residual_fig["layout"].update(width=400)
777 fig = fig.to_dict()
778 residual_fig = residual_fig.to_dict()
779 return pn.Row(pn.pane.Plotly(fig), pn.pane.Plotly(residual_fig))
782def get_cluster(cluster):
783 tabulator_stylesheet = """
784 .tabulator-cell {
785 font-size: 12px;
786 }
787 .tabulator-headers {
788 font-size: 12px;
789 }
790 """
791 data = data_singleton_check()
792 cluster_groups = data.df.groupby("clustid")
793 cluster_group = cluster_groups.get_group(cluster)
794 df_pane = pn.widgets.Tabulator(
795 cluster_group[
796 [
797 "assignment",
798 "clustid",
799 "memcnt",
800 "plane",
801 "amp",
802 "height",
803 "center_x_ppm",
804 "center_y_ppm",
805 "fwhm_x_hz",
806 "fwhm_y_hz",
807 "lineshape",
808 ]
809 ],
810 selectable=False,
811 disabled=True,
812 width=800,
813 show_index=False,
814 frozen_columns=["assignment","clustid","plane"],
815 stylesheets=[tabulator_stylesheet],
816 )
817 return df_pane
820def update_peakipy_data_on_edit_of_table(event):
821 data = data_singleton_edit()
822 column = event.column
823 row = event.row
824 value = event.value
825 data.bs.peakipy_data.df.loc[row, column] = value
826 data.bs.update_memcnt()
829def panel_app(test=False):
830 data = data_singleton_edit()
831 bs = data.bs
832 bokeh_pane = pn.pane.Bokeh(bs.p)
833 spectrum_view_settings = pn.WidgetBox(
834 "# Contour settings", bs.pos_neg_contour_radiobutton, bs.contour_start
835 )
836 save_peaklist_box = pn.WidgetBox(
837 "# Save your peaklist",
838 bs.savefilename,
839 bs.button,
840 pn.layout.Divider(),
841 bs.exit_button,
842 )
843 recluster_settings = pn.WidgetBox(
844 "# Re-cluster your peaks",
845 bs.clust_div,
846 bs.struct_el,
847 bs.struct_el_size,
848 pn.layout.Divider(),
849 bs.recluster_warning,
850 bs.recluster,
851 sizing_mode="stretch_width",
852 )
853 button = pn.widgets.Button(name="Fit selected cluster(s)", button_type="primary")
854 fit_controls = pn.WidgetBox(
855 "# Fit controls",
856 button,
857 pn.layout.Divider(),
858 bs.select_plane,
859 bs.checkbox_group,
860 pn.layout.Divider(),
861 bs.select_reference_planes_help,
862 bs.select_reference_planes,
863 pn.layout.Divider(),
864 bs.set_initial_fit_threshold_help,
865 bs.set_initial_fit_threshold,
866 pn.layout.Divider(),
867 bs.select_fixed_parameters_help,
868 bs.select_fixed_parameters,
869 pn.layout.Divider(),
870 bs.select_lineshape_radiobuttons_help,
871 bs.select_lineshape_radiobuttons,
872 )
874 mask_adjustment_controls = pn.WidgetBox(
875 "# Fitting mask adjustment", bs.slider_X_RADIUS, bs.slider_Y_RADIUS
876 )
878 # bs.source.on_change()
879 def fit_peaks_button_click(event):
880 check_app.loading = True
881 bs.fit_selected(None)
882 check_panel = create_check_panel(bs.TEMP_OUT_CSV, bs.data_path, edit_panel=True)
883 check_app.objects = check_panel.objects
884 check_app.loading = False
886 button.on_click(fit_peaks_button_click)
888 def update_source_selected_indices(event):
889 bs.source.selected.indices = bs.tabulator_widget.selection
891 # Use on_selection_changed to immediately capture the updated selection
892 bs.tabulator_widget.param.watch(update_source_selected_indices, 'selection')
893 bs.tabulator_widget.on_edit(update_peakipy_data_on_edit_of_table)
895 template = pn.template.BootstrapTemplate(
896 title="Peakipy",
897 sidebar=[mask_adjustment_controls, fit_controls],
898 )
899 spectrum = pn.Card(
900 pn.Column(
901 pn.Row(
902 bokeh_pane,
903 bs.tabulator_widget,),
904 pn.Row(
905 spectrum_view_settings,recluster_settings, save_peaklist_box,
906 ),
907 ),
908 title="Peakipy fit",
909 )
910 check_app = pn.Card(title="Peakipy check")
911 template.main.append(pn.Column(check_app, spectrum))
912 if test:
913 return
914 else:
915 template.show()
918def create_check_panel(
919 fits_path: Path,
920 data_path: Path,
921 config_path: Path = Path("./peakipy.config"),
922 edit_panel: bool = False,
923 test: bool = False,
924):
925 data = data_singleton_check()
926 data.fits_path = fits_path
927 data.data_path = data_path
928 data.config_path = config_path
929 data.load_dataframe()
931 clusters = [(row.clustid, row.memcnt) for _, row in data.df.iterrows()]
933 select_cluster = pn.widgets.Select(
934 name="Cluster (number of peaks)", options={f"{c} ({m})": c for c, m in clusters}
935 )
936 select_plane = pn.widgets.Select(
937 name="Plane", options={f"{plane}": plane for plane in data.df.plane.unique()}
938 )
939 result_table_pane = pn.bind(get_cluster, select_cluster)
940 interactive_plotly_pane = pn.bind(
941 create_plotly_pane, cluster=select_cluster, plane=select_plane
942 )
943 check_pane = pn.Card(
944 # info_pane,
945 # pn.Row(select_cluster, select_plane),
946 pn.Row(
947 pn.Column(
948 pn.Row(pn.Card(result_table_pane, title="Fitted parameters for cluster"),
949 pn.Card(select_cluster, select_plane, title="Select cluster and plane")),
950 pn.Card(interactive_plotly_pane, title="Fitted cluster"),
951 ),
952 ),
953 title="Peakipy check",
954 )
955 if edit_panel:
956 return check_pane
957 elif test:
958 return
959 else:
960 check_pane.show()
963if __name__ == "__main__":
964 app()