Coverage for graphica/core/methods_text.py: 93.0%

82 statements  

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

1"""処理の履歴(provenance)を日本語の説明にする。履歴パネルと「方法」の文で表記を揃えるため、ここに1つにする。""" 

2from typing import TYPE_CHECKING, Any 

3 

4from graphica.core.fit_models import fit_type_label 

5 

6if TYPE_CHECKING: 

7 from graphica.core.dataset import Dataset 

8 from graphica.models.project import ProjectModel 

9 

10 

11def describe_operation(provenance: dict[str, Any] | None) -> str: 

12 """1つの処理を要約した文(履歴パネルの項目にも、方法の文の1文にも使える)。""" 

13 if not provenance: 

14 return "不明な操作" 

15 operation = provenance.get('operation') 

16 params: dict[str, Any] = provenance.get('params') or {} 

17 

18 if operation == 'savgol': 

19 return ( 

20 f"Savitzky-Golayフィルタ(window={params.get('window_length')}, " 

21 f"polyorder={params.get('polyorder')}, deriv={params.get('deriv')})" 

22 ) 

23 if operation and operation.startswith('baseline_'): 

24 method = operation[len('baseline_'):] 

25 method_label = { 

26 'als': 'ALS法', 'polynomial': '多項式法', 

27 'rubberband': 'ラバーバンド法', 'manual': '手動点指定', 

28 }.get(method, method) 

29 return f"ベースライン補正({method_label})" 

30 if operation == 'normalize': 

31 mode = params.get('mode', '') 

32 if mode == '特定X値での強度基準': 

33 return f"規格化(X={params.get('reference_x')}での値基準)" 

34 return f"規格化({mode})" if mode else "規格化" 

35 if operation == 'resample': 

36 return f"共通X格子へのリサンプリング/補間(手法: {params.get('method')})" 

37 if operation == 'arithmetic': 

38 error_note = "、誤差を伝播" if params.get('error_propagated') else "" 

39 return f"データセット間演算({params.get('operation_symbol')}{error_note})" 

40 if operation == 'mean_sd': 

41 return f"複数データセットの平均±SD生成({params.get('n_source')}件、手法: {params.get('method')})" 

42 if operation == 'cumulative_integral': 

43 method_labels: dict[Any, str] = {'trapezoid': '台形則', 'simpson': 'Simpson則'} 

44 method_label = method_labels.get(params.get('method'), params.get('method')) 

45 return f"累積積分({method_label})" 

46 if operation == 'average_duplicate_x': 

47 return ( 

48 f"重複X値の平均化({params.get('n_duplicate_groups')}グループ、" 

49 f"{params.get('n_points_in')}点 → {params.get('n_points_out')}点)" 

50 ) 

51 if operation == 'xaxis_alignment': 

52 shift = params.get('shift') 

53 shift_text = f"{shift:+.4g}" if isinstance(shift, (int, float)) else "不明" 

54 return f"X軸アライメント(相互相関、シフト量: {shift_text})" 

55 if operation == 'histogram': 

56 density_text = "確率密度" if params.get('density') else "度数" 

57 return f"ヒストグラム(列: {params.get('column')}、{density_text}、ビン: {params.get('bins')})" 

58 if operation == 'kde': 

59 return f"カーネル密度推定(列: {params.get('column')}、評価点数: {params.get('n_points')})" 

60 if operation in ('curve_fit', 'batch_curve_fit'): 

61 fit_type = fit_type_label(params, '不明') 

62 r_squared = params.get('r_squared') 

63 r2_text = f", R²={r_squared:.4f}" if isinstance(r_squared, (int, float)) else "" 

64 return f"カーブフィット({fit_type}{r2_text})" 

65 if operation == 'multi_peak_fit': 

66 return f"多峰分離フィット({params.get('component_type', '不明')} x{params.get('n_components', '?')})" 

67 if operation == '2d_slice': 67 ↛ 68line 67 didn't jump to line 68 because the condition on line 67 was never true

68 start, end = params.get('start'), params.get('end') 

69 axis_labels: dict[Any, str] = {'x': 'X軸方向', 'y': 'Y軸方向', 'distance': '斜め方向'} 

70 axis_label = axis_labels.get(params.get('axis_kind'), params.get('axis_kind')) 

71 if start and end: 

72 return ( 

73 f"2Dマップからの1Dスライス抽出({axis_label}、" 

74 f"始点=({start[0]:.4g}, {start[1]:.4g}), 終点=({end[0]:.4g}, {end[1]:.4g}))" 

75 ) 

76 return f"2Dマップからの1Dスライス抽出({axis_label})" 

77 return operation or "不明な操作" 

78 

79 

80def generate_methods_text(dataset: "Dataset", project: "ProjectModel") -> str: 

81 """祖先から順に辿って「方法」の文にする。元データか、親が消されていればそこで止める(循環も止める)。""" 

82 chain = [] 

83 visited = set() 

84 current: "Dataset | None" = dataset 

85 while current is not None and current.provenance and current.dataset_id not in visited: 

86 visited.add(current.dataset_id) 

87 chain.append(current.provenance) 

88 source_ids = current.provenance.get('source_dataset_ids') or [] 

89 if len(source_ids) != 1: 

90 # 親が複数(データセット間の演算など)なら一本の文にならないので、ここで止めて親の名前を並べる 

91 break 

92 source_id = source_ids[0] 

93 current = next((ds for ds in project.datasets if ds.dataset_id == source_id), None) 

94 

95 if not chain: 

96 return f"「{dataset.name}」は処理履歴を持たない元データです。" 

97 

98 chain.reverse() # 古い処理から順に 

99 steps = [describe_operation(prov) for prov in chain] 

100 

101 if len(steps) == 1: 

102 body = steps[0] 

103 else: 

104 body = "、続いて".join(steps[:-1]) + f"を行った上で、{steps[-1]}" 

105 

106 root_provenance = chain[0] 

107 root_source_names = root_provenance.get('source_dataset_names') or [] 

108 if len(root_source_names) > 1: 

109 origin_text = "、".join(f"「{name}」" for name in root_source_names) + "を元データとして" 

110 elif root_source_names: 

111 origin_text = f"元データ「{root_source_names[0]}」に対し" 

112 else: 

113 origin_text = "" 

114 

115 return f"{origin_text}{body}を実施した(出力データセット: 「{dataset.name}」)。"