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,379 @@
|
|
|
1
|
+
"""曲線フィットのモデルの表。種類の判定・パラメータ名・関数・初期値の推定をここ 1 か所に持つ。
|
|
2
|
+
|
|
3
|
+
種類は表示名(例「ガウシアン (y = …)」)で渡され、保存ファイルと方法の文章にもそのまま入る。判定の順は、
|
|
4
|
+
プラグインの関数(名前の完全一致)→ カスタム数式 → 組み込み(語の部分一致、表の順:「2成分指数」を「指数関数」より、
|
|
5
|
+
「擬似フォークト」を「フォークト」より、「ボルツマン」を「シグモイド」より先に)。プラグインを先に見ないと、
|
|
6
|
+
「二重ガウシアン」のような名前が黙って組み込みのガウシアンになる。
|
|
7
|
+
初期値の式は結果の popt が 1 ビットも変わらないよう、演算の順まで変えないこと。
|
|
8
|
+
"""
|
|
9
|
+
import re
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Any, Callable, Mapping
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
from scipy.special import wofz
|
|
15
|
+
|
|
16
|
+
from graphica.core.safe_eval import DEFAULT_FUNCTIONS, safe_eval_formula
|
|
17
|
+
|
|
18
|
+
CUSTOM_FORMULA_KEYWORD = "カスタム数式"
|
|
19
|
+
CUSTOM_FORMULA_LABEL = "カスタム数式..."
|
|
20
|
+
CUSTOM_FORMULA_MODEL_ID = "custom_formula"
|
|
21
|
+
PLUGIN_MODEL_ID_PREFIX = "plugin:"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def is_custom_formula_type(fit_type: str) -> bool:
|
|
25
|
+
return CUSTOM_FORMULA_KEYWORD in fit_type
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _linear(x: Any, a: float, b: float) -> Any:
|
|
29
|
+
return a * x + b
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _poly2(x: Any, a: float, b: float, c: float) -> Any:
|
|
33
|
+
return a * x**2 + b * x + c
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _poly3(x: Any, a: float, b: float, c: float, d: float) -> Any:
|
|
37
|
+
return a * x**3 + b * x**2 + c * x + d
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _exponential(x: Any, a: float, b: float) -> Any:
|
|
41
|
+
return a * np.exp(b * x)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _logarithmic(x: Any, a: float, b: float) -> Any:
|
|
45
|
+
return a * np.log(x) + b
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _power(x: Any, a: float, b: float) -> Any:
|
|
49
|
+
return a * np.power(x, b)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _gaussian(x: Any, a: float, b: float, c: float, d: float) -> Any:
|
|
53
|
+
return a * np.exp(-((x - b) ** 2) / (2 * c ** 2)) + d
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _sigmoid(x: Any, a: float, b: float, c: float) -> Any:
|
|
57
|
+
return a / (1 + np.exp(-b * (x - c)))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _two_exponentials(x: Any, a1: float, b1: float, a2: float, b2: float, c: float) -> Any:
|
|
61
|
+
return a1 * np.exp(b1 * x) + a2 * np.exp(b2 * x) + c
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _lorentzian(x: Any, a: float, b: float, c: float, d: float) -> Any:
|
|
65
|
+
return a / (1 + ((x - b) / c) ** 2) + d
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _pseudo_voigt(x: Any, a: float, b: float, c: float, eta: float, d: float) -> Any:
|
|
69
|
+
# 共通の中心 b と FWHM c を持つローレンツ型とガウス型を eta で混ぜる
|
|
70
|
+
lorentzian_shape = 1 / (1 + ((x - b) / c) ** 2)
|
|
71
|
+
gaussian_shape = np.exp(-4 * np.log(2) * ((x - b) / c) ** 2)
|
|
72
|
+
return a * (eta * lorentzian_shape + (1 - eta) * gaussian_shape) + d
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _voigt(x: Any, a: float, b: float, sigma: float, gamma: float, d: float) -> Any:
|
|
76
|
+
# Faddeeva 関数による Voigt。この正規化で gamma→0 のとき a がピーク高さになる
|
|
77
|
+
z = ((x - b) + 1j * gamma) / (sigma * np.sqrt(2))
|
|
78
|
+
return a * np.real(wofz(z)) / (sigma * np.sqrt(2 * np.pi)) + d
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _boltzmann_sigmoid(x: Any, a1: float, a2: float, x0: float, dx: float) -> Any:
|
|
82
|
+
return a2 + (a1 - a2) / (1 + np.exp((x - x0) / dx))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _hill(x: Any, vmax: float, k: float, n: float) -> Any:
|
|
86
|
+
return (vmax * np.power(x, n)) / (np.power(k, n) + np.power(x, n))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _estimate_fwhm(x_arr: Any, y_arr: Any, amplitude: float) -> float:
|
|
90
|
+
"""半値を横切る X の幅を FWHM の粗い推定にする(裾の広い形で誤った局所解に落ちないため)。"""
|
|
91
|
+
half_level = np.nanmin(y_arr) + amplitude / 2
|
|
92
|
+
above_half = x_arr[y_arr >= half_level]
|
|
93
|
+
if len(above_half) == 0:
|
|
94
|
+
return (np.nanmax(x_arr) - np.nanmin(x_arr)) / 4 or 1.0
|
|
95
|
+
return (above_half.max() - above_half.min()) or 1.0
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _p0_linear(x: Any, y: Any) -> list[Any]:
|
|
99
|
+
return [1.0, 0.0]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _p0_poly2(x: Any, y: Any) -> list[Any]:
|
|
103
|
+
return [1.0, 1.0, 0.0]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _p0_poly3(x: Any, y: Any) -> list[Any]:
|
|
107
|
+
return [1.0, 1.0, 1.0, 0.0]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _p0_two_exponentials(x: Any, y: Any) -> list[Any]:
|
|
111
|
+
amplitude = (np.nanmax(y) - np.nanmin(y)) / 2 or 1.0
|
|
112
|
+
x_span = (np.nanmax(x) - np.nanmin(x)) or 1.0
|
|
113
|
+
# 2成分が同じ初期値だと縮退して収束しないので、符号の違う率から始める
|
|
114
|
+
rate0 = 2.0 / x_span
|
|
115
|
+
return [amplitude, rate0, amplitude, -rate0, np.nanmin(y)]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _p0_exponential(x: Any, y: Any) -> list[Any]:
|
|
119
|
+
amplitude = np.nanmean(np.abs(y)) or 1.0
|
|
120
|
+
return [amplitude, 0.01]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _p0_logarithmic(x: Any, y: Any) -> list[Any]:
|
|
124
|
+
return [1.0, 0.0]
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _p0_power(x: Any, y: Any) -> list[Any]:
|
|
128
|
+
return [1.0, 1.0]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _peak_amplitude_and_center(x: Any, y: Any) -> tuple[Any, Any]:
|
|
132
|
+
amplitude = (np.nanmax(y) - np.nanmin(y)) or 1.0
|
|
133
|
+
center = x[np.nanargmax(y)] if len(x) else 0.0
|
|
134
|
+
return amplitude, center
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _p0_gaussian(x: Any, y: Any) -> list[Any]:
|
|
138
|
+
amplitude, center = _peak_amplitude_and_center(x, y)
|
|
139
|
+
width = (np.nanmax(x) - np.nanmin(x)) / 4 or 1.0
|
|
140
|
+
return [amplitude, center, width, np.nanmin(y)]
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _p0_lorentzian(x: Any, y: Any) -> list[Any]:
|
|
144
|
+
amplitude, center = _peak_amplitude_and_center(x, y)
|
|
145
|
+
# c は HWHM なので FWHM の半分
|
|
146
|
+
fwhm0 = _estimate_fwhm(x, y, amplitude)
|
|
147
|
+
return [amplitude, center, fwhm0 / 2, np.nanmin(y)]
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _p0_pseudo_voigt(x: Any, y: Any) -> list[Any]:
|
|
151
|
+
amplitude, center = _peak_amplitude_and_center(x, y)
|
|
152
|
+
# この定義の c は FWHM そのもの
|
|
153
|
+
fwhm0 = _estimate_fwhm(x, y, amplitude)
|
|
154
|
+
return [amplitude, center, fwhm0, 0.5, np.nanmin(y)]
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _p0_voigt(x: Any, y: Any) -> list[Any]:
|
|
158
|
+
amplitude, center = _peak_amplitude_and_center(x, y)
|
|
159
|
+
fwhm0 = _estimate_fwhm(x, y, amplitude)
|
|
160
|
+
# FWHM をガウス成分とローレンツ成分に大まかに割り振る経験的な初期値
|
|
161
|
+
sigma0 = (fwhm0 / 2.355) or 1.0
|
|
162
|
+
gamma0 = (fwhm0 / 4) or 1.0
|
|
163
|
+
# _voigt の正規化に合わせ、ピーク高さが振幅に近くなるよう a をスケールする
|
|
164
|
+
return [amplitude * sigma0 * np.sqrt(2 * np.pi), center, sigma0, gamma0, np.nanmin(y)]
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _p0_boltzmann_sigmoid(x: Any, y: Any) -> list[Any]:
|
|
168
|
+
order = np.argsort(x)
|
|
169
|
+
x_sorted, y_sorted = x[order], y[order]
|
|
170
|
+
y_start = y_sorted[0]
|
|
171
|
+
y_end = y_sorted[-1]
|
|
172
|
+
mid_level = (y_start + y_end) / 2
|
|
173
|
+
# 遷移の中心は Y の中点を最初に横切る X から推定する(X の平均だと局所解に落ちやすい)
|
|
174
|
+
crossing_mask = y_sorted < mid_level if y_start >= y_end else y_sorted > mid_level
|
|
175
|
+
crossing_indices = np.flatnonzero(crossing_mask)
|
|
176
|
+
x0 = x_sorted[crossing_indices[0]] if len(crossing_indices) else np.nanmean(x)
|
|
177
|
+
dx0 = (np.nanmax(x) - np.nanmin(x)) / 10 or 1.0
|
|
178
|
+
return [y_start, y_end, x0, dx0]
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _p0_sigmoid(x: Any, y: Any) -> list[Any]:
|
|
182
|
+
amplitude = np.nanmax(y) or 1.0
|
|
183
|
+
return [amplitude, 1.0, np.nanmean(x)]
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _p0_hill(x: Any, y: Any) -> list[Any]:
|
|
187
|
+
vmax0 = np.nanmax(y) or 1.0
|
|
188
|
+
positive_x = x[x > 0]
|
|
189
|
+
k0 = np.nanmedian(positive_x) if len(positive_x) else 1.0
|
|
190
|
+
return [vmax0, k0, 1.0]
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _require_positive_x(message: str) -> Callable[[Any], None]:
|
|
194
|
+
def check(x: Any) -> None:
|
|
195
|
+
if np.any(x <= 0):
|
|
196
|
+
raise ValueError(message)
|
|
197
|
+
return check
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _require_non_negative_x(x: Any) -> None:
|
|
201
|
+
if np.any(x < 0):
|
|
202
|
+
raise ValueError("ヒル式は X >= 0 のデータにのみ使用できます。")
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@dataclass(frozen=True)
|
|
206
|
+
class FitModel:
|
|
207
|
+
model_id: str # 保存用の ID(fit_result['fit_model_id'])。一度決めたら変えない
|
|
208
|
+
keyword: str # 種類の名前にこの語が含まれればこのモデル
|
|
209
|
+
menu_label: str # ダイアログの選択肢(= 保存される fit_type)
|
|
210
|
+
param_names: tuple[str, ...]
|
|
211
|
+
func: Callable[..., Any]
|
|
212
|
+
initial_guess: Callable[[Any, Any], list[Any]]
|
|
213
|
+
check_data: Callable[[Any], None] | None = None # 使えないデータなら ValueError
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# 判定の順。ダイアログの並びは MENU_ORDER。
|
|
217
|
+
BUILTIN_FIT_MODELS: tuple[FitModel, ...] = (
|
|
218
|
+
FitModel("linear", "線形", "線形 (y = ax + b)", ("a", "b"), _linear, _p0_linear),
|
|
219
|
+
FitModel("poly2", "2次多項式", "2次多項式 (y = ax^2 + bx + c)", ("a", "b", "c"), _poly2, _p0_poly2),
|
|
220
|
+
FitModel("poly3", "3次多項式", "3次多項式 (y = ax^3 + bx^2 + cx + d)", ("a", "b", "c", "d"), _poly3, _p0_poly3),
|
|
221
|
+
FitModel("two_exponentials", "2成分指数", "2成分指数関数 (y = a1*exp(b1*x) + a2*exp(b2*x) + c)", ("a1", "b1", "a2", "b2", "c"),
|
|
222
|
+
_two_exponentials, _p0_two_exponentials),
|
|
223
|
+
FitModel("exponential", "指数関数", "指数関数 (y = a * exp(bx))", ("a", "b"), _exponential, _p0_exponential),
|
|
224
|
+
FitModel("logarithmic", "対数", "対数 (y = a * ln(x) + b)", ("a", "b"), _logarithmic, _p0_logarithmic,
|
|
225
|
+
_require_positive_x("対数フィットは X > 0 のデータにのみ使用できます。")),
|
|
226
|
+
FitModel("power", "べき乗", "べき乗 (y = a * x^b)", ("a", "b"), _power, _p0_power,
|
|
227
|
+
_require_positive_x("べき乗フィットは X > 0 のデータにのみ使用できます。")),
|
|
228
|
+
FitModel("gaussian", "ガウシアン", "ガウシアン (y = a * exp(-(x-b)^2 / (2c^2)) + d)", ("a", "b", "c", "d"),
|
|
229
|
+
_gaussian, _p0_gaussian),
|
|
230
|
+
FitModel("lorentzian", "ローレンツ", "ローレンツ関数 (y = a / (1 + ((x-b)/c)^2) + d)", ("a", "b", "c", "d"),
|
|
231
|
+
_lorentzian, _p0_lorentzian),
|
|
232
|
+
FitModel("pseudo_voigt", "擬似フォークト", "擬似フォークト関数 (y = a*(η/(1+((x-b)/c)^2) + (1-η)*exp(-4ln2*((x-b)/c)^2)) + d)",
|
|
233
|
+
("a", "b", "c", "eta", "d"), _pseudo_voigt, _p0_pseudo_voigt),
|
|
234
|
+
FitModel("voigt", "フォークト", "フォークト関数 (y = a*Re[wofz((x-b+iγ)/(σ√2))] / (σ√(2π)) + d)",
|
|
235
|
+
("a", "b", "sigma", "gamma", "d"), _voigt, _p0_voigt),
|
|
236
|
+
FitModel("boltzmann_sigmoid", "ボルツマン", "ボルツマンシグモイド (y = a2 + (a1-a2) / (1 + exp((x-x0)/dx)))",
|
|
237
|
+
("a1", "a2", "x0", "dx"), _boltzmann_sigmoid, _p0_boltzmann_sigmoid),
|
|
238
|
+
FitModel("sigmoid", "シグモイド", "シグモイド (y = a / (1 + exp(-b(x-c))))", ("a", "b", "c"), _sigmoid, _p0_sigmoid),
|
|
239
|
+
FitModel("hill", "ヒル", "ヒルの式 (y = vmax*x^n / (k^n + x^n))", ("vmax", "k", "n"), _hill, _p0_hill,
|
|
240
|
+
_require_non_negative_x),
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
MENU_ORDER: tuple[str, ...] = (
|
|
244
|
+
"線形", "2次多項式", "3次多項式", "指数関数", "対数", "べき乗", "ガウシアン", "ローレンツ",
|
|
245
|
+
"擬似フォークト", "フォークト", "2成分指数", "ボルツマン", "シグモイド", "ヒル",
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# プラグインの関数名がこれらと同じだと、保存済みの組み込みの種類を取り違えるので、登録を断る
|
|
249
|
+
RESERVED_FIT_TYPE_NAMES: tuple[str, ...] = (CUSTOM_FORMULA_KEYWORD,) + tuple(m.keyword for m in BUILTIN_FIT_MODELS)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def builtin_menu_labels() -> list[str]:
|
|
253
|
+
by_keyword = {m.keyword: m for m in BUILTIN_FIT_MODELS}
|
|
254
|
+
return [by_keyword[keyword].menu_label for keyword in MENU_ORDER]
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
_RESERVED_FORMULA_NAMES = set(DEFAULT_FUNCTIONS.keys()) | {'x'}
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def extract_formula_params(formula: str) -> list[str]:
|
|
261
|
+
"""x でも既知の関数名でもない識別子を、出現順にパラメータとして取り出す。"""
|
|
262
|
+
params = []
|
|
263
|
+
for name in re.findall(r'[a-zA-Z_][a-zA-Z_0-9]*', formula):
|
|
264
|
+
if name in _RESERVED_FORMULA_NAMES or name in params:
|
|
265
|
+
continue
|
|
266
|
+
params.append(name)
|
|
267
|
+
if not params:
|
|
268
|
+
raise ValueError("数式にフィットパラメータ(x以外の文字)が見つかりません。")
|
|
269
|
+
return params
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def build_custom_fit_func(formula: str, param_names: list[str]) -> Callable[..., Any]:
|
|
273
|
+
def custom_func(x: Any, *params: float) -> Any:
|
|
274
|
+
variables = {'x': x}
|
|
275
|
+
variables.update(zip(param_names, params))
|
|
276
|
+
try:
|
|
277
|
+
return safe_eval_formula(formula, variables)
|
|
278
|
+
except Exception as e:
|
|
279
|
+
raise ValueError(f"数式の評価に失敗しました: {e}") from e
|
|
280
|
+
return custom_func
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
@dataclass
|
|
284
|
+
class ResolvedFitModel:
|
|
285
|
+
"""種類の名前から決まったモデル。param_names はプラグインのときは登録されたリストそのもの。"""
|
|
286
|
+
param_names: list[str]
|
|
287
|
+
func: Callable[..., Any]
|
|
288
|
+
initial_guess: Callable[[Any, Any], list[Any]]
|
|
289
|
+
check_data: Callable[[Any], None] | None = None
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _custom_formula_or_raise(custom_formula: str | None) -> str:
|
|
293
|
+
if not custom_formula or not custom_formula.strip():
|
|
294
|
+
raise ValueError("カスタム数式が入力されていません。")
|
|
295
|
+
return custom_formula
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
_BUILTIN_BY_ID = {m.model_id: m for m in BUILTIN_FIT_MODELS}
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _identify(fit_type: str, model_id: str | None,
|
|
302
|
+
plugin_functions: Mapping[str, dict[str, Any]]) -> tuple[str, Any] | None:
|
|
303
|
+
"""("custom", None) / ("builtin", FitModel) / ("plugin", 名前)。ID が分かればそれを先に使う(表示名が変わっても同じモデル)。"""
|
|
304
|
+
if model_id:
|
|
305
|
+
if model_id == CUSTOM_FORMULA_MODEL_ID:
|
|
306
|
+
return ("custom", None)
|
|
307
|
+
if model_id in _BUILTIN_BY_ID:
|
|
308
|
+
return ("builtin", _BUILTIN_BY_ID[model_id])
|
|
309
|
+
plugin_name = model_id[len(PLUGIN_MODEL_ID_PREFIX):]
|
|
310
|
+
if model_id.startswith(PLUGIN_MODEL_ID_PREFIX) and plugin_name in plugin_functions:
|
|
311
|
+
return ("plugin", plugin_name)
|
|
312
|
+
if fit_type in plugin_functions:
|
|
313
|
+
return ("plugin", fit_type)
|
|
314
|
+
if is_custom_formula_type(fit_type):
|
|
315
|
+
return ("custom", None)
|
|
316
|
+
for model in BUILTIN_FIT_MODELS:
|
|
317
|
+
if model.keyword in fit_type:
|
|
318
|
+
return ("builtin", model)
|
|
319
|
+
return None
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def fit_model_id(fit_type: str, plugin_functions: Mapping[str, dict[str, Any]]) -> str | None:
|
|
323
|
+
found = _identify(fit_type, None, plugin_functions)
|
|
324
|
+
if found is None:
|
|
325
|
+
return None
|
|
326
|
+
kind, target = found
|
|
327
|
+
if kind == "custom":
|
|
328
|
+
return CUSTOM_FORMULA_MODEL_ID
|
|
329
|
+
if kind == "builtin":
|
|
330
|
+
return str(target.model_id)
|
|
331
|
+
return PLUGIN_MODEL_ID_PREFIX + target
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def fit_type_label(fit_result: Mapping[str, Any], default: Any = None) -> Any:
|
|
335
|
+
"""保存したフィット結果の種類の表示名。組み込みは ID から今の表示名を引く(表示名を変えても古いファイルとそろう)。"""
|
|
336
|
+
model = _BUILTIN_BY_ID.get(fit_result.get('fit_model_id') or "")
|
|
337
|
+
return model.menu_label if model is not None else fit_result.get('fit_type', default)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def resolve_fit_param_names(fit_type: str, custom_formula: str | None,
|
|
341
|
+
plugin_functions: Mapping[str, dict[str, Any]], model_id: str | None = None) -> list[str]:
|
|
342
|
+
"""パラメータ名だけを決める(カスタム数式の関数は作らない)。"""
|
|
343
|
+
found = _identify(fit_type, model_id, plugin_functions)
|
|
344
|
+
if found is None:
|
|
345
|
+
raise ValueError(f"不明なフィットタイプ: {fit_type}")
|
|
346
|
+
kind, target = found
|
|
347
|
+
if kind == "custom":
|
|
348
|
+
return extract_formula_params(_custom_formula_or_raise(custom_formula))
|
|
349
|
+
if kind == "builtin":
|
|
350
|
+
return list(target.param_names)
|
|
351
|
+
return list(plugin_functions[target]["params"])
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def resolve_fit_model(fit_type: str, custom_formula: str | None,
|
|
355
|
+
plugin_functions: Mapping[str, dict[str, Any]], model_id: str | None = None) -> ResolvedFitModel:
|
|
356
|
+
found = _identify(fit_type, model_id, plugin_functions)
|
|
357
|
+
if found is None:
|
|
358
|
+
raise ValueError(f"不明なフィットタイプ: {fit_type}")
|
|
359
|
+
kind, target = found
|
|
360
|
+
if kind == "custom":
|
|
361
|
+
params = extract_formula_params(_custom_formula_or_raise(custom_formula))
|
|
362
|
+
# *params 形式ではパラメータ数を推定できないので p0 で数を伝える
|
|
363
|
+
return ResolvedFitModel(params, build_custom_fit_func(custom_formula or "", params),
|
|
364
|
+
lambda x, y: [1.0] * len(params))
|
|
365
|
+
if kind == "builtin":
|
|
366
|
+
return ResolvedFitModel(list(target.param_names), target.func, target.initial_guess, target.check_data)
|
|
367
|
+
entry = plugin_functions[target]
|
|
368
|
+
return ResolvedFitModel(entry["params"], entry["func"], _plugin_initial_guess(entry))
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _plugin_initial_guess(entry: dict[str, Any]) -> Callable[[Any, Any], list[Any]]:
|
|
372
|
+
def initial_guess(x: Any, y: Any) -> list[Any]:
|
|
373
|
+
plugin_p0 = entry["p0"]
|
|
374
|
+
if callable(plugin_p0):
|
|
375
|
+
return list(plugin_p0(x, y))
|
|
376
|
+
if plugin_p0 is not None:
|
|
377
|
+
return list(plugin_p0)
|
|
378
|
+
return [1.0] * len(entry["params"])
|
|
379
|
+
return initial_guess
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""(x, y, z) の点から 2D マップの格子を作る。GUI には依存しない。
|
|
2
|
+
|
|
3
|
+
(x, y) が完全な格子なら pivot でそのまま並べ(is_regular=True、値を変えない)、そうでなければ griddata で補間する。
|
|
4
|
+
"""
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from scipy.interpolate import griddata, RegularGridInterpolator
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
# ドラッグでぴったり水平・垂直には引けないので、各軸の範囲に対するこの割合までは水平・垂直とみなす
|
|
11
|
+
SLICE_AXIS_ALIGNMENT_TOLERANCE = 0.01
|
|
12
|
+
|
|
13
|
+
GRID_INTERP_METHODS = ('linear', 'cubic', 'nearest')
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class GridDataError(ValueError):
|
|
17
|
+
"""2D の格子を作れない。"""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def is_regular_grid(x: Any, y: Any) -> bool:
|
|
21
|
+
"""x と y の全部の組み合わせが、ちょうど1回ずつあるか。"""
|
|
22
|
+
unique_x = np.unique(x)
|
|
23
|
+
unique_y = np.unique(y)
|
|
24
|
+
if len(unique_x) * len(unique_y) != len(x):
|
|
25
|
+
return False
|
|
26
|
+
pairs = set(zip(x.tolist(), y.tolist()))
|
|
27
|
+
return len(pairs) == len(x)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def compute_z_grid(x: Any, y: Any, z: Any, interp_method: str = 'linear',
|
|
31
|
+
resolution: tuple[int, int] | list[int] | None = None) -> dict[str, Any]:
|
|
32
|
+
"""{'x_grid', 'y_grid'(ソート済みの1次元), 'z_grid'(shape=(len(y), len(x))、データの無い所は nan), 'is_regular'}。
|
|
33
|
+
|
|
34
|
+
resolution=(nx, ny) は補間するときだけ使う(None なら点の数から決める)。interp_method も補間のときだけ。
|
|
35
|
+
"""
|
|
36
|
+
if interp_method not in GRID_INTERP_METHODS:
|
|
37
|
+
raise GridDataError(f"不明な補間方法です: {interp_method}")
|
|
38
|
+
|
|
39
|
+
x = np.asarray(x, dtype=float)
|
|
40
|
+
y = np.asarray(y, dtype=float)
|
|
41
|
+
z = np.asarray(z, dtype=float)
|
|
42
|
+
|
|
43
|
+
valid = ~(np.isnan(x) | np.isnan(y) | np.isnan(z))
|
|
44
|
+
x, y, z = x[valid], y[valid], z[valid]
|
|
45
|
+
|
|
46
|
+
if len(x) == 0:
|
|
47
|
+
raise GridDataError("有効なデータ点がありません(すべて欠損値です)。")
|
|
48
|
+
|
|
49
|
+
if is_regular_grid(x, y):
|
|
50
|
+
x_grid = np.unique(x)
|
|
51
|
+
y_grid = np.unique(y)
|
|
52
|
+
pivot = pd.DataFrame({'x': x, 'y': y, 'z': z}).pivot(index='y', columns='x', values='z')
|
|
53
|
+
pivot = pivot.reindex(index=y_grid, columns=x_grid)
|
|
54
|
+
return {
|
|
55
|
+
'x_grid': x_grid,
|
|
56
|
+
'y_grid': y_grid,
|
|
57
|
+
'z_grid': pivot.values,
|
|
58
|
+
'is_regular': True,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if resolution is None:
|
|
62
|
+
n = max(int(np.sqrt(len(x)) * 2), 10)
|
|
63
|
+
nx, ny = n, n
|
|
64
|
+
else:
|
|
65
|
+
nx, ny = resolution
|
|
66
|
+
if nx < 2 or ny < 2:
|
|
67
|
+
raise GridDataError("補間グリッドの解像度は2以上である必要があります。")
|
|
68
|
+
|
|
69
|
+
if len(x) < 3:
|
|
70
|
+
raise GridDataError(
|
|
71
|
+
"散在データの補間には最低3点が必要です(規則格子ではない点が"
|
|
72
|
+
f"{len(x)}点しかありません)。"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
x_grid = np.linspace(x.min(), x.max(), nx)
|
|
76
|
+
y_grid = np.linspace(y.min(), y.max(), ny)
|
|
77
|
+
xx, yy = np.meshgrid(x_grid, y_grid)
|
|
78
|
+
z_grid = griddata((x, y), z, (xx, yy), method=interp_method)
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
'x_grid': x_grid,
|
|
82
|
+
'y_grid': y_grid,
|
|
83
|
+
'z_grid': z_grid,
|
|
84
|
+
'is_regular': False,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def extract_slice(x_grid: Any, y_grid: Any, z_grid: Any, start: tuple[float, float], end: tuple[float, float],
|
|
89
|
+
n_points: int = 200) -> dict[str, Any]:
|
|
90
|
+
"""格子上の線分に沿った断面を返す({'axis_values', 'axis_kind', 'z_values'})。
|
|
91
|
+
|
|
92
|
+
axis_kind はほぼ水平なら 'x'、ほぼ垂直なら 'y'、斜めなら 'distance'(始点からの距離)。格子の外は nan。
|
|
93
|
+
"""
|
|
94
|
+
x0, y0 = float(start[0]), float(start[1])
|
|
95
|
+
x1, y1 = float(end[0]), float(end[1])
|
|
96
|
+
if x0 == x1 and y0 == y1:
|
|
97
|
+
raise GridDataError("始点と終点が同じ位置です(長さ0の線分は抽出できません)。")
|
|
98
|
+
|
|
99
|
+
x_grid = np.asarray(x_grid, dtype=float)
|
|
100
|
+
y_grid = np.asarray(y_grid, dtype=float)
|
|
101
|
+
z_grid = np.asarray(z_grid, dtype=float)
|
|
102
|
+
|
|
103
|
+
x_range = x_grid.max() - x_grid.min() if len(x_grid) > 1 else 1.0
|
|
104
|
+
y_range = y_grid.max() - y_grid.min() if len(y_grid) > 1 else 1.0
|
|
105
|
+
|
|
106
|
+
is_horizontal = abs(y1 - y0) <= SLICE_AXIS_ALIGNMENT_TOLERANCE * (y_range or 1.0)
|
|
107
|
+
is_vertical = abs(x1 - x0) <= SLICE_AXIS_ALIGNMENT_TOLERANCE * (x_range or 1.0)
|
|
108
|
+
|
|
109
|
+
if is_horizontal and not is_vertical:
|
|
110
|
+
axis_kind = 'x'
|
|
111
|
+
axis_values = np.linspace(x0, x1, n_points)
|
|
112
|
+
sample_x, sample_y = axis_values, np.full(n_points, y0)
|
|
113
|
+
elif is_vertical and not is_horizontal:
|
|
114
|
+
axis_kind = 'y'
|
|
115
|
+
axis_values = np.linspace(y0, y1, n_points)
|
|
116
|
+
sample_x, sample_y = np.full(n_points, x0), axis_values
|
|
117
|
+
else:
|
|
118
|
+
axis_kind = 'distance'
|
|
119
|
+
sample_x = np.linspace(x0, x1, n_points)
|
|
120
|
+
sample_y = np.linspace(y0, y1, n_points)
|
|
121
|
+
axis_values = np.sqrt((sample_x - x0) ** 2 + (sample_y - y0) ** 2)
|
|
122
|
+
|
|
123
|
+
interpolator = RegularGridInterpolator(
|
|
124
|
+
(y_grid, x_grid), z_grid, method='linear', bounds_error=False, fill_value=np.nan,
|
|
125
|
+
)
|
|
126
|
+
z_values = interpolator(np.column_stack([sample_y, sample_x]))
|
|
127
|
+
|
|
128
|
+
return {'axis_values': axis_values, 'axis_kind': axis_kind, 'z_values': z_values}
|
graphica/core/i18n.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""簡単な翻訳の仕組み: 日本語の原文をキーに、言語ごとの辞書を引く。
|
|
2
|
+
|
|
3
|
+
Qt の .ts / .qm は外部のツールが要るので使わない。訳の無い文字列は原文のまま返る。
|
|
4
|
+
対象はメニューや主なボタンなど目に触れる所だけ。言語の切り替えは次の起動から(作った画面は訳し直さない)。
|
|
5
|
+
"""
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
SUPPORTED_LANGUAGES = {"ja": "日本語", "en": "English"}
|
|
11
|
+
DEFAULT_LANGUAGE = "ja"
|
|
12
|
+
|
|
13
|
+
_current_language = DEFAULT_LANGUAGE
|
|
14
|
+
_translations: dict[str, dict[str, str]] = {} # {言語コード: {原文: 訳文}}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def register_translations(lang_code: str, mapping: dict[str, str]) -> None:
|
|
18
|
+
_translations.setdefault(lang_code, {}).update(mapping)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def set_language(lang_code: str) -> None:
|
|
22
|
+
"""対応していない言語なら日本語にする。"""
|
|
23
|
+
global _current_language
|
|
24
|
+
if lang_code not in SUPPORTED_LANGUAGES:
|
|
25
|
+
lang_code = DEFAULT_LANGUAGE
|
|
26
|
+
_current_language = lang_code
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_language() -> str:
|
|
30
|
+
return _current_language
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def tr(text: str) -> str:
|
|
34
|
+
"""日本語のとき、または訳が無いときは原文のまま。"""
|
|
35
|
+
if _current_language == DEFAULT_LANGUAGE:
|
|
36
|
+
return text
|
|
37
|
+
return _translations.get(_current_language, {}).get(text, text)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
from graphica.core.translations_en import TRANSLATIONS as _EN_TRANSLATIONS
|
|
42
|
+
register_translations("en", _EN_TRANSLATIONS)
|
|
43
|
+
except ImportError:
|
|
44
|
+
logger.warning("英語翻訳辞書 (core/translations_en.py) の読み込みに失敗しました。")
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# core/json_utils.py
|
|
2
|
+
"""
|
|
3
|
+
JSON形式でのプロジェクト保存(.graphica)のための共通ユーティリティ。
|
|
4
|
+
|
|
5
|
+
numpy のスカラ型(numpy.int64, numpy.float64 等)や ndarray は標準の
|
|
6
|
+
json.JSONEncoder ではそのままシリアライズできない場合があるため、
|
|
7
|
+
これらを素のPython型へ変換する安全網として GraphicaJSONEncoder を提供する。
|
|
8
|
+
プロジェクトデータ中のどこかに numpy スカラが紛れ込んでいても、この
|
|
9
|
+
エンコーダを使う限り個別に洗い出さなくても json.dump が失敗しないようにする。
|
|
10
|
+
"""
|
|
11
|
+
import json
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class GraphicaJSONEncoder(json.JSONEncoder):
|
|
18
|
+
"""numpy型を素のPython型に変換してからシリアライズするJSONEncoder。"""
|
|
19
|
+
|
|
20
|
+
def default(self, obj: Any) -> Any:
|
|
21
|
+
if isinstance(obj, np.integer):
|
|
22
|
+
return int(obj)
|
|
23
|
+
if isinstance(obj, np.floating):
|
|
24
|
+
return float(obj)
|
|
25
|
+
if isinstance(obj, np.ndarray):
|
|
26
|
+
return obj.tolist()
|
|
27
|
+
return super().default(obj)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""列名から軸ラベルを推測する。"""
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
# 「ラベル (単位)」か「ラベル [単位]」。出力は丸括弧にそろえる
|
|
5
|
+
_LABEL_WITH_UNIT_PATTERN = re.compile(r'^(?P<label>.+?)\s*[\(\[](?P<unit>[^()\[\]]+)[\)\]]\s*$')
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def infer_axis_label_from_column_name(column_name: object) -> str | None:
|
|
9
|
+
"""列名が「ラベル (単位)」の形ならそのラベルを返す('Wavelength[nm]' -> 'Wavelength (nm)')。
|
|
10
|
+
|
|
11
|
+
形が合わなければ None(単位の無い列名にラベルをでっちあげない)。
|
|
12
|
+
"""
|
|
13
|
+
if not isinstance(column_name, str):
|
|
14
|
+
return None
|
|
15
|
+
match = _LABEL_WITH_UNIT_PATTERN.match(column_name.strip())
|
|
16
|
+
if not match:
|
|
17
|
+
return None
|
|
18
|
+
label, unit = match.group('label').strip(), match.group('unit').strip()
|
|
19
|
+
if not label or not unit:
|
|
20
|
+
return None
|
|
21
|
+
return f"{label} ({unit})"
|