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
« 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
4from graphica.core.fit_models import fit_type_label
6if TYPE_CHECKING:
7 from graphica.core.dataset import Dataset
8 from graphica.models.project import ProjectModel
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 {}
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 "不明な操作"
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)
95 if not chain:
96 return f"「{dataset.name}」は処理履歴を持たない元データです。"
98 chain.reverse() # 古い処理から順に
99 steps = [describe_operation(prov) for prov in chain]
101 if len(steps) == 1:
102 body = steps[0]
103 else:
104 body = "、続いて".join(steps[:-1]) + f"を行った上で、{steps[-1]}"
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 = ""
115 return f"{origin_text}{body}を実施した(出力データセット: 「{dataset.name}」)。"