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.
Files changed (181) hide show
  1. graphica/Graphica.ico +0 -0
  2. graphica/__init__.py +1 -0
  3. graphica/__main__.py +111 -0
  4. graphica/assets/__init__.py +0 -0
  5. graphica/assets/icons/__init__.py +0 -0
  6. graphica/assets/icons/arrow-right.svg +21 -0
  7. graphica/assets/icons/bold.svg +20 -0
  8. graphica/assets/icons/calculator.svg +26 -0
  9. graphica/assets/icons/chart-histogram.svg +24 -0
  10. graphica/assets/icons/chart-line.svg +20 -0
  11. graphica/assets/icons/chevron-down.svg +19 -0
  12. graphica/assets/icons/chevron-right.svg +19 -0
  13. graphica/assets/icons/color-swatch.svg +22 -0
  14. graphica/assets/icons/column-insert-right.svg +21 -0
  15. graphica/assets/icons/column-remove.svg +21 -0
  16. graphica/assets/icons/copy.svg +20 -0
  17. graphica/assets/icons/download.svg +21 -0
  18. graphica/assets/icons/edit.svg +21 -0
  19. graphica/assets/icons/eye-off.svg +21 -0
  20. graphica/assets/icons/eye.svg +20 -0
  21. graphica/assets/icons/file-plus.svg +22 -0
  22. graphica/assets/icons/folder-plus.svg +21 -0
  23. graphica/assets/icons/folder.svg +19 -0
  24. graphica/assets/icons/highlight.svg +22 -0
  25. graphica/assets/icons/history.svg +20 -0
  26. graphica/assets/icons/italic.svg +21 -0
  27. graphica/assets/icons/layout-grid.svg +22 -0
  28. graphica/assets/icons/math-function.svg +22 -0
  29. graphica/assets/icons/message-2.svg +21 -0
  30. graphica/assets/icons/mountain.svg +20 -0
  31. graphica/assets/icons/palette.svg +22 -0
  32. graphica/assets/icons/pointer.svg +19 -0
  33. graphica/assets/icons/refresh.svg +20 -0
  34. graphica/assets/icons/row-insert-bottom.svg +21 -0
  35. graphica/assets/icons/row-remove.svg +21 -0
  36. graphica/assets/icons/search.svg +20 -0
  37. graphica/assets/icons/select-all.svg +35 -0
  38. graphica/assets/icons/subscript.svg +20 -0
  39. graphica/assets/icons/superscript.svg +20 -0
  40. graphica/assets/icons/table.svg +21 -0
  41. graphica/assets/icons/trash.svg +23 -0
  42. graphica/assets/icons/typography.svg +23 -0
  43. graphica/assets/icons/x.svg +20 -0
  44. graphica/core/__init__.py +0 -0
  45. graphica/core/analysis.py +1278 -0
  46. graphica/core/app_paths.py +35 -0
  47. graphica/core/axis_settings.py +128 -0
  48. graphica/core/caption_export.py +47 -0
  49. graphica/core/color_palettes.py +56 -0
  50. graphica/core/commands.py +226 -0
  51. graphica/core/cvd_simulation.py +49 -0
  52. graphica/core/dataset.py +443 -0
  53. graphica/core/diagnostics.py +105 -0
  54. graphica/core/excel_utils.py +58 -0
  55. graphica/core/fit_models.py +379 -0
  56. graphica/core/grid_data.py +128 -0
  57. graphica/core/i18n.py +44 -0
  58. graphica/core/json_utils.py +27 -0
  59. graphica/core/label_utils.py +21 -0
  60. graphica/core/methods_text.py +115 -0
  61. graphica/core/named_colors.py +126 -0
  62. graphica/core/plugin_api.py +515 -0
  63. graphica/core/plugin_context.py +167 -0
  64. graphica/core/plugin_install.py +82 -0
  65. graphica/core/plugin_manifest.py +67 -0
  66. graphica/core/plugin_testing.py +213 -0
  67. graphica/core/plugin_types.py +118 -0
  68. graphica/core/provenance.py +19 -0
  69. graphica/core/report_export.py +51 -0
  70. graphica/core/safe_eval.py +194 -0
  71. graphica/core/script_export.py +272 -0
  72. graphica/core/translations_en.py +530 -0
  73. graphica/core/unit_conversion.py +51 -0
  74. graphica/core/update_check.py +43 -0
  75. graphica/core/version.py +14 -0
  76. graphica/gui/__init__.py +0 -0
  77. graphica/gui/app_settings.py +119 -0
  78. graphica/gui/axis_bindings.py +173 -0
  79. graphica/gui/binding.py +135 -0
  80. graphica/gui/builders/__init__.py +1 -0
  81. graphica/gui/builders/axis_panel.py +431 -0
  82. graphica/gui/builders/canvas_area.py +176 -0
  83. graphica/gui/builders/common.py +120 -0
  84. graphica/gui/builders/dataset_panel.py +433 -0
  85. graphica/gui/builders/property_sections.py +208 -0
  86. graphica/gui/canvas.py +519 -0
  87. graphica/gui/color_history.py +47 -0
  88. graphica/gui/color_picker_widget.py +198 -0
  89. graphica/gui/crash_handler.py +77 -0
  90. graphica/gui/cvd_preview.py +23 -0
  91. graphica/gui/data_editor.py +761 -0
  92. graphica/gui/data_import_flow.py +355 -0
  93. graphica/gui/dataset_bindings.py +107 -0
  94. graphica/gui/dataset_style_icon.py +80 -0
  95. graphica/gui/datasets/__init__.py +1 -0
  96. graphica/gui/datasets/actions_menu.py +114 -0
  97. graphica/gui/datasets/colors.py +218 -0
  98. graphica/gui/datasets/fitting.py +70 -0
  99. graphica/gui/datasets/host.py +162 -0
  100. graphica/gui/datasets/operations/__init__.py +1 -0
  101. graphica/gui/datasets/operations/fitting.py +468 -0
  102. graphica/gui/datasets/operations/peaks.py +109 -0
  103. graphica/gui/datasets/operations/processing.py +659 -0
  104. graphica/gui/datasets/operations/runner.py +148 -0
  105. graphica/gui/datasets/operations/transfer.py +227 -0
  106. graphica/gui/datasets/order.py +310 -0
  107. graphica/gui/datasets/overlays.py +74 -0
  108. graphica/gui/datasets/peaks.py +21 -0
  109. graphica/gui/datasets/plugin_runs.py +99 -0
  110. graphica/gui/datasets/processing.py +68 -0
  111. graphica/gui/datasets/property_panel.py +538 -0
  112. graphica/gui/datasets/transfer.py +36 -0
  113. graphica/gui/detached_canvas_window.py +22 -0
  114. graphica/gui/dialogs/__init__.py +137 -0
  115. graphica/gui/dialogs/analysis.py +1322 -0
  116. graphica/gui/dialogs/app.py +1015 -0
  117. graphica/gui/dialogs/appearance.py +851 -0
  118. graphica/gui/dialogs/data_edit.py +577 -0
  119. graphica/gui/dialogs/data_import.py +679 -0
  120. graphica/gui/dialogs/export.py +433 -0
  121. graphica/gui/dock_layout.py +176 -0
  122. graphica/gui/export_preview_panel.py +321 -0
  123. graphica/gui/export_settings.py +16 -0
  124. graphica/gui/file_association.py +97 -0
  125. graphica/gui/icon_utils.py +44 -0
  126. graphica/gui/main_app_window.py +210 -0
  127. graphica/gui/main_window.py +1151 -0
  128. graphica/gui/mathtext_preview.py +168 -0
  129. graphica/gui/menu_bar.py +320 -0
  130. graphica/gui/minimap_widget.py +151 -0
  131. graphica/gui/mixins/__init__.py +0 -0
  132. graphica/gui/mixins/export_mixin.py +520 -0
  133. graphica/gui/mixins/help_mixin.py +127 -0
  134. graphica/gui/mixins/project_io_mixin.py +289 -0
  135. graphica/gui/mixins/quick_access_mixin.py +194 -0
  136. graphica/gui/mixins/ui_setup_mixin.py +206 -0
  137. graphica/gui/notify.py +61 -0
  138. graphica/gui/panels/__init__.py +43 -0
  139. graphica/gui/panels/axis_settings.py +605 -0
  140. graphica/gui/panels/dataset_tree.py +424 -0
  141. graphica/gui/plot_type_drawers.py +131 -0
  142. graphica/gui/plugin_context.py +119 -0
  143. graphica/gui/project_files.py +361 -0
  144. graphica/gui/provenance_panel.py +65 -0
  145. graphica/gui/rendering/__init__.py +1 -0
  146. graphica/gui/rendering/annotations.py +177 -0
  147. graphica/gui/rendering/appearance.py +368 -0
  148. graphica/gui/rendering/common.py +316 -0
  149. graphica/gui/rendering/data_1d.py +376 -0
  150. graphica/gui/rendering/data_2d.py +66 -0
  151. graphica/gui/residual_panel.py +62 -0
  152. graphica/gui/resources.py +16 -0
  153. graphica/gui/single_instance.py +141 -0
  154. graphica/gui/splash.py +83 -0
  155. graphica/gui/task_runner.py +41 -0
  156. graphica/gui/theme.py +864 -0
  157. graphica/gui/tools/__init__.py +56 -0
  158. graphica/gui/tools/annotation.py +275 -0
  159. graphica/gui/tools/cursor.py +259 -0
  160. graphica/gui/tools/layout_edit.py +285 -0
  161. graphica/gui/tools/manager.py +78 -0
  162. graphica/gui/tools/peak_placement.py +132 -0
  163. graphica/gui/tools/range_select.py +191 -0
  164. graphica/gui/tools/region_highlight.py +222 -0
  165. graphica/gui/tools/slice_extraction.py +200 -0
  166. graphica/gui/widget_translation.py +46 -0
  167. graphica/gui/workers.py +280 -0
  168. graphica/models/__init__.py +0 -0
  169. graphica/models/project.py +197 -0
  170. graphica/plugin/__init__.py +20 -0
  171. graphica/plugin/testing.py +57 -0
  172. graphica/sample_data/__init__.py +0 -0
  173. graphica/sample_data/cooling_curve_sample.csv +42 -0
  174. graphica/ui_main_window.py +581 -0
  175. graphica_plot-2.0.0.dist-info/METADATA +317 -0
  176. graphica_plot-2.0.0.dist-info/RECORD +181 -0
  177. graphica_plot-2.0.0.dist-info/WHEEL +5 -0
  178. graphica_plot-2.0.0.dist-info/entry_points.txt +2 -0
  179. graphica_plot-2.0.0.dist-info/licenses/LICENSE +30 -0
  180. graphica_plot-2.0.0.dist-info/licenses/THIRD_PARTY_LICENSES.md +91 -0
  181. 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})"