Coverage for graphica/gui/datasets/operations/peaks.py: 97.3%

59 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-12 16:38 +0000

1"""ピーク検出と、ピーク位置への自動ラベルの本体。""" 

2import logging 

3 

4import numpy as np 

5import pandas as pd 

6 

7from graphica.core.analysis import assign_peak_label_levels, calculate_peak_quantification, calculate_peaks 

8from graphica.core.dataset import Dataset 

9from graphica.gui.datasets.operations.runner import Operation 

10from graphica.gui.dialogs import PeakSettingsDialog, ResultDialog 

11 

12logger = logging.getLogger(__name__) 

13 

14_DEFAULT_PEAK_TYPE = "上に凸 (Peaks)" 

15PEAK_RESULT_WINDOW = "peaks" 

16 

17 

18def _ask_settings(op, dataset): 

19 """点数の確認と設定ダイアログ。""" 

20 if len(dataset.x_data) < 3: 

21 op.stop_with_warning("データ点数が少なすぎます (最低3点必要)。") 

22 settings = PeakSettingsDialog.get_peak_settings(op.parent) 

23 if settings is None: 

24 op.stop() 

25 return settings 

26 

27 

28def _detect(op, compute): 

29 # 検出の失敗は想定外のものも含めて知らせる(文字の X 列などで計算の途中から例外が出る) 

30 try: 

31 return compute() 

32 except Exception as e: 

33 logger.exception("ピーク検出に失敗しました") 

34 op.stop_with_warning(f"エラーが発生しました:\n{e}", title="ピーク検出エラー") 

35 

36 

37def find_peaks(op): 

38 """現在のデータセットのピーク(または谷)を探し、位置のデータセットと定量結果の表を出す。""" 

39 source = op.current_dataset() 

40 settings = _ask_settings(op, source) 

41 peak_type = settings.get("peak_type", _DEFAULT_PEAK_TYPE) 

42 

43 quant = _detect(op, lambda: calculate_peak_quantification(source.x_data, source.y_data, peak_type, settings)) 

44 

45 peak_x, peak_y = quant['peak_x'], quant['peak_y'] 

46 fwhm, area, centroid = quant['fwhm'], quant['area'], quant['centroid'] 

47 if len(peak_x) == 0: 

48 op.stop_with_information(f"指定された条件で {peak_type} は見つかりませんでした。") 

49 

50 is_valley = "下に凸" in peak_type 

51 op.host.add_derived_dataset(Dataset( 

52 name=f"{peak_type.split(' ')[0]} ({source.name})", 

53 df=pd.DataFrame({'peak_x': peak_x, 'peak_y': peak_y}), 

54 x_col_name='peak_x', y_col_name='peak_y', 

55 plot_type='Scatter', color='blue' if is_valley else 'red', linestyle='None', 

56 marker='^' if is_valley else 'v', markersize=8, 

57 use_secondary_y=source.use_secondary_y, 

58 subplot_target=source.subplot_target, 

59 ), source, op.title) 

60 

61 order = np.argsort(peak_x) 

62 text = ( 

63 f"検出された {peak_type} ({len(peak_x)}個):\n" 

64 " X座標\t\tY座標\t\tFWHM\t\t面積\t\t重心X\n" + "-" * 70 + "\n" 

65 ) 

66 for i in order: 

67 text += (f" {peak_x[i]:.4g}\t\t{peak_y[i]:.4g}\t\t{fwhm[i]:.4g}" 

68 f"\t\t{area[i]:.4g}\t\t{centroid[i]:.4g}\n") 

69 table = pd.DataFrame({ 

70 'X座標': peak_x[order], 'Y座標': peak_y[order], 'FWHM': fwhm[order], 

71 '面積': area[order], '重心X': centroid[order], 

72 }) 

73 op.show_result_window(PEAK_RESULT_WINDOW, lambda: ResultDialog("ピーク検出完了", text, op.parent, csv_data=table)) 

74 

75 

76def add_peak_labels(op): 

77 """ 

78 検出した各ピークの位置に X 値のラベルを付ける。近いピークは段を分けて縦にずらし、重ならないようにする。 

79 ずらす幅は軸の Y 表示幅に対する比率なので、スケールの違うデータでも見た目の間隔がそろう。 

80 """ 

81 dataset = op.current_dataset() 

82 settings = _ask_settings(op, dataset) 

83 peak_type = settings.get("peak_type", _DEFAULT_PEAK_TYPE) 

84 

85 peak_x, peak_y = _detect(op, lambda: calculate_peaks(dataset.x_data, dataset.y_data, peak_type, settings)) 

86 if len(peak_x) == 0: 

87 op.stop_with_information(f"指定された条件で {peak_type} は見つかりませんでした。") 

88 

89 order = np.argsort(peak_x) 

90 peak_x, peak_y = peak_x[order], peak_y[order] 

91 axis_index = dataset.subplot_target 

92 y_span = op.host.axis_y_span(axis_index) 

93 if y_span is None: 93 ↛ 94line 93 didn't jump to line 94 because the condition on line 93 was never true

94 y_span = 1.0 

95 x_span = float(np.max(peak_x) - np.min(peak_x)) if len(peak_x) > 1 else 0.0 

96 levels = assign_peak_label_levels(peak_x, x_span) 

97 

98 with op.host.undo_macro(f"{op.title}追加 ({len(peak_x)}件)", enabled=len(peak_x) > 1): 

99 for x, y, level in zip(peak_x, peak_y, levels): 

100 op.host.add_annotation(axis_index, { 

101 'type': 'text', 'text': f"{x:.4g}", 

102 'xy': (float(x), float(y + y_span * (0.06 + 0.05 * level))), 'color': '#000000', 

103 }, description=f"{op.title}追加") 

104 

105 

106PEAK_OPERATIONS = { 

107 "find_peaks": Operation("ピーク検出", find_peaks), 

108 "add_peak_labels": Operation("ピーク位置への自動ラベル", add_peak_labels), 

109}