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,1322 @@
|
|
|
1
|
+
"""解析のダイアログ。呼び出し側は `from graphica.gui.dialogs import X` で参照する。"""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from PySide6.QtWidgets import (
|
|
7
|
+
QApplication,
|
|
8
|
+
QCheckBox,
|
|
9
|
+
QComboBox,
|
|
10
|
+
QDialog,
|
|
11
|
+
QDialogButtonBox,
|
|
12
|
+
QDoubleSpinBox,
|
|
13
|
+
QFormLayout,
|
|
14
|
+
QHBoxLayout,
|
|
15
|
+
QLabel,
|
|
16
|
+
QLineEdit,
|
|
17
|
+
QPlainTextEdit,
|
|
18
|
+
QPushButton,
|
|
19
|
+
QSpinBox,
|
|
20
|
+
QStackedWidget,
|
|
21
|
+
QTableWidget,
|
|
22
|
+
QTableWidgetItem,
|
|
23
|
+
QVBoxLayout,
|
|
24
|
+
QWidget,
|
|
25
|
+
)
|
|
26
|
+
from PySide6.QtCore import QTimer, Qt
|
|
27
|
+
from PySide6.QtGui import QFont
|
|
28
|
+
from graphica.gui import notify
|
|
29
|
+
from graphica.gui.theme import apply_form_spacing
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class FitDialog(QDialog):
|
|
35
|
+
"""フィットのモデルと条件を選ぶ。"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, parent=None, x_min=None, x_max=None):
|
|
38
|
+
"""x_min / x_max はフィット範囲の欄の初期値(ふつうはデータの X の範囲)。"""
|
|
39
|
+
super().__init__(parent)
|
|
40
|
+
self.setWindowTitle("曲線フィット")
|
|
41
|
+
|
|
42
|
+
layout = QVBoxLayout(self)
|
|
43
|
+
|
|
44
|
+
layout.addWidget(QLabel("フィットする関数の種類を選択してください:"))
|
|
45
|
+
|
|
46
|
+
self.fit_type_combo = QComboBox()
|
|
47
|
+
# scipy を読み込むので、ダイアログを開くときまで遅らせる
|
|
48
|
+
from graphica.core.analysis import get_plugin_fit_type_names
|
|
49
|
+
from graphica.core.fit_models import CUSTOM_FORMULA_LABEL, builtin_menu_labels
|
|
50
|
+
# プラグインのフィット関数は、組み込みと「カスタム数式...」の間に入れる
|
|
51
|
+
self.fit_type_combo.addItems(builtin_menu_labels())
|
|
52
|
+
self.fit_type_combo.addItems(get_plugin_fit_type_names())
|
|
53
|
+
self.fit_type_combo.addItem(CUSTOM_FORMULA_LABEL)
|
|
54
|
+
self.fit_type_combo.currentTextChanged.connect(self._on_fit_type_changed)
|
|
55
|
+
layout.addWidget(self.fit_type_combo)
|
|
56
|
+
|
|
57
|
+
self.custom_formula_label = QLabel("数式 (xとパラメータ名を使って入力、例: a*exp(-b*x)+c)")
|
|
58
|
+
self.custom_formula_edit = QLineEdit()
|
|
59
|
+
self.custom_formula_edit.setPlaceholderText("a*exp(-b*x)+c")
|
|
60
|
+
self.custom_formula_label.setVisible(False)
|
|
61
|
+
self.custom_formula_edit.setVisible(False)
|
|
62
|
+
layout.addWidget(self.custom_formula_label)
|
|
63
|
+
layout.addWidget(self.custom_formula_edit)
|
|
64
|
+
|
|
65
|
+
self.weighted_checkbox = QCheckBox("Y誤差列を重みとして使用する(設定されている場合)")
|
|
66
|
+
self.weighted_checkbox.setToolTip(
|
|
67
|
+
"誤差が大きい点ほどフィットへの影響を小さくする(scipy.optimize.curve_fitのsigma)。\n"
|
|
68
|
+
"対象データセットにY誤差列が設定されていない場合はチェックしても効果がありません。"
|
|
69
|
+
)
|
|
70
|
+
layout.addWidget(self.weighted_checkbox)
|
|
71
|
+
|
|
72
|
+
loss_form = QFormLayout()
|
|
73
|
+
self.loss_combo = QComboBox()
|
|
74
|
+
self.loss_combo.addItem("通常の最小二乗(既定)", "linear")
|
|
75
|
+
self.loss_combo.addItem("ロバスト(Soft L1)", "soft_l1")
|
|
76
|
+
self.loss_combo.addItem("ロバスト(Huber)", "huber")
|
|
77
|
+
self.loss_combo.setToolTip(
|
|
78
|
+
"外れ値に引きずられにくい損失関数に切り替える(scipy.optimize.least_squares)。\n"
|
|
79
|
+
"Soft L1/Huberはいずれも外れ値の寄与を線形二乗より抑える点で似ているが、\n"
|
|
80
|
+
"Huberの方が正常値付近では通常の最小二乗に近い挙動になる。"
|
|
81
|
+
)
|
|
82
|
+
loss_form.addRow("損失関数", self.loss_combo)
|
|
83
|
+
layout.addLayout(loss_form)
|
|
84
|
+
|
|
85
|
+
self.range_checkbox = QCheckBox("フィット範囲を指定する")
|
|
86
|
+
layout.addWidget(self.range_checkbox)
|
|
87
|
+
|
|
88
|
+
range_form = QFormLayout()
|
|
89
|
+
self.range_min_spinbox = QDoubleSpinBox()
|
|
90
|
+
self.range_min_spinbox.setRange(-1e12, 1e12)
|
|
91
|
+
self.range_min_spinbox.setDecimals(6)
|
|
92
|
+
self.range_min_spinbox.setEnabled(False)
|
|
93
|
+
if x_min is not None:
|
|
94
|
+
self.range_min_spinbox.setValue(x_min)
|
|
95
|
+
range_form.addRow("最小X", self.range_min_spinbox)
|
|
96
|
+
|
|
97
|
+
self.range_max_spinbox = QDoubleSpinBox()
|
|
98
|
+
self.range_max_spinbox.setRange(-1e12, 1e12)
|
|
99
|
+
self.range_max_spinbox.setDecimals(6)
|
|
100
|
+
self.range_max_spinbox.setEnabled(False)
|
|
101
|
+
if x_max is not None:
|
|
102
|
+
self.range_max_spinbox.setValue(x_max)
|
|
103
|
+
range_form.addRow("最大X", self.range_max_spinbox)
|
|
104
|
+
layout.addLayout(range_form)
|
|
105
|
+
|
|
106
|
+
self.range_checkbox.toggled.connect(self.range_min_spinbox.setEnabled)
|
|
107
|
+
self.range_checkbox.toggled.connect(self.range_max_spinbox.setEnabled)
|
|
108
|
+
|
|
109
|
+
layout.addWidget(QLabel("パラメータごとの初期値・固定・範囲拘束(収束しない場合に指定してください):"))
|
|
110
|
+
self.param_table = QTableWidget(0, 6)
|
|
111
|
+
self.param_table.setHorizontalHeaderLabels(
|
|
112
|
+
["パラメータ", "初期値/固定値", "固定", "範囲拘束", "最小", "最大"]
|
|
113
|
+
)
|
|
114
|
+
self.param_table.verticalHeader().setVisible(False)
|
|
115
|
+
self.param_table.setMaximumHeight(180)
|
|
116
|
+
layout.addWidget(self.param_table)
|
|
117
|
+
|
|
118
|
+
self.fit_type_combo.currentTextChanged.connect(self._rebuild_param_table)
|
|
119
|
+
# 入力途中でもパラメータの数が変わるので、1文字ごとに作り直す(解釈できなければ0行になるだけ)
|
|
120
|
+
self.custom_formula_edit.textChanged.connect(self._rebuild_param_table)
|
|
121
|
+
|
|
122
|
+
band_form = QFormLayout()
|
|
123
|
+
self.band_combo = QComboBox()
|
|
124
|
+
self.band_combo.addItems(["表示しない", "信頼帯 (95%)", "予測帯 (95%)"])
|
|
125
|
+
self.band_combo.setToolTip(
|
|
126
|
+
"信頼帯: 真の回帰曲線が収まると考えられる範囲(パラメータの不確かさのみ)。\n"
|
|
127
|
+
"予測帯: 次に測定する新しい1点が収まると考えられる範囲\n"
|
|
128
|
+
"(パラメータの不確かさに加え、観測ノイズの分散も加味するため信頼帯より広くなる)。"
|
|
129
|
+
)
|
|
130
|
+
band_form.addRow("信頼帯/予測帯", self.band_combo)
|
|
131
|
+
layout.addLayout(band_form)
|
|
132
|
+
|
|
133
|
+
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok |
|
|
134
|
+
QDialogButtonBox.StandardButton.Cancel)
|
|
135
|
+
button_box.accepted.connect(self.accept)
|
|
136
|
+
button_box.rejected.connect(self.reject)
|
|
137
|
+
layout.addWidget(button_box)
|
|
138
|
+
|
|
139
|
+
self._rebuild_param_table()
|
|
140
|
+
|
|
141
|
+
def _on_fit_type_changed(self, text):
|
|
142
|
+
from graphica.core.fit_models import is_custom_formula_type
|
|
143
|
+
is_custom = is_custom_formula_type(text)
|
|
144
|
+
self.custom_formula_label.setVisible(is_custom)
|
|
145
|
+
self.custom_formula_edit.setVisible(is_custom)
|
|
146
|
+
|
|
147
|
+
def _rebuild_param_table(self, *_args):
|
|
148
|
+
"""フィットの種類か数式が変わるたびにパラメータの行を作り直す。まだ決まらない間は0行。
|
|
149
|
+
|
|
150
|
+
値の欄は、「固定」が外れていれば初期値(触った行だけ p0_overrides に入る)、入っていれば固定値。
|
|
151
|
+
固定したパラメータには範囲拘束を付けられない。
|
|
152
|
+
"""
|
|
153
|
+
from graphica.core.fit_models import is_custom_formula_type
|
|
154
|
+
fit_type = self.fit_type_combo.currentText()
|
|
155
|
+
custom_formula = self.custom_formula_edit.text().strip() if is_custom_formula_type(fit_type) else None
|
|
156
|
+
try:
|
|
157
|
+
from graphica.core.analysis import get_fit_param_names
|
|
158
|
+
param_names = get_fit_param_names(fit_type, custom_formula)
|
|
159
|
+
except ValueError:
|
|
160
|
+
param_names = []
|
|
161
|
+
|
|
162
|
+
self.param_table.setRowCount(0)
|
|
163
|
+
self.param_table.setRowCount(len(param_names))
|
|
164
|
+
for row, name in enumerate(param_names):
|
|
165
|
+
name_item = QTableWidgetItem(name)
|
|
166
|
+
name_item.setFlags(name_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
|
|
167
|
+
self.param_table.setItem(row, 0, name_item)
|
|
168
|
+
|
|
169
|
+
value_spin = QDoubleSpinBox()
|
|
170
|
+
value_spin.setRange(-1e12, 1e12)
|
|
171
|
+
value_spin.setDecimals(6)
|
|
172
|
+
value_spin.blockSignals(True)
|
|
173
|
+
value_spin.setValue(1.0)
|
|
174
|
+
value_spin.blockSignals(False)
|
|
175
|
+
# 触った行だけ初期値を上書きする(触らなければ種類ごとの自動推定のまま)。
|
|
176
|
+
# 自動推定の値はデータが要るので、このダイアログでは出せない
|
|
177
|
+
value_spin.setProperty("_user_overridden", False)
|
|
178
|
+
value_spin.valueChanged.connect(
|
|
179
|
+
lambda _v, w=value_spin: w.setProperty("_user_overridden", True)
|
|
180
|
+
)
|
|
181
|
+
self.param_table.setCellWidget(row, 1, value_spin)
|
|
182
|
+
|
|
183
|
+
fixed_check = QCheckBox()
|
|
184
|
+
self.param_table.setCellWidget(row, 2, fixed_check)
|
|
185
|
+
|
|
186
|
+
range_check = QCheckBox()
|
|
187
|
+
self.param_table.setCellWidget(row, 3, range_check)
|
|
188
|
+
|
|
189
|
+
min_spin = QDoubleSpinBox()
|
|
190
|
+
min_spin.setRange(-1e12, 1e12)
|
|
191
|
+
min_spin.setDecimals(6)
|
|
192
|
+
min_spin.setValue(-1e12)
|
|
193
|
+
min_spin.setEnabled(False)
|
|
194
|
+
self.param_table.setCellWidget(row, 4, min_spin)
|
|
195
|
+
|
|
196
|
+
max_spin = QDoubleSpinBox()
|
|
197
|
+
max_spin.setRange(-1e12, 1e12)
|
|
198
|
+
max_spin.setDecimals(6)
|
|
199
|
+
max_spin.setValue(1e12)
|
|
200
|
+
max_spin.setEnabled(False)
|
|
201
|
+
self.param_table.setCellWidget(row, 5, max_spin)
|
|
202
|
+
|
|
203
|
+
fixed_check.toggled.connect(
|
|
204
|
+
lambda checked, rc=range_check, mn=min_spin, mx=max_spin:
|
|
205
|
+
self._on_param_fixed_toggled(checked, rc, mn, mx)
|
|
206
|
+
)
|
|
207
|
+
range_check.toggled.connect(
|
|
208
|
+
lambda checked, mn=min_spin, mx=max_spin: (mn.setEnabled(checked), mx.setEnabled(checked))
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
self.param_table.resizeColumnsToContents()
|
|
212
|
+
|
|
213
|
+
@staticmethod
|
|
214
|
+
def _on_param_fixed_toggled(checked, range_check, min_spin, max_spin):
|
|
215
|
+
"""固定したパラメータは最適化されないので、範囲拘束の欄を無効にする。"""
|
|
216
|
+
range_check.setEnabled(not checked)
|
|
217
|
+
min_spin.setEnabled((not checked) and range_check.isChecked())
|
|
218
|
+
max_spin.setEnabled((not checked) and range_check.isChecked())
|
|
219
|
+
|
|
220
|
+
def get_param_settings(self):
|
|
221
|
+
"""calculate_curve_fit() に渡す (p0_overrides, fixed_params, bounds)。何も変えなければ空の dict。"""
|
|
222
|
+
p0_overrides, fixed_params, bounds = {}, {}, {}
|
|
223
|
+
for row in range(self.param_table.rowCount()):
|
|
224
|
+
name = self.param_table.item(row, 0).text()
|
|
225
|
+
value_spin = self.param_table.cellWidget(row, 1)
|
|
226
|
+
fixed_check = self.param_table.cellWidget(row, 2)
|
|
227
|
+
range_check = self.param_table.cellWidget(row, 3)
|
|
228
|
+
min_spin = self.param_table.cellWidget(row, 4)
|
|
229
|
+
max_spin = self.param_table.cellWidget(row, 5)
|
|
230
|
+
|
|
231
|
+
if fixed_check.isChecked():
|
|
232
|
+
fixed_params[name] = value_spin.value()
|
|
233
|
+
continue
|
|
234
|
+
|
|
235
|
+
if value_spin.property("_user_overridden"):
|
|
236
|
+
p0_overrides[name] = value_spin.value()
|
|
237
|
+
|
|
238
|
+
if range_check.isChecked():
|
|
239
|
+
bounds[name] = (min_spin.value(), max_spin.value())
|
|
240
|
+
|
|
241
|
+
return p0_overrides, fixed_params, bounds
|
|
242
|
+
|
|
243
|
+
def get_band_type(self):
|
|
244
|
+
""""confidence" / "prediction" / None(表示しない)。"""
|
|
245
|
+
text = self.band_combo.currentText()
|
|
246
|
+
if "信頼帯" in text:
|
|
247
|
+
return "confidence"
|
|
248
|
+
if "予測帯" in text:
|
|
249
|
+
return "prediction"
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
def get_weighted(self):
|
|
253
|
+
return self.weighted_checkbox.isChecked()
|
|
254
|
+
|
|
255
|
+
def get_loss(self):
|
|
256
|
+
"""'linear' / 'soft_l1' / 'huber'。"""
|
|
257
|
+
return self.loss_combo.currentData()
|
|
258
|
+
|
|
259
|
+
def get_x_range(self):
|
|
260
|
+
"""指定があれば (最小X, 最大X)、無ければ None。"""
|
|
261
|
+
if not self.range_checkbox.isChecked():
|
|
262
|
+
return None
|
|
263
|
+
return (self.range_min_spinbox.value(), self.range_max_spinbox.value())
|
|
264
|
+
|
|
265
|
+
@staticmethod
|
|
266
|
+
def get_fit_type(parent=None, x_min=None, x_max=None):
|
|
267
|
+
"""OK なら (種類, 数式, 重み付け, 範囲, p0_overrides, fixed_params, bounds, band_type, loss)。
|
|
268
|
+
|
|
269
|
+
キャンセルなら (None, None, False, None, {}, {}, {}, None, 'linear')。
|
|
270
|
+
"""
|
|
271
|
+
dialog = FitDialog(parent, x_min=x_min, x_max=x_max)
|
|
272
|
+
if dialog.exec() == QDialog.DialogCode.Accepted:
|
|
273
|
+
from graphica.core.fit_models import is_custom_formula_type
|
|
274
|
+
fit_type = dialog.fit_type_combo.currentText()
|
|
275
|
+
custom_formula = dialog.custom_formula_edit.text().strip() if is_custom_formula_type(fit_type) else None
|
|
276
|
+
p0_overrides, fixed_params, bounds = dialog.get_param_settings()
|
|
277
|
+
return (fit_type, custom_formula, dialog.get_weighted(), dialog.get_x_range(),
|
|
278
|
+
p0_overrides, fixed_params, bounds, dialog.get_band_type(), dialog.get_loss())
|
|
279
|
+
return None, None, False, None, {}, {}, {}, None, 'linear'
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class MultiPeakFitDialog(QDialog):
|
|
283
|
+
"""多峰分離の設定。各ピークの初期値の行は、手で、ピーク検出から、またはキャンバスのクリックで入れる。"""
|
|
284
|
+
|
|
285
|
+
COMPONENT_TYPES = [
|
|
286
|
+
('gaussian', 'ガウシアン'),
|
|
287
|
+
('lorentzian', 'ローレンツ'),
|
|
288
|
+
('pseudo_voigt', '擬似フォークト'),
|
|
289
|
+
('voigt', 'フォークト'),
|
|
290
|
+
]
|
|
291
|
+
BASELINE_TYPES = [
|
|
292
|
+
('none', 'なし'),
|
|
293
|
+
('constant', '定数'),
|
|
294
|
+
('linear', '1次'),
|
|
295
|
+
]
|
|
296
|
+
|
|
297
|
+
def __init__(self, parent=None, x_data=None, y_data=None, initial_guesses=None):
|
|
298
|
+
"""x_data / y_data が無ければ「ピーク検出から自動配置」を無効にする。initial_guesses はクリックで集めた初期値。"""
|
|
299
|
+
super().__init__(parent)
|
|
300
|
+
self.setWindowTitle("多峰分離フィット")
|
|
301
|
+
self._x_data = x_data
|
|
302
|
+
self._y_data = y_data
|
|
303
|
+
|
|
304
|
+
layout = QVBoxLayout(self)
|
|
305
|
+
|
|
306
|
+
form = QFormLayout()
|
|
307
|
+
self.component_combo = QComboBox()
|
|
308
|
+
for key, label in self.COMPONENT_TYPES:
|
|
309
|
+
self.component_combo.addItem(label, key)
|
|
310
|
+
form.addRow("成分タイプ(全ピーク共通)", self.component_combo)
|
|
311
|
+
|
|
312
|
+
self.baseline_combo = QComboBox()
|
|
313
|
+
for key, label in self.BASELINE_TYPES:
|
|
314
|
+
self.baseline_combo.addItem(label, key)
|
|
315
|
+
self.baseline_combo.setCurrentIndex(1) # 定数
|
|
316
|
+
form.addRow("ベースライン", self.baseline_combo)
|
|
317
|
+
layout.addLayout(form)
|
|
318
|
+
|
|
319
|
+
layout.addWidget(QLabel(
|
|
320
|
+
"各ピークの初期値(中心X・高さ・おおよその幅[FWHM]):"
|
|
321
|
+
))
|
|
322
|
+
self.guess_table = QTableWidget(0, 3)
|
|
323
|
+
self.guess_table.setHorizontalHeaderLabels(["中心 (X)", "高さ", "幅 (FWHM)"])
|
|
324
|
+
self.guess_table.verticalHeader().setVisible(False)
|
|
325
|
+
self.guess_table.setMaximumHeight(200)
|
|
326
|
+
layout.addWidget(self.guess_table)
|
|
327
|
+
|
|
328
|
+
row_button_layout = QHBoxLayout()
|
|
329
|
+
add_row_button = QPushButton("行を追加")
|
|
330
|
+
add_row_button.clicked.connect(lambda: self._add_guess_row())
|
|
331
|
+
remove_row_button = QPushButton("選択行を削除")
|
|
332
|
+
remove_row_button.clicked.connect(self._remove_selected_rows)
|
|
333
|
+
self.auto_detect_button = QPushButton("ピーク検出から自動配置...")
|
|
334
|
+
self.auto_detect_button.clicked.connect(self._on_auto_detect)
|
|
335
|
+
self.auto_detect_button.setEnabled(x_data is not None and y_data is not None)
|
|
336
|
+
row_button_layout.addWidget(add_row_button)
|
|
337
|
+
row_button_layout.addWidget(remove_row_button)
|
|
338
|
+
row_button_layout.addWidget(self.auto_detect_button)
|
|
339
|
+
row_button_layout.addStretch()
|
|
340
|
+
layout.addLayout(row_button_layout)
|
|
341
|
+
|
|
342
|
+
for guess in (initial_guesses or []):
|
|
343
|
+
self._add_guess_row(
|
|
344
|
+
center=guess.get('center', 0.0),
|
|
345
|
+
height=guess.get('height', 0.0),
|
|
346
|
+
width=guess.get('width', 1.0),
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok |
|
|
350
|
+
QDialogButtonBox.StandardButton.Cancel)
|
|
351
|
+
button_box.accepted.connect(self._on_accept)
|
|
352
|
+
button_box.rejected.connect(self.reject)
|
|
353
|
+
layout.addWidget(button_box)
|
|
354
|
+
|
|
355
|
+
def _add_guess_row(self, center=0.0, height=0.0, width=1.0):
|
|
356
|
+
row = self.guess_table.rowCount()
|
|
357
|
+
self.guess_table.insertRow(row)
|
|
358
|
+
for col, value in enumerate((center, height, width)):
|
|
359
|
+
spin = QDoubleSpinBox()
|
|
360
|
+
spin.setRange(-1e12, 1e12)
|
|
361
|
+
spin.setDecimals(6)
|
|
362
|
+
spin.setValue(float(value))
|
|
363
|
+
self.guess_table.setCellWidget(row, col, spin)
|
|
364
|
+
|
|
365
|
+
def _remove_selected_rows(self):
|
|
366
|
+
rows = sorted({idx.row() for idx in self.guess_table.selectedIndexes()}, reverse=True)
|
|
367
|
+
for row in rows:
|
|
368
|
+
self.guess_table.removeRow(row)
|
|
369
|
+
|
|
370
|
+
def _on_auto_detect(self):
|
|
371
|
+
"""ピーク検出の結果(中心・高さ・FWHM)を行に足す。既にある行は消さない(クリックでの配置と併用できる)。"""
|
|
372
|
+
settings = PeakSettingsDialog.get_peak_settings(self)
|
|
373
|
+
if settings is None:
|
|
374
|
+
return
|
|
375
|
+
from graphica.core.analysis import calculate_peak_quantification
|
|
376
|
+
try:
|
|
377
|
+
result = calculate_peak_quantification(
|
|
378
|
+
self._x_data, self._y_data, settings['peak_type'], settings
|
|
379
|
+
)
|
|
380
|
+
except Exception as e:
|
|
381
|
+
logger.exception("ピークの自動検出に失敗しました")
|
|
382
|
+
notify.warning(self, "ピーク検出", f"ピーク検出に失敗しました:\n{e}")
|
|
383
|
+
return
|
|
384
|
+
if len(result['peak_x']) == 0:
|
|
385
|
+
notify.information(self, "ピーク検出", "条件に一致するピークが見つかりませんでした。")
|
|
386
|
+
return
|
|
387
|
+
for x, y, fwhm in zip(result['peak_x'], result['peak_y'], result['fwhm']):
|
|
388
|
+
self._add_guess_row(center=float(x), height=float(y), width=float(fwhm) or 1.0)
|
|
389
|
+
|
|
390
|
+
def _on_accept(self):
|
|
391
|
+
if self.guess_table.rowCount() == 0:
|
|
392
|
+
notify.warning(self, "多峰分離フィット", "少なくとも1つのピークの初期値が必要です。")
|
|
393
|
+
return
|
|
394
|
+
self.accept()
|
|
395
|
+
|
|
396
|
+
def get_component_type(self):
|
|
397
|
+
return self.component_combo.currentData()
|
|
398
|
+
|
|
399
|
+
def get_baseline_type(self):
|
|
400
|
+
return self.baseline_combo.currentData()
|
|
401
|
+
|
|
402
|
+
def get_initial_guesses(self):
|
|
403
|
+
"""calculate_multi_peak_fit() に渡す [{'center', 'height', 'width'}, ...]。"""
|
|
404
|
+
guesses = []
|
|
405
|
+
for row in range(self.guess_table.rowCount()):
|
|
406
|
+
center = self.guess_table.cellWidget(row, 0).value()
|
|
407
|
+
height = self.guess_table.cellWidget(row, 1).value()
|
|
408
|
+
width = self.guess_table.cellWidget(row, 2).value()
|
|
409
|
+
guesses.append({'center': center, 'height': height, 'width': width})
|
|
410
|
+
return guesses
|
|
411
|
+
|
|
412
|
+
@staticmethod
|
|
413
|
+
def get_multi_peak_fit_settings(parent=None, x_data=None, y_data=None, initial_guesses=None):
|
|
414
|
+
"""OK なら (component_type, baseline_type, initial_guesses)、キャンセルなら (None, None, None)。"""
|
|
415
|
+
dialog = MultiPeakFitDialog(parent, x_data=x_data, y_data=y_data, initial_guesses=initial_guesses)
|
|
416
|
+
if dialog.exec() == QDialog.DialogCode.Accepted:
|
|
417
|
+
return dialog.get_component_type(), dialog.get_baseline_type(), dialog.get_initial_guesses()
|
|
418
|
+
return None, None, None
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
class PeakSettingsDialog(QDialog):
|
|
422
|
+
"""ピーク/谷検出(scipy.signal.find_peaks)の height・distance・prominence を入力する。"""
|
|
423
|
+
|
|
424
|
+
# height の最小値を「なし」(高さで絞らない)として表示する。
|
|
425
|
+
_HEIGHT_NONE = -1e12
|
|
426
|
+
|
|
427
|
+
def __init__(self, parent=None):
|
|
428
|
+
super().__init__(parent)
|
|
429
|
+
self.setWindowTitle("ピーク検出 設定")
|
|
430
|
+
layout = QFormLayout(self)
|
|
431
|
+
|
|
432
|
+
self.type_combo = QComboBox()
|
|
433
|
+
self.type_combo.addItems(["上に凸 (Peaks)", "下に凸 (Valleys)"])
|
|
434
|
+
layout.addRow("検出タイプ", self.type_combo)
|
|
435
|
+
|
|
436
|
+
# 既定を 0 にすると、Y<0 の山や Y>0 の谷が1つも見つからない。
|
|
437
|
+
self.height_spinbox = QDoubleSpinBox()
|
|
438
|
+
self.height_spinbox.setToolTip("検出するピーク/谷の最小高さ (Y値)。「なし」は高さで絞らない。\n例: 5 を指定すると Y > 5 のピークのみ検出。\n例: -10 を指定すると Y < -10 の谷のみ検出。")
|
|
439
|
+
self.height_spinbox.setDecimals(4)
|
|
440
|
+
self.height_spinbox.setRange(self._HEIGHT_NONE, 1e12)
|
|
441
|
+
self.height_spinbox.setSpecialValueText("なし")
|
|
442
|
+
self.height_spinbox.setValue(self._HEIGHT_NONE)
|
|
443
|
+
|
|
444
|
+
self.distance_spinbox = QDoubleSpinBox()
|
|
445
|
+
self.distance_spinbox.setToolTip("隣接するピーク/谷の間の最小距離 (X軸の値)。\n近すぎるピーク/谷を間引きます。")
|
|
446
|
+
self.distance_spinbox.setDecimals(4)
|
|
447
|
+
self.distance_spinbox.setRange(0.0001, np.inf)
|
|
448
|
+
self.distance_spinbox.setValue(1.0)
|
|
449
|
+
|
|
450
|
+
self.prominence_spinbox = QDoubleSpinBox()
|
|
451
|
+
self.prominence_spinbox.setToolTip("ピーク/谷の突出度 (周囲のデータからの際立ち)。\nノイズのような小さなピーク/谷を除去するのに有効です。")
|
|
452
|
+
self.prominence_spinbox.setDecimals(4)
|
|
453
|
+
self.prominence_spinbox.setRange(0.0, np.inf)
|
|
454
|
+
self.prominence_spinbox.setValue(0.0)
|
|
455
|
+
|
|
456
|
+
layout.addRow("Y値の閾値 (Height)", self.height_spinbox)
|
|
457
|
+
layout.addRow("最小X距離 (Distance)", self.distance_spinbox)
|
|
458
|
+
layout.addRow("最小突出度 (Prominence)", self.prominence_spinbox)
|
|
459
|
+
|
|
460
|
+
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok |
|
|
461
|
+
QDialogButtonBox.StandardButton.Cancel)
|
|
462
|
+
button_box.accepted.connect(self.accept)
|
|
463
|
+
button_box.rejected.connect(self.reject)
|
|
464
|
+
layout.addRow(button_box)
|
|
465
|
+
|
|
466
|
+
apply_form_spacing(self)
|
|
467
|
+
|
|
468
|
+
def get_settings(self):
|
|
469
|
+
"""distance_x は X の値のまま(点の数への換算は core.analysis)。height と prominence は無効なら None。"""
|
|
470
|
+
height = self.height_spinbox.value()
|
|
471
|
+
prominence = self.prominence_spinbox.value()
|
|
472
|
+
return {
|
|
473
|
+
"peak_type": self.type_combo.currentText(),
|
|
474
|
+
"height": None if height == self._HEIGHT_NONE else height,
|
|
475
|
+
"distance_x": self.distance_spinbox.value(),
|
|
476
|
+
"prominence": prominence if prominence > 0 else None,
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
@staticmethod
|
|
480
|
+
def get_peak_settings(parent=None):
|
|
481
|
+
"""モーダルで開き、OK なら設定の辞書、キャンセルなら None を返す。"""
|
|
482
|
+
dialog = PeakSettingsDialog(parent)
|
|
483
|
+
if dialog.exec() == QDialog.DialogCode.Accepted:
|
|
484
|
+
return dialog.get_settings()
|
|
485
|
+
return None
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
class ResultDialog(QDialog):
|
|
489
|
+
"""結果の文章をスクロールできる欄で見せる(ピークが多いと QMessageBox は画面からはみ出す)。"""
|
|
490
|
+
|
|
491
|
+
def __init__(self, title, text, parent=None, csv_data=None, residual_x=None, residual_y=None):
|
|
492
|
+
"""csv_data を渡すと「CSVとして保存」を出す。residual_x と residual_y を両方渡すと残差の図も出す。"""
|
|
493
|
+
super().__init__(parent)
|
|
494
|
+
self.setWindowTitle(title)
|
|
495
|
+
self.resize(480, 420)
|
|
496
|
+
self.csv_data = csv_data
|
|
497
|
+
|
|
498
|
+
layout = QVBoxLayout(self)
|
|
499
|
+
|
|
500
|
+
self.text_edit = QPlainTextEdit()
|
|
501
|
+
self.text_edit.setReadOnly(True)
|
|
502
|
+
self.text_edit.setPlainText(text)
|
|
503
|
+
# タブ区切りの桁が揃う
|
|
504
|
+
mono_font = QFont("Consolas")
|
|
505
|
+
mono_font.setStyleHint(QFont.StyleHint.Monospace)
|
|
506
|
+
self.text_edit.setFont(mono_font)
|
|
507
|
+
layout.addWidget(self.text_edit)
|
|
508
|
+
|
|
509
|
+
if residual_x is not None and residual_y is not None and len(residual_x) > 0:
|
|
510
|
+
self.resize(480, 620)
|
|
511
|
+
layout.addWidget(QLabel("残差プロット (実測値 - フィット値)"))
|
|
512
|
+
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg
|
|
513
|
+
from matplotlib.figure import Figure
|
|
514
|
+
from graphica.gui import theme
|
|
515
|
+
# 独立した Figure なのでキャンバスのダークモードの配色を継承しない。テーマの色を当てる
|
|
516
|
+
_tokens = theme.current_tokens()
|
|
517
|
+
fig = Figure(figsize=(4, 2.2), dpi=100, tight_layout=True,
|
|
518
|
+
facecolor=_tokens['surface'])
|
|
519
|
+
canvas = FigureCanvasQTAgg(fig)
|
|
520
|
+
canvas.setFixedHeight(200)
|
|
521
|
+
ax = fig.add_subplot(111)
|
|
522
|
+
ax.set_facecolor(_tokens['surface'])
|
|
523
|
+
ax.axhline(0, color=_tokens['border_strong'], linewidth=0.8, linestyle='--')
|
|
524
|
+
ax.scatter(residual_x, residual_y, s=14, color='#1F6F78')
|
|
525
|
+
ax.set_xlabel("X", fontsize=8, color=_tokens['text_primary'])
|
|
526
|
+
ax.set_ylabel("残差", fontsize=8, color=_tokens['text_primary'])
|
|
527
|
+
ax.tick_params(labelsize=7, colors=_tokens['text_primary'])
|
|
528
|
+
for spine in ax.spines.values():
|
|
529
|
+
spine.set_color(_tokens['border_strong'])
|
|
530
|
+
layout.addWidget(canvas)
|
|
531
|
+
|
|
532
|
+
button_layout = QHBoxLayout()
|
|
533
|
+
|
|
534
|
+
self.copy_button = QPushButton("コピー")
|
|
535
|
+
self.copy_button.clicked.connect(self._on_copy)
|
|
536
|
+
button_layout.addWidget(self.copy_button)
|
|
537
|
+
|
|
538
|
+
if csv_data is not None:
|
|
539
|
+
save_csv_button = QPushButton("CSVとして保存...")
|
|
540
|
+
save_csv_button.clicked.connect(self._on_save_csv)
|
|
541
|
+
button_layout.addWidget(save_csv_button)
|
|
542
|
+
|
|
543
|
+
button_layout.addStretch()
|
|
544
|
+
|
|
545
|
+
close_button = QPushButton("閉じる")
|
|
546
|
+
close_button.clicked.connect(self.reject)
|
|
547
|
+
button_layout.addWidget(close_button)
|
|
548
|
+
|
|
549
|
+
layout.addLayout(button_layout)
|
|
550
|
+
|
|
551
|
+
def _on_copy(self):
|
|
552
|
+
QApplication.clipboard().setText(self.text_edit.toPlainText())
|
|
553
|
+
original_text = self.copy_button.text()
|
|
554
|
+
self.copy_button.setText("コピーしました ✓")
|
|
555
|
+
self.copy_button.setEnabled(False)
|
|
556
|
+
|
|
557
|
+
def _restore():
|
|
558
|
+
self.copy_button.setText(original_text)
|
|
559
|
+
self.copy_button.setEnabled(True)
|
|
560
|
+
|
|
561
|
+
QTimer.singleShot(1200, _restore)
|
|
562
|
+
|
|
563
|
+
def _on_save_csv(self):
|
|
564
|
+
file_path, _ = notify.get_save_file_name(
|
|
565
|
+
self, "CSVとして保存", "", "CSV Files (*.csv);;All Files (*)"
|
|
566
|
+
)
|
|
567
|
+
if not file_path:
|
|
568
|
+
return
|
|
569
|
+
try:
|
|
570
|
+
self.csv_data.to_csv(file_path, index=False, encoding='utf-8-sig')
|
|
571
|
+
notify.information(self, "保存完了", f"CSVファイルとして保存しました:\n{file_path}")
|
|
572
|
+
except Exception as e:
|
|
573
|
+
logger.exception("結果の CSV 保存に失敗しました")
|
|
574
|
+
notify.warning(self, "保存エラー", f"CSV保存中にエラーが発生しました:\n{e}")
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
class BaselineCorrectionDialog(QDialog):
|
|
578
|
+
METHOD_ALS = "ALS(Asymmetric Least Squares)"
|
|
579
|
+
METHOD_POLYNOMIAL = "多項式(反復フィット)"
|
|
580
|
+
METHOD_RUBBERBAND = "ラバーバンド(下側凸包)"
|
|
581
|
+
METHOD_MANUAL = "手動点"
|
|
582
|
+
METHODS = [METHOD_ALS, METHOD_POLYNOMIAL, METHOD_RUBBERBAND, METHOD_MANUAL]
|
|
583
|
+
|
|
584
|
+
_MANUAL_LINEAR = "直線"
|
|
585
|
+
_MANUAL_SPLINE = "3次スプライン"
|
|
586
|
+
|
|
587
|
+
def __init__(self, name, x_min=None, x_max=None, parent=None):
|
|
588
|
+
super().__init__(parent)
|
|
589
|
+
self.setWindowTitle("ベースライン補正")
|
|
590
|
+
self.resize(420, 440)
|
|
591
|
+
self._name = name
|
|
592
|
+
|
|
593
|
+
layout = QVBoxLayout(self)
|
|
594
|
+
layout.addWidget(QLabel(f"対象: {name}"))
|
|
595
|
+
|
|
596
|
+
method_form = QFormLayout()
|
|
597
|
+
self.method_combo = QComboBox()
|
|
598
|
+
self.method_combo.addItems(self.METHODS)
|
|
599
|
+
method_form.addRow("手法", self.method_combo)
|
|
600
|
+
layout.addLayout(method_form)
|
|
601
|
+
|
|
602
|
+
self.stack = QStackedWidget()
|
|
603
|
+
layout.addWidget(self.stack)
|
|
604
|
+
|
|
605
|
+
als_page = QWidget()
|
|
606
|
+
als_form = QFormLayout(als_page)
|
|
607
|
+
self.als_lam_spinbox = QDoubleSpinBox()
|
|
608
|
+
self.als_lam_spinbox.setRange(1.0, 1e12)
|
|
609
|
+
self.als_lam_spinbox.setDecimals(0)
|
|
610
|
+
self.als_lam_spinbox.setValue(1e5)
|
|
611
|
+
als_form.addRow("lam(平滑さ)", self.als_lam_spinbox)
|
|
612
|
+
self.als_p_spinbox = QDoubleSpinBox()
|
|
613
|
+
self.als_p_spinbox.setRange(0.0001, 0.9999)
|
|
614
|
+
self.als_p_spinbox.setDecimals(4)
|
|
615
|
+
self.als_p_spinbox.setSingleStep(0.001)
|
|
616
|
+
self.als_p_spinbox.setValue(0.01)
|
|
617
|
+
als_form.addRow("p(非対称重み)", self.als_p_spinbox)
|
|
618
|
+
self.als_niter_spinbox = QSpinBox()
|
|
619
|
+
self.als_niter_spinbox.setRange(1, 1000)
|
|
620
|
+
self.als_niter_spinbox.setValue(10)
|
|
621
|
+
als_form.addRow("反復回数", self.als_niter_spinbox)
|
|
622
|
+
self.stack.addWidget(als_page)
|
|
623
|
+
|
|
624
|
+
poly_page = QWidget()
|
|
625
|
+
poly_form = QFormLayout(poly_page)
|
|
626
|
+
self.poly_degree_spinbox = QSpinBox()
|
|
627
|
+
self.poly_degree_spinbox.setRange(0, 10)
|
|
628
|
+
self.poly_degree_spinbox.setValue(3)
|
|
629
|
+
poly_form.addRow("次数", self.poly_degree_spinbox)
|
|
630
|
+
self.poly_iterations_spinbox = QSpinBox()
|
|
631
|
+
self.poly_iterations_spinbox.setRange(1, 1000)
|
|
632
|
+
self.poly_iterations_spinbox.setValue(10)
|
|
633
|
+
poly_form.addRow("反復回数", self.poly_iterations_spinbox)
|
|
634
|
+
self.stack.addWidget(poly_page)
|
|
635
|
+
|
|
636
|
+
rubberband_page = QWidget()
|
|
637
|
+
rubberband_layout = QVBoxLayout(rubberband_page)
|
|
638
|
+
rubberband_info = QLabel(
|
|
639
|
+
"データ点群の下側凸包(convex hull)を結んだ曲線をベースラインとします。"
|
|
640
|
+
"設定できるパラメータはありません。"
|
|
641
|
+
)
|
|
642
|
+
rubberband_info.setWordWrap(True)
|
|
643
|
+
rubberband_layout.addWidget(rubberband_info)
|
|
644
|
+
rubberband_layout.addStretch()
|
|
645
|
+
self.stack.addWidget(rubberband_page)
|
|
646
|
+
|
|
647
|
+
manual_page = QWidget()
|
|
648
|
+
manual_layout = QVBoxLayout(manual_page)
|
|
649
|
+
manual_layout.addWidget(QLabel("アンカー点のX座標をカンマ区切りで入力してください(2点以上):"))
|
|
650
|
+
self.manual_anchor_edit = QPlainTextEdit()
|
|
651
|
+
self.manual_anchor_edit.setPlaceholderText("例: 0, 20, 50, 80, 100")
|
|
652
|
+
if x_min is not None and x_max is not None:
|
|
653
|
+
self.manual_anchor_edit.setPlainText(f"{x_min:g}, {x_max:g}")
|
|
654
|
+
self.manual_anchor_edit.setMaximumHeight(60)
|
|
655
|
+
manual_layout.addWidget(self.manual_anchor_edit)
|
|
656
|
+
manual_method_form = QFormLayout()
|
|
657
|
+
self.manual_method_combo = QComboBox()
|
|
658
|
+
self.manual_method_combo.addItems([self._MANUAL_LINEAR, self._MANUAL_SPLINE])
|
|
659
|
+
manual_method_form.addRow("補間方法", self.manual_method_combo)
|
|
660
|
+
manual_layout.addLayout(manual_method_form)
|
|
661
|
+
manual_info = QLabel(
|
|
662
|
+
"各アンカー点のYは、指定したX位置での実データを線形補間して自動的に求めます"
|
|
663
|
+
"(3次スプラインには3点以上必要です)。"
|
|
664
|
+
)
|
|
665
|
+
manual_info.setWordWrap(True)
|
|
666
|
+
manual_layout.addWidget(manual_info)
|
|
667
|
+
manual_layout.addStretch()
|
|
668
|
+
self.stack.addWidget(manual_page)
|
|
669
|
+
|
|
670
|
+
self.method_combo.currentIndexChanged.connect(self.stack.setCurrentIndex)
|
|
671
|
+
|
|
672
|
+
form = QFormLayout()
|
|
673
|
+
self.output_name_edit = QLineEdit(f"{name}_baseline_corrected")
|
|
674
|
+
form.addRow("出力データセット名", self.output_name_edit)
|
|
675
|
+
layout.addLayout(form)
|
|
676
|
+
|
|
677
|
+
self.add_baseline_checkbox = QCheckBox("推定したベースライン曲線も別データセットとして追加する")
|
|
678
|
+
layout.addWidget(self.add_baseline_checkbox)
|
|
679
|
+
|
|
680
|
+
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok |
|
|
681
|
+
QDialogButtonBox.StandardButton.Cancel)
|
|
682
|
+
button_box.accepted.connect(self.accept)
|
|
683
|
+
button_box.rejected.connect(self.reject)
|
|
684
|
+
layout.addWidget(button_box)
|
|
685
|
+
|
|
686
|
+
apply_form_spacing(self)
|
|
687
|
+
|
|
688
|
+
def get_settings(self):
|
|
689
|
+
"""(手法, 手法の引数, 出力名, ベースラインも追加するか)。
|
|
690
|
+
|
|
691
|
+
"manual" の引数は anchor_x ではなく未解釈の anchor_x_text(書式の誤りは呼び出し側が知らせる)。
|
|
692
|
+
"""
|
|
693
|
+
method_text = self.method_combo.currentText()
|
|
694
|
+
if method_text == self.METHOD_ALS:
|
|
695
|
+
method = "als"
|
|
696
|
+
params = {
|
|
697
|
+
"lam": self.als_lam_spinbox.value(),
|
|
698
|
+
"p": self.als_p_spinbox.value(),
|
|
699
|
+
"niter": self.als_niter_spinbox.value(),
|
|
700
|
+
}
|
|
701
|
+
elif method_text == self.METHOD_POLYNOMIAL:
|
|
702
|
+
method = "polynomial"
|
|
703
|
+
params = {
|
|
704
|
+
"degree": self.poly_degree_spinbox.value(),
|
|
705
|
+
"iterations": self.poly_iterations_spinbox.value(),
|
|
706
|
+
}
|
|
707
|
+
elif method_text == self.METHOD_RUBBERBAND:
|
|
708
|
+
method = "rubberband"
|
|
709
|
+
params = {}
|
|
710
|
+
else:
|
|
711
|
+
method = "manual"
|
|
712
|
+
params = {
|
|
713
|
+
"anchor_x_text": self.manual_anchor_edit.toPlainText(),
|
|
714
|
+
"method": "spline" if self.manual_method_combo.currentText() == self._MANUAL_SPLINE else "linear",
|
|
715
|
+
}
|
|
716
|
+
return method, params, self.output_name_edit.text().strip(), self.add_baseline_checkbox.isChecked()
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
class SavGolDialog(QDialog):
|
|
720
|
+
MODE_SMOOTH = "平滑化"
|
|
721
|
+
MODE_DERIV1 = "1次微分"
|
|
722
|
+
MODE_DERIV2 = "2次微分"
|
|
723
|
+
MODES = [MODE_SMOOTH, MODE_DERIV1, MODE_DERIV2]
|
|
724
|
+
_DERIV_BY_MODE = {MODE_SMOOTH: 0, MODE_DERIV1: 1, MODE_DERIV2: 2}
|
|
725
|
+
_SUFFIX_BY_MODE = {MODE_SMOOTH: "_smoothed", MODE_DERIV1: "_deriv1", MODE_DERIV2: "_deriv2"}
|
|
726
|
+
|
|
727
|
+
def __init__(self, name, max_window=None, parent=None):
|
|
728
|
+
super().__init__(parent)
|
|
729
|
+
self.setWindowTitle("Savitzky-Golayフィルタ")
|
|
730
|
+
self.resize(380, 260)
|
|
731
|
+
self._name = name
|
|
732
|
+
|
|
733
|
+
layout = QVBoxLayout(self)
|
|
734
|
+
layout.addWidget(QLabel(f"対象: {name}"))
|
|
735
|
+
|
|
736
|
+
form = QFormLayout()
|
|
737
|
+
self.mode_combo = QComboBox()
|
|
738
|
+
self.mode_combo.addItems(self.MODES)
|
|
739
|
+
form.addRow("種類", self.mode_combo)
|
|
740
|
+
|
|
741
|
+
self.window_spinbox = QSpinBox()
|
|
742
|
+
self.window_spinbox.setRange(3, max_window if max_window else 999999)
|
|
743
|
+
self.window_spinbox.setSingleStep(2)
|
|
744
|
+
self.window_spinbox.setValue(min(5, max_window) if max_window else 5)
|
|
745
|
+
form.addRow("窓幅(奇数)", self.window_spinbox)
|
|
746
|
+
|
|
747
|
+
self.polyorder_spinbox = QSpinBox()
|
|
748
|
+
self.polyorder_spinbox.setRange(1, 10)
|
|
749
|
+
self.polyorder_spinbox.setValue(2)
|
|
750
|
+
form.addRow("多項式の次数", self.polyorder_spinbox)
|
|
751
|
+
|
|
752
|
+
self.output_name_edit = QLineEdit(f"{name}{self._SUFFIX_BY_MODE[self.MODE_SMOOTH]}")
|
|
753
|
+
form.addRow("出力データセット名", self.output_name_edit)
|
|
754
|
+
layout.addLayout(form)
|
|
755
|
+
|
|
756
|
+
self.mode_combo.currentTextChanged.connect(self._on_mode_changed)
|
|
757
|
+
|
|
758
|
+
info_label = QLabel(
|
|
759
|
+
"窓幅は奇数かつ多項式の次数より大きい値を指定してください。\n"
|
|
760
|
+
"微分はX軸の間隔が概ね等間隔であることを前提とします。"
|
|
761
|
+
)
|
|
762
|
+
info_label.setWordWrap(True)
|
|
763
|
+
layout.addWidget(info_label)
|
|
764
|
+
|
|
765
|
+
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok |
|
|
766
|
+
QDialogButtonBox.StandardButton.Cancel)
|
|
767
|
+
button_box.accepted.connect(self.accept)
|
|
768
|
+
button_box.rejected.connect(self.reject)
|
|
769
|
+
layout.addWidget(button_box)
|
|
770
|
+
|
|
771
|
+
apply_form_spacing(self)
|
|
772
|
+
|
|
773
|
+
def _on_mode_changed(self, mode):
|
|
774
|
+
self.output_name_edit.setText(f"{self._name}{self._SUFFIX_BY_MODE[mode]}")
|
|
775
|
+
|
|
776
|
+
def get_settings(self):
|
|
777
|
+
"""(窓幅, 多項式の次数, 微分の階数, 出力名)"""
|
|
778
|
+
mode = self.mode_combo.currentText()
|
|
779
|
+
return (
|
|
780
|
+
self.window_spinbox.value(),
|
|
781
|
+
self.polyorder_spinbox.value(),
|
|
782
|
+
self._DERIV_BY_MODE[mode],
|
|
783
|
+
self.output_name_edit.text().strip(),
|
|
784
|
+
)
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
class IntervalIntegralDialog(QDialog):
|
|
788
|
+
"""区間積分の設定。X の範囲はデータの範囲を初期値にする(そのまま OK で全体を積分)。"""
|
|
789
|
+
|
|
790
|
+
METHOD_TRAPEZOID = "台形則(Trapezoidal)"
|
|
791
|
+
METHOD_SIMPSON = "Simpson則"
|
|
792
|
+
METHODS = [METHOD_TRAPEZOID, METHOD_SIMPSON]
|
|
793
|
+
_METHOD_KEY_BY_LABEL = {METHOD_TRAPEZOID: "trapezoid", METHOD_SIMPSON: "simpson"}
|
|
794
|
+
|
|
795
|
+
def __init__(self, name, x_min=None, x_max=None, parent=None):
|
|
796
|
+
super().__init__(parent)
|
|
797
|
+
self.setWindowTitle("区間積分")
|
|
798
|
+
self.resize(380, 260)
|
|
799
|
+
|
|
800
|
+
layout = QVBoxLayout(self)
|
|
801
|
+
layout.addWidget(QLabel(f"対象: {name}"))
|
|
802
|
+
|
|
803
|
+
form = QFormLayout()
|
|
804
|
+
self.method_combo = QComboBox()
|
|
805
|
+
self.method_combo.addItems(self.METHODS)
|
|
806
|
+
form.addRow("積分方法", self.method_combo)
|
|
807
|
+
|
|
808
|
+
self.range_min_spinbox = QDoubleSpinBox()
|
|
809
|
+
self.range_min_spinbox.setRange(-1e12, 1e12)
|
|
810
|
+
self.range_min_spinbox.setDecimals(6)
|
|
811
|
+
if x_min is not None:
|
|
812
|
+
self.range_min_spinbox.setValue(x_min)
|
|
813
|
+
form.addRow("最小X", self.range_min_spinbox)
|
|
814
|
+
|
|
815
|
+
self.range_max_spinbox = QDoubleSpinBox()
|
|
816
|
+
self.range_max_spinbox.setRange(-1e12, 1e12)
|
|
817
|
+
self.range_max_spinbox.setDecimals(6)
|
|
818
|
+
if x_max is not None:
|
|
819
|
+
self.range_max_spinbox.setValue(x_max)
|
|
820
|
+
form.addRow("最大X", self.range_max_spinbox)
|
|
821
|
+
layout.addLayout(form)
|
|
822
|
+
|
|
823
|
+
self.subtract_baseline_checkbox = QCheckBox(
|
|
824
|
+
"ベースラインを差し引く(範囲の両端を結ぶ直線を差し引いてから積分)"
|
|
825
|
+
)
|
|
826
|
+
self.subtract_baseline_checkbox.setToolTip(
|
|
827
|
+
"積分範囲の両端のY値(実データを線形補間して求めた値)を結ぶ直線を\n"
|
|
828
|
+
"ベースラインとしてYから差し引いてから積分します。\n"
|
|
829
|
+
"スペクトルのピーク面積など「傾いた背景の上のピーク」を求める用途向けの\n"
|
|
830
|
+
"簡易オプションです。ALS等の本格的なベースライン補正を使いたい場合は、\n"
|
|
831
|
+
"先に「ベースライン補正...」で補正したデータセットに対してこの機能を\n"
|
|
832
|
+
"使ってください。"
|
|
833
|
+
)
|
|
834
|
+
layout.addWidget(self.subtract_baseline_checkbox)
|
|
835
|
+
|
|
836
|
+
info_label = QLabel(
|
|
837
|
+
"台形則・Simpson則いずれもデータ点数の偶奇による制約はありません。"
|
|
838
|
+
)
|
|
839
|
+
info_label.setWordWrap(True)
|
|
840
|
+
layout.addWidget(info_label)
|
|
841
|
+
|
|
842
|
+
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok |
|
|
843
|
+
QDialogButtonBox.StandardButton.Cancel)
|
|
844
|
+
button_box.accepted.connect(self.accept)
|
|
845
|
+
button_box.rejected.connect(self.reject)
|
|
846
|
+
layout.addWidget(button_box)
|
|
847
|
+
|
|
848
|
+
apply_form_spacing(self)
|
|
849
|
+
|
|
850
|
+
def get_settings(self):
|
|
851
|
+
"""(積分の方法, (最小X, 最大X), ベースラインを引くか)"""
|
|
852
|
+
method_text = self.method_combo.currentText()
|
|
853
|
+
method = self._METHOD_KEY_BY_LABEL[method_text]
|
|
854
|
+
x_range = (self.range_min_spinbox.value(), self.range_max_spinbox.value())
|
|
855
|
+
return method, x_range, self.subtract_baseline_checkbox.isChecked()
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
class CumulativeIntegralDialog(QDialog):
|
|
859
|
+
METHOD_TRAPEZOID = "台形則(Trapezoidal)"
|
|
860
|
+
METHOD_SIMPSON = "Simpson則"
|
|
861
|
+
METHODS = [METHOD_TRAPEZOID, METHOD_SIMPSON]
|
|
862
|
+
_METHOD_KEY_BY_LABEL = {METHOD_TRAPEZOID: "trapezoid", METHOD_SIMPSON: "simpson"}
|
|
863
|
+
|
|
864
|
+
def __init__(self, name, parent=None):
|
|
865
|
+
super().__init__(parent)
|
|
866
|
+
self.setWindowTitle("累積積分")
|
|
867
|
+
self.resize(380, 200)
|
|
868
|
+
|
|
869
|
+
layout = QVBoxLayout(self)
|
|
870
|
+
layout.addWidget(QLabel(f"対象: {name}"))
|
|
871
|
+
|
|
872
|
+
form = QFormLayout()
|
|
873
|
+
self.method_combo = QComboBox()
|
|
874
|
+
self.method_combo.addItems(self.METHODS)
|
|
875
|
+
form.addRow("積分方法", self.method_combo)
|
|
876
|
+
|
|
877
|
+
self.output_name_edit = QLineEdit(f"{name}_cumsum")
|
|
878
|
+
form.addRow("出力データセット名", self.output_name_edit)
|
|
879
|
+
layout.addLayout(form)
|
|
880
|
+
|
|
881
|
+
info_label = QLabel(
|
|
882
|
+
"Xの各点までの積分値 ∫[x_min, x_i] y dx を新しいデータセットとして追加します。\n"
|
|
883
|
+
"範囲を絞りたい場合は「区間積分」を使ってください。"
|
|
884
|
+
)
|
|
885
|
+
info_label.setWordWrap(True)
|
|
886
|
+
layout.addWidget(info_label)
|
|
887
|
+
|
|
888
|
+
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok |
|
|
889
|
+
QDialogButtonBox.StandardButton.Cancel)
|
|
890
|
+
button_box.accepted.connect(self.accept)
|
|
891
|
+
button_box.rejected.connect(self.reject)
|
|
892
|
+
layout.addWidget(button_box)
|
|
893
|
+
|
|
894
|
+
apply_form_spacing(self)
|
|
895
|
+
|
|
896
|
+
def get_settings(self):
|
|
897
|
+
"""(積分の方法, 出力名)"""
|
|
898
|
+
method_text = self.method_combo.currentText()
|
|
899
|
+
method = self._METHOD_KEY_BY_LABEL[method_text]
|
|
900
|
+
return method, self.output_name_edit.text().strip()
|
|
901
|
+
|
|
902
|
+
|
|
903
|
+
class NormalizeDatasetDialog(QDialog):
|
|
904
|
+
"""最大値か、指定した X での値を 1.0 にする。"""
|
|
905
|
+
|
|
906
|
+
MODE_MAX = "最大値基準"
|
|
907
|
+
MODE_X_VALUE = "特定X値での強度基準"
|
|
908
|
+
MODES = [MODE_MAX, MODE_X_VALUE]
|
|
909
|
+
|
|
910
|
+
def __init__(self, name, x_min=None, x_max=None, parent=None):
|
|
911
|
+
super().__init__(parent)
|
|
912
|
+
self.setWindowTitle("規格化(ノーマライズ)")
|
|
913
|
+
self.resize(360, 220)
|
|
914
|
+
|
|
915
|
+
layout = QVBoxLayout(self)
|
|
916
|
+
label = QLabel(f"対象: {name}")
|
|
917
|
+
layout.addWidget(label)
|
|
918
|
+
|
|
919
|
+
form = QFormLayout()
|
|
920
|
+
self.mode_combo = QComboBox()
|
|
921
|
+
self.mode_combo.addItems(self.MODES)
|
|
922
|
+
form.addRow("基準", self.mode_combo)
|
|
923
|
+
|
|
924
|
+
self.reference_x_spinbox = QDoubleSpinBox()
|
|
925
|
+
self.reference_x_spinbox.setRange(-1e12, 1e12)
|
|
926
|
+
self.reference_x_spinbox.setDecimals(6)
|
|
927
|
+
self.reference_x_spinbox.setEnabled(False)
|
|
928
|
+
if x_min is not None:
|
|
929
|
+
self.reference_x_spinbox.setValue(x_min)
|
|
930
|
+
form.addRow("基準X値", self.reference_x_spinbox)
|
|
931
|
+
|
|
932
|
+
self.output_name_edit = QLineEdit(f"{name}_normalized")
|
|
933
|
+
form.addRow("出力データセット名", self.output_name_edit)
|
|
934
|
+
layout.addLayout(form)
|
|
935
|
+
|
|
936
|
+
self.mode_combo.currentTextChanged.connect(
|
|
937
|
+
lambda text: self.reference_x_spinbox.setEnabled(text == self.MODE_X_VALUE)
|
|
938
|
+
)
|
|
939
|
+
|
|
940
|
+
info_label = QLabel(
|
|
941
|
+
"最大値基準: Yの最大値が1.0になるよう規格化します(マスク中の行は除外)。\n"
|
|
942
|
+
"特定X値での強度基準: 指定したX値でのYを線形補間し、その値が1.0になるよう規格化します。"
|
|
943
|
+
)
|
|
944
|
+
info_label.setWordWrap(True)
|
|
945
|
+
layout.addWidget(info_label)
|
|
946
|
+
|
|
947
|
+
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok |
|
|
948
|
+
QDialogButtonBox.StandardButton.Cancel)
|
|
949
|
+
button_box.accepted.connect(self.accept)
|
|
950
|
+
button_box.rejected.connect(self.reject)
|
|
951
|
+
layout.addWidget(button_box)
|
|
952
|
+
|
|
953
|
+
apply_form_spacing(self)
|
|
954
|
+
|
|
955
|
+
def get_settings(self):
|
|
956
|
+
"""(基準の種類, 基準の X(MODE_X_VALUE のときだけ), 出力名)"""
|
|
957
|
+
mode = self.mode_combo.currentText()
|
|
958
|
+
reference_x = self.reference_x_spinbox.value() if mode == self.MODE_X_VALUE else None
|
|
959
|
+
return mode, reference_x, self.output_name_edit.text().strip()
|
|
960
|
+
|
|
961
|
+
|
|
962
|
+
class ResampleDatasetDialog(QDialog):
|
|
963
|
+
"""ほかのデータセットの X か等間隔のグリッドに補間する。"""
|
|
964
|
+
|
|
965
|
+
SOURCE_DATASET = "他のデータセットのX格子"
|
|
966
|
+
SOURCE_LINSPACE = "等間隔グリッド"
|
|
967
|
+
SOURCES = [SOURCE_DATASET, SOURCE_LINSPACE]
|
|
968
|
+
|
|
969
|
+
METHOD_LINEAR = "線形補間"
|
|
970
|
+
METHOD_CUBIC = "3次スプライン補間"
|
|
971
|
+
METHODS = [METHOD_LINEAR, METHOD_CUBIC]
|
|
972
|
+
_METHOD_KEY_BY_LABEL = {METHOD_LINEAR: "linear", METHOD_CUBIC: "cubic"}
|
|
973
|
+
|
|
974
|
+
def __init__(self, name, other_dataset_names, x_min=None, x_max=None, parent=None):
|
|
975
|
+
super().__init__(parent)
|
|
976
|
+
self.setWindowTitle("共通X格子へのリサンプリング/補間")
|
|
977
|
+
self.resize(420, 380)
|
|
978
|
+
self._name = name
|
|
979
|
+
|
|
980
|
+
layout = QVBoxLayout(self)
|
|
981
|
+
layout.addWidget(QLabel(f"対象: {name}"))
|
|
982
|
+
|
|
983
|
+
source_form = QFormLayout()
|
|
984
|
+
self.source_combo = QComboBox()
|
|
985
|
+
self.source_combo.addItems(self.SOURCES)
|
|
986
|
+
source_form.addRow("リサンプリング先の格子", self.source_combo)
|
|
987
|
+
layout.addLayout(source_form)
|
|
988
|
+
|
|
989
|
+
self.stack = QStackedWidget()
|
|
990
|
+
layout.addWidget(self.stack)
|
|
991
|
+
|
|
992
|
+
dataset_page = QWidget()
|
|
993
|
+
dataset_form = QFormLayout(dataset_page)
|
|
994
|
+
self.other_dataset_combo = QComboBox()
|
|
995
|
+
self.other_dataset_combo.addItems(other_dataset_names)
|
|
996
|
+
dataset_form.addRow("データセット", self.other_dataset_combo)
|
|
997
|
+
if not other_dataset_names:
|
|
998
|
+
# ほかにデータセットが無いので、空のまま OK されないよう無効にする
|
|
999
|
+
self.other_dataset_combo.setEnabled(False)
|
|
1000
|
+
no_dataset_label = QLabel("(他に読み込まれているデータセットがありません)")
|
|
1001
|
+
no_dataset_label.setWordWrap(True)
|
|
1002
|
+
dataset_form.addRow(no_dataset_label)
|
|
1003
|
+
self.stack.addWidget(dataset_page)
|
|
1004
|
+
|
|
1005
|
+
linspace_page = QWidget()
|
|
1006
|
+
linspace_form = QFormLayout(linspace_page)
|
|
1007
|
+
self.linspace_start_spinbox = QDoubleSpinBox()
|
|
1008
|
+
self.linspace_start_spinbox.setRange(-1e12, 1e12)
|
|
1009
|
+
self.linspace_start_spinbox.setDecimals(6)
|
|
1010
|
+
if x_min is not None:
|
|
1011
|
+
self.linspace_start_spinbox.setValue(x_min)
|
|
1012
|
+
linspace_form.addRow("開始X", self.linspace_start_spinbox)
|
|
1013
|
+
|
|
1014
|
+
self.linspace_stop_spinbox = QDoubleSpinBox()
|
|
1015
|
+
self.linspace_stop_spinbox.setRange(-1e12, 1e12)
|
|
1016
|
+
self.linspace_stop_spinbox.setDecimals(6)
|
|
1017
|
+
if x_max is not None:
|
|
1018
|
+
self.linspace_stop_spinbox.setValue(x_max)
|
|
1019
|
+
linspace_form.addRow("終了X", self.linspace_stop_spinbox)
|
|
1020
|
+
|
|
1021
|
+
self.linspace_num_points_spinbox = QSpinBox()
|
|
1022
|
+
self.linspace_num_points_spinbox.setRange(2, 1_000_000)
|
|
1023
|
+
self.linspace_num_points_spinbox.setValue(100)
|
|
1024
|
+
linspace_form.addRow("点数", self.linspace_num_points_spinbox)
|
|
1025
|
+
self.stack.addWidget(linspace_page)
|
|
1026
|
+
|
|
1027
|
+
self.source_combo.currentIndexChanged.connect(self.stack.setCurrentIndex)
|
|
1028
|
+
if not other_dataset_names:
|
|
1029
|
+
self.source_combo.setCurrentText(self.SOURCE_LINSPACE)
|
|
1030
|
+
|
|
1031
|
+
method_form = QFormLayout()
|
|
1032
|
+
self.method_combo = QComboBox()
|
|
1033
|
+
self.method_combo.addItems(self.METHODS)
|
|
1034
|
+
method_form.addRow("補間方法", self.method_combo)
|
|
1035
|
+
layout.addLayout(method_form)
|
|
1036
|
+
|
|
1037
|
+
self.extrapolate_checkbox = QCheckBox("範囲外を外挿する(既定はNaNにする)")
|
|
1038
|
+
self.extrapolate_checkbox.setToolTip(
|
|
1039
|
+
"元データのX範囲外にある格子点の扱いです。\n"
|
|
1040
|
+
"オフ(既定): 範囲外はNaNになります(安全側)。\n"
|
|
1041
|
+
"オン: 線形補間は両端の傾きで、3次スプラインはscipy標準の\n"
|
|
1042
|
+
"区間多項式の延長で外挿します(いずれも範囲から離れるほど\n"
|
|
1043
|
+
"不正確になりうる点に注意してください)。"
|
|
1044
|
+
)
|
|
1045
|
+
layout.addWidget(self.extrapolate_checkbox)
|
|
1046
|
+
|
|
1047
|
+
output_form = QFormLayout()
|
|
1048
|
+
self.output_name_edit = QLineEdit(f"{name}_resampled")
|
|
1049
|
+
output_form.addRow("出力データセット名", self.output_name_edit)
|
|
1050
|
+
layout.addLayout(output_form)
|
|
1051
|
+
|
|
1052
|
+
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok |
|
|
1053
|
+
QDialogButtonBox.StandardButton.Cancel)
|
|
1054
|
+
button_box.accepted.connect(self.accept)
|
|
1055
|
+
button_box.rejected.connect(self.reject)
|
|
1056
|
+
layout.addWidget(button_box)
|
|
1057
|
+
|
|
1058
|
+
apply_form_spacing(self)
|
|
1059
|
+
|
|
1060
|
+
def get_settings(self):
|
|
1061
|
+
"""(グリッドの元, 元ごとの引数, 補間の方法, 外挿するか, 出力名)。
|
|
1062
|
+
|
|
1063
|
+
引数は "dataset" なら {"dataset_name"}、"linspace" なら {"start", "stop", "num_points"}。
|
|
1064
|
+
"""
|
|
1065
|
+
source_text = self.source_combo.currentText()
|
|
1066
|
+
if source_text == self.SOURCE_DATASET:
|
|
1067
|
+
source = "dataset"
|
|
1068
|
+
params = {"dataset_name": self.other_dataset_combo.currentText()}
|
|
1069
|
+
else:
|
|
1070
|
+
source = "linspace"
|
|
1071
|
+
params = {
|
|
1072
|
+
"start": self.linspace_start_spinbox.value(),
|
|
1073
|
+
"stop": self.linspace_stop_spinbox.value(),
|
|
1074
|
+
"num_points": self.linspace_num_points_spinbox.value(),
|
|
1075
|
+
}
|
|
1076
|
+
method_text = self.method_combo.currentText()
|
|
1077
|
+
method = self._METHOD_KEY_BY_LABEL[method_text]
|
|
1078
|
+
return (
|
|
1079
|
+
source, params, method,
|
|
1080
|
+
self.extrapolate_checkbox.isChecked(),
|
|
1081
|
+
self.output_name_edit.text().strip(),
|
|
1082
|
+
)
|
|
1083
|
+
|
|
1084
|
+
|
|
1085
|
+
class OutlierDetectionDialog(QDialog):
|
|
1086
|
+
"""外れ値の検出の設定。マスクに適用するかは別のチェック(既定はオフ、結果を見てから決められる)。"""
|
|
1087
|
+
|
|
1088
|
+
METHOD_ZSCORE = "Z-score"
|
|
1089
|
+
METHOD_IQR = "IQR(四分位範囲)"
|
|
1090
|
+
METHODS = [METHOD_ZSCORE, METHOD_IQR]
|
|
1091
|
+
_METHOD_KEY_BY_LABEL = {METHOD_ZSCORE: "zscore", METHOD_IQR: "iqr"}
|
|
1092
|
+
|
|
1093
|
+
def __init__(self, name, parent=None):
|
|
1094
|
+
super().__init__(parent)
|
|
1095
|
+
self.setWindowTitle("外れ値検出")
|
|
1096
|
+
self.resize(420, 280)
|
|
1097
|
+
|
|
1098
|
+
layout = QVBoxLayout(self)
|
|
1099
|
+
layout.addWidget(QLabel(f"対象: {name}(Y値をもとに検出します)"))
|
|
1100
|
+
|
|
1101
|
+
form = QFormLayout()
|
|
1102
|
+
self.method_combo = QComboBox()
|
|
1103
|
+
self.method_combo.addItems(self.METHODS)
|
|
1104
|
+
form.addRow("検出方法", self.method_combo)
|
|
1105
|
+
layout.addLayout(form)
|
|
1106
|
+
|
|
1107
|
+
self.stack = QStackedWidget()
|
|
1108
|
+
|
|
1109
|
+
zscore_page = QWidget()
|
|
1110
|
+
zscore_form = QFormLayout(zscore_page)
|
|
1111
|
+
self.threshold_spinbox = QDoubleSpinBox()
|
|
1112
|
+
self.threshold_spinbox.setRange(0.1, 100.0)
|
|
1113
|
+
self.threshold_spinbox.setDecimals(2)
|
|
1114
|
+
self.threshold_spinbox.setValue(3.0)
|
|
1115
|
+
zscore_form.addRow("しきい値(|Z| >)", self.threshold_spinbox)
|
|
1116
|
+
self.stack.addWidget(zscore_page)
|
|
1117
|
+
|
|
1118
|
+
iqr_page = QWidget()
|
|
1119
|
+
iqr_form = QFormLayout(iqr_page)
|
|
1120
|
+
self.multiplier_spinbox = QDoubleSpinBox()
|
|
1121
|
+
self.multiplier_spinbox.setRange(0.1, 100.0)
|
|
1122
|
+
self.multiplier_spinbox.setDecimals(2)
|
|
1123
|
+
self.multiplier_spinbox.setValue(1.5)
|
|
1124
|
+
iqr_form.addRow("係数(IQR ×)", self.multiplier_spinbox)
|
|
1125
|
+
self.stack.addWidget(iqr_page)
|
|
1126
|
+
|
|
1127
|
+
layout.addWidget(self.stack)
|
|
1128
|
+
self.method_combo.currentIndexChanged.connect(self.stack.setCurrentIndex)
|
|
1129
|
+
|
|
1130
|
+
self.apply_mask_checkbox = QCheckBox("検出した外れ値をマスク(除外)に適用する")
|
|
1131
|
+
self.apply_mask_checkbox.setToolTip(
|
|
1132
|
+
"チェックを外すと検出結果を表示するだけでマスクは変更しません。\n"
|
|
1133
|
+
"マスクは非破壊で、いつでも解除できます。"
|
|
1134
|
+
)
|
|
1135
|
+
layout.addWidget(self.apply_mask_checkbox)
|
|
1136
|
+
|
|
1137
|
+
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok |
|
|
1138
|
+
QDialogButtonBox.StandardButton.Cancel)
|
|
1139
|
+
button_box.accepted.connect(self.accept)
|
|
1140
|
+
button_box.rejected.connect(self.reject)
|
|
1141
|
+
layout.addWidget(button_box)
|
|
1142
|
+
|
|
1143
|
+
apply_form_spacing(self)
|
|
1144
|
+
|
|
1145
|
+
def get_settings(self):
|
|
1146
|
+
"""(方法, しきい値か係数, マスクに適用するか)"""
|
|
1147
|
+
method = self._METHOD_KEY_BY_LABEL[self.method_combo.currentText()]
|
|
1148
|
+
value = self.threshold_spinbox.value() if method == "zscore" else self.multiplier_spinbox.value()
|
|
1149
|
+
return method, value, self.apply_mask_checkbox.isChecked()
|
|
1150
|
+
|
|
1151
|
+
|
|
1152
|
+
class HistogramKDEDialog(QDialog):
|
|
1153
|
+
"""ヒストグラムか KDE。対象の列は Y に限らない(分布を見たいのは Y とは限らない)。"""
|
|
1154
|
+
|
|
1155
|
+
MODE_HISTOGRAM = "ヒストグラム"
|
|
1156
|
+
MODE_KDE = "カーネル密度推定(KDE)"
|
|
1157
|
+
MODES = [MODE_HISTOGRAM, MODE_KDE]
|
|
1158
|
+
|
|
1159
|
+
def __init__(self, name, column_names, default_column=None, parent=None):
|
|
1160
|
+
super().__init__(parent)
|
|
1161
|
+
self.setWindowTitle("ヒストグラム / KDE")
|
|
1162
|
+
self.resize(420, 320)
|
|
1163
|
+
self._name = name
|
|
1164
|
+
|
|
1165
|
+
layout = QVBoxLayout(self)
|
|
1166
|
+
layout.addWidget(QLabel(f"対象: {name}"))
|
|
1167
|
+
|
|
1168
|
+
form = QFormLayout()
|
|
1169
|
+
self.column_combo = QComboBox()
|
|
1170
|
+
self.column_combo.addItems(column_names)
|
|
1171
|
+
if default_column and default_column in column_names:
|
|
1172
|
+
self.column_combo.setCurrentText(default_column)
|
|
1173
|
+
form.addRow("対象列", self.column_combo)
|
|
1174
|
+
|
|
1175
|
+
self.mode_combo = QComboBox()
|
|
1176
|
+
self.mode_combo.addItems(self.MODES)
|
|
1177
|
+
form.addRow("モード", self.mode_combo)
|
|
1178
|
+
layout.addLayout(form)
|
|
1179
|
+
|
|
1180
|
+
self.stack = QStackedWidget()
|
|
1181
|
+
|
|
1182
|
+
hist_page = QWidget()
|
|
1183
|
+
hist_form = QFormLayout(hist_page)
|
|
1184
|
+
self.auto_bins_checkbox = QCheckBox("ビン数を自動決定")
|
|
1185
|
+
self.auto_bins_checkbox.setChecked(True)
|
|
1186
|
+
hist_form.addRow(self.auto_bins_checkbox)
|
|
1187
|
+
self.bins_spinbox = QSpinBox()
|
|
1188
|
+
self.bins_spinbox.setRange(2, 500)
|
|
1189
|
+
self.bins_spinbox.setValue(10)
|
|
1190
|
+
self.bins_spinbox.setEnabled(False)
|
|
1191
|
+
hist_form.addRow("ビン数", self.bins_spinbox)
|
|
1192
|
+
self.auto_bins_checkbox.toggled.connect(lambda checked: self.bins_spinbox.setEnabled(not checked))
|
|
1193
|
+
self.density_checkbox = QCheckBox("確率密度で正規化する(合計面積 = 1)")
|
|
1194
|
+
hist_form.addRow(self.density_checkbox)
|
|
1195
|
+
self.stack.addWidget(hist_page)
|
|
1196
|
+
|
|
1197
|
+
kde_page = QWidget()
|
|
1198
|
+
kde_form = QFormLayout(kde_page)
|
|
1199
|
+
self.kde_points_spinbox = QSpinBox()
|
|
1200
|
+
self.kde_points_spinbox.setRange(10, 2000)
|
|
1201
|
+
self.kde_points_spinbox.setValue(200)
|
|
1202
|
+
kde_form.addRow("評価点数", self.kde_points_spinbox)
|
|
1203
|
+
self.stack.addWidget(kde_page)
|
|
1204
|
+
|
|
1205
|
+
layout.addWidget(self.stack)
|
|
1206
|
+
self.mode_combo.currentIndexChanged.connect(self.stack.setCurrentIndex)
|
|
1207
|
+
|
|
1208
|
+
output_form = QFormLayout()
|
|
1209
|
+
self.output_name_edit = QLineEdit(f"{name}_hist")
|
|
1210
|
+
output_form.addRow("出力データセット名", self.output_name_edit)
|
|
1211
|
+
layout.addLayout(output_form)
|
|
1212
|
+
self.mode_combo.currentTextChanged.connect(self._update_default_output_name)
|
|
1213
|
+
|
|
1214
|
+
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok |
|
|
1215
|
+
QDialogButtonBox.StandardButton.Cancel)
|
|
1216
|
+
button_box.accepted.connect(self.accept)
|
|
1217
|
+
button_box.rejected.connect(self.reject)
|
|
1218
|
+
layout.addWidget(button_box)
|
|
1219
|
+
|
|
1220
|
+
apply_form_spacing(self)
|
|
1221
|
+
|
|
1222
|
+
def _update_default_output_name(self, mode_text):
|
|
1223
|
+
# 利用者が書き換えた名前は上書きしない
|
|
1224
|
+
current = self.output_name_edit.text()
|
|
1225
|
+
if current not in (f"{self._name}_hist", f"{self._name}_kde"):
|
|
1226
|
+
return
|
|
1227
|
+
suffix = "_hist" if mode_text == self.MODE_HISTOGRAM else "_kde"
|
|
1228
|
+
self.output_name_edit.setText(f"{self._name}{suffix}")
|
|
1229
|
+
|
|
1230
|
+
def get_settings(self):
|
|
1231
|
+
"""{'mode', 'column', 'output_name'} と、histogram なら 'bins' / 'density'、kde なら 'n_points'。"""
|
|
1232
|
+
mode = "histogram" if self.mode_combo.currentText() == self.MODE_HISTOGRAM else "kde"
|
|
1233
|
+
settings = {
|
|
1234
|
+
'mode': mode,
|
|
1235
|
+
'column': self.column_combo.currentText(),
|
|
1236
|
+
'output_name': self.output_name_edit.text().strip(),
|
|
1237
|
+
}
|
|
1238
|
+
if mode == "histogram":
|
|
1239
|
+
settings['bins'] = 'auto' if self.auto_bins_checkbox.isChecked() else self.bins_spinbox.value()
|
|
1240
|
+
settings['density'] = self.density_checkbox.isChecked()
|
|
1241
|
+
else:
|
|
1242
|
+
settings['n_points'] = self.kde_points_spinbox.value()
|
|
1243
|
+
return settings
|
|
1244
|
+
|
|
1245
|
+
|
|
1246
|
+
class DatasetArithmeticDialog(QDialog):
|
|
1247
|
+
"""A と B の演算。X が揃わないので、呼び出し側が B を A の X に補間してから計算する。"""
|
|
1248
|
+
|
|
1249
|
+
OPERATIONS = ["A - B", "B - A", "A + B", "A × B", "A ÷ B", "B ÷ A"]
|
|
1250
|
+
|
|
1251
|
+
def __init__(self, name_a, name_b, parent=None):
|
|
1252
|
+
super().__init__(parent)
|
|
1253
|
+
self.setWindowTitle("データセット間演算")
|
|
1254
|
+
self.resize(360, 200)
|
|
1255
|
+
|
|
1256
|
+
layout = QVBoxLayout(self)
|
|
1257
|
+
label = QLabel(f"A: {name_a}\nB: {name_b}")
|
|
1258
|
+
layout.addWidget(label)
|
|
1259
|
+
|
|
1260
|
+
form = QFormLayout()
|
|
1261
|
+
self.operation_combo = QComboBox()
|
|
1262
|
+
self.operation_combo.addItems(self.OPERATIONS)
|
|
1263
|
+
form.addRow("演算", self.operation_combo)
|
|
1264
|
+
|
|
1265
|
+
self.output_name_edit = QLineEdit(f"{name_a} vs {name_b}")
|
|
1266
|
+
form.addRow("出力データセット名", self.output_name_edit)
|
|
1267
|
+
layout.addLayout(form)
|
|
1268
|
+
|
|
1269
|
+
info_label = QLabel(
|
|
1270
|
+
"B側のY値を、A側のX値に線形補間してから演算します。"
|
|
1271
|
+
"2つのデータセットのX軸の値が重なる範囲のみが対象になります。"
|
|
1272
|
+
)
|
|
1273
|
+
info_label.setWordWrap(True)
|
|
1274
|
+
layout.addWidget(info_label)
|
|
1275
|
+
|
|
1276
|
+
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok |
|
|
1277
|
+
QDialogButtonBox.StandardButton.Cancel)
|
|
1278
|
+
button_box.accepted.connect(self.accept)
|
|
1279
|
+
button_box.rejected.connect(self.reject)
|
|
1280
|
+
layout.addWidget(button_box)
|
|
1281
|
+
|
|
1282
|
+
apply_form_spacing(self)
|
|
1283
|
+
|
|
1284
|
+
def get_settings(self):
|
|
1285
|
+
"""(演算の種類, 出力名)"""
|
|
1286
|
+
return self.operation_combo.currentText(), self.output_name_edit.text().strip()
|
|
1287
|
+
|
|
1288
|
+
|
|
1289
|
+
class XAxisAlignmentDialog(QDialog):
|
|
1290
|
+
"""B(動かす側)を A に重ねる。シフト量は自動で求めるので、出力名だけ尋ねる。"""
|
|
1291
|
+
|
|
1292
|
+
def __init__(self, name_a, name_b, parent=None):
|
|
1293
|
+
super().__init__(parent)
|
|
1294
|
+
self.setWindowTitle("X軸アライメント(相互相関)")
|
|
1295
|
+
self.resize(380, 200)
|
|
1296
|
+
|
|
1297
|
+
layout = QVBoxLayout(self)
|
|
1298
|
+
label = QLabel(f"基準(移動しない): {name_a}\n位置合わせ対象(移動する): {name_b}")
|
|
1299
|
+
layout.addWidget(label)
|
|
1300
|
+
|
|
1301
|
+
form = QFormLayout()
|
|
1302
|
+
self.output_name_edit = QLineEdit(f"{name_b}_aligned")
|
|
1303
|
+
form.addRow("出力データセット名", self.output_name_edit)
|
|
1304
|
+
layout.addLayout(form)
|
|
1305
|
+
|
|
1306
|
+
info_label = QLabel(
|
|
1307
|
+
"2つのデータセットの相互相関が最大になるよう、位置合わせ対象のX値を"
|
|
1308
|
+
"シフトした新しいデータセットを作成します(元のデータセットは変更しません)。"
|
|
1309
|
+
)
|
|
1310
|
+
info_label.setWordWrap(True)
|
|
1311
|
+
layout.addWidget(info_label)
|
|
1312
|
+
|
|
1313
|
+
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok |
|
|
1314
|
+
QDialogButtonBox.StandardButton.Cancel)
|
|
1315
|
+
button_box.accepted.connect(self.accept)
|
|
1316
|
+
button_box.rejected.connect(self.reject)
|
|
1317
|
+
layout.addWidget(button_box)
|
|
1318
|
+
|
|
1319
|
+
apply_form_spacing(self)
|
|
1320
|
+
|
|
1321
|
+
def get_settings(self):
|
|
1322
|
+
return self.output_name_edit.text().strip()
|