graphica-plot 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- graphica/Graphica.ico +0 -0
- graphica/__init__.py +1 -0
- graphica/__main__.py +111 -0
- graphica/assets/__init__.py +0 -0
- graphica/assets/icons/__init__.py +0 -0
- graphica/assets/icons/arrow-right.svg +21 -0
- graphica/assets/icons/bold.svg +20 -0
- graphica/assets/icons/calculator.svg +26 -0
- graphica/assets/icons/chart-histogram.svg +24 -0
- graphica/assets/icons/chart-line.svg +20 -0
- graphica/assets/icons/chevron-down.svg +19 -0
- graphica/assets/icons/chevron-right.svg +19 -0
- graphica/assets/icons/color-swatch.svg +22 -0
- graphica/assets/icons/column-insert-right.svg +21 -0
- graphica/assets/icons/column-remove.svg +21 -0
- graphica/assets/icons/copy.svg +20 -0
- graphica/assets/icons/download.svg +21 -0
- graphica/assets/icons/edit.svg +21 -0
- graphica/assets/icons/eye-off.svg +21 -0
- graphica/assets/icons/eye.svg +20 -0
- graphica/assets/icons/file-plus.svg +22 -0
- graphica/assets/icons/folder-plus.svg +21 -0
- graphica/assets/icons/folder.svg +19 -0
- graphica/assets/icons/highlight.svg +22 -0
- graphica/assets/icons/history.svg +20 -0
- graphica/assets/icons/italic.svg +21 -0
- graphica/assets/icons/layout-grid.svg +22 -0
- graphica/assets/icons/math-function.svg +22 -0
- graphica/assets/icons/message-2.svg +21 -0
- graphica/assets/icons/mountain.svg +20 -0
- graphica/assets/icons/palette.svg +22 -0
- graphica/assets/icons/pointer.svg +19 -0
- graphica/assets/icons/refresh.svg +20 -0
- graphica/assets/icons/row-insert-bottom.svg +21 -0
- graphica/assets/icons/row-remove.svg +21 -0
- graphica/assets/icons/search.svg +20 -0
- graphica/assets/icons/select-all.svg +35 -0
- graphica/assets/icons/subscript.svg +20 -0
- graphica/assets/icons/superscript.svg +20 -0
- graphica/assets/icons/table.svg +21 -0
- graphica/assets/icons/trash.svg +23 -0
- graphica/assets/icons/typography.svg +23 -0
- graphica/assets/icons/x.svg +20 -0
- graphica/core/__init__.py +0 -0
- graphica/core/analysis.py +1278 -0
- graphica/core/app_paths.py +35 -0
- graphica/core/axis_settings.py +128 -0
- graphica/core/caption_export.py +47 -0
- graphica/core/color_palettes.py +56 -0
- graphica/core/commands.py +226 -0
- graphica/core/cvd_simulation.py +49 -0
- graphica/core/dataset.py +443 -0
- graphica/core/diagnostics.py +105 -0
- graphica/core/excel_utils.py +58 -0
- graphica/core/fit_models.py +379 -0
- graphica/core/grid_data.py +128 -0
- graphica/core/i18n.py +44 -0
- graphica/core/json_utils.py +27 -0
- graphica/core/label_utils.py +21 -0
- graphica/core/methods_text.py +115 -0
- graphica/core/named_colors.py +126 -0
- graphica/core/plugin_api.py +515 -0
- graphica/core/plugin_context.py +167 -0
- graphica/core/plugin_install.py +82 -0
- graphica/core/plugin_manifest.py +67 -0
- graphica/core/plugin_testing.py +213 -0
- graphica/core/plugin_types.py +118 -0
- graphica/core/provenance.py +19 -0
- graphica/core/report_export.py +51 -0
- graphica/core/safe_eval.py +194 -0
- graphica/core/script_export.py +272 -0
- graphica/core/translations_en.py +530 -0
- graphica/core/unit_conversion.py +51 -0
- graphica/core/update_check.py +43 -0
- graphica/core/version.py +14 -0
- graphica/gui/__init__.py +0 -0
- graphica/gui/app_settings.py +119 -0
- graphica/gui/axis_bindings.py +173 -0
- graphica/gui/binding.py +135 -0
- graphica/gui/builders/__init__.py +1 -0
- graphica/gui/builders/axis_panel.py +431 -0
- graphica/gui/builders/canvas_area.py +176 -0
- graphica/gui/builders/common.py +120 -0
- graphica/gui/builders/dataset_panel.py +433 -0
- graphica/gui/builders/property_sections.py +208 -0
- graphica/gui/canvas.py +519 -0
- graphica/gui/color_history.py +47 -0
- graphica/gui/color_picker_widget.py +198 -0
- graphica/gui/crash_handler.py +77 -0
- graphica/gui/cvd_preview.py +23 -0
- graphica/gui/data_editor.py +761 -0
- graphica/gui/data_import_flow.py +355 -0
- graphica/gui/dataset_bindings.py +107 -0
- graphica/gui/dataset_style_icon.py +80 -0
- graphica/gui/datasets/__init__.py +1 -0
- graphica/gui/datasets/actions_menu.py +114 -0
- graphica/gui/datasets/colors.py +218 -0
- graphica/gui/datasets/fitting.py +70 -0
- graphica/gui/datasets/host.py +162 -0
- graphica/gui/datasets/operations/__init__.py +1 -0
- graphica/gui/datasets/operations/fitting.py +468 -0
- graphica/gui/datasets/operations/peaks.py +109 -0
- graphica/gui/datasets/operations/processing.py +659 -0
- graphica/gui/datasets/operations/runner.py +148 -0
- graphica/gui/datasets/operations/transfer.py +227 -0
- graphica/gui/datasets/order.py +310 -0
- graphica/gui/datasets/overlays.py +74 -0
- graphica/gui/datasets/peaks.py +21 -0
- graphica/gui/datasets/plugin_runs.py +99 -0
- graphica/gui/datasets/processing.py +68 -0
- graphica/gui/datasets/property_panel.py +538 -0
- graphica/gui/datasets/transfer.py +36 -0
- graphica/gui/detached_canvas_window.py +22 -0
- graphica/gui/dialogs/__init__.py +137 -0
- graphica/gui/dialogs/analysis.py +1322 -0
- graphica/gui/dialogs/app.py +1015 -0
- graphica/gui/dialogs/appearance.py +851 -0
- graphica/gui/dialogs/data_edit.py +577 -0
- graphica/gui/dialogs/data_import.py +679 -0
- graphica/gui/dialogs/export.py +433 -0
- graphica/gui/dock_layout.py +176 -0
- graphica/gui/export_preview_panel.py +321 -0
- graphica/gui/export_settings.py +16 -0
- graphica/gui/file_association.py +97 -0
- graphica/gui/icon_utils.py +44 -0
- graphica/gui/main_app_window.py +210 -0
- graphica/gui/main_window.py +1151 -0
- graphica/gui/mathtext_preview.py +168 -0
- graphica/gui/menu_bar.py +320 -0
- graphica/gui/minimap_widget.py +151 -0
- graphica/gui/mixins/__init__.py +0 -0
- graphica/gui/mixins/export_mixin.py +520 -0
- graphica/gui/mixins/help_mixin.py +127 -0
- graphica/gui/mixins/project_io_mixin.py +289 -0
- graphica/gui/mixins/quick_access_mixin.py +194 -0
- graphica/gui/mixins/ui_setup_mixin.py +206 -0
- graphica/gui/notify.py +61 -0
- graphica/gui/panels/__init__.py +43 -0
- graphica/gui/panels/axis_settings.py +605 -0
- graphica/gui/panels/dataset_tree.py +424 -0
- graphica/gui/plot_type_drawers.py +131 -0
- graphica/gui/plugin_context.py +119 -0
- graphica/gui/project_files.py +361 -0
- graphica/gui/provenance_panel.py +65 -0
- graphica/gui/rendering/__init__.py +1 -0
- graphica/gui/rendering/annotations.py +177 -0
- graphica/gui/rendering/appearance.py +368 -0
- graphica/gui/rendering/common.py +316 -0
- graphica/gui/rendering/data_1d.py +376 -0
- graphica/gui/rendering/data_2d.py +66 -0
- graphica/gui/residual_panel.py +62 -0
- graphica/gui/resources.py +16 -0
- graphica/gui/single_instance.py +141 -0
- graphica/gui/splash.py +83 -0
- graphica/gui/task_runner.py +41 -0
- graphica/gui/theme.py +864 -0
- graphica/gui/tools/__init__.py +56 -0
- graphica/gui/tools/annotation.py +275 -0
- graphica/gui/tools/cursor.py +259 -0
- graphica/gui/tools/layout_edit.py +285 -0
- graphica/gui/tools/manager.py +78 -0
- graphica/gui/tools/peak_placement.py +132 -0
- graphica/gui/tools/range_select.py +191 -0
- graphica/gui/tools/region_highlight.py +222 -0
- graphica/gui/tools/slice_extraction.py +200 -0
- graphica/gui/widget_translation.py +46 -0
- graphica/gui/workers.py +280 -0
- graphica/models/__init__.py +0 -0
- graphica/models/project.py +197 -0
- graphica/plugin/__init__.py +20 -0
- graphica/plugin/testing.py +57 -0
- graphica/sample_data/__init__.py +0 -0
- graphica/sample_data/cooling_curve_sample.csv +42 -0
- graphica/ui_main_window.py +581 -0
- graphica_plot-2.0.0.dist-info/METADATA +317 -0
- graphica_plot-2.0.0.dist-info/RECORD +181 -0
- graphica_plot-2.0.0.dist-info/WHEEL +5 -0
- graphica_plot-2.0.0.dist-info/entry_points.txt +2 -0
- graphica_plot-2.0.0.dist-info/licenses/LICENSE +30 -0
- graphica_plot-2.0.0.dist-info/licenses/THIRD_PARTY_LICENSES.md +91 -0
- graphica_plot-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
"""曲線フィット(単発・一括・多峰分離)と、フィット結果の再表示・注釈への焼き込みの本体。"""
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from PySide6.QtCore import Qt
|
|
5
|
+
from PySide6.QtWidgets import QMessageBox, QProgressDialog
|
|
6
|
+
|
|
7
|
+
from graphica.gui import notify
|
|
8
|
+
from graphica.core.analysis import (calculate_confidence_band, calculate_curve_fit, fit_curve_task,
|
|
9
|
+
get_fit_model_id, multi_peak_fit_task)
|
|
10
|
+
from graphica.core.dataset import Dataset
|
|
11
|
+
from graphica.core.fit_models import fit_type_label
|
|
12
|
+
from graphica.core.provenance import build_provenance
|
|
13
|
+
from graphica.gui.datasets.operations.runner import Operation
|
|
14
|
+
from graphica.gui.dialogs import FitDialog, MultiPeakFitDialog, ResultDialog
|
|
15
|
+
from graphica.gui.task_runner import TaskRunner
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def build_fit_result_dict(fit_type, custom_formula, fit, weighted, x_range, source_dataset,
|
|
19
|
+
p0_overrides=None, fixed_params=None, bounds=None, loss='linear'):
|
|
20
|
+
"""Dataset.fit_result に保存する形。pickle と JSON の両方で往復できるよう素の Python 型にする。"""
|
|
21
|
+
popt, pcov, perr = fit['popt'], fit['pcov'], fit['perr']
|
|
22
|
+
return {
|
|
23
|
+
'fit_type': fit_type,
|
|
24
|
+
'fit_model_id': get_fit_model_id(fit_type),
|
|
25
|
+
'custom_formula': custom_formula,
|
|
26
|
+
'param_names': list(fit['param_names']),
|
|
27
|
+
'params': [float(v) for v in popt],
|
|
28
|
+
'param_errors': [float(v) for v in perr],
|
|
29
|
+
'covariance': [[float(v) for v in row] for row in pcov],
|
|
30
|
+
'r_squared': float(fit['r_squared']),
|
|
31
|
+
'residuals': [float(v) for v in fit['residuals']],
|
|
32
|
+
'residual_x': [float(v) for v in fit['x_data_used']],
|
|
33
|
+
'weighted': bool(weighted),
|
|
34
|
+
'x_range': [float(x_range[0]), float(x_range[1])] if x_range is not None else None,
|
|
35
|
+
'source_dataset_id': source_dataset.dataset_id,
|
|
36
|
+
'source_dataset_name': source_dataset.name,
|
|
37
|
+
'p0_overrides': dict(p0_overrides) if p0_overrides else {},
|
|
38
|
+
'fixed_params': dict(fixed_params) if fixed_params else {},
|
|
39
|
+
'bounds': {k: [float(v[0]), float(v[1])] for k, v in bounds.items()} if bounds else {},
|
|
40
|
+
'loss': loss,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def build_multi_peak_fit_result_dict(fit, source_dataset):
|
|
45
|
+
"""多峰分離版。component_type / n_components は core/methods_text.py が参照するので名前を変えない。"""
|
|
46
|
+
popt, pcov, perr = fit['popt'], fit['pcov'], fit['perr']
|
|
47
|
+
return {
|
|
48
|
+
'fit_type': 'multi_peak',
|
|
49
|
+
'component_type': fit['component_type'],
|
|
50
|
+
'n_components': fit['n_components'],
|
|
51
|
+
'baseline_type': fit['baseline_type'],
|
|
52
|
+
'components': fit['components'],
|
|
53
|
+
'param_names': list(fit['param_names']),
|
|
54
|
+
'params': [float(v) for v in popt],
|
|
55
|
+
'param_errors': [float(v) for v in perr],
|
|
56
|
+
'covariance': [[float(v) for v in row] for row in pcov],
|
|
57
|
+
'r_squared': float(fit['r_squared']),
|
|
58
|
+
'residuals': [float(v) for v in fit['residuals']],
|
|
59
|
+
'residual_x': [float(v) for v in fit['x_data_used']],
|
|
60
|
+
'source_dataset_id': source_dataset.dataset_id,
|
|
61
|
+
'source_dataset_name': source_dataset.name,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def add_band_columns_to_fit_df(fit_df, fit, band_type):
|
|
66
|
+
"""
|
|
67
|
+
信頼帯・予測帯の y_lower / y_upper 列を足し、足せた band_type を返す(Dataset.fit_band_display 用)。
|
|
68
|
+
自由度不足などで計算できなければ列を足さず None。フィット自体は成功なので失敗にしない。
|
|
69
|
+
"""
|
|
70
|
+
if band_type is None:
|
|
71
|
+
return None
|
|
72
|
+
try:
|
|
73
|
+
band = calculate_confidence_band(
|
|
74
|
+
fit['x_fit'], fit['fit_func'], fit['popt'], fit['pcov'], fit['residuals'], band_type=band_type,
|
|
75
|
+
)
|
|
76
|
+
except ValueError:
|
|
77
|
+
return None
|
|
78
|
+
fit_df['y_lower'] = band['y_lower']
|
|
79
|
+
fit_df['y_upper'] = band['y_upper']
|
|
80
|
+
return band_type
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _param_lines(param_names, params, errors, fixed_params=None):
|
|
84
|
+
"""「名前 = 値 ± 標準誤差」。固定したパラメータと、誤差を持たない古い結果は値だけ。"""
|
|
85
|
+
fixed = fixed_params or {}
|
|
86
|
+
if errors is None:
|
|
87
|
+
errors = [None] * len(params)
|
|
88
|
+
text = ""
|
|
89
|
+
for name, value, err in zip(param_names, params, errors):
|
|
90
|
+
if err is None or name in fixed:
|
|
91
|
+
text += f" {name} = {value: .4e}\n"
|
|
92
|
+
else:
|
|
93
|
+
text += f" {name} = {value: .4e} ± {err:.1e}\n"
|
|
94
|
+
return text
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _fit_summary_text(fit_type, custom_formula, param_names, params, param_errors, r_squared,
|
|
98
|
+
weighted, x_range, fixed_params, bounds, loss):
|
|
99
|
+
fit_label = fit_type if custom_formula is None else f"{fit_type} {custom_formula}"
|
|
100
|
+
text = f"[{fit_label}] のフィッティング結果:\n"
|
|
101
|
+
text += _param_lines(param_names, params, param_errors, fixed_params)
|
|
102
|
+
text += f" R^2 = {r_squared: .5f}\n"
|
|
103
|
+
if weighted:
|
|
104
|
+
text += " (Y誤差列を重みとして使用)\n"
|
|
105
|
+
if x_range is not None:
|
|
106
|
+
text += f" (フィット範囲: {x_range[0]: .4g} 〜 {x_range[1]: .4g})\n"
|
|
107
|
+
if fixed_params:
|
|
108
|
+
text += f" (固定: {fixed_params})\n"
|
|
109
|
+
if bounds:
|
|
110
|
+
text += f" (範囲拘束: {bounds})\n"
|
|
111
|
+
if loss != 'linear':
|
|
112
|
+
text += f" (ロバストフィット: {loss})\n"
|
|
113
|
+
return text
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def multi_peak_summary_text(component_label, n_components, param_names, params, param_errors, r_squared):
|
|
117
|
+
text = f"[多峰分離({component_label} x{n_components})] のフィッティング結果:\n"
|
|
118
|
+
text += _param_lines(param_names, params, param_errors)
|
|
119
|
+
return text + f" R^2 = {r_squared: .5f}\n"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def format_fit_result_text(fit_result):
|
|
123
|
+
"""保存済みの fit_result だけから、フィット直後と同じ体裁の結果文を作る(再フィットしない)。"""
|
|
124
|
+
return _fit_summary_text(
|
|
125
|
+
fit_type_label(fit_result), fit_result.get('custom_formula'),
|
|
126
|
+
fit_result.get('param_names', []), fit_result.get('params', []), fit_result.get('param_errors'),
|
|
127
|
+
fit_result.get('r_squared', float('nan')), fit_result.get('weighted'), fit_result.get('x_range'),
|
|
128
|
+
fit_result.get('fixed_params'), fit_result.get('bounds'), fit_result.get('loss', 'linear'),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _fit_dataset(source, fit, fit_type, custom_formula, sigma, x_range,
|
|
133
|
+
p0_overrides, fixed_params, bounds, band_type, loss, operation):
|
|
134
|
+
"""フィット結果から「Fit (元の名前)」のデータセットを作る。GUI に触れないのでワーカーからも呼べる。"""
|
|
135
|
+
fit_result = build_fit_result_dict(
|
|
136
|
+
fit_type=fit_type, custom_formula=custom_formula, fit=fit,
|
|
137
|
+
weighted=sigma is not None, x_range=x_range, source_dataset=source,
|
|
138
|
+
p0_overrides=p0_overrides, fixed_params=fixed_params, bounds=bounds, loss=loss,
|
|
139
|
+
)
|
|
140
|
+
text = _fit_summary_text(fit_type, custom_formula, fit['param_names'], fit['popt'], fit['perr'], fit['r_squared'],
|
|
141
|
+
sigma is not None, x_range, fixed_params, bounds, loss)
|
|
142
|
+
fit_df = pd.DataFrame({'x_fit': fit['x_fit'], 'y_fit': fit['y_fit']})
|
|
143
|
+
applied_band_type = add_band_columns_to_fit_df(fit_df, fit, band_type)
|
|
144
|
+
dataset = Dataset(
|
|
145
|
+
name=f"Fit ({source.name})", df=fit_df, x_col_name='x_fit', y_col_name='y_fit',
|
|
146
|
+
color=source.color, linestyle='--', marker='None', linewidth=source.linewidth,
|
|
147
|
+
use_secondary_y=source.use_secondary_y, subplot_target=source.subplot_target,
|
|
148
|
+
fit_info=text, fit_result=fit_result, fit_band_display=applied_band_type,
|
|
149
|
+
provenance=build_provenance(operation, fit_result, [source]),
|
|
150
|
+
)
|
|
151
|
+
return dataset, text
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def batch_fit_worker(datasets, fit_type, custom_formula, use_weighted, x_range,
|
|
155
|
+
p0_overrides, fixed_params, bounds, band_type, loss='linear',
|
|
156
|
+
report_progress=None, is_cancelled=None):
|
|
157
|
+
"""
|
|
158
|
+
一括フィットの計算(バックグラウンドのスレッドで動く。Qt には触れない)。
|
|
159
|
+
1件のフィットは中断できないので、キャンセルは「残りを飛ばす」単位。
|
|
160
|
+
"""
|
|
161
|
+
results = []
|
|
162
|
+
total = len(datasets)
|
|
163
|
+
for i, dataset in enumerate(datasets):
|
|
164
|
+
if is_cancelled is not None and is_cancelled():
|
|
165
|
+
break
|
|
166
|
+
if report_progress is not None:
|
|
167
|
+
report_progress(i, total, dataset.name)
|
|
168
|
+
sigma = dataset.y_err_data if use_weighted else None
|
|
169
|
+
try:
|
|
170
|
+
fit = calculate_curve_fit(
|
|
171
|
+
dataset.x_data, dataset.y_data, fit_type, custom_formula=custom_formula,
|
|
172
|
+
sigma=sigma, x_range=x_range,
|
|
173
|
+
p0_overrides=p0_overrides, fixed_params=fixed_params, bounds=bounds, loss=loss,
|
|
174
|
+
)
|
|
175
|
+
except Exception as e:
|
|
176
|
+
results.append({'source_name': dataset.name, 'fit_dataset': None, 'error': str(e)})
|
|
177
|
+
continue
|
|
178
|
+
fit_dataset, _ = _fit_dataset(dataset, fit, fit_type, custom_formula, sigma, x_range,
|
|
179
|
+
p0_overrides, fixed_params, bounds, band_type, loss, 'batch_curve_fit')
|
|
180
|
+
results.append({'source_name': dataset.name, 'fit_dataset': fit_dataset, 'error': None})
|
|
181
|
+
return results
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def stop_runner(runner):
|
|
185
|
+
"""実行中の QThread を破棄すると Qt がプロセスを落とすので、切断してから終わるまで待つ。"""
|
|
186
|
+
try:
|
|
187
|
+
runner.succeeded.disconnect()
|
|
188
|
+
runner.failed.disconnect()
|
|
189
|
+
except (RuntimeError, TypeError):
|
|
190
|
+
pass
|
|
191
|
+
runner.requestInterruption()
|
|
192
|
+
runner.wait()
|
|
193
|
+
runner.deleteLater()
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
FIT_RESULT_WINDOW = "fit"
|
|
197
|
+
FIT_RUNNER = "fit_runner"
|
|
198
|
+
BATCH_FIT_RUNNER = "batch_fit_runner"
|
|
199
|
+
MULTI_PEAK_FIT_RUNNER = "multi_peak_fit_runner"
|
|
200
|
+
RUNNER_NAMES = (FIT_RUNNER, BATCH_FIT_RUNNER, MULTI_PEAK_FIT_RUNNER)
|
|
201
|
+
|
|
202
|
+
_BUSY_TITLE = "実行中"
|
|
203
|
+
_FIT_BUSY_TEXT = "別のフィット処理が実行中です。完了までお待ちください。"
|
|
204
|
+
_EXPORT_TITLE = "フィット結果のエクスポート"
|
|
205
|
+
|
|
206
|
+
# 計算の終わりを受ける関数は run_operation の外(Qt のシグナル)で呼ばれるので、流れを止める例外は投げない。
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _show_result(op, title, text, csv_data, residual_x, residual_y):
|
|
210
|
+
op.show_result_window(FIT_RESULT_WINDOW, lambda: ResultDialog(
|
|
211
|
+
title, text, op.parent, csv_data=csv_data, residual_x=residual_x, residual_y=residual_y))
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _ensure_idle(op, runner_name, text):
|
|
215
|
+
if op.state.runners.get(runner_name) is not None:
|
|
216
|
+
op.stop_with_information(text, title=_BUSY_TITLE)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _finish_runner(op, runner_name):
|
|
220
|
+
runner = op.state.runners.get(runner_name)
|
|
221
|
+
if runner is not None:
|
|
222
|
+
runner.wait()
|
|
223
|
+
runner.deleteLater()
|
|
224
|
+
op.state.runners[runner_name] = None
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# --- 単発フィット ---
|
|
228
|
+
|
|
229
|
+
def fit_current_dataset(op):
|
|
230
|
+
"""設定はダイアログで集め、計算はバックグラウンドで行う。"""
|
|
231
|
+
source = op.current_dataset()
|
|
232
|
+
_ensure_idle(op, FIT_RUNNER, _FIT_BUSY_TEXT)
|
|
233
|
+
x_data, y_data = source.x_data, source.y_data
|
|
234
|
+
numeric_x = op.as_numbers(source, "x")
|
|
235
|
+
x_min = float(np.min(numeric_x)) if len(numeric_x) else None
|
|
236
|
+
x_max = float(np.max(numeric_x)) if len(numeric_x) else None
|
|
237
|
+
(fit_type, custom_formula, use_weighted, x_range,
|
|
238
|
+
p0_overrides, fixed_params, bounds, band_type, loss) = FitDialog.get_fit_type(op.parent, x_min=x_min, x_max=x_max)
|
|
239
|
+
if fit_type is None:
|
|
240
|
+
op.stop()
|
|
241
|
+
sigma = source.y_err_data if use_weighted else None
|
|
242
|
+
|
|
243
|
+
runner = TaskRunner(
|
|
244
|
+
fit_curve_task, x_data, y_data, fit_type, custom_formula=custom_formula,
|
|
245
|
+
sigma=sigma, x_range=x_range,
|
|
246
|
+
p0_overrides=p0_overrides, fixed_params=fixed_params, bounds=bounds, loss=loss,
|
|
247
|
+
)
|
|
248
|
+
runner.succeeded.connect(lambda fit: _on_fit_succeeded(
|
|
249
|
+
op, source, fit_type, custom_formula, sigma, x_range, p0_overrides, fixed_params, bounds, band_type, loss, fit,
|
|
250
|
+
))
|
|
251
|
+
runner.failed.connect(lambda error_message: _on_fit_failed(op, error_message))
|
|
252
|
+
op.state.runners[FIT_RUNNER] = runner
|
|
253
|
+
op.host.set_fit_button_enabled(False)
|
|
254
|
+
runner.start()
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _finish_fit_runner(op):
|
|
258
|
+
_finish_runner(op, FIT_RUNNER)
|
|
259
|
+
op.host.set_fit_button_enabled(True)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _on_fit_failed(op, error_message):
|
|
263
|
+
_finish_fit_runner(op)
|
|
264
|
+
notify.warning(op.parent, "フィットエラー", f"フィッティングに失敗しました:\n{error_message}")
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _on_fit_succeeded(op, source, fit_type, custom_formula, sigma, x_range,
|
|
268
|
+
p0_overrides, fixed_params, bounds, band_type, loss, fit):
|
|
269
|
+
_finish_fit_runner(op)
|
|
270
|
+
dataset, text = _fit_dataset(source, fit, fit_type, custom_formula, sigma, x_range,
|
|
271
|
+
p0_overrides, fixed_params, bounds, band_type, loss, 'curve_fit')
|
|
272
|
+
op.host.add_derived_dataset(dataset, source, op.title)
|
|
273
|
+
csv_data = pd.DataFrame({
|
|
274
|
+
'パラメータ': list(fit['param_names']) + ['R^2'],
|
|
275
|
+
'値': list(fit['popt']) + [fit['r_squared']],
|
|
276
|
+
})
|
|
277
|
+
# 残差は NaN の除外とフィット範囲の適用後の点なので、x もそれに合わせる
|
|
278
|
+
_show_result(op, "フィッティング完了", text, csv_data, fit['x_data_used'], fit['residuals'])
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
# --- 一括フィット ---
|
|
282
|
+
|
|
283
|
+
def batch_fit_selected(op):
|
|
284
|
+
"""選択中の各データセットに同じ設定でフィットする。結果はまとめて足し、描き直しは1回だけ。"""
|
|
285
|
+
parent = op.parent
|
|
286
|
+
selected = op.selected_datasets(at_least=2, message="2つ以上のデータセットを選択してください。")
|
|
287
|
+
_ensure_idle(op, BATCH_FIT_RUNNER, "別のバッチフィット処理が実行中です。完了までお待ちください。")
|
|
288
|
+
(fit_type, custom_formula, use_weighted, x_range,
|
|
289
|
+
p0_overrides, fixed_params, bounds, band_type, loss) = FitDialog.get_fit_type(parent)
|
|
290
|
+
if fit_type is None:
|
|
291
|
+
op.stop()
|
|
292
|
+
target_folder = op.host.target_folder_for_new_dataset()
|
|
293
|
+
|
|
294
|
+
progress = QProgressDialog("バッチカーブフィットを実行中...", "キャンセル", 0, len(selected), parent)
|
|
295
|
+
progress.setWindowModality(Qt.WindowModality.WindowModal)
|
|
296
|
+
progress.setMinimumDuration(0)
|
|
297
|
+
progress.setValue(0)
|
|
298
|
+
|
|
299
|
+
runner = TaskRunner(
|
|
300
|
+
batch_fit_worker, selected, fit_type, custom_formula, use_weighted, x_range,
|
|
301
|
+
p0_overrides, fixed_params, bounds, band_type, loss,
|
|
302
|
+
)
|
|
303
|
+
runner.progress.connect(lambda done, total, message: progress.setValue(done))
|
|
304
|
+
progress.canceled.connect(runner.requestInterruption)
|
|
305
|
+
runner.succeeded.connect(lambda results: _on_batch_succeeded(op, results, target_folder, progress))
|
|
306
|
+
runner.failed.connect(lambda msg: _on_batch_failed(op, msg, progress))
|
|
307
|
+
op.state.runners[BATCH_FIT_RUNNER] = runner
|
|
308
|
+
runner.start()
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _finish_batch_runner(op, progress):
|
|
312
|
+
_finish_runner(op, BATCH_FIT_RUNNER)
|
|
313
|
+
progress.close()
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _on_batch_failed(op, error_message, progress):
|
|
317
|
+
_finish_batch_runner(op, progress)
|
|
318
|
+
notify.warning(op.parent, op.title, f"バッチフィット処理に失敗しました:\n{error_message}")
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _on_batch_succeeded(op, results, target_folder, progress):
|
|
322
|
+
"""キャンセルされたときは、終わった分だけが results に入っている。"""
|
|
323
|
+
_finish_batch_runner(op, progress)
|
|
324
|
+
added = [r['fit_dataset'] for r in results if r['fit_dataset'] is not None]
|
|
325
|
+
succeeded = [r['source_name'] for r in results if r['fit_dataset'] is not None]
|
|
326
|
+
failed = [f"{r['source_name']}: {r['error']}" for r in results
|
|
327
|
+
if r['fit_dataset'] is None and r['error'] is not None]
|
|
328
|
+
if added:
|
|
329
|
+
op.host.add_datasets_to_folder(added, target_folder, op.title)
|
|
330
|
+
if not succeeded and not failed:
|
|
331
|
+
return
|
|
332
|
+
message = f"{len(succeeded)}件のフィットに成功しました。"
|
|
333
|
+
if failed:
|
|
334
|
+
message += "\n\n失敗:\n" + "\n".join(failed)
|
|
335
|
+
notify.information(op.parent, op.title, message)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
# --- 多峰分離フィット ---
|
|
339
|
+
|
|
340
|
+
def multi_peak_fit_current_dataset(op):
|
|
341
|
+
"""
|
|
342
|
+
グラフ上のクリックで置いたピークの初期値をダイアログに引き継ぐ。ダイアログを閉じたら
|
|
343
|
+
(OK でもキャンセルでも)置いた初期値は消し、配置モードも抜ける。
|
|
344
|
+
"""
|
|
345
|
+
source = op.current_dataset()
|
|
346
|
+
_ensure_idle(op, MULTI_PEAK_FIT_RUNNER, _FIT_BUSY_TEXT)
|
|
347
|
+
x_data, y_data = source.x_data, source.y_data
|
|
348
|
+
component_type, baseline_type, initial_guesses = MultiPeakFitDialog.get_multi_peak_fit_settings(
|
|
349
|
+
op.parent, x_data=x_data, y_data=y_data, initial_guesses=op.host.pending_peak_guesses(),
|
|
350
|
+
)
|
|
351
|
+
op.host.finish_peak_placement()
|
|
352
|
+
if component_type is None:
|
|
353
|
+
op.stop()
|
|
354
|
+
|
|
355
|
+
runner = TaskRunner(multi_peak_fit_task, x_data, y_data, component_type, initial_guesses,
|
|
356
|
+
baseline_type=baseline_type)
|
|
357
|
+
runner.succeeded.connect(lambda fit: _on_multi_peak_fit_succeeded(op, source, fit))
|
|
358
|
+
runner.failed.connect(lambda error_message: _on_multi_peak_fit_failed(op, error_message))
|
|
359
|
+
op.state.runners[MULTI_PEAK_FIT_RUNNER] = runner
|
|
360
|
+
op.host.set_multi_peak_fit_button_enabled(False)
|
|
361
|
+
runner.start()
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _finish_multi_peak_runner(op):
|
|
365
|
+
_finish_runner(op, MULTI_PEAK_FIT_RUNNER)
|
|
366
|
+
op.host.set_multi_peak_fit_button_enabled(True)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _on_multi_peak_fit_failed(op, error_message):
|
|
370
|
+
_finish_multi_peak_runner(op)
|
|
371
|
+
notify.warning(op.parent, "多峰分離フィットエラー", f"フィッティングに失敗しました:\n{error_message}")
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _on_multi_peak_fit_succeeded(op, source, fit):
|
|
375
|
+
_finish_multi_peak_runner(op)
|
|
376
|
+
popt, param_names, r_squared = fit['popt'], fit['param_names'], fit['r_squared']
|
|
377
|
+
component_label = dict(MultiPeakFitDialog.COMPONENT_TYPES).get(fit['component_type'], fit['component_type'])
|
|
378
|
+
text = multi_peak_summary_text(component_label, fit['n_components'], param_names, popt, fit['perr'], r_squared)
|
|
379
|
+
|
|
380
|
+
fit_result = build_multi_peak_fit_result_dict(fit, source_dataset=source)
|
|
381
|
+
op.host.add_derived_dataset(Dataset(
|
|
382
|
+
name=f"MultiPeakFit ({source.name})",
|
|
383
|
+
df=pd.DataFrame({'x_fit': fit['x_fit'], 'y_fit': fit['y_fit']}),
|
|
384
|
+
x_col_name='x_fit', y_col_name='y_fit',
|
|
385
|
+
color=source.color, linestyle='--', marker='None', linewidth=source.linewidth,
|
|
386
|
+
use_secondary_y=source.use_secondary_y, subplot_target=source.subplot_target,
|
|
387
|
+
fit_info=text, fit_result=fit_result,
|
|
388
|
+
provenance=build_provenance('multi_peak_fit', fit_result, [source]),
|
|
389
|
+
), source, op.title)
|
|
390
|
+
csv_data = pd.DataFrame({'パラメータ': list(param_names) + ['R^2'], '値': list(popt) + [r_squared]})
|
|
391
|
+
_show_result(op, "多峰分離フィット完了", text, csv_data, fit['x_data_used'], fit['residuals'])
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
# --- 保存済みのフィット結果 ---
|
|
395
|
+
|
|
396
|
+
def show_fit_result(op):
|
|
397
|
+
"""
|
|
398
|
+
現在のデータセットに保存されたフィット結果(fit_result)を、再フィットせずに表と残差で出し直す。
|
|
399
|
+
続けて、要約をグラフの注釈として焼き込むかを尋ねる。
|
|
400
|
+
"""
|
|
401
|
+
dataset = op.current_dataset()
|
|
402
|
+
fit_result = dataset.fit_result
|
|
403
|
+
if fit_result is None:
|
|
404
|
+
# メニューは無効化しているが、別の経路から呼ばれたときのため
|
|
405
|
+
op.stop_with_information(
|
|
406
|
+
"このデータセットは曲線フィットの結果を持っていません。\n"
|
|
407
|
+
"曲線フィットで生成されたデータセット(名前が「Fit (...)」の\n"
|
|
408
|
+
"もの、またはfit_resultを保持しているもの)を選択してください。"
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
csv_data = pd.DataFrame({
|
|
412
|
+
'パラメータ': list(fit_result.get('param_names', [])) + ['R^2'],
|
|
413
|
+
'値': list(fit_result.get('params', [])) + [fit_result.get('r_squared')],
|
|
414
|
+
})
|
|
415
|
+
_show_result(op, op.title, format_fit_result_text(fit_result), csv_data,
|
|
416
|
+
fit_result.get('residual_x'), fit_result.get('residuals'))
|
|
417
|
+
|
|
418
|
+
reply = notify.question(
|
|
419
|
+
op.parent, op.title,
|
|
420
|
+
"このフィット結果の要約(パラメータ値±誤差・R^2)を、\n"
|
|
421
|
+
"グラフ上のテキスト注釈として焼き込みますか?\n"
|
|
422
|
+
"(焼き込み後は注釈モードで通常の注釈と同様に移動・編集・削除できます)",
|
|
423
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
424
|
+
QMessageBox.StandardButton.No
|
|
425
|
+
)
|
|
426
|
+
if reply == QMessageBox.StandardButton.Yes:
|
|
427
|
+
burn_fit_result_annotation(op, dataset, fit_result)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def burn_fit_result_annotation(op, dataset, fit_result):
|
|
431
|
+
"""
|
|
432
|
+
フィット結果の要約を、手で置く注釈と同じテキスト注釈として足す(Undo でき、あとから編集できる)。
|
|
433
|
+
置き場所はフィット曲線の中央の点。端やピークより、どの形の曲線でも曲線上の無難な位置になる。
|
|
434
|
+
"""
|
|
435
|
+
axis_index = dataset.subplot_target
|
|
436
|
+
if axis_index is None or axis_index >= op.host.axis_count():
|
|
437
|
+
op.stop_with_warning("注釈を追加する対象のプロットが見つかりませんでした。")
|
|
438
|
+
x_data, y_data = dataset.x_data, dataset.y_data
|
|
439
|
+
if len(x_data) == 0:
|
|
440
|
+
op.stop_with_warning("フィット曲線にデータ点が無いため、注釈を追加できませんでした。")
|
|
441
|
+
mid = len(x_data) // 2
|
|
442
|
+
anchor = (float(x_data[mid]), float(y_data[mid]))
|
|
443
|
+
|
|
444
|
+
lines = [f"フィット: {fit_type_label(fit_result, '')}"]
|
|
445
|
+
params = fit_result.get('params', [])
|
|
446
|
+
errors = fit_result.get('param_errors', [None] * len(params))
|
|
447
|
+
for name, value, err in zip(fit_result.get('param_names', []), params, errors):
|
|
448
|
+
lines.append(f"{name} = {value:.4g} ± {err:.4g}" if err is not None else f"{name} = {value:.4g}")
|
|
449
|
+
r_squared = fit_result.get('r_squared')
|
|
450
|
+
if r_squared is not None:
|
|
451
|
+
lines.append(f"R^2 = {r_squared:.5f}")
|
|
452
|
+
|
|
453
|
+
op.host.add_annotation(axis_index, {
|
|
454
|
+
'type': 'text', 'text': "\n".join(lines), 'xy': anchor, 'xytext': anchor, 'color': '#000000',
|
|
455
|
+
}, description="フィット結果の注釈焼き込み")
|
|
456
|
+
op.host.show_status("フィット結果を注釈として焼き込みました")
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
FITTING_OPERATIONS = {
|
|
460
|
+
"fit_current_dataset": Operation("カーブフィット", fit_current_dataset),
|
|
461
|
+
"batch_fit_selected": Operation("バッチカーブフィット", batch_fit_selected),
|
|
462
|
+
"multi_peak_fit_current_dataset": Operation("多峰分離フィット", multi_peak_fit_current_dataset),
|
|
463
|
+
"show_fit_result": Operation(_EXPORT_TITLE, show_fit_result),
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def burn_operation(dataset, fit_result):
|
|
468
|
+
return Operation(_EXPORT_TITLE, lambda op: burn_fit_result_annotation(op, dataset, fit_result))
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""ピーク検出と、ピーク位置への自動ラベルの本体。"""
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
from graphica.core.analysis import assign_peak_label_levels, calculate_peak_quantification, calculate_peaks
|
|
8
|
+
from graphica.core.dataset import Dataset
|
|
9
|
+
from graphica.gui.datasets.operations.runner import Operation
|
|
10
|
+
from graphica.gui.dialogs import PeakSettingsDialog, ResultDialog
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
_DEFAULT_PEAK_TYPE = "上に凸 (Peaks)"
|
|
15
|
+
PEAK_RESULT_WINDOW = "peaks"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _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
|
+
|
|
28
|
+
def _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
|
+
|
|
37
|
+
def 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
|
+
|
|
76
|
+
def 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:
|
|
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
|
+
|
|
106
|
+
PEAK_OPERATIONS = {
|
|
107
|
+
"find_peaks": Operation("ピーク検出", find_peaks),
|
|
108
|
+
"add_peak_labels": Operation("ピーク位置への自動ラベル", add_peak_labels),
|
|
109
|
+
}
|