batplot 1.8.4__py3-none-any.whl → 1.8.6__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.
- batplot/__init__.py +1 -1
- batplot/args.py +22 -4
- batplot/batch.py +12 -0
- batplot/batplot.py +340 -126
- batplot/converters.py +170 -122
- batplot/cpc_interactive.py +319 -161
- batplot/data/USER_MANUAL.md +49 -0
- batplot/electrochem_interactive.py +120 -80
- batplot/interactive.py +1763 -75
- batplot/modes.py +12 -11
- batplot/operando.py +22 -0
- batplot/operando_ec_interactive.py +390 -16
- batplot/session.py +85 -9
- batplot/style.py +198 -21
- {batplot-1.8.4.dist-info → batplot-1.8.6.dist-info}/METADATA +1 -1
- {batplot-1.8.4.dist-info → batplot-1.8.6.dist-info}/RECORD +20 -20
- {batplot-1.8.4.dist-info → batplot-1.8.6.dist-info}/WHEEL +1 -1
- {batplot-1.8.4.dist-info → batplot-1.8.6.dist-info}/licenses/LICENSE +0 -0
- {batplot-1.8.4.dist-info → batplot-1.8.6.dist-info}/entry_points.txt +0 -0
- {batplot-1.8.4.dist-info → batplot-1.8.6.dist-info}/top_level.txt +0 -0
batplot/interactive.py
CHANGED
|
@@ -57,6 +57,7 @@ from .color_utils import (
|
|
|
57
57
|
ensure_colormap,
|
|
58
58
|
_CUSTOM_CMAPS,
|
|
59
59
|
)
|
|
60
|
+
from .config import load_config, save_config
|
|
60
61
|
|
|
61
62
|
|
|
62
63
|
class _FilterIMKWarning:
|
|
@@ -253,16 +254,28 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
253
254
|
has_cif = bool(getattr(_bp, 'cif_tick_series', None))
|
|
254
255
|
except Exception:
|
|
255
256
|
pass
|
|
256
|
-
col1 = ["c: colors", "f: font", "l: line", "t: toggle axes", "g: size", "h: legend"]
|
|
257
|
+
col1 = ["c: colors", "f: font", "l: line", "t: toggle axes", "g: size", "h: legend", "sm: smooth"]
|
|
257
258
|
if has_cif:
|
|
258
259
|
col1.append("z: hkl")
|
|
259
260
|
col1.append("j: CIF titles")
|
|
260
|
-
col2 = ["a: rearrange", "
|
|
261
|
+
col2 = ["a: rearrange", "o: offset", "r: rename", "x: change X", "y: change Y", "d: derivative"]
|
|
261
262
|
col3 = ["v: find peaks", "n: crosshair", "p: print(export) style/geom", "i: import style/geom", "e: export figure", "s: save project", "b: undo", "q: quit"]
|
|
263
|
+
|
|
264
|
+
# Conditional overwrite shortcuts under (Options)
|
|
265
|
+
last_session = getattr(fig, "_last_session_save_path", None)
|
|
266
|
+
last_style = getattr(fig, "_last_style_export_path", None)
|
|
267
|
+
last_figure = getattr(fig, "_last_figure_export_path", None)
|
|
268
|
+
if last_session:
|
|
269
|
+
col3.append("os: overwrite session")
|
|
270
|
+
if last_style:
|
|
271
|
+
col3.append("ops: overwrite style")
|
|
272
|
+
col3.append("opsg: overwrite style+geom")
|
|
273
|
+
if last_figure:
|
|
274
|
+
col3.append("oe: overwrite figure")
|
|
262
275
|
|
|
263
276
|
# Hide offset/y-range in stack mode
|
|
264
277
|
if args.stack:
|
|
265
|
-
col2 = [item for item in col2 if not item.startswith("
|
|
278
|
+
col2 = [item for item in col2 if not item.startswith("o:") and not item.startswith("y:")]
|
|
266
279
|
|
|
267
280
|
if not is_diffraction:
|
|
268
281
|
col3 = [item for item in col3 if not item.startswith("n:")]
|
|
@@ -884,7 +897,17 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
884
897
|
# NEW: style / diagnostics printer (clean version)
|
|
885
898
|
def print_style_info():
|
|
886
899
|
cts = getattr(_bp, 'cif_tick_series', None) if _bp is not None else None
|
|
887
|
-
|
|
900
|
+
# Read show_cif_hkl from __main__ module (where it's stored when toggled)
|
|
901
|
+
show_hkl = None
|
|
902
|
+
try:
|
|
903
|
+
_bp_module = sys.modules.get('__main__')
|
|
904
|
+
if _bp_module is not None and hasattr(_bp_module, 'show_cif_hkl'):
|
|
905
|
+
show_hkl = bool(getattr(_bp_module, 'show_cif_hkl', False))
|
|
906
|
+
except Exception:
|
|
907
|
+
pass
|
|
908
|
+
# Fall back to _bp object if not in __main__
|
|
909
|
+
if show_hkl is None and _bp is not None:
|
|
910
|
+
show_hkl = bool(getattr(_bp, 'show_cif_hkl', False)) if hasattr(_bp, 'show_cif_hkl') else None
|
|
888
911
|
return _bp_print_style_info(
|
|
889
912
|
fig, ax,
|
|
890
913
|
y_data_list, labels,
|
|
@@ -898,7 +921,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
898
921
|
)
|
|
899
922
|
|
|
900
923
|
# NEW: export current style to .bpcfg
|
|
901
|
-
def export_style_config(filename, base_path=None, overwrite_path=None):
|
|
924
|
+
def export_style_config(filename, base_path=None, overwrite_path=None, force_kind=None):
|
|
902
925
|
cts = getattr(_bp, 'cif_tick_series', None) if _bp is not None else None
|
|
903
926
|
show_titles = bool(getattr(_bp, 'show_cif_titles', True)) if _bp is not None else True
|
|
904
927
|
from .style import export_style_config as _export_style_config
|
|
@@ -917,6 +940,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
917
940
|
base_path,
|
|
918
941
|
show_cif_titles=show_titles,
|
|
919
942
|
overwrite_path=overwrite_path,
|
|
943
|
+
force_kind=force_kind,
|
|
920
944
|
)
|
|
921
945
|
|
|
922
946
|
# NEW: apply imported style config (restricted application)
|
|
@@ -1083,6 +1107,342 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
1083
1107
|
# history management:
|
|
1084
1108
|
state_history = []
|
|
1085
1109
|
|
|
1110
|
+
# ====================================================================
|
|
1111
|
+
# SMOOTHING AND REDUCE ROWS HELPER FUNCTIONS
|
|
1112
|
+
# ====================================================================
|
|
1113
|
+
|
|
1114
|
+
def _savgol_kernel(window: int, poly: int) -> np.ndarray:
|
|
1115
|
+
"""Return Savitzky–Golay smoothing kernel of given window/poly."""
|
|
1116
|
+
half = window // 2
|
|
1117
|
+
x = np.arange(-half, half + 1, dtype=float)
|
|
1118
|
+
A = np.vander(x, poly + 1, increasing=True)
|
|
1119
|
+
ATA = A.T @ A
|
|
1120
|
+
ATA_inv = np.linalg.pinv(ATA)
|
|
1121
|
+
target = np.zeros(poly + 1, dtype=float)
|
|
1122
|
+
target[0] = 1.0 # evaluate polynomial at x=0
|
|
1123
|
+
coeffs = target @ ATA_inv @ A.T
|
|
1124
|
+
return coeffs
|
|
1125
|
+
|
|
1126
|
+
def _savgol_smooth(y: np.ndarray, window: int = 9, poly: int = 3) -> np.ndarray:
|
|
1127
|
+
"""Apply Savitzky–Golay smoothing (defaults from DiffCapAnalyzer) to data."""
|
|
1128
|
+
n = y.size
|
|
1129
|
+
if n < 3:
|
|
1130
|
+
return y
|
|
1131
|
+
if window > n:
|
|
1132
|
+
window = n if n % 2 == 1 else n - 1
|
|
1133
|
+
if window < 3:
|
|
1134
|
+
return y
|
|
1135
|
+
if window % 2 == 0:
|
|
1136
|
+
window -= 1
|
|
1137
|
+
if window < 3:
|
|
1138
|
+
return y
|
|
1139
|
+
if poly >= window:
|
|
1140
|
+
poly = window - 1
|
|
1141
|
+
coeffs = _savgol_kernel(window, poly)
|
|
1142
|
+
half = window // 2
|
|
1143
|
+
padded = np.pad(y, (half, half), mode='edge')
|
|
1144
|
+
smoothed = np.convolve(padded, coeffs[::-1], mode='valid')
|
|
1145
|
+
return smoothed
|
|
1146
|
+
|
|
1147
|
+
def _fft_smooth(y: np.ndarray, points: int = 5, cutoff: float = 0.1) -> np.ndarray:
|
|
1148
|
+
"""Apply FFT filter smoothing to data."""
|
|
1149
|
+
n = y.size
|
|
1150
|
+
if n < 3:
|
|
1151
|
+
return y
|
|
1152
|
+
# FFT
|
|
1153
|
+
fft_vals = np.fft.rfft(y)
|
|
1154
|
+
freq = np.fft.rfftfreq(n)
|
|
1155
|
+
# Low-pass filter: zero out frequencies above cutoff
|
|
1156
|
+
mask = freq <= cutoff
|
|
1157
|
+
fft_vals[~mask] = 0
|
|
1158
|
+
# Inverse FFT
|
|
1159
|
+
smoothed = np.fft.irfft(fft_vals, n)
|
|
1160
|
+
return smoothed
|
|
1161
|
+
|
|
1162
|
+
def _adjacent_average_smooth(y: np.ndarray, points: int = 5) -> np.ndarray:
|
|
1163
|
+
"""Apply Adjacent-Averaging smoothing to data."""
|
|
1164
|
+
n = y.size
|
|
1165
|
+
if n < points:
|
|
1166
|
+
return y
|
|
1167
|
+
if points < 2:
|
|
1168
|
+
return y
|
|
1169
|
+
# Use convolution for moving average
|
|
1170
|
+
kernel = np.ones(points) / points
|
|
1171
|
+
# Pad edges
|
|
1172
|
+
padded = np.pad(y, (points//2, points//2), mode='edge')
|
|
1173
|
+
smoothed = np.convolve(padded, kernel, mode='valid')
|
|
1174
|
+
return smoothed
|
|
1175
|
+
|
|
1176
|
+
def _get_last_reduce_rows_settings(method: str) -> dict:
|
|
1177
|
+
"""Get last reduce rows settings from config file.
|
|
1178
|
+
|
|
1179
|
+
Args:
|
|
1180
|
+
method: Method name ('delete_skip', 'delete_missing', 'merge')
|
|
1181
|
+
|
|
1182
|
+
Returns:
|
|
1183
|
+
Dictionary with last settings for the method, or empty dict if none
|
|
1184
|
+
"""
|
|
1185
|
+
config = load_config()
|
|
1186
|
+
last_settings = config.get('last_reduce_rows_settings', {})
|
|
1187
|
+
return last_settings.get(method, {})
|
|
1188
|
+
|
|
1189
|
+
def _save_last_reduce_rows_settings(method: str, settings: dict) -> None:
|
|
1190
|
+
"""Save last reduce rows settings to config file.
|
|
1191
|
+
|
|
1192
|
+
Args:
|
|
1193
|
+
method: Method name ('delete_skip', 'delete_missing', 'merge')
|
|
1194
|
+
settings: Dictionary with settings to save
|
|
1195
|
+
"""
|
|
1196
|
+
config = load_config()
|
|
1197
|
+
if 'last_reduce_rows_settings' not in config:
|
|
1198
|
+
config['last_reduce_rows_settings'] = {}
|
|
1199
|
+
config['last_reduce_rows_settings'][method] = settings
|
|
1200
|
+
save_config(config)
|
|
1201
|
+
|
|
1202
|
+
def _get_last_smooth_settings_from_config() -> dict:
|
|
1203
|
+
"""Get last smooth settings from config file (persistent across sessions).
|
|
1204
|
+
|
|
1205
|
+
Returns:
|
|
1206
|
+
Dictionary with last smooth settings, or empty dict if none
|
|
1207
|
+
"""
|
|
1208
|
+
config = load_config()
|
|
1209
|
+
return config.get('last_smooth_settings', {})
|
|
1210
|
+
|
|
1211
|
+
def _save_last_smooth_settings_to_config(settings: dict) -> None:
|
|
1212
|
+
"""Save last smooth settings to config file (persistent across sessions).
|
|
1213
|
+
|
|
1214
|
+
Args:
|
|
1215
|
+
settings: Dictionary with smooth settings to save
|
|
1216
|
+
"""
|
|
1217
|
+
config = load_config()
|
|
1218
|
+
config['last_smooth_settings'] = settings
|
|
1219
|
+
save_config(config)
|
|
1220
|
+
|
|
1221
|
+
def _ensure_original_data():
|
|
1222
|
+
"""Ensure original data is stored for all curves."""
|
|
1223
|
+
if not hasattr(fig, '_original_x_data_list'):
|
|
1224
|
+
fig._original_x_data_list = [np.array(a, copy=True) for a in x_data_list]
|
|
1225
|
+
fig._original_y_data_list = [np.array(a, copy=True) for a in y_data_list]
|
|
1226
|
+
|
|
1227
|
+
def _update_full_processed_data():
|
|
1228
|
+
"""Update the full processed data (after all processing steps, before any X-range filtering)."""
|
|
1229
|
+
# This stores the complete processed data (reduce + smooth + derivative) for X-range filtering
|
|
1230
|
+
fig._full_processed_x_data_list = [np.array(a, copy=True) for a in x_data_list]
|
|
1231
|
+
fig._full_processed_y_data_list = [np.array(a, copy=True) for a in y_data_list]
|
|
1232
|
+
|
|
1233
|
+
def _reset_to_original():
|
|
1234
|
+
"""Reset all curves to original data."""
|
|
1235
|
+
if not hasattr(fig, '_original_x_data_list'):
|
|
1236
|
+
return (False, 0, 0)
|
|
1237
|
+
reset_count = 0
|
|
1238
|
+
total_points = 0
|
|
1239
|
+
for i in range(min(len(fig._original_x_data_list), len(ax.lines))):
|
|
1240
|
+
try:
|
|
1241
|
+
orig_x = fig._original_x_data_list[i]
|
|
1242
|
+
orig_y = fig._original_y_data_list[i]
|
|
1243
|
+
# Restore offsets
|
|
1244
|
+
if i < len(offsets_list):
|
|
1245
|
+
orig_y_with_offset = orig_y + offsets_list[i]
|
|
1246
|
+
else:
|
|
1247
|
+
orig_y_with_offset = orig_y.copy()
|
|
1248
|
+
ax.lines[i].set_data(orig_x, orig_y_with_offset)
|
|
1249
|
+
x_data_list[i] = orig_x.copy()
|
|
1250
|
+
y_data_list[i] = orig_y_with_offset.copy()
|
|
1251
|
+
reset_count += 1
|
|
1252
|
+
total_points += len(orig_x)
|
|
1253
|
+
except Exception:
|
|
1254
|
+
pass
|
|
1255
|
+
# Clear processing settings
|
|
1256
|
+
if hasattr(fig, '_smooth_settings'):
|
|
1257
|
+
delattr(fig, '_smooth_settings')
|
|
1258
|
+
return (reset_count > 0, reset_count, total_points)
|
|
1259
|
+
|
|
1260
|
+
def _apply_data_changes():
|
|
1261
|
+
"""Update plot and data lists after data modification."""
|
|
1262
|
+
for i in range(min(len(ax.lines), len(x_data_list), len(y_data_list))):
|
|
1263
|
+
try:
|
|
1264
|
+
ax.lines[i].set_data(x_data_list[i], y_data_list[i])
|
|
1265
|
+
except Exception:
|
|
1266
|
+
pass
|
|
1267
|
+
try:
|
|
1268
|
+
fig.canvas.draw_idle()
|
|
1269
|
+
except Exception:
|
|
1270
|
+
pass
|
|
1271
|
+
|
|
1272
|
+
def _calculate_derivative(x: np.ndarray, y: np.ndarray, order: int = 1) -> np.ndarray:
|
|
1273
|
+
"""Calculate 1st or 2nd derivative using numpy gradient.
|
|
1274
|
+
|
|
1275
|
+
Args:
|
|
1276
|
+
x: X values
|
|
1277
|
+
y: Y values
|
|
1278
|
+
order: 1 for first derivative (dy/dx), 2 for second derivative (d²y/dx²)
|
|
1279
|
+
|
|
1280
|
+
Returns:
|
|
1281
|
+
Derivative array (same length as input)
|
|
1282
|
+
"""
|
|
1283
|
+
if len(y) < 2:
|
|
1284
|
+
return y.copy()
|
|
1285
|
+
# Calculate dy/dx
|
|
1286
|
+
dy_dx = np.gradient(y, x)
|
|
1287
|
+
if order == 1:
|
|
1288
|
+
return dy_dx
|
|
1289
|
+
elif order == 2:
|
|
1290
|
+
# Calculate d²y/dx² = d(dy/dx)/dx
|
|
1291
|
+
if len(dy_dx) < 2:
|
|
1292
|
+
return np.zeros_like(y)
|
|
1293
|
+
d2y_dx2 = np.gradient(dy_dx, x)
|
|
1294
|
+
return d2y_dx2
|
|
1295
|
+
else:
|
|
1296
|
+
return y.copy()
|
|
1297
|
+
|
|
1298
|
+
def _calculate_reversed_derivative(x, y, order):
|
|
1299
|
+
"""Calculate reversed 1st or 2nd derivative (dx/dy or d²x/dy²).
|
|
1300
|
+
|
|
1301
|
+
Args:
|
|
1302
|
+
x: X values
|
|
1303
|
+
y: Y values
|
|
1304
|
+
order: 1 for first reversed derivative (dx/dy), 2 for second reversed derivative (d²x/dy²)
|
|
1305
|
+
|
|
1306
|
+
Returns:
|
|
1307
|
+
Reversed derivative array (same length as input)
|
|
1308
|
+
"""
|
|
1309
|
+
if len(y) < 2:
|
|
1310
|
+
return y.copy()
|
|
1311
|
+
# First calculate dy/dx
|
|
1312
|
+
dy_dx = np.gradient(y, x)
|
|
1313
|
+
# Avoid division by zero - replace zeros with small epsilon
|
|
1314
|
+
epsilon = 1e-10
|
|
1315
|
+
dy_dx_safe = np.where(np.abs(dy_dx) < epsilon, np.sign(dy_dx) * epsilon, dy_dx)
|
|
1316
|
+
# Calculate dx/dy = 1 / (dy/dx)
|
|
1317
|
+
dx_dy = 1.0 / dy_dx_safe
|
|
1318
|
+
if order == 1:
|
|
1319
|
+
return dx_dy
|
|
1320
|
+
elif order == 2:
|
|
1321
|
+
# Calculate d²x/dy² = d(dx/dy)/dy
|
|
1322
|
+
# d(dx/dy)/dy = d(1/(dy/dx))/dy = -1/(dy/dx)² * d²y/dx²
|
|
1323
|
+
if len(dx_dy) < 2:
|
|
1324
|
+
return np.zeros_like(y)
|
|
1325
|
+
# Calculate d²y/dx² first
|
|
1326
|
+
d2y_dx2 = np.gradient(dy_dx, x)
|
|
1327
|
+
# d²x/dy² = -d²y/dx² / (dy/dx)³
|
|
1328
|
+
d2x_dy2 = -d2y_dx2 / (dy_dx_safe ** 3)
|
|
1329
|
+
return d2x_dy2
|
|
1330
|
+
else:
|
|
1331
|
+
return y.copy()
|
|
1332
|
+
|
|
1333
|
+
def _update_ylabel_for_derivative(order: int, current_label: str = None, is_reversed: bool = False) -> str:
|
|
1334
|
+
"""Generate appropriate y-axis label for derivative.
|
|
1335
|
+
|
|
1336
|
+
Args:
|
|
1337
|
+
order: 1 for first derivative, 2 for second derivative
|
|
1338
|
+
current_label: Current y-axis label (optional)
|
|
1339
|
+
is_reversed: True for reversed derivative (dx/dy), False for normal (dy/dx)
|
|
1340
|
+
|
|
1341
|
+
Returns:
|
|
1342
|
+
New y-axis label string
|
|
1343
|
+
"""
|
|
1344
|
+
if current_label is None:
|
|
1345
|
+
current_label = ax.get_ylabel() or "Y"
|
|
1346
|
+
|
|
1347
|
+
# Try to detect common patterns and update accordingly
|
|
1348
|
+
current_lower = current_label.lower()
|
|
1349
|
+
|
|
1350
|
+
if is_reversed:
|
|
1351
|
+
# Reversed derivative: dx/dy or d²x/dy²
|
|
1352
|
+
y_label = current_label if current_label and current_label != "Y" else (ax.get_ylabel() or "Y")
|
|
1353
|
+
if order == 1:
|
|
1354
|
+
# First reversed derivative: dx/dy
|
|
1355
|
+
if x_label:
|
|
1356
|
+
return f"d({x_label})/d({y_label})"
|
|
1357
|
+
else:
|
|
1358
|
+
return f"dx/d({y_label})"
|
|
1359
|
+
else: # order == 2
|
|
1360
|
+
# Second reversed derivative: d²x/dy²
|
|
1361
|
+
if x_label:
|
|
1362
|
+
return f"d²({x_label})/d({y_label})²"
|
|
1363
|
+
else:
|
|
1364
|
+
return f"d²x/d({y_label})²"
|
|
1365
|
+
|
|
1366
|
+
# Normal derivative: dy/dx or d²y/dx²
|
|
1367
|
+
if order == 1:
|
|
1368
|
+
# First derivative: dy/dx or dY/dX
|
|
1369
|
+
if "/" in current_label:
|
|
1370
|
+
# If already has derivative notation, try to increment
|
|
1371
|
+
if "d²" in current_label or "d2" in current_lower:
|
|
1372
|
+
# Change from 2nd to 1st (shouldn't normally happen, but handle it)
|
|
1373
|
+
new_label = current_label.replace("d²", "d").replace("d2", "d")
|
|
1374
|
+
return new_label
|
|
1375
|
+
elif "d" in current_label.lower() and "/" in current_label:
|
|
1376
|
+
# Already has derivative, keep as is but update order if needed
|
|
1377
|
+
return current_label
|
|
1378
|
+
# Add d/dx prefix or suffix
|
|
1379
|
+
if x_label:
|
|
1380
|
+
if any(op in current_label for op in ["/", "(", "["]):
|
|
1381
|
+
# Complex label, prepend d/dx
|
|
1382
|
+
return f"d({current_label})/d({x_label})"
|
|
1383
|
+
else:
|
|
1384
|
+
# Simple label, use d/dx notation
|
|
1385
|
+
return f"d({current_label})/d({x_label})"
|
|
1386
|
+
else:
|
|
1387
|
+
return f"d({current_label})/dx"
|
|
1388
|
+
else: # order == 2
|
|
1389
|
+
# Second derivative: d²y/dx² or d2Y/dX2
|
|
1390
|
+
if "/" in current_label:
|
|
1391
|
+
if "d²" in current_label or "d2" in current_lower:
|
|
1392
|
+
# Already 2nd derivative, keep as is
|
|
1393
|
+
return current_label
|
|
1394
|
+
elif "d" in current_label.lower() and "/" in current_label:
|
|
1395
|
+
# First derivative, convert to second
|
|
1396
|
+
new_label = current_label.replace("d(", "d²(").replace("d2(", "d²(").replace("d/", "d²/").replace("/d(", "²/d(")
|
|
1397
|
+
return new_label
|
|
1398
|
+
# Add d²/dx² prefix
|
|
1399
|
+
if x_label:
|
|
1400
|
+
if any(op in current_label for op in ["/", "(", "["]):
|
|
1401
|
+
return f"d²({current_label})/d({x_label})²"
|
|
1402
|
+
else:
|
|
1403
|
+
return f"d²({current_label})/d({x_label})²"
|
|
1404
|
+
else:
|
|
1405
|
+
return f"d²({current_label})/dx²"
|
|
1406
|
+
|
|
1407
|
+
return current_label
|
|
1408
|
+
|
|
1409
|
+
def _ensure_pre_derivative_data():
|
|
1410
|
+
"""Ensure pre-derivative data is stored for reset."""
|
|
1411
|
+
if not hasattr(fig, '_pre_derivative_x_data_list'):
|
|
1412
|
+
fig._pre_derivative_x_data_list = [np.array(a, copy=True) for a in x_data_list]
|
|
1413
|
+
fig._pre_derivative_y_data_list = [np.array(a, copy=True) for a in y_data_list]
|
|
1414
|
+
fig._pre_derivative_ylabel = ax.get_ylabel() or ""
|
|
1415
|
+
|
|
1416
|
+
def _reset_from_derivative():
|
|
1417
|
+
"""Reset all curves from derivative back to pre-derivative state."""
|
|
1418
|
+
if not hasattr(fig, '_pre_derivative_x_data_list'):
|
|
1419
|
+
return (False, 0, 0)
|
|
1420
|
+
reset_count = 0
|
|
1421
|
+
total_points = 0
|
|
1422
|
+
for i in range(min(len(fig._pre_derivative_x_data_list), len(ax.lines))):
|
|
1423
|
+
try:
|
|
1424
|
+
pre_x = fig._pre_derivative_x_data_list[i]
|
|
1425
|
+
pre_y = fig._pre_derivative_y_data_list[i]
|
|
1426
|
+
# Restore offsets
|
|
1427
|
+
if i < len(offsets_list):
|
|
1428
|
+
pre_y_with_offset = pre_y + offsets_list[i]
|
|
1429
|
+
else:
|
|
1430
|
+
pre_y_with_offset = pre_y.copy()
|
|
1431
|
+
ax.lines[i].set_data(pre_x, pre_y_with_offset)
|
|
1432
|
+
x_data_list[i] = pre_x.copy()
|
|
1433
|
+
y_data_list[i] = pre_y_with_offset.copy()
|
|
1434
|
+
reset_count += 1
|
|
1435
|
+
total_points += len(pre_x)
|
|
1436
|
+
except Exception:
|
|
1437
|
+
pass
|
|
1438
|
+
# Restore y-axis label
|
|
1439
|
+
if hasattr(fig, '_pre_derivative_ylabel'):
|
|
1440
|
+
ax.set_ylabel(fig._pre_derivative_ylabel)
|
|
1441
|
+
# Clear derivative settings
|
|
1442
|
+
if hasattr(fig, '_derivative_order'):
|
|
1443
|
+
delattr(fig, '_derivative_order')
|
|
1444
|
+
return (reset_count > 0, reset_count, total_points)
|
|
1445
|
+
|
|
1086
1446
|
def push_state(note=""):
|
|
1087
1447
|
"""Snapshot current editable state (before a modifying action)."""
|
|
1088
1448
|
try:
|
|
@@ -1161,6 +1521,26 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
1161
1521
|
snap["y_data_list"] = [np.array(a, copy=True) for a in y_data_list]
|
|
1162
1522
|
snap["orig_y"] = [np.array(a, copy=True) for a in orig_y]
|
|
1163
1523
|
snap["offsets"] = list(offsets_list)
|
|
1524
|
+
# Processed data (for smooth/reduce operations)
|
|
1525
|
+
if hasattr(fig, '_original_x_data_list'):
|
|
1526
|
+
snap["original_x_data_list"] = [np.array(a, copy=True) for a in fig._original_x_data_list]
|
|
1527
|
+
snap["original_y_data_list"] = [np.array(a, copy=True) for a in fig._original_y_data_list]
|
|
1528
|
+
if hasattr(fig, '_full_processed_x_data_list'):
|
|
1529
|
+
snap["full_processed_x_data_list"] = [np.array(a, copy=True) for a in fig._full_processed_x_data_list]
|
|
1530
|
+
snap["full_processed_y_data_list"] = [np.array(a, copy=True) for a in fig._full_processed_y_data_list]
|
|
1531
|
+
if hasattr(fig, '_smooth_settings'):
|
|
1532
|
+
snap["smooth_settings"] = dict(fig._smooth_settings)
|
|
1533
|
+
if hasattr(fig, '_last_smooth_settings'):
|
|
1534
|
+
snap["last_smooth_settings"] = dict(fig._last_smooth_settings)
|
|
1535
|
+
# Derivative data (for derivative operations)
|
|
1536
|
+
if hasattr(fig, '_pre_derivative_x_data_list'):
|
|
1537
|
+
snap["pre_derivative_x_data_list"] = [np.array(a, copy=True) for a in fig._pre_derivative_x_data_list]
|
|
1538
|
+
snap["pre_derivative_y_data_list"] = [np.array(a, copy=True) for a in fig._pre_derivative_y_data_list]
|
|
1539
|
+
snap["pre_derivative_ylabel"] = str(getattr(fig, '_pre_derivative_ylabel', ''))
|
|
1540
|
+
if hasattr(fig, '_derivative_order'):
|
|
1541
|
+
snap["derivative_order"] = int(fig._derivative_order)
|
|
1542
|
+
if hasattr(fig, '_derivative_reversed'):
|
|
1543
|
+
snap["derivative_reversed"] = bool(fig._derivative_reversed)
|
|
1164
1544
|
# Label text content
|
|
1165
1545
|
snap["label_texts"] = [t.get_text() for t in label_text_objects]
|
|
1166
1546
|
state_history.append(snap)
|
|
@@ -1381,12 +1761,72 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
1381
1761
|
offsets_list[:] = list(snap["offsets"])
|
|
1382
1762
|
delta = snap.get("delta", delta)
|
|
1383
1763
|
|
|
1764
|
+
# Restore processed data (for smooth/reduce operations)
|
|
1765
|
+
if "original_x_data_list" in snap:
|
|
1766
|
+
fig._original_x_data_list = [np.array(a, copy=True) for a in snap["original_x_data_list"]]
|
|
1767
|
+
fig._original_y_data_list = [np.array(a, copy=True) for a in snap["original_y_data_list"]]
|
|
1768
|
+
elif hasattr(fig, '_original_x_data_list'):
|
|
1769
|
+
# Clear if not in snapshot
|
|
1770
|
+
delattr(fig, '_original_x_data_list')
|
|
1771
|
+
delattr(fig, '_original_y_data_list')
|
|
1772
|
+
if "full_processed_x_data_list" in snap:
|
|
1773
|
+
fig._full_processed_x_data_list = [np.array(a, copy=True) for a in snap["full_processed_x_data_list"]]
|
|
1774
|
+
fig._full_processed_y_data_list = [np.array(a, copy=True) for a in snap["full_processed_y_data_list"]]
|
|
1775
|
+
elif hasattr(fig, '_full_processed_x_data_list'):
|
|
1776
|
+
# Clear if not in snapshot
|
|
1777
|
+
delattr(fig, '_full_processed_x_data_list')
|
|
1778
|
+
delattr(fig, '_full_processed_y_data_list')
|
|
1779
|
+
if "smooth_settings" in snap:
|
|
1780
|
+
fig._smooth_settings = dict(snap["smooth_settings"])
|
|
1781
|
+
elif hasattr(fig, '_smooth_settings'):
|
|
1782
|
+
delattr(fig, '_smooth_settings')
|
|
1783
|
+
if "last_smooth_settings" in snap:
|
|
1784
|
+
fig._last_smooth_settings = dict(snap["last_smooth_settings"])
|
|
1785
|
+
elif hasattr(fig, '_last_smooth_settings'):
|
|
1786
|
+
delattr(fig, '_last_smooth_settings')
|
|
1787
|
+
# Restore derivative data (for derivative operations)
|
|
1788
|
+
if "pre_derivative_x_data_list" in snap:
|
|
1789
|
+
fig._pre_derivative_x_data_list = [np.array(a, copy=True) for a in snap["pre_derivative_x_data_list"]]
|
|
1790
|
+
fig._pre_derivative_y_data_list = [np.array(a, copy=True) for a in snap["pre_derivative_y_data_list"]]
|
|
1791
|
+
fig._pre_derivative_ylabel = str(snap.get("pre_derivative_ylabel", ""))
|
|
1792
|
+
elif hasattr(fig, '_pre_derivative_x_data_list'):
|
|
1793
|
+
delattr(fig, '_pre_derivative_x_data_list')
|
|
1794
|
+
delattr(fig, '_pre_derivative_y_data_list')
|
|
1795
|
+
if hasattr(fig, '_pre_derivative_ylabel'):
|
|
1796
|
+
delattr(fig, '_pre_derivative_ylabel')
|
|
1797
|
+
if "derivative_order" in snap:
|
|
1798
|
+
fig._derivative_order = int(snap["derivative_order"])
|
|
1799
|
+
elif hasattr(fig, '_derivative_order'):
|
|
1800
|
+
delattr(fig, '_derivative_order')
|
|
1801
|
+
if "derivative_reversed" in snap:
|
|
1802
|
+
fig._derivative_reversed = bool(snap["derivative_reversed"])
|
|
1803
|
+
elif hasattr(fig, '_derivative_reversed'):
|
|
1804
|
+
delattr(fig, '_derivative_reversed')
|
|
1805
|
+
# Restore y-axis label if derivative was applied
|
|
1806
|
+
if "derivative_order" in snap:
|
|
1807
|
+
try:
|
|
1808
|
+
current_ylabel = ax.get_ylabel() or ""
|
|
1809
|
+
order = int(snap["derivative_order"])
|
|
1810
|
+
is_reversed = snap.get("derivative_reversed", False)
|
|
1811
|
+
new_ylabel = _update_ylabel_for_derivative(order, current_ylabel, is_reversed=is_reversed)
|
|
1812
|
+
ax.set_ylabel(new_ylabel)
|
|
1813
|
+
except Exception:
|
|
1814
|
+
pass
|
|
1815
|
+
|
|
1384
1816
|
# Recalculate y_data_list from orig_y and offsets_list to ensure consistency
|
|
1817
|
+
# Ensure lists have the same length before assigning
|
|
1818
|
+
max_len = max(len(orig_y), len(y_data_list), len(offsets_list))
|
|
1819
|
+
while len(orig_y) < max_len:
|
|
1820
|
+
orig_y.append(np.array([]))
|
|
1821
|
+
while len(y_data_list) < max_len:
|
|
1822
|
+
y_data_list.append(np.array([]))
|
|
1823
|
+
while len(offsets_list) < max_len:
|
|
1824
|
+
offsets_list.append(0.0)
|
|
1385
1825
|
for i in range(len(orig_y)):
|
|
1386
1826
|
if i < len(offsets_list):
|
|
1387
1827
|
y_data_list[i] = orig_y[i] + offsets_list[i]
|
|
1388
1828
|
else:
|
|
1389
|
-
y_data_list[i] = orig_y[i].copy()
|
|
1829
|
+
y_data_list[i] = orig_y[i].copy() if orig_y[i].size > 0 else np.array([])
|
|
1390
1830
|
|
|
1391
1831
|
# Update line data with restored values
|
|
1392
1832
|
for i in range(min(len(ax.lines), len(x_data_list), len(y_data_list))):
|
|
@@ -1423,7 +1863,15 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
1423
1863
|
pass
|
|
1424
1864
|
if _bp is not None and 'show_cif_hkl' in snap:
|
|
1425
1865
|
try:
|
|
1426
|
-
|
|
1866
|
+
new_state = bool(snap['show_cif_hkl'])
|
|
1867
|
+
setattr(_bp, 'show_cif_hkl', new_state)
|
|
1868
|
+
# Also store in __main__ module so draw function can access it
|
|
1869
|
+
try:
|
|
1870
|
+
_bp_module = sys.modules.get('__main__')
|
|
1871
|
+
if _bp_module is not None:
|
|
1872
|
+
setattr(_bp_module, 'show_cif_hkl', new_state)
|
|
1873
|
+
except Exception:
|
|
1874
|
+
pass
|
|
1427
1875
|
except Exception:
|
|
1428
1876
|
pass
|
|
1429
1877
|
if _bp is not None and 'show_cif_titles' in snap:
|
|
@@ -1508,8 +1956,16 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
1508
1956
|
try:
|
|
1509
1957
|
# Flip visibility flag in batplot module
|
|
1510
1958
|
cur = bool(getattr(_bp, 'show_cif_hkl', False)) if _bp is not None else False
|
|
1959
|
+
new_state = not cur
|
|
1511
1960
|
if _bp is not None:
|
|
1512
|
-
setattr(_bp, 'show_cif_hkl',
|
|
1961
|
+
setattr(_bp, 'show_cif_hkl', new_state)
|
|
1962
|
+
# Also store in __main__ module so draw function can access it
|
|
1963
|
+
try:
|
|
1964
|
+
_bp_module = sys.modules.get('__main__')
|
|
1965
|
+
if _bp_module is not None:
|
|
1966
|
+
setattr(_bp_module, 'show_cif_hkl', new_state)
|
|
1967
|
+
except Exception:
|
|
1968
|
+
pass
|
|
1513
1969
|
# Avoid re-entrant extension while redrawing
|
|
1514
1970
|
prev_ext = bool(getattr(_bp, 'cif_extend_suspended', False)) if _bp is not None else False
|
|
1515
1971
|
if _bp is not None:
|
|
@@ -1636,6 +2092,141 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
1636
2092
|
except Exception as e:
|
|
1637
2093
|
print(f"Error toggling crosshair: {e}")
|
|
1638
2094
|
continue
|
|
2095
|
+
elif key == 'os':
|
|
2096
|
+
# Quick overwrite of last saved session (.pkl)
|
|
2097
|
+
try:
|
|
2098
|
+
last_session_path = getattr(fig, '_last_session_save_path', None)
|
|
2099
|
+
if not last_session_path:
|
|
2100
|
+
print("No previous session save found.")
|
|
2101
|
+
continue
|
|
2102
|
+
if not os.path.exists(last_session_path):
|
|
2103
|
+
print(f"Previous save file not found: {last_session_path}")
|
|
2104
|
+
continue
|
|
2105
|
+
yn = _safe_input(f"Overwrite session '{os.path.basename(last_session_path)}'? (y/n): ").strip().lower()
|
|
2106
|
+
if yn != 'y':
|
|
2107
|
+
print("Canceled.")
|
|
2108
|
+
continue
|
|
2109
|
+
_bp_dump_session(
|
|
2110
|
+
last_session_path,
|
|
2111
|
+
fig=fig,
|
|
2112
|
+
ax=ax,
|
|
2113
|
+
x_data_list=x_data_list,
|
|
2114
|
+
y_data_list=y_data_list,
|
|
2115
|
+
orig_y=orig_y,
|
|
2116
|
+
offsets_list=offsets_list,
|
|
2117
|
+
labels=labels,
|
|
2118
|
+
delta=delta,
|
|
2119
|
+
args=args,
|
|
2120
|
+
tick_state=tick_state,
|
|
2121
|
+
cif_tick_series=(getattr(_bp, 'cif_tick_series', None) if _bp is not None else None),
|
|
2122
|
+
cif_hkl_map=(getattr(_bp, 'cif_hkl_map', None) if _bp is not None else None),
|
|
2123
|
+
cif_hkl_label_map=(getattr(_bp, 'cif_hkl_label_map', None) if _bp is not None else None),
|
|
2124
|
+
show_cif_hkl=(bool(getattr(_bp, 'show_cif_hkl', False)) if _bp is not None else False),
|
|
2125
|
+
show_cif_titles=(bool(getattr(_bp, 'show_cif_titles', True)) if _bp is not None else True),
|
|
2126
|
+
skip_confirm=True,
|
|
2127
|
+
)
|
|
2128
|
+
fig._last_session_save_path = last_session_path
|
|
2129
|
+
print(f"Overwritten session to {last_session_path}")
|
|
2130
|
+
except Exception as e:
|
|
2131
|
+
print(f"Error overwriting session: {e}")
|
|
2132
|
+
continue
|
|
2133
|
+
elif key in ('ops', 'opsg'):
|
|
2134
|
+
# Quick overwrite of last exported style file (.bps / .bpsg)
|
|
2135
|
+
try:
|
|
2136
|
+
last_style_path = getattr(fig, '_last_style_export_path', None)
|
|
2137
|
+
if not last_style_path:
|
|
2138
|
+
print("No previous style export found.")
|
|
2139
|
+
continue
|
|
2140
|
+
if not os.path.exists(last_style_path):
|
|
2141
|
+
print(f"Previous style file not found: {last_style_path}")
|
|
2142
|
+
continue
|
|
2143
|
+
if key == 'ops':
|
|
2144
|
+
mode = 'ps'
|
|
2145
|
+
label = "style-only"
|
|
2146
|
+
else:
|
|
2147
|
+
mode = 'psg'
|
|
2148
|
+
label = "style+geometry"
|
|
2149
|
+
yn = _safe_input(
|
|
2150
|
+
f"Overwrite {label} file '{os.path.basename(last_style_path)}'? (y/n): "
|
|
2151
|
+
).strip().lower()
|
|
2152
|
+
if yn != 'y':
|
|
2153
|
+
print("Canceled.")
|
|
2154
|
+
continue
|
|
2155
|
+
exported = export_style_config(
|
|
2156
|
+
None,
|
|
2157
|
+
base_path=None,
|
|
2158
|
+
overwrite_path=last_style_path,
|
|
2159
|
+
force_kind=mode,
|
|
2160
|
+
)
|
|
2161
|
+
if exported:
|
|
2162
|
+
fig._last_style_export_path = exported
|
|
2163
|
+
print(f"Overwritten {label} style to {exported}")
|
|
2164
|
+
except Exception as e:
|
|
2165
|
+
print(f"Error overwriting style: {e}")
|
|
2166
|
+
continue
|
|
2167
|
+
elif key == 'oe':
|
|
2168
|
+
# Quick overwrite of last exported figure
|
|
2169
|
+
try:
|
|
2170
|
+
last_figure_path = getattr(fig, '_last_figure_export_path', None)
|
|
2171
|
+
if not last_figure_path:
|
|
2172
|
+
print("No previous figure export found.")
|
|
2173
|
+
continue
|
|
2174
|
+
if not os.path.exists(last_figure_path):
|
|
2175
|
+
print(f"Previous export file not found: {last_figure_path}")
|
|
2176
|
+
continue
|
|
2177
|
+
yn = _safe_input(
|
|
2178
|
+
f"Overwrite figure '{os.path.basename(last_figure_path)}'? (y/n): "
|
|
2179
|
+
).strip().lower()
|
|
2180
|
+
if yn != 'y':
|
|
2181
|
+
print("Canceled.")
|
|
2182
|
+
continue
|
|
2183
|
+
export_target = last_figure_path
|
|
2184
|
+
from .utils import ensure_exact_case_filename
|
|
2185
|
+
export_target = ensure_exact_case_filename(export_target)
|
|
2186
|
+
# Temporarily remove numbering for export
|
|
2187
|
+
for i, txt in enumerate(label_text_objects):
|
|
2188
|
+
txt.set_text(labels[i])
|
|
2189
|
+
_, _ext = os.path.splitext(export_target)
|
|
2190
|
+
if _ext.lower() == '.svg':
|
|
2191
|
+
try:
|
|
2192
|
+
_fig_fc = fig.get_facecolor()
|
|
2193
|
+
except Exception:
|
|
2194
|
+
_fig_fc = None
|
|
2195
|
+
try:
|
|
2196
|
+
_ax_fc = ax.get_facecolor()
|
|
2197
|
+
except Exception:
|
|
2198
|
+
_ax_fc = None
|
|
2199
|
+
try:
|
|
2200
|
+
if getattr(fig, 'patch', None) is not None:
|
|
2201
|
+
fig.patch.set_alpha(0.0); fig.patch.set_facecolor('none')
|
|
2202
|
+
if getattr(ax, 'patch', None) is not None:
|
|
2203
|
+
ax.patch.set_alpha(0.0); ax.patch.set_facecolor('none')
|
|
2204
|
+
except Exception:
|
|
2205
|
+
pass
|
|
2206
|
+
try:
|
|
2207
|
+
fig.savefig(export_target, dpi=300, transparent=True, facecolor='none', edgecolor='none')
|
|
2208
|
+
finally:
|
|
2209
|
+
try:
|
|
2210
|
+
if _fig_fc is not None and getattr(fig, 'patch', None) is not None:
|
|
2211
|
+
fig.patch.set_alpha(1.0); fig.patch.set_facecolor(_fig_fc)
|
|
2212
|
+
except Exception:
|
|
2213
|
+
pass
|
|
2214
|
+
try:
|
|
2215
|
+
if _ax_fc is not None and getattr(ax, 'patch', None) is not None:
|
|
2216
|
+
ax.patch.set_alpha(1.0); ax.patch.set_facecolor(_ax_fc)
|
|
2217
|
+
except Exception:
|
|
2218
|
+
pass
|
|
2219
|
+
else:
|
|
2220
|
+
fig.savefig(export_target, dpi=300)
|
|
2221
|
+
print(f"Figure saved to {export_target}")
|
|
2222
|
+
fig._last_figure_export_path = export_target
|
|
2223
|
+
# Restore numbering
|
|
2224
|
+
for i, txt in enumerate(label_text_objects):
|
|
2225
|
+
txt.set_text(f"{i+1}: {labels[i]}")
|
|
2226
|
+
fig.canvas.draw()
|
|
2227
|
+
except Exception as e:
|
|
2228
|
+
print(f"Error overwriting figure: {e}")
|
|
2229
|
+
continue
|
|
1639
2230
|
elif key == 's':
|
|
1640
2231
|
# Save current interactive session with numbered overwrite picker
|
|
1641
2232
|
try:
|
|
@@ -2433,7 +3024,49 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
2433
3024
|
print("Invalid value, ignored.")
|
|
2434
3025
|
continue
|
|
2435
3026
|
push_state("xrange")
|
|
2436
|
-
|
|
3027
|
+
new_min = current_xlim[0]
|
|
3028
|
+
new_max = new_upper
|
|
3029
|
+
ax.set_xlim(new_min, new_max)
|
|
3030
|
+
# Re-filter data from original processed data if available
|
|
3031
|
+
data_is_processed = (hasattr(fig, '_original_x_data_list') or
|
|
3032
|
+
hasattr(fig, '_smooth_settings') or
|
|
3033
|
+
hasattr(fig, '_derivative_order') or
|
|
3034
|
+
hasattr(fig, '_pre_derivative_x_data_list'))
|
|
3035
|
+
if data_is_processed and hasattr(fig, '_original_x_data_list'):
|
|
3036
|
+
for i in range(len(labels)):
|
|
3037
|
+
if i < len(fig._original_x_data_list):
|
|
3038
|
+
x_current = fig._original_x_data_list[i]
|
|
3039
|
+
y_current = fig._original_y_data_list[i]
|
|
3040
|
+
if i < len(offsets_list):
|
|
3041
|
+
y_current_no_offset = y_current - offsets_list[i]
|
|
3042
|
+
else:
|
|
3043
|
+
y_current_no_offset = y_current.copy()
|
|
3044
|
+
mask = (x_current >= new_min) & (x_current <= new_max)
|
|
3045
|
+
x_sub = np.asarray(x_current[mask], dtype=float).flatten()
|
|
3046
|
+
y_sub = np.asarray(y_current_no_offset[mask], dtype=float).flatten()
|
|
3047
|
+
if x_sub.size == 0:
|
|
3048
|
+
ax.lines[i].set_data([], [])
|
|
3049
|
+
x_data_list[i] = np.array([], dtype=float)
|
|
3050
|
+
y_data_list[i] = np.array([], dtype=float)
|
|
3051
|
+
if i < len(orig_y):
|
|
3052
|
+
orig_y[i] = np.array([], dtype=float)
|
|
3053
|
+
continue
|
|
3054
|
+
if i < len(offsets_list):
|
|
3055
|
+
y_sub = y_sub + offsets_list[i]
|
|
3056
|
+
ax.lines[i].set_data(x_sub, y_sub)
|
|
3057
|
+
x_data_list[i] = np.asarray(x_sub, dtype=float).flatten()
|
|
3058
|
+
y_data_list[i] = np.asarray(y_sub, dtype=float).flatten()
|
|
3059
|
+
# Update orig_y with robust method
|
|
3060
|
+
while len(orig_y) <= i:
|
|
3061
|
+
orig_y.append(np.array([], dtype=float))
|
|
3062
|
+
try:
|
|
3063
|
+
y_no_offset = y_sub - offsets_list[i] if i < len(offsets_list) else y_sub
|
|
3064
|
+
y_no_offset_1d = np.array(y_no_offset, dtype=float).ravel()
|
|
3065
|
+
if i < len(orig_y):
|
|
3066
|
+
del orig_y[i]
|
|
3067
|
+
orig_y.insert(i, y_no_offset_1d)
|
|
3068
|
+
except Exception:
|
|
3069
|
+
pass
|
|
2437
3070
|
ax.relim()
|
|
2438
3071
|
ax.autoscale_view(scalex=False, scaley=True)
|
|
2439
3072
|
update_labels(ax, y_data_list, label_text_objects, args.stack, getattr(fig, '_stack_label_at_bottom', False))
|
|
@@ -2464,7 +3097,49 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
2464
3097
|
print("Invalid value, ignored.")
|
|
2465
3098
|
continue
|
|
2466
3099
|
push_state("xrange")
|
|
2467
|
-
|
|
3100
|
+
new_min = new_lower
|
|
3101
|
+
new_max = current_xlim[1]
|
|
3102
|
+
ax.set_xlim(new_min, new_max)
|
|
3103
|
+
# Re-filter data from original processed data if available
|
|
3104
|
+
data_is_processed = (hasattr(fig, '_original_x_data_list') or
|
|
3105
|
+
hasattr(fig, '_smooth_settings') or
|
|
3106
|
+
hasattr(fig, '_derivative_order') or
|
|
3107
|
+
hasattr(fig, '_pre_derivative_x_data_list'))
|
|
3108
|
+
if data_is_processed and hasattr(fig, '_original_x_data_list'):
|
|
3109
|
+
for i in range(len(labels)):
|
|
3110
|
+
if i < len(fig._original_x_data_list):
|
|
3111
|
+
x_current = fig._original_x_data_list[i]
|
|
3112
|
+
y_current = fig._original_y_data_list[i]
|
|
3113
|
+
if i < len(offsets_list):
|
|
3114
|
+
y_current_no_offset = y_current - offsets_list[i]
|
|
3115
|
+
else:
|
|
3116
|
+
y_current_no_offset = y_current.copy()
|
|
3117
|
+
mask = (x_current >= new_min) & (x_current <= new_max)
|
|
3118
|
+
x_sub = np.asarray(x_current[mask], dtype=float).flatten()
|
|
3119
|
+
y_sub = np.asarray(y_current_no_offset[mask], dtype=float).flatten()
|
|
3120
|
+
if x_sub.size == 0:
|
|
3121
|
+
ax.lines[i].set_data([], [])
|
|
3122
|
+
x_data_list[i] = np.array([], dtype=float)
|
|
3123
|
+
y_data_list[i] = np.array([], dtype=float)
|
|
3124
|
+
if i < len(orig_y):
|
|
3125
|
+
orig_y[i] = np.array([], dtype=float)
|
|
3126
|
+
continue
|
|
3127
|
+
if i < len(offsets_list):
|
|
3128
|
+
y_sub = y_sub + offsets_list[i]
|
|
3129
|
+
ax.lines[i].set_data(x_sub, y_sub)
|
|
3130
|
+
x_data_list[i] = np.asarray(x_sub, dtype=float).flatten()
|
|
3131
|
+
y_data_list[i] = np.asarray(y_sub, dtype=float).flatten()
|
|
3132
|
+
# Update orig_y with robust method
|
|
3133
|
+
while len(orig_y) <= i:
|
|
3134
|
+
orig_y.append(np.array([], dtype=float))
|
|
3135
|
+
try:
|
|
3136
|
+
y_no_offset = y_sub - offsets_list[i] if i < len(offsets_list) else y_sub
|
|
3137
|
+
y_no_offset_1d = np.array(y_no_offset, dtype=float).ravel()
|
|
3138
|
+
if i < len(orig_y):
|
|
3139
|
+
del orig_y[i]
|
|
3140
|
+
orig_y.insert(i, y_no_offset_1d)
|
|
3141
|
+
except Exception:
|
|
3142
|
+
pass
|
|
2468
3143
|
ax.relim()
|
|
2469
3144
|
ax.autoscale_view(scalex=False, scaley=True)
|
|
2470
3145
|
update_labels(ax, y_data_list, label_text_objects, args.stack, getattr(fig, '_stack_label_at_bottom', False))
|
|
@@ -2482,22 +3157,266 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
2482
3157
|
print(f"X range updated: {ax.get_xlim()[0]:.6g} to {ax.get_xlim()[1]:.6g}")
|
|
2483
3158
|
continue
|
|
2484
3159
|
if rng.lower() == 'a':
|
|
2485
|
-
# Auto: restore original range from
|
|
3160
|
+
# Auto: restore original range from CURRENT PROCESSED data (not original unprocessed)
|
|
2486
3161
|
push_state("xrange-auto")
|
|
2487
|
-
|
|
3162
|
+
try:
|
|
3163
|
+
# Check if data has been processed
|
|
3164
|
+
data_is_processed = (hasattr(fig, '_original_x_data_list') or
|
|
3165
|
+
hasattr(fig, '_smooth_settings') or
|
|
3166
|
+
hasattr(fig, '_derivative_order') or
|
|
3167
|
+
hasattr(fig, '_pre_derivative_x_data_list'))
|
|
3168
|
+
if data_is_processed and x_data_list and all(xd.size > 0 for xd in x_data_list):
|
|
3169
|
+
# Use CURRENT processed data to determine full range (preserves all processing)
|
|
3170
|
+
print(f"DEBUG: Using current processed data for auto restore (has {len(x_data_list)} curves)")
|
|
3171
|
+
new_min = min(xd.min() for xd in x_data_list if xd.size)
|
|
3172
|
+
new_max = max(xd.max() for xd in x_data_list if xd.size)
|
|
3173
|
+
print(f"DEBUG: Processed data range: {new_min:.6g} to {new_max:.6g}")
|
|
3174
|
+
elif x_full_list:
|
|
3175
|
+
print(f"DEBUG: Using original full data (no processing detected)")
|
|
3176
|
+
new_min = min(xf.min() for xf in x_full_list if xf.size)
|
|
3177
|
+
new_max = max(xf.max() for xf in x_full_list if xf.size)
|
|
3178
|
+
else:
|
|
3179
|
+
print("No original data available.")
|
|
3180
|
+
continue
|
|
3181
|
+
# Restore all data - use CURRENT PROCESSED data (preserves all processing steps)
|
|
3182
|
+
for i in range(len(labels)):
|
|
3183
|
+
if data_is_processed and hasattr(fig, '_full_processed_x_data_list') and i < len(fig._full_processed_x_data_list):
|
|
3184
|
+
# Use FULL processed data (preserves all processing: reduce + smooth + derivative)
|
|
3185
|
+
print(f"DEBUG: Auto restore curve {i+1}: Using full processed data ({len(fig._full_processed_x_data_list[i])} points)")
|
|
3186
|
+
xf = np.asarray(fig._full_processed_x_data_list[i], dtype=float).flatten()
|
|
3187
|
+
yf = np.asarray(fig._full_processed_y_data_list[i], dtype=float).flatten()
|
|
3188
|
+
yf_raw = yf - (offsets_list[i] if i < len(offsets_list) else 0.0)
|
|
3189
|
+
elif data_is_processed and i < len(x_data_list) and x_data_list[i].size > 0:
|
|
3190
|
+
# Fallback: use current processed data
|
|
3191
|
+
print(f"DEBUG: Auto restore curve {i+1}: Using current processed data ({len(x_data_list[i])} points)")
|
|
3192
|
+
xf = np.asarray(x_data_list[i], dtype=float).flatten()
|
|
3193
|
+
yf = np.asarray(y_data_list[i], dtype=float).flatten()
|
|
3194
|
+
yf_raw = yf - (offsets_list[i] if i < len(offsets_list) else 0.0)
|
|
3195
|
+
else:
|
|
3196
|
+
# Use full original data (no processing)
|
|
3197
|
+
print(f"DEBUG: Auto restore curve {i+1}: Using original full data")
|
|
3198
|
+
xf = x_full_list[i] if i < len(x_full_list) else x_data_list[i]
|
|
3199
|
+
yf_raw = raw_y_full_list[i] if i < len(raw_y_full_list) else (orig_y[i] if i < len(orig_y) else y_data_list[i])
|
|
3200
|
+
xf = np.asarray(xf, dtype=float).flatten()
|
|
3201
|
+
yf_raw = np.asarray(yf_raw, dtype=float).flatten()
|
|
3202
|
+
mask = (xf >= new_min) & (xf <= new_max)
|
|
3203
|
+
x_sub = np.asarray(xf[mask], dtype=float).flatten()
|
|
3204
|
+
y_sub_raw = np.asarray(yf_raw[mask], dtype=float).flatten()
|
|
3205
|
+
if x_sub.size == 0:
|
|
3206
|
+
ax.lines[i].set_data([], [])
|
|
3207
|
+
x_data_list[i] = np.array([], dtype=float)
|
|
3208
|
+
y_data_list[i] = np.array([], dtype=float)
|
|
3209
|
+
if i < len(orig_y):
|
|
3210
|
+
orig_y[i] = np.array([], dtype=float)
|
|
3211
|
+
continue
|
|
3212
|
+
should_normalize = args.stack or getattr(args, 'norm', False)
|
|
3213
|
+
if should_normalize:
|
|
3214
|
+
if y_sub_raw.size:
|
|
3215
|
+
y_min = float(y_sub_raw.min())
|
|
3216
|
+
y_max = float(y_sub_raw.max())
|
|
3217
|
+
span = y_max - y_min
|
|
3218
|
+
if span > 0:
|
|
3219
|
+
y_sub_norm = (y_sub_raw - y_min) / span
|
|
3220
|
+
else:
|
|
3221
|
+
y_sub_norm = np.zeros_like(y_sub_raw)
|
|
3222
|
+
else:
|
|
3223
|
+
y_sub_norm = y_sub_raw
|
|
3224
|
+
else:
|
|
3225
|
+
y_sub_norm = y_sub_raw
|
|
3226
|
+
offset_val = offsets_list[i] if i < len(offsets_list) else 0.0
|
|
3227
|
+
y_with_offset = y_sub_norm + offset_val
|
|
3228
|
+
ax.lines[i].set_data(x_sub, y_with_offset)
|
|
3229
|
+
x_data_list[i] = np.asarray(x_sub, dtype=float).flatten()
|
|
3230
|
+
y_data_list[i] = np.asarray(y_with_offset, dtype=float).flatten()
|
|
3231
|
+
# Ensure orig_y list has enough elements
|
|
3232
|
+
while len(orig_y) <= i:
|
|
3233
|
+
orig_y.append(np.array([], dtype=float))
|
|
3234
|
+
# Create a new 1D array - ensure it's a proper numpy array
|
|
3235
|
+
# Handle all edge cases: scalar, 0-d array, multi-d array
|
|
3236
|
+
try:
|
|
3237
|
+
if isinstance(y_sub_norm, np.ndarray):
|
|
3238
|
+
if y_sub_norm.ndim == 0:
|
|
3239
|
+
y_sub_norm_1d = np.array([float(y_sub_norm)], dtype=float)
|
|
3240
|
+
else:
|
|
3241
|
+
y_sub_norm_1d = np.array(y_sub_norm.flatten(), dtype=float, copy=True)
|
|
3242
|
+
else:
|
|
3243
|
+
# It's a scalar or list
|
|
3244
|
+
y_sub_norm_1d = np.array(y_sub_norm, dtype=float).flatten()
|
|
3245
|
+
# Ensure it's 1D
|
|
3246
|
+
if y_sub_norm_1d.ndim != 1:
|
|
3247
|
+
y_sub_norm_1d = y_sub_norm_1d.reshape(-1)
|
|
3248
|
+
# Replace list element - delete old one first if needed
|
|
3249
|
+
if i < len(orig_y):
|
|
3250
|
+
del orig_y[i]
|
|
3251
|
+
orig_y.insert(i, y_sub_norm_1d)
|
|
3252
|
+
except Exception as e:
|
|
3253
|
+
# Fallback: just create a simple array
|
|
3254
|
+
try:
|
|
3255
|
+
y_sub_norm_1d = np.array(y_sub_norm, dtype=float).ravel()
|
|
3256
|
+
if i < len(orig_y):
|
|
3257
|
+
orig_y[i] = y_sub_norm_1d
|
|
3258
|
+
else:
|
|
3259
|
+
orig_y.append(y_sub_norm_1d)
|
|
3260
|
+
except Exception:
|
|
3261
|
+
# Last resort: skip orig_y update
|
|
3262
|
+
pass
|
|
3263
|
+
ax.set_xlim(new_min, new_max)
|
|
3264
|
+
ax.relim(); ax.autoscale_view(scalex=False, scaley=True)
|
|
3265
|
+
update_labels(ax, y_data_list, label_text_objects, args.stack, getattr(fig, '_stack_label_at_bottom', False))
|
|
3266
|
+
try:
|
|
3267
|
+
if hasattr(ax, '_cif_extend_func'):
|
|
3268
|
+
ax._cif_extend_func(ax.get_xlim()[1])
|
|
3269
|
+
except Exception:
|
|
3270
|
+
pass
|
|
3271
|
+
try:
|
|
3272
|
+
if hasattr(ax, '_cif_draw_func'):
|
|
3273
|
+
ax._cif_draw_func()
|
|
3274
|
+
except Exception:
|
|
3275
|
+
pass
|
|
3276
|
+
fig.canvas.draw()
|
|
3277
|
+
print(f"X range restored to original: {ax.get_xlim()[0]:.6g} to {ax.get_xlim()[1]:.6g}")
|
|
3278
|
+
except Exception as e:
|
|
3279
|
+
print(f"Error during auto restore: {e}")
|
|
3280
|
+
import traceback
|
|
3281
|
+
traceback.print_exc()
|
|
3282
|
+
continue
|
|
3283
|
+
push_state("xrange")
|
|
3284
|
+
if rng.lower() == 'full':
|
|
3285
|
+
# Use full data if available, otherwise use current processed data
|
|
3286
|
+
if x_full_list and all(xf.size > 0 for xf in x_full_list):
|
|
2488
3287
|
new_min = min(xf.min() for xf in x_full_list if xf.size)
|
|
2489
3288
|
new_max = max(xf.max() for xf in x_full_list if xf.size)
|
|
2490
3289
|
else:
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
3290
|
+
new_min = min(xd.min() for xd in x_data_list if xd.size)
|
|
3291
|
+
new_max = max(xd.max() for xd in x_data_list if xd.size)
|
|
3292
|
+
else:
|
|
3293
|
+
new_min, new_max = map(float, rng.split())
|
|
3294
|
+
ax.set_xlim(new_min, new_max)
|
|
3295
|
+
# Check if data has been processed (smooth/derivative/reduce)
|
|
3296
|
+
data_is_processed = (hasattr(fig, '_original_x_data_list') or
|
|
3297
|
+
hasattr(fig, '_smooth_settings') or
|
|
3298
|
+
hasattr(fig, '_derivative_order') or
|
|
3299
|
+
hasattr(fig, '_pre_derivative_x_data_list'))
|
|
3300
|
+
|
|
3301
|
+
for i in range(len(labels)):
|
|
3302
|
+
if data_is_processed and i < len(x_data_list) and x_data_list[i].size > 0:
|
|
3303
|
+
# Use full processed data if available (allows expansion), otherwise use current filtered data
|
|
3304
|
+
curr_x = np.asarray(x_data_list[i], dtype=float)
|
|
3305
|
+
curr_min = curr_x.min() if curr_x.size > 0 else float('inf')
|
|
3306
|
+
curr_max = curr_x.max() if curr_x.size > 0 else float('-inf')
|
|
3307
|
+
|
|
3308
|
+
# Check if we need full processed data (for expansion beyond current filter)
|
|
3309
|
+
need_full = (new_min < curr_min or new_max > curr_max)
|
|
3310
|
+
|
|
3311
|
+
if need_full and hasattr(fig, '_full_processed_x_data_list') and i < len(fig._full_processed_x_data_list):
|
|
3312
|
+
# Use full processed data to allow expansion
|
|
3313
|
+
full_x = np.asarray(fig._full_processed_x_data_list[i], dtype=float)
|
|
3314
|
+
if full_x.size > 0:
|
|
3315
|
+
full_min = full_x.min()
|
|
3316
|
+
full_max = full_x.max()
|
|
3317
|
+
print(f"DEBUG: Curve {i+1}: Expanding range ({curr_min:.6g}-{curr_max:.6g} -> {new_min:.6g}-{new_max:.6g}), using full processed data (range {full_min:.6g} to {full_max:.6g})")
|
|
3318
|
+
x_current = full_x
|
|
3319
|
+
y_current = np.asarray(fig._full_processed_y_data_list[i], dtype=float)
|
|
3320
|
+
else:
|
|
3321
|
+
print(f"DEBUG: Curve {i+1}: Full processed data empty, using current data")
|
|
3322
|
+
x_current = curr_x
|
|
3323
|
+
y_current = np.asarray(y_data_list[i], dtype=float)
|
|
3324
|
+
else:
|
|
3325
|
+
print(f"DEBUG: Curve {i+1}: Using current processed data (range {curr_min:.6g} to {curr_max:.6g}, requested {new_min:.6g} to {new_max:.6g})")
|
|
3326
|
+
x_current = curr_x
|
|
3327
|
+
y_current = np.asarray(y_data_list[i], dtype=float)
|
|
3328
|
+
# Remove offset for filtering
|
|
3329
|
+
if i < len(offsets_list):
|
|
3330
|
+
y_current_no_offset = y_current - offsets_list[i]
|
|
3331
|
+
else:
|
|
3332
|
+
y_current_no_offset = y_current.copy()
|
|
3333
|
+
mask = (x_current >= new_min) & (x_current <= new_max)
|
|
3334
|
+
x_sub = np.asarray(x_current[mask], dtype=float).flatten()
|
|
3335
|
+
y_sub = np.asarray(y_current_no_offset[mask], dtype=float).flatten()
|
|
3336
|
+
if x_sub.size == 0:
|
|
3337
|
+
ax.lines[i].set_data([], [])
|
|
3338
|
+
x_data_list[i] = np.array([], dtype=float)
|
|
3339
|
+
y_data_list[i] = np.array([], dtype=float)
|
|
3340
|
+
if i < len(orig_y):
|
|
3341
|
+
orig_y[i] = np.array([], dtype=float)
|
|
3342
|
+
continue
|
|
3343
|
+
# Restore offset
|
|
3344
|
+
if i < len(offsets_list):
|
|
3345
|
+
y_sub = y_sub + offsets_list[i]
|
|
3346
|
+
ax.lines[i].set_data(x_sub, y_sub)
|
|
3347
|
+
x_data_list[i] = np.asarray(x_sub, dtype=float).flatten()
|
|
3348
|
+
y_data_list[i] = np.asarray(y_sub, dtype=float).flatten()
|
|
3349
|
+
# Update orig_y
|
|
3350
|
+
# Update orig_y with robust method
|
|
3351
|
+
while len(orig_y) <= i:
|
|
3352
|
+
orig_y.append(np.array([], dtype=float))
|
|
3353
|
+
try:
|
|
3354
|
+
y_no_offset = y_sub - offsets_list[i] if i < len(offsets_list) else y_sub
|
|
3355
|
+
y_no_offset_1d = np.array(y_no_offset, dtype=float).ravel()
|
|
3356
|
+
if i < len(orig_y):
|
|
3357
|
+
del orig_y[i]
|
|
3358
|
+
orig_y.insert(i, y_no_offset_1d)
|
|
3359
|
+
except Exception:
|
|
3360
|
+
pass
|
|
3361
|
+
elif data_is_processed and i < len(x_data_list) and x_data_list[i].size > 0:
|
|
3362
|
+
# Fallback: use current data if _original_x_data_list not available
|
|
3363
|
+
x_current = np.asarray(x_data_list[i], dtype=float)
|
|
3364
|
+
y_current = np.asarray(y_data_list[i], dtype=float)
|
|
3365
|
+
mask = (x_current >= new_min) & (x_current <= new_max)
|
|
3366
|
+
x_sub = np.asarray(x_current[mask], dtype=float).flatten()
|
|
3367
|
+
y_sub = np.asarray(y_current[mask], dtype=float).flatten()
|
|
2498
3368
|
if x_sub.size == 0:
|
|
2499
3369
|
ax.lines[i].set_data([], [])
|
|
2500
|
-
|
|
3370
|
+
x_data_list[i] = np.array([], dtype=float)
|
|
3371
|
+
y_data_list[i] = np.array([], dtype=float)
|
|
3372
|
+
if i < len(orig_y):
|
|
3373
|
+
orig_y[i] = np.array([], dtype=float)
|
|
3374
|
+
continue
|
|
3375
|
+
ax.lines[i].set_data(x_sub, y_sub)
|
|
3376
|
+
x_data_list[i] = np.asarray(x_sub, dtype=float).flatten()
|
|
3377
|
+
y_data_list[i] = np.asarray(y_sub, dtype=float).flatten()
|
|
3378
|
+
# Update orig_y - use same robust method as in 'a' branch
|
|
3379
|
+
while len(orig_y) <= i:
|
|
3380
|
+
orig_y.append(np.array([], dtype=float))
|
|
3381
|
+
try:
|
|
3382
|
+
y_no_offset = y_sub - offsets_list[i] if i < len(offsets_list) else y_sub
|
|
3383
|
+
if isinstance(y_no_offset, np.ndarray):
|
|
3384
|
+
if y_no_offset.ndim == 0:
|
|
3385
|
+
y_no_offset_1d = np.array([float(y_no_offset)], dtype=float)
|
|
3386
|
+
else:
|
|
3387
|
+
y_no_offset_1d = np.array(y_no_offset.flatten(), dtype=float, copy=True)
|
|
3388
|
+
else:
|
|
3389
|
+
y_no_offset_1d = np.array(y_no_offset, dtype=float).flatten()
|
|
3390
|
+
if y_no_offset_1d.ndim != 1:
|
|
3391
|
+
y_no_offset_1d = y_no_offset_1d.reshape(-1)
|
|
3392
|
+
if i < len(orig_y):
|
|
3393
|
+
del orig_y[i]
|
|
3394
|
+
orig_y.insert(i, y_no_offset_1d)
|
|
3395
|
+
except Exception:
|
|
3396
|
+
try:
|
|
3397
|
+
y_no_offset = y_sub - offsets_list[i] if i < len(offsets_list) else y_sub
|
|
3398
|
+
y_no_offset_1d = np.array(y_no_offset, dtype=float).ravel()
|
|
3399
|
+
if i < len(orig_y):
|
|
3400
|
+
orig_y[i] = y_no_offset_1d
|
|
3401
|
+
else:
|
|
3402
|
+
orig_y.append(y_no_offset_1d)
|
|
3403
|
+
except Exception:
|
|
3404
|
+
pass
|
|
3405
|
+
else:
|
|
3406
|
+
# Use original full data as source
|
|
3407
|
+
xf = x_full_list[i] if i < len(x_full_list) else x_data_list[i]
|
|
3408
|
+
yf_raw = raw_y_full_list[i] if i < len(raw_y_full_list) else (orig_y[i] if i < len(orig_y) else y_data_list[i])
|
|
3409
|
+
mask = (xf >= new_min) & (xf <= new_max)
|
|
3410
|
+
x_sub = np.array(xf[mask], copy=True)
|
|
3411
|
+
y_sub_raw = np.array(yf_raw[mask], copy=True)
|
|
3412
|
+
if x_sub.size == 0:
|
|
3413
|
+
ax.lines[i].set_data([], [])
|
|
3414
|
+
x_data_list[i] = np.array([])
|
|
3415
|
+
y_data_list[i] = np.array([])
|
|
3416
|
+
if i < len(orig_y):
|
|
3417
|
+
orig_y[i] = np.array([])
|
|
3418
|
+
continue
|
|
3419
|
+
# Auto-normalize for --stack mode, or explicit --norm flag
|
|
2501
3420
|
should_normalize = args.stack or getattr(args, 'norm', False)
|
|
2502
3421
|
if should_normalize:
|
|
2503
3422
|
if y_sub_raw.size:
|
|
@@ -2512,63 +3431,13 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
2512
3431
|
y_sub_norm = y_sub_raw
|
|
2513
3432
|
else:
|
|
2514
3433
|
y_sub_norm = y_sub_raw
|
|
2515
|
-
offset_val = offsets_list[i]
|
|
3434
|
+
offset_val = offsets_list[i] if i < len(offsets_list) else 0.0
|
|
2516
3435
|
y_with_offset = y_sub_norm + offset_val
|
|
2517
3436
|
ax.lines[i].set_data(x_sub, y_with_offset)
|
|
2518
3437
|
x_data_list[i] = x_sub
|
|
2519
3438
|
y_data_list[i] = y_with_offset
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
ax.relim(); ax.autoscale_view(scalex=False, scaley=True)
|
|
2523
|
-
update_labels(ax, y_data_list, label_text_objects, args.stack, getattr(fig, '_stack_label_at_bottom', False))
|
|
2524
|
-
try:
|
|
2525
|
-
if hasattr(ax, '_cif_extend_func'):
|
|
2526
|
-
ax._cif_extend_func(ax.get_xlim()[1])
|
|
2527
|
-
except Exception:
|
|
2528
|
-
pass
|
|
2529
|
-
try:
|
|
2530
|
-
if hasattr(ax, '_cif_draw_func'):
|
|
2531
|
-
ax._cif_draw_func()
|
|
2532
|
-
except Exception:
|
|
2533
|
-
pass
|
|
2534
|
-
fig.canvas.draw()
|
|
2535
|
-
print(f"X range restored to original: {ax.get_xlim()[0]:.6g} to {ax.get_xlim()[1]:.6g}")
|
|
2536
|
-
continue
|
|
2537
|
-
push_state("xrange")
|
|
2538
|
-
if rng.lower() == 'full':
|
|
2539
|
-
new_min = min(xf.min() for xf in x_full_list if xf.size)
|
|
2540
|
-
new_max = max(xf.max() for xf in x_full_list if xf.size)
|
|
2541
|
-
else:
|
|
2542
|
-
new_min, new_max = map(float, rng.split())
|
|
2543
|
-
ax.set_xlim(new_min, new_max)
|
|
2544
|
-
for i in range(len(labels)):
|
|
2545
|
-
xf = x_full_list[i]; yf_raw = raw_y_full_list[i]
|
|
2546
|
-
mask = (xf>=new_min) & (xf<=new_max)
|
|
2547
|
-
x_sub = xf[mask]; y_sub_raw = yf_raw[mask]
|
|
2548
|
-
if x_sub.size == 0:
|
|
2549
|
-
ax.lines[i].set_data([], [])
|
|
2550
|
-
y_data_list[i] = np.array([]); orig_y[i] = np.array([]); continue
|
|
2551
|
-
# Auto-normalize for --stack mode, or explicit --norm flag
|
|
2552
|
-
should_normalize = args.stack or getattr(args, 'norm', False)
|
|
2553
|
-
if should_normalize:
|
|
2554
|
-
if y_sub_raw.size:
|
|
2555
|
-
y_min = float(y_sub_raw.min())
|
|
2556
|
-
y_max = float(y_sub_raw.max())
|
|
2557
|
-
span = y_max - y_min
|
|
2558
|
-
if span > 0:
|
|
2559
|
-
y_sub_norm = (y_sub_raw - y_min) / span
|
|
2560
|
-
else:
|
|
2561
|
-
y_sub_norm = np.zeros_like(y_sub_raw)
|
|
2562
|
-
else:
|
|
2563
|
-
y_sub_norm = y_sub_raw
|
|
2564
|
-
else:
|
|
2565
|
-
y_sub_norm = y_sub_raw
|
|
2566
|
-
offset_val = offsets_list[i]
|
|
2567
|
-
y_with_offset = y_sub_norm + offset_val
|
|
2568
|
-
ax.lines[i].set_data(x_sub, y_with_offset)
|
|
2569
|
-
x_data_list[i] = x_sub
|
|
2570
|
-
y_data_list[i] = y_with_offset
|
|
2571
|
-
orig_y[i] = y_sub_norm
|
|
3439
|
+
if i < len(orig_y):
|
|
3440
|
+
orig_y[i] = y_sub_norm
|
|
2572
3441
|
ax.relim(); ax.autoscale_view(scalex=False, scaley=True)
|
|
2573
3442
|
update_labels(ax, y_data_list, label_text_objects, args.stack, getattr(fig, '_stack_label_at_bottom', False))
|
|
2574
3443
|
# Extend CIF ticks after x-range change
|
|
@@ -2697,7 +3566,98 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
2697
3566
|
print(f"Y range set to ({float(ymin)}, {float(ymax)})")
|
|
2698
3567
|
except Exception as e:
|
|
2699
3568
|
print(f"Error setting Y-axis range: {e}")
|
|
2700
|
-
elif key == 'd': # <--
|
|
3569
|
+
elif key == 'd': # <-- DERIVATIVE HANDLER
|
|
3570
|
+
while True:
|
|
3571
|
+
try:
|
|
3572
|
+
print("\n\033[1mDerivative Menu\033[0m")
|
|
3573
|
+
print("Commands:")
|
|
3574
|
+
print(" 1: Calculate 1st derivative (dy/dx)")
|
|
3575
|
+
print(" 2: Calculate 2nd derivative (d²y/dx²)")
|
|
3576
|
+
print(" 3: Calculate reversed 1st derivative (dx/dy)")
|
|
3577
|
+
print(" 4: Calculate reversed 2nd derivative (d²x/dy²)")
|
|
3578
|
+
print(" reset: Reset to data before derivative")
|
|
3579
|
+
print(" q: back to main menu")
|
|
3580
|
+
sub = _safe_input(colorize_prompt("d> ")).strip().lower()
|
|
3581
|
+
if not sub or sub == 'q':
|
|
3582
|
+
break
|
|
3583
|
+
if sub == 'reset':
|
|
3584
|
+
push_state("derivative-reset")
|
|
3585
|
+
success, reset_count, total_points = _reset_from_derivative()
|
|
3586
|
+
if success:
|
|
3587
|
+
print(f"Reset {reset_count} curve(s) from derivative to original data ({total_points} total points restored).")
|
|
3588
|
+
ax.relim()
|
|
3589
|
+
ax.autoscale_view(scalex=False, scaley=True)
|
|
3590
|
+
update_labels(ax, y_data_list, label_text_objects, args.stack, getattr(fig, '_stack_label_at_bottom', False))
|
|
3591
|
+
_apply_data_changes()
|
|
3592
|
+
else:
|
|
3593
|
+
print("No derivative data to reset.")
|
|
3594
|
+
continue
|
|
3595
|
+
if sub in ('1', '2', '3', '4'):
|
|
3596
|
+
try:
|
|
3597
|
+
option = int(sub)
|
|
3598
|
+
is_reversed = (option == 3 or option == 4)
|
|
3599
|
+
order = 1 if option in (1, 3) else 2
|
|
3600
|
+
push_state(f"derivative-{option}")
|
|
3601
|
+
_ensure_pre_derivative_data()
|
|
3602
|
+
processed = 0
|
|
3603
|
+
total_points = 0
|
|
3604
|
+
for i in range(len(x_data_list)):
|
|
3605
|
+
try:
|
|
3606
|
+
# Use current data (may already be processed)
|
|
3607
|
+
current_x = x_data_list[i].copy()
|
|
3608
|
+
current_y = y_data_list[i].copy()
|
|
3609
|
+
# Remove offset for processing
|
|
3610
|
+
if i < len(offsets_list):
|
|
3611
|
+
current_y_no_offset = current_y - offsets_list[i]
|
|
3612
|
+
else:
|
|
3613
|
+
current_y_no_offset = current_y.copy()
|
|
3614
|
+
n_points = len(current_y_no_offset)
|
|
3615
|
+
if n_points < 2:
|
|
3616
|
+
print(f"Curve {i+1} has too few points (<2) for derivative calculation.")
|
|
3617
|
+
continue
|
|
3618
|
+
# Calculate derivative
|
|
3619
|
+
if is_reversed:
|
|
3620
|
+
derivative_y = _calculate_reversed_derivative(current_x, current_y_no_offset, order)
|
|
3621
|
+
else:
|
|
3622
|
+
derivative_y = _calculate_derivative(current_x, current_y_no_offset, order)
|
|
3623
|
+
if len(derivative_y) > 0:
|
|
3624
|
+
# Restore offset
|
|
3625
|
+
if i < len(offsets_list):
|
|
3626
|
+
derivative_y = derivative_y + offsets_list[i]
|
|
3627
|
+
# Update data (keep same x, replace y with derivative)
|
|
3628
|
+
x_data_list[i] = current_x.copy()
|
|
3629
|
+
y_data_list[i] = derivative_y
|
|
3630
|
+
processed += 1
|
|
3631
|
+
total_points += n_points
|
|
3632
|
+
except Exception as e:
|
|
3633
|
+
print(f"Error processing curve {i+1}: {e}")
|
|
3634
|
+
if processed > 0:
|
|
3635
|
+
# Update y-axis label
|
|
3636
|
+
current_ylabel = ax.get_ylabel() or ""
|
|
3637
|
+
new_ylabel = _update_ylabel_for_derivative(order, current_ylabel, is_reversed=is_reversed)
|
|
3638
|
+
ax.set_ylabel(new_ylabel)
|
|
3639
|
+
# Store derivative order and reversed flag
|
|
3640
|
+
fig._derivative_order = order
|
|
3641
|
+
fig._derivative_reversed = is_reversed
|
|
3642
|
+
# Update plot
|
|
3643
|
+
_apply_data_changes()
|
|
3644
|
+
ax.relim()
|
|
3645
|
+
ax.autoscale_view(scalex=False, scaley=True)
|
|
3646
|
+
update_labels(ax, y_data_list, label_text_objects, args.stack, getattr(fig, '_stack_label_at_bottom', False))
|
|
3647
|
+
fig.canvas.draw_idle()
|
|
3648
|
+
order_name = "1st" if order == 1 else "2nd"
|
|
3649
|
+
direction = "reversed " if is_reversed else ""
|
|
3650
|
+
print(f"Applied {direction}{order_name} derivative to {processed} curve(s) with {total_points} total points.")
|
|
3651
|
+
print(f"Y-axis label updated to: {new_ylabel}")
|
|
3652
|
+
_update_full_processed_data() # Store full processed data for X-range filtering
|
|
3653
|
+
else:
|
|
3654
|
+
print("No curves were processed.")
|
|
3655
|
+
except ValueError:
|
|
3656
|
+
print("Invalid input.")
|
|
3657
|
+
continue
|
|
3658
|
+
except Exception as e:
|
|
3659
|
+
print(f"Error in derivative menu: {e}")
|
|
3660
|
+
elif key == 'o': # <-- OFFSET HANDLER (now only reachable if not args.stack)
|
|
2701
3661
|
print("\n\033[1mOffset adjustment menu:\033[0m")
|
|
2702
3662
|
print(f" {colorize_menu('1-{}: adjust individual curve offset'.format(len(labels)))}")
|
|
2703
3663
|
print(f" {colorize_menu('a: set spacing between curves')}")
|
|
@@ -3622,8 +4582,13 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
3622
4582
|
else:
|
|
3623
4583
|
print(f" {_i}: {fname}")
|
|
3624
4584
|
last_style_path = getattr(fig, '_last_style_export_path', None)
|
|
3625
|
-
if
|
|
4585
|
+
n_style = len(style_file_list) if style_file_list else 0
|
|
4586
|
+
if last_style_path and n_style:
|
|
4587
|
+
sub = _safe_input(colorize_prompt("Style submenu: (e=export, o=overwrite last, q=return, r=refresh). Press number to overwrite: ")).strip().lower()
|
|
4588
|
+
elif last_style_path:
|
|
3626
4589
|
sub = _safe_input(colorize_prompt("Style submenu: (e=export, o=overwrite last, q=return, r=refresh): ")).strip().lower()
|
|
4590
|
+
elif n_style:
|
|
4591
|
+
sub = _safe_input(colorize_prompt("Style submenu: (e=export, q=return, r=refresh). Press number to overwrite: ")).strip().lower()
|
|
3627
4592
|
else:
|
|
3628
4593
|
sub = _safe_input(colorize_prompt("Style submenu: (e=export, q=return, r=refresh): ")).strip().lower()
|
|
3629
4594
|
if sub == 'q':
|
|
@@ -3647,6 +4612,19 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
3647
4612
|
fig._last_style_export_path = exported_path
|
|
3648
4613
|
style_menu_active = False
|
|
3649
4614
|
break
|
|
4615
|
+
if sub.isdigit() and n_style and 1 <= int(sub) <= n_style:
|
|
4616
|
+
# Overwrite listed style file by number (same as e → export → number)
|
|
4617
|
+
idx = int(sub) - 1
|
|
4618
|
+
target_path = style_file_list[idx][1]
|
|
4619
|
+
fname = style_file_list[idx][0]
|
|
4620
|
+
yn = _safe_input(f"Overwrite '{fname}'? (y/n): ").strip().lower()
|
|
4621
|
+
if yn != 'y':
|
|
4622
|
+
continue
|
|
4623
|
+
exported_path = export_style_config(None, base_path=None, overwrite_path=target_path)
|
|
4624
|
+
if exported_path:
|
|
4625
|
+
fig._last_style_export_path = exported_path
|
|
4626
|
+
style_menu_active = False
|
|
4627
|
+
break
|
|
3650
4628
|
if sub == 'e':
|
|
3651
4629
|
save_base = choose_save_path(source_file_paths, purpose="style export")
|
|
3652
4630
|
if not save_base:
|
|
@@ -3669,6 +4647,12 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
3669
4647
|
if not fname:
|
|
3670
4648
|
print("Style import canceled.")
|
|
3671
4649
|
continue
|
|
4650
|
+
import os
|
|
4651
|
+
bname = os.path.basename(fname)
|
|
4652
|
+
yn = _safe_input(colorize_prompt(f"Apply style '{bname}'? (y/n): ")).strip().lower()
|
|
4653
|
+
if yn != 'y':
|
|
4654
|
+
print("Style import canceled.")
|
|
4655
|
+
continue
|
|
3672
4656
|
push_state("style-import")
|
|
3673
4657
|
apply_style_config(fname)
|
|
3674
4658
|
except Exception as e:
|
|
@@ -3797,6 +4781,668 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
3797
4781
|
fig.canvas.draw()
|
|
3798
4782
|
except Exception as e:
|
|
3799
4783
|
print(f"Error saving figure: {e}")
|
|
4784
|
+
elif key == 'sm':
|
|
4785
|
+
# Smoothing and data reduction menu
|
|
4786
|
+
_ensure_original_data()
|
|
4787
|
+
while True:
|
|
4788
|
+
print("\n\033[1mSmoothing and Data Reduction\033[0m")
|
|
4789
|
+
print("Commands:")
|
|
4790
|
+
print(" r: reduce rows (delete/merge rows based on pattern)")
|
|
4791
|
+
print(" s: smooth data (various smoothing methods)")
|
|
4792
|
+
print(" reset: reset all curves to original data")
|
|
4793
|
+
print(" q: back to main menu")
|
|
4794
|
+
sub = _safe_input(colorize_prompt("sm> ")).strip().lower()
|
|
4795
|
+
if not sub:
|
|
4796
|
+
continue
|
|
4797
|
+
if sub == 'q':
|
|
4798
|
+
break
|
|
4799
|
+
if sub == 'reset':
|
|
4800
|
+
push_state("smooth-reset")
|
|
4801
|
+
success, reset_count, total_points = _reset_to_original()
|
|
4802
|
+
if success:
|
|
4803
|
+
print(f"Reset {reset_count} curve(s) to original data ({total_points} total points restored).")
|
|
4804
|
+
_apply_data_changes()
|
|
4805
|
+
else:
|
|
4806
|
+
print("No processed data to reset.")
|
|
4807
|
+
continue
|
|
4808
|
+
if sub == 'r':
|
|
4809
|
+
# Reduce rows submenu
|
|
4810
|
+
while True:
|
|
4811
|
+
print("\n\033[1mReduce Rows\033[0m")
|
|
4812
|
+
print("Methods:")
|
|
4813
|
+
print(" 1: Delete N rows, then skip M rows")
|
|
4814
|
+
print(" 2: Delete rows with missing values")
|
|
4815
|
+
print(" 3: Reduce N rows with merged values (average/sum/min/max)")
|
|
4816
|
+
print(" q: back to smooth menu")
|
|
4817
|
+
method = _safe_input(colorize_prompt("sm>r> ")).strip().lower()
|
|
4818
|
+
if not method or method == 'q':
|
|
4819
|
+
break
|
|
4820
|
+
if method == '1':
|
|
4821
|
+
# Delete N rows, then skip M rows
|
|
4822
|
+
try:
|
|
4823
|
+
# Check for last settings
|
|
4824
|
+
last_settings = _get_last_reduce_rows_settings('delete_skip')
|
|
4825
|
+
last_n = last_settings.get('n')
|
|
4826
|
+
last_m = last_settings.get('m')
|
|
4827
|
+
last_start_row = last_settings.get('start_row')
|
|
4828
|
+
|
|
4829
|
+
if last_n is not None and last_m is not None and last_start_row is not None:
|
|
4830
|
+
use_last = _safe_input(f"Use last settings? (N={last_n}, M={last_m}, start_row={last_start_row+1}, y/n or enter N): ").strip().lower()
|
|
4831
|
+
# Check if user entered a number directly (skip "use last settings")
|
|
4832
|
+
if use_last and use_last.replace('-', '').replace('.', '').isdigit():
|
|
4833
|
+
n = int(float(use_last))
|
|
4834
|
+
if n < 1:
|
|
4835
|
+
print("N must be >= 1.")
|
|
4836
|
+
continue
|
|
4837
|
+
m_in = _safe_input(f"Enter M (rows to skip, default {last_m}): ").strip()
|
|
4838
|
+
m = int(m_in) if m_in else last_m
|
|
4839
|
+
if m < 0:
|
|
4840
|
+
print("M must be >= 0.")
|
|
4841
|
+
continue
|
|
4842
|
+
start_in = _safe_input(f"Starting row (1-based, default {last_start_row+1}): ").strip()
|
|
4843
|
+
start_row = int(start_in) - 1 if start_in else last_start_row
|
|
4844
|
+
elif use_last != 'n':
|
|
4845
|
+
n = last_n
|
|
4846
|
+
m = last_m
|
|
4847
|
+
start_row = last_start_row # Already 0-based in config
|
|
4848
|
+
else:
|
|
4849
|
+
n_in = _safe_input(f"Enter N (rows to delete, default {last_n}): ").strip()
|
|
4850
|
+
n = int(n_in) if n_in else last_n
|
|
4851
|
+
if n < 1:
|
|
4852
|
+
print("N must be >= 1.")
|
|
4853
|
+
continue
|
|
4854
|
+
m_in = _safe_input(f"Enter M (rows to skip, default {last_m}): ").strip()
|
|
4855
|
+
m = int(m_in) if m_in else last_m
|
|
4856
|
+
if m < 0:
|
|
4857
|
+
print("M must be >= 0.")
|
|
4858
|
+
continue
|
|
4859
|
+
start_in = _safe_input(f"Starting row (1-based, default {last_start_row+1}): ").strip()
|
|
4860
|
+
start_row = int(start_in) - 1 if start_in else last_start_row
|
|
4861
|
+
else:
|
|
4862
|
+
n_in = _safe_input("Enter N (rows to delete, default 1): ").strip()
|
|
4863
|
+
n = int(n_in) if n_in else 1
|
|
4864
|
+
if n < 1:
|
|
4865
|
+
print("N must be >= 1.")
|
|
4866
|
+
continue
|
|
4867
|
+
m_in = _safe_input("Enter M (rows to skip, default 0): ").strip()
|
|
4868
|
+
m = int(m_in) if m_in else 0
|
|
4869
|
+
if m < 0:
|
|
4870
|
+
print("M must be >= 0.")
|
|
4871
|
+
continue
|
|
4872
|
+
start_in = _safe_input("Starting row (1-based, default 1): ").strip()
|
|
4873
|
+
start_row = int(start_in) - 1 if start_in else 0
|
|
4874
|
+
|
|
4875
|
+
if start_row < 0:
|
|
4876
|
+
start_row = 0
|
|
4877
|
+
push_state("reduce-rows-delete-skip")
|
|
4878
|
+
_ensure_original_data()
|
|
4879
|
+
processed = 0
|
|
4880
|
+
total_before = 0
|
|
4881
|
+
total_after = 0
|
|
4882
|
+
for i in range(len(x_data_list)):
|
|
4883
|
+
try:
|
|
4884
|
+
# Use current data (may already be processed), not original
|
|
4885
|
+
orig_x = x_data_list[i].copy()
|
|
4886
|
+
orig_y = y_data_list[i].copy()
|
|
4887
|
+
# Remove offset for processing
|
|
4888
|
+
if i < len(offsets_list):
|
|
4889
|
+
orig_y = orig_y - offsets_list[i]
|
|
4890
|
+
if start_row >= len(orig_x):
|
|
4891
|
+
continue
|
|
4892
|
+
before = len(orig_x)
|
|
4893
|
+
# Create mask: delete n rows, then skip m rows, repeat
|
|
4894
|
+
mask = np.ones(len(orig_x), dtype=bool)
|
|
4895
|
+
idx = start_row
|
|
4896
|
+
while idx < len(orig_x):
|
|
4897
|
+
# Delete n rows
|
|
4898
|
+
end_del = min(idx + n, len(orig_x))
|
|
4899
|
+
mask[idx:end_del] = False
|
|
4900
|
+
idx = end_del
|
|
4901
|
+
# Skip m rows
|
|
4902
|
+
idx = min(idx + m, len(orig_x))
|
|
4903
|
+
new_x = orig_x[mask]
|
|
4904
|
+
new_y = orig_y[mask]
|
|
4905
|
+
after = len(new_x)
|
|
4906
|
+
if len(new_x) > 0:
|
|
4907
|
+
# Restore offset
|
|
4908
|
+
if i < len(offsets_list):
|
|
4909
|
+
new_y = new_y + offsets_list[i]
|
|
4910
|
+
x_data_list[i] = new_x
|
|
4911
|
+
y_data_list[i] = new_y
|
|
4912
|
+
processed += 1
|
|
4913
|
+
total_before += before
|
|
4914
|
+
total_after += after
|
|
4915
|
+
except Exception as e:
|
|
4916
|
+
print(f"Error processing curve {i+1}: {e}")
|
|
4917
|
+
if processed > 0:
|
|
4918
|
+
removed = total_before - total_after
|
|
4919
|
+
pct = 100 * removed / total_before if total_before else 0
|
|
4920
|
+
print(f"Processed {processed} curve(s); removed {removed} of {total_before} points ({pct:.1f}%).")
|
|
4921
|
+
_update_full_processed_data() # Store full processed data for X-range filtering
|
|
4922
|
+
_apply_data_changes()
|
|
4923
|
+
# Save settings for next time
|
|
4924
|
+
_save_last_reduce_rows_settings('delete_skip', {
|
|
4925
|
+
'n': n,
|
|
4926
|
+
'm': m,
|
|
4927
|
+
'start_row': start_row # Save as 0-based
|
|
4928
|
+
})
|
|
4929
|
+
else:
|
|
4930
|
+
print("No curves were processed.")
|
|
4931
|
+
except ValueError:
|
|
4932
|
+
print("Invalid number.")
|
|
4933
|
+
continue
|
|
4934
|
+
if method == '2':
|
|
4935
|
+
# Delete rows with missing values
|
|
4936
|
+
try:
|
|
4937
|
+
# Check for last settings
|
|
4938
|
+
last_settings = _get_last_reduce_rows_settings('delete_missing')
|
|
4939
|
+
last_delete_entire_row = last_settings.get('delete_entire_row')
|
|
4940
|
+
|
|
4941
|
+
if last_delete_entire_row is not None:
|
|
4942
|
+
default_str = "y" if last_delete_entire_row else "n"
|
|
4943
|
+
use_last = _safe_input(f"Use last settings? (delete_entire_row={'y' if last_delete_entire_row else 'n'}, y/n or enter y/n): ").strip().lower()
|
|
4944
|
+
# Check if user entered y/n directly (skip "use last settings")
|
|
4945
|
+
if use_last in ('y', 'n', 'yes', 'no'):
|
|
4946
|
+
delete_entire_row = use_last in ('y', 'yes')
|
|
4947
|
+
elif use_last != 'n':
|
|
4948
|
+
delete_entire_row = last_delete_entire_row
|
|
4949
|
+
else:
|
|
4950
|
+
delete_entire_row_in = _safe_input(f"Delete entire row? (y/n, default {default_str}): ").strip().lower()
|
|
4951
|
+
delete_entire_row = delete_entire_row_in != 'n'
|
|
4952
|
+
else:
|
|
4953
|
+
delete_entire_row_in = _safe_input("Delete entire row? (y/n, default y): ").strip().lower()
|
|
4954
|
+
delete_entire_row = delete_entire_row_in != 'n'
|
|
4955
|
+
push_state("reduce-rows-delete-missing")
|
|
4956
|
+
_ensure_original_data()
|
|
4957
|
+
processed = 0
|
|
4958
|
+
total_before = 0
|
|
4959
|
+
total_after = 0
|
|
4960
|
+
for i in range(len(x_data_list)):
|
|
4961
|
+
try:
|
|
4962
|
+
# Use current data (may already be processed), not original
|
|
4963
|
+
orig_x = x_data_list[i].copy()
|
|
4964
|
+
orig_y = y_data_list[i].copy()
|
|
4965
|
+
# Remove offset for processing
|
|
4966
|
+
if i < len(offsets_list):
|
|
4967
|
+
orig_y = orig_y - offsets_list[i]
|
|
4968
|
+
before = len(orig_x)
|
|
4969
|
+
# Check for missing values (NaN or inf)
|
|
4970
|
+
if delete_entire_row:
|
|
4971
|
+
mask = np.isfinite(orig_x) & np.isfinite(orig_y)
|
|
4972
|
+
else:
|
|
4973
|
+
# Only delete missing in current column
|
|
4974
|
+
mask = np.isfinite(orig_y)
|
|
4975
|
+
new_x = orig_x[mask]
|
|
4976
|
+
new_y = orig_y[mask]
|
|
4977
|
+
after = len(new_x)
|
|
4978
|
+
if len(new_x) > 0:
|
|
4979
|
+
# Restore offset
|
|
4980
|
+
if i < len(offsets_list):
|
|
4981
|
+
new_y = new_y + offsets_list[i]
|
|
4982
|
+
x_data_list[i] = new_x
|
|
4983
|
+
y_data_list[i] = new_y
|
|
4984
|
+
processed += 1
|
|
4985
|
+
total_before += before
|
|
4986
|
+
total_after += after
|
|
4987
|
+
except Exception as e:
|
|
4988
|
+
print(f"Error processing curve {i+1}: {e}")
|
|
4989
|
+
if processed > 0:
|
|
4990
|
+
removed = total_before - total_after
|
|
4991
|
+
pct = 100 * removed / total_before if total_before else 0
|
|
4992
|
+
print(f"Processed {processed} curve(s); removed {removed} of {total_before} points ({pct:.1f}%).")
|
|
4993
|
+
_update_full_processed_data() # Store full processed data for X-range filtering
|
|
4994
|
+
_apply_data_changes()
|
|
4995
|
+
# Save settings for next time
|
|
4996
|
+
_save_last_reduce_rows_settings('delete_missing', {
|
|
4997
|
+
'delete_entire_row': delete_entire_row
|
|
4998
|
+
})
|
|
4999
|
+
else:
|
|
5000
|
+
print("No curves were processed.")
|
|
5001
|
+
except Exception:
|
|
5002
|
+
print("Error processing data.")
|
|
5003
|
+
continue
|
|
5004
|
+
if method == '3':
|
|
5005
|
+
# Reduce N rows with merged values
|
|
5006
|
+
try:
|
|
5007
|
+
# Check for last settings
|
|
5008
|
+
last_settings = _get_last_reduce_rows_settings('merge')
|
|
5009
|
+
last_n = last_settings.get('n')
|
|
5010
|
+
last_merge_by = last_settings.get('merge_by')
|
|
5011
|
+
last_start_row = last_settings.get('start_row')
|
|
5012
|
+
|
|
5013
|
+
if last_n is not None and last_merge_by is not None and last_start_row is not None:
|
|
5014
|
+
merge_names = {
|
|
5015
|
+
'1': 'First point',
|
|
5016
|
+
'2': 'Last point',
|
|
5017
|
+
'3': 'Average',
|
|
5018
|
+
'4': 'Min',
|
|
5019
|
+
'5': 'Max',
|
|
5020
|
+
'6': 'Sum'
|
|
5021
|
+
}
|
|
5022
|
+
merge_name = merge_names.get(last_merge_by, 'Average')
|
|
5023
|
+
use_last = _safe_input(f"Use last settings? (N={last_n}, merge_by={merge_name}, start_row={last_start_row+1}, y/n or enter N): ").strip().lower()
|
|
5024
|
+
# Check if user entered a number directly (skip "use last settings")
|
|
5025
|
+
if use_last and use_last.replace('-', '').replace('.', '').isdigit():
|
|
5026
|
+
n = int(float(use_last))
|
|
5027
|
+
if n < 2:
|
|
5028
|
+
print("N must be >= 2.")
|
|
5029
|
+
continue
|
|
5030
|
+
print("Merge by:")
|
|
5031
|
+
print(" 1: First point")
|
|
5032
|
+
print(" 2: Last point")
|
|
5033
|
+
print(" 3: Average")
|
|
5034
|
+
print(" 4: Min")
|
|
5035
|
+
print(" 5: Max")
|
|
5036
|
+
print(" 6: Sum")
|
|
5037
|
+
merge_by_in = _safe_input(f"Choose (1-6, default {last_merge_by}): ").strip()
|
|
5038
|
+
merge_by = merge_by_in if merge_by_in else last_merge_by
|
|
5039
|
+
start_in = _safe_input(f"Starting row (1-based, default {last_start_row+1}): ").strip()
|
|
5040
|
+
start_row = int(start_in) - 1 if start_in else last_start_row
|
|
5041
|
+
elif use_last != 'n':
|
|
5042
|
+
n = last_n
|
|
5043
|
+
merge_by = last_merge_by
|
|
5044
|
+
start_row = last_start_row # Already 0-based in config
|
|
5045
|
+
else:
|
|
5046
|
+
n_in = _safe_input(f"Enter N (rows to merge, default {last_n}): ").strip()
|
|
5047
|
+
n = int(n_in) if n_in else last_n
|
|
5048
|
+
if n < 2:
|
|
5049
|
+
print("N must be >= 2.")
|
|
5050
|
+
continue
|
|
5051
|
+
print("Merge by:")
|
|
5052
|
+
print(" 1: First point")
|
|
5053
|
+
print(" 2: Last point")
|
|
5054
|
+
print(" 3: Average")
|
|
5055
|
+
print(" 4: Min")
|
|
5056
|
+
print(" 5: Max")
|
|
5057
|
+
print(" 6: Sum")
|
|
5058
|
+
merge_by_in = _safe_input(f"Choose (1-6, default {last_merge_by}): ").strip()
|
|
5059
|
+
merge_by = merge_by_in if merge_by_in else last_merge_by
|
|
5060
|
+
start_in = _safe_input(f"Starting row (1-based, default {last_start_row+1}): ").strip()
|
|
5061
|
+
start_row = int(start_in) - 1 if start_in else last_start_row
|
|
5062
|
+
else:
|
|
5063
|
+
n_in = _safe_input("Enter N (rows to merge, default 2): ").strip()
|
|
5064
|
+
n = int(n_in) if n_in else 2
|
|
5065
|
+
if n < 2:
|
|
5066
|
+
print("N must be >= 2.")
|
|
5067
|
+
continue
|
|
5068
|
+
print("Merge by:")
|
|
5069
|
+
print(" 1: First point")
|
|
5070
|
+
print(" 2: Last point")
|
|
5071
|
+
print(" 3: Average")
|
|
5072
|
+
print(" 4: Min")
|
|
5073
|
+
print(" 5: Max")
|
|
5074
|
+
print(" 6: Sum")
|
|
5075
|
+
merge_by_in = _safe_input("Choose (1-6, default 3): ").strip()
|
|
5076
|
+
merge_by = merge_by_in if merge_by_in else '3'
|
|
5077
|
+
start_in = _safe_input("Starting row (1-based, default 1): ").strip()
|
|
5078
|
+
start_row = int(start_in) - 1 if start_in else 0
|
|
5079
|
+
|
|
5080
|
+
if start_row < 0:
|
|
5081
|
+
start_row = 0
|
|
5082
|
+
|
|
5083
|
+
merge_funcs = {
|
|
5084
|
+
'1': lambda arr: arr[0] if len(arr) > 0 else np.nan,
|
|
5085
|
+
'2': lambda arr: arr[-1] if len(arr) > 0 else np.nan,
|
|
5086
|
+
'3': np.nanmean,
|
|
5087
|
+
'4': np.nanmin,
|
|
5088
|
+
'5': np.nanmax,
|
|
5089
|
+
'6': np.nansum,
|
|
5090
|
+
}
|
|
5091
|
+
merge_func = merge_funcs.get(merge_by, np.nanmean)
|
|
5092
|
+
push_state("reduce-rows-merge")
|
|
5093
|
+
_ensure_original_data()
|
|
5094
|
+
processed = 0
|
|
5095
|
+
total_before = 0
|
|
5096
|
+
total_after = 0
|
|
5097
|
+
for i in range(len(x_data_list)):
|
|
5098
|
+
try:
|
|
5099
|
+
# Use current data (may already be processed), not original
|
|
5100
|
+
orig_x = x_data_list[i].copy()
|
|
5101
|
+
orig_y = y_data_list[i].copy()
|
|
5102
|
+
# Remove offset for processing
|
|
5103
|
+
if i < len(offsets_list):
|
|
5104
|
+
orig_y = orig_y - offsets_list[i]
|
|
5105
|
+
if start_row >= len(orig_x):
|
|
5106
|
+
continue
|
|
5107
|
+
before = len(orig_x)
|
|
5108
|
+
# Group into chunks of N
|
|
5109
|
+
new_x_list = []
|
|
5110
|
+
new_y_list = []
|
|
5111
|
+
idx = 0
|
|
5112
|
+
while idx < start_row:
|
|
5113
|
+
new_x_list.append(orig_x[idx])
|
|
5114
|
+
new_y_list.append(orig_y[idx])
|
|
5115
|
+
idx += 1
|
|
5116
|
+
while idx < len(orig_x):
|
|
5117
|
+
end_idx = min(idx + n, len(orig_x))
|
|
5118
|
+
chunk_x = orig_x[idx:end_idx]
|
|
5119
|
+
chunk_y = orig_y[idx:end_idx]
|
|
5120
|
+
# Merge: use first x, merge y based on method
|
|
5121
|
+
new_x = chunk_x[0] if len(chunk_x) > 0 else np.nan
|
|
5122
|
+
new_y = merge_func(chunk_y) if len(chunk_y) > 0 else np.nan
|
|
5123
|
+
if np.isfinite(new_x) and np.isfinite(new_y):
|
|
5124
|
+
new_x_list.append(new_x)
|
|
5125
|
+
new_y_list.append(new_y)
|
|
5126
|
+
idx = end_idx
|
|
5127
|
+
if len(new_x_list) > 0:
|
|
5128
|
+
new_x = np.array(new_x_list)
|
|
5129
|
+
new_y = np.array(new_y_list)
|
|
5130
|
+
after = len(new_x)
|
|
5131
|
+
# Restore offset
|
|
5132
|
+
if i < len(offsets_list):
|
|
5133
|
+
new_y = new_y + offsets_list[i]
|
|
5134
|
+
x_data_list[i] = new_x
|
|
5135
|
+
y_data_list[i] = new_y
|
|
5136
|
+
processed += 1
|
|
5137
|
+
total_before += before
|
|
5138
|
+
total_after += after
|
|
5139
|
+
except Exception as e:
|
|
5140
|
+
print(f"Error processing curve {i+1}: {e}")
|
|
5141
|
+
if processed > 0:
|
|
5142
|
+
removed = total_before - total_after
|
|
5143
|
+
pct = 100 * removed / total_before if total_before else 0
|
|
5144
|
+
print(f"Processed {processed} curve(s); reduced {total_before} to {total_after} points (removed {removed}, {pct:.1f}%).")
|
|
5145
|
+
_update_full_processed_data() # Store full processed data for X-range filtering
|
|
5146
|
+
_apply_data_changes()
|
|
5147
|
+
# Save settings for next time
|
|
5148
|
+
_save_last_reduce_rows_settings('merge', {
|
|
5149
|
+
'n': n,
|
|
5150
|
+
'merge_by': merge_by,
|
|
5151
|
+
'start_row': start_row # Save as 0-based
|
|
5152
|
+
})
|
|
5153
|
+
else:
|
|
5154
|
+
print("No curves were processed.")
|
|
5155
|
+
except (ValueError, KeyError):
|
|
5156
|
+
print("Invalid input.")
|
|
5157
|
+
continue
|
|
5158
|
+
if sub == 's':
|
|
5159
|
+
# Smooth submenu
|
|
5160
|
+
while True:
|
|
5161
|
+
print("\n\033[1mSmooth Data\033[0m")
|
|
5162
|
+
print("Methods:")
|
|
5163
|
+
print(" 1: Adjacent-Averaging (moving average)")
|
|
5164
|
+
print(" 2: Savitzky-Golay (polynomial smoothing)")
|
|
5165
|
+
print(" 3: FFT Filter (low-pass frequency filter)")
|
|
5166
|
+
print(" q: back to smooth menu")
|
|
5167
|
+
method = _safe_input(colorize_prompt("sm>s> ")).strip().lower()
|
|
5168
|
+
if not method or method == 'q':
|
|
5169
|
+
break
|
|
5170
|
+
if method == '1':
|
|
5171
|
+
# Adjacent-Averaging
|
|
5172
|
+
try:
|
|
5173
|
+
# Check for last settings (from config file for persistence)
|
|
5174
|
+
config_settings = _get_last_smooth_settings_from_config()
|
|
5175
|
+
session_settings = getattr(fig, '_last_smooth_settings', {})
|
|
5176
|
+
# Prefer config settings (persistent) over session settings
|
|
5177
|
+
last_settings = config_settings if config_settings.get('method') == 'adjacent_average' else session_settings
|
|
5178
|
+
last_method = last_settings.get('method')
|
|
5179
|
+
last_points = last_settings.get('points')
|
|
5180
|
+
|
|
5181
|
+
if last_method == 'adjacent_average' and last_points is not None:
|
|
5182
|
+
use_last = _safe_input(f"Use last settings? (points={last_points}, y/n or enter points): ").strip().lower()
|
|
5183
|
+
# Check if user entered a number directly (skip "use last settings")
|
|
5184
|
+
if use_last and use_last.replace('-', '').replace('.', '').isdigit():
|
|
5185
|
+
points = int(float(use_last))
|
|
5186
|
+
elif use_last != 'n':
|
|
5187
|
+
points = last_points
|
|
5188
|
+
else:
|
|
5189
|
+
points_in = _safe_input(f"Number of points (default {last_points}): ").strip()
|
|
5190
|
+
points = int(points_in) if points_in else last_points
|
|
5191
|
+
else:
|
|
5192
|
+
points_in = _safe_input("Number of points (default 5): ").strip()
|
|
5193
|
+
points = int(points_in) if points_in else 5
|
|
5194
|
+
|
|
5195
|
+
if points < 2:
|
|
5196
|
+
print("Points must be >= 2.")
|
|
5197
|
+
continue
|
|
5198
|
+
push_state("smooth-adjacent-average")
|
|
5199
|
+
_ensure_original_data()
|
|
5200
|
+
processed = 0
|
|
5201
|
+
total_points = 0
|
|
5202
|
+
for i in range(len(x_data_list)):
|
|
5203
|
+
try:
|
|
5204
|
+
# Use current data (may already be processed), not original
|
|
5205
|
+
orig_x = x_data_list[i].copy()
|
|
5206
|
+
orig_y = y_data_list[i].copy()
|
|
5207
|
+
# Remove offset for processing
|
|
5208
|
+
if i < len(offsets_list):
|
|
5209
|
+
orig_y = orig_y - offsets_list[i]
|
|
5210
|
+
n_points = len(orig_y)
|
|
5211
|
+
# Apply smoothing
|
|
5212
|
+
smoothed_y = _adjacent_average_smooth(orig_y, points)
|
|
5213
|
+
if len(smoothed_y) > 0:
|
|
5214
|
+
# Restore offset
|
|
5215
|
+
if i < len(offsets_list):
|
|
5216
|
+
smoothed_y = smoothed_y + offsets_list[i]
|
|
5217
|
+
# Keep original x, update y
|
|
5218
|
+
x_data_list[i] = orig_x.copy()
|
|
5219
|
+
y_data_list[i] = smoothed_y
|
|
5220
|
+
processed += 1
|
|
5221
|
+
total_points += n_points
|
|
5222
|
+
except Exception as e:
|
|
5223
|
+
print(f"Error processing curve {i+1}: {e}")
|
|
5224
|
+
if processed > 0:
|
|
5225
|
+
print(f"Smoothed {processed} curve(s) with {total_points} total points using Adjacent-Averaging (window={points}).")
|
|
5226
|
+
_update_full_processed_data() # Store full processed data for X-range filtering
|
|
5227
|
+
_apply_data_changes()
|
|
5228
|
+
# Store settings (both current and last)
|
|
5229
|
+
if not hasattr(fig, '_smooth_settings'):
|
|
5230
|
+
fig._smooth_settings = {}
|
|
5231
|
+
fig._smooth_settings['method'] = 'adjacent_average'
|
|
5232
|
+
fig._smooth_settings['points'] = points
|
|
5233
|
+
# Store as last settings for next time (both in-memory and config file)
|
|
5234
|
+
if not hasattr(fig, '_last_smooth_settings'):
|
|
5235
|
+
fig._last_smooth_settings = {}
|
|
5236
|
+
fig._last_smooth_settings['method'] = 'adjacent_average'
|
|
5237
|
+
fig._last_smooth_settings['points'] = points
|
|
5238
|
+
# Save to config file for persistence across sessions
|
|
5239
|
+
_save_last_smooth_settings_to_config({
|
|
5240
|
+
'method': 'adjacent_average',
|
|
5241
|
+
'points': points
|
|
5242
|
+
})
|
|
5243
|
+
else:
|
|
5244
|
+
print("No curves were smoothed.")
|
|
5245
|
+
except ValueError:
|
|
5246
|
+
print("Invalid number.")
|
|
5247
|
+
continue
|
|
5248
|
+
if method == '2':
|
|
5249
|
+
# Savitzky-Golay
|
|
5250
|
+
try:
|
|
5251
|
+
# Check for last settings (from config file for persistence)
|
|
5252
|
+
config_settings = _get_last_smooth_settings_from_config()
|
|
5253
|
+
session_settings = getattr(fig, '_last_smooth_settings', {})
|
|
5254
|
+
# Prefer config settings (persistent) over session settings
|
|
5255
|
+
last_settings = config_settings if config_settings.get('method') == 'savgol' else session_settings
|
|
5256
|
+
last_method = last_settings.get('method')
|
|
5257
|
+
last_window = last_settings.get('window')
|
|
5258
|
+
last_poly = last_settings.get('poly')
|
|
5259
|
+
|
|
5260
|
+
if last_method == 'savgol' and last_window is not None and last_poly is not None:
|
|
5261
|
+
use_last = _safe_input(f"Use last settings? (window={last_window}, poly={last_poly}, y/n or enter window): ").strip().lower()
|
|
5262
|
+
# Check if user entered a number directly (skip "use last settings")
|
|
5263
|
+
if use_last and use_last.replace('-', '').replace('.', '').isdigit():
|
|
5264
|
+
window = int(float(use_last))
|
|
5265
|
+
if window < 3:
|
|
5266
|
+
window = 3
|
|
5267
|
+
if window % 2 == 0:
|
|
5268
|
+
window += 1
|
|
5269
|
+
poly_in = _safe_input(f"Polynomial order (default {last_poly}): ").strip()
|
|
5270
|
+
poly = int(poly_in) if poly_in else last_poly
|
|
5271
|
+
elif use_last != 'n':
|
|
5272
|
+
window = last_window
|
|
5273
|
+
poly = last_poly
|
|
5274
|
+
else:
|
|
5275
|
+
window_in = _safe_input(f"Window size (odd >= 3, default {last_window}): ").strip()
|
|
5276
|
+
window = int(window_in) if window_in else last_window
|
|
5277
|
+
if window < 3:
|
|
5278
|
+
window = 3
|
|
5279
|
+
if window % 2 == 0:
|
|
5280
|
+
window += 1
|
|
5281
|
+
poly_in = _safe_input(f"Polynomial order (default {last_poly}): ").strip()
|
|
5282
|
+
poly = int(poly_in) if poly_in else last_poly
|
|
5283
|
+
else:
|
|
5284
|
+
window_in = _safe_input("Window size (odd >= 3, default 9): ").strip()
|
|
5285
|
+
window = int(window_in) if window_in else 9
|
|
5286
|
+
if window < 3:
|
|
5287
|
+
window = 3
|
|
5288
|
+
if window % 2 == 0:
|
|
5289
|
+
window += 1
|
|
5290
|
+
poly_in = _safe_input("Polynomial order (default 3): ").strip()
|
|
5291
|
+
poly = int(poly_in) if poly_in else 3
|
|
5292
|
+
|
|
5293
|
+
if poly < 1:
|
|
5294
|
+
poly = 1
|
|
5295
|
+
if poly >= window:
|
|
5296
|
+
poly = window - 1
|
|
5297
|
+
push_state("smooth-savgol")
|
|
5298
|
+
_ensure_original_data()
|
|
5299
|
+
processed = 0
|
|
5300
|
+
total_points = 0
|
|
5301
|
+
for i in range(len(x_data_list)):
|
|
5302
|
+
try:
|
|
5303
|
+
# Use current data (may already be processed), not original
|
|
5304
|
+
orig_x = x_data_list[i].copy()
|
|
5305
|
+
orig_y = y_data_list[i].copy()
|
|
5306
|
+
# Remove offset for processing
|
|
5307
|
+
if i < len(offsets_list):
|
|
5308
|
+
orig_y = orig_y - offsets_list[i]
|
|
5309
|
+
n_points = len(orig_y)
|
|
5310
|
+
# Apply smoothing
|
|
5311
|
+
smoothed_y = _savgol_smooth(orig_y, window, poly)
|
|
5312
|
+
if len(smoothed_y) > 0:
|
|
5313
|
+
# Restore offset
|
|
5314
|
+
if i < len(offsets_list):
|
|
5315
|
+
smoothed_y = smoothed_y + offsets_list[i]
|
|
5316
|
+
# Keep original x, update y
|
|
5317
|
+
x_data_list[i] = orig_x.copy()
|
|
5318
|
+
y_data_list[i] = smoothed_y
|
|
5319
|
+
processed += 1
|
|
5320
|
+
total_points += n_points
|
|
5321
|
+
except Exception as e:
|
|
5322
|
+
print(f"Error processing curve {i+1}: {e}")
|
|
5323
|
+
if processed > 0:
|
|
5324
|
+
print(f"Smoothed {processed} curve(s) with {total_points} total points using Savitzky-Golay (window={window}, poly={poly}).")
|
|
5325
|
+
_update_full_processed_data() # Store full processed data for X-range filtering
|
|
5326
|
+
_apply_data_changes()
|
|
5327
|
+
# Store settings (both current and last)
|
|
5328
|
+
if not hasattr(fig, '_smooth_settings'):
|
|
5329
|
+
fig._smooth_settings = {}
|
|
5330
|
+
fig._smooth_settings['method'] = 'savgol'
|
|
5331
|
+
fig._smooth_settings['window'] = window
|
|
5332
|
+
fig._smooth_settings['poly'] = poly
|
|
5333
|
+
# Store as last settings for next time (both in-memory and config file)
|
|
5334
|
+
if not hasattr(fig, '_last_smooth_settings'):
|
|
5335
|
+
fig._last_smooth_settings = {}
|
|
5336
|
+
fig._last_smooth_settings['method'] = 'savgol'
|
|
5337
|
+
fig._last_smooth_settings['window'] = window
|
|
5338
|
+
fig._last_smooth_settings['poly'] = poly
|
|
5339
|
+
# Save to config file for persistence across sessions
|
|
5340
|
+
_save_last_smooth_settings_to_config({
|
|
5341
|
+
'method': 'savgol',
|
|
5342
|
+
'window': window,
|
|
5343
|
+
'poly': poly
|
|
5344
|
+
})
|
|
5345
|
+
else:
|
|
5346
|
+
print("No curves were smoothed.")
|
|
5347
|
+
except ValueError:
|
|
5348
|
+
print("Invalid number.")
|
|
5349
|
+
continue
|
|
5350
|
+
if method == '3':
|
|
5351
|
+
# FFT Filter
|
|
5352
|
+
try:
|
|
5353
|
+
# Check for last settings (from config file for persistence)
|
|
5354
|
+
config_settings = _get_last_smooth_settings_from_config()
|
|
5355
|
+
session_settings = getattr(fig, '_last_smooth_settings', {})
|
|
5356
|
+
# Prefer config settings (persistent) over session settings
|
|
5357
|
+
last_settings = config_settings if config_settings.get('method') == 'fft' else session_settings
|
|
5358
|
+
last_method = last_settings.get('method')
|
|
5359
|
+
last_points = last_settings.get('points')
|
|
5360
|
+
last_cutoff = last_settings.get('cutoff')
|
|
5361
|
+
|
|
5362
|
+
if last_method == 'fft' and last_points is not None and last_cutoff is not None:
|
|
5363
|
+
use_last = _safe_input(f"Use last settings? (points={last_points}, cutoff={last_cutoff:.3f}, y/n or enter points): ").strip().lower()
|
|
5364
|
+
# Check if user entered a number directly (skip "use last settings")
|
|
5365
|
+
if use_last and use_last.replace('-', '').replace('.', '').isdigit():
|
|
5366
|
+
points = int(float(use_last))
|
|
5367
|
+
if points < 2:
|
|
5368
|
+
points = 2
|
|
5369
|
+
cutoff_in = _safe_input(f"Cutoff frequency (0-1, default {last_cutoff:.3f}): ").strip()
|
|
5370
|
+
cutoff = float(cutoff_in) if cutoff_in else last_cutoff
|
|
5371
|
+
elif use_last != 'n':
|
|
5372
|
+
points = last_points
|
|
5373
|
+
cutoff = last_cutoff
|
|
5374
|
+
else:
|
|
5375
|
+
points_in = _safe_input(f"Points for FFT (default {last_points}): ").strip()
|
|
5376
|
+
points = int(points_in) if points_in else last_points
|
|
5377
|
+
if points < 2:
|
|
5378
|
+
points = 2
|
|
5379
|
+
cutoff_in = _safe_input(f"Cutoff frequency (0-1, default {last_cutoff:.3f}): ").strip()
|
|
5380
|
+
cutoff = float(cutoff_in) if cutoff_in else last_cutoff
|
|
5381
|
+
else:
|
|
5382
|
+
points_in = _safe_input("Points for FFT (default 5): ").strip()
|
|
5383
|
+
points = int(points_in) if points_in else 5
|
|
5384
|
+
if points < 2:
|
|
5385
|
+
points = 2
|
|
5386
|
+
cutoff_in = _safe_input("Cutoff frequency (0-1, default 0.1): ").strip()
|
|
5387
|
+
cutoff = float(cutoff_in) if cutoff_in else 0.1
|
|
5388
|
+
|
|
5389
|
+
if cutoff < 0:
|
|
5390
|
+
cutoff = 0
|
|
5391
|
+
if cutoff > 1:
|
|
5392
|
+
cutoff = 1
|
|
5393
|
+
push_state("smooth-fft")
|
|
5394
|
+
_ensure_original_data()
|
|
5395
|
+
processed = 0
|
|
5396
|
+
total_points = 0
|
|
5397
|
+
for i in range(len(x_data_list)):
|
|
5398
|
+
try:
|
|
5399
|
+
# Use current data (may already be processed), not original
|
|
5400
|
+
orig_x = x_data_list[i].copy()
|
|
5401
|
+
orig_y = y_data_list[i].copy()
|
|
5402
|
+
# Remove offset for processing
|
|
5403
|
+
if i < len(offsets_list):
|
|
5404
|
+
orig_y = orig_y - offsets_list[i]
|
|
5405
|
+
n_points = len(orig_y)
|
|
5406
|
+
# Apply smoothing
|
|
5407
|
+
smoothed_y = _fft_smooth(orig_y, points, cutoff)
|
|
5408
|
+
if len(smoothed_y) > 0:
|
|
5409
|
+
# Restore offset
|
|
5410
|
+
if i < len(offsets_list):
|
|
5411
|
+
smoothed_y = smoothed_y + offsets_list[i]
|
|
5412
|
+
# Keep original x, update y
|
|
5413
|
+
x_data_list[i] = orig_x.copy()
|
|
5414
|
+
y_data_list[i] = smoothed_y
|
|
5415
|
+
processed += 1
|
|
5416
|
+
total_points += n_points
|
|
5417
|
+
except Exception as e:
|
|
5418
|
+
print(f"Error processing curve {i+1}: {e}")
|
|
5419
|
+
if processed > 0:
|
|
5420
|
+
print(f"Smoothed {processed} curve(s) with {total_points} total points using FFT Filter (cutoff={cutoff:.3f}).")
|
|
5421
|
+
_update_full_processed_data() # Store full processed data for X-range filtering
|
|
5422
|
+
_apply_data_changes()
|
|
5423
|
+
# Store settings (both current and last)
|
|
5424
|
+
if not hasattr(fig, '_smooth_settings'):
|
|
5425
|
+
fig._smooth_settings = {}
|
|
5426
|
+
fig._smooth_settings['method'] = 'fft'
|
|
5427
|
+
fig._smooth_settings['points'] = points
|
|
5428
|
+
fig._smooth_settings['cutoff'] = cutoff
|
|
5429
|
+
# Store as last settings for next time (both in-memory and config file)
|
|
5430
|
+
if not hasattr(fig, '_last_smooth_settings'):
|
|
5431
|
+
fig._last_smooth_settings = {}
|
|
5432
|
+
fig._last_smooth_settings['method'] = 'fft'
|
|
5433
|
+
fig._last_smooth_settings['points'] = points
|
|
5434
|
+
fig._last_smooth_settings['cutoff'] = cutoff
|
|
5435
|
+
# Save to config file for persistence across sessions
|
|
5436
|
+
_save_last_smooth_settings_to_config({
|
|
5437
|
+
'method': 'fft',
|
|
5438
|
+
'points': points,
|
|
5439
|
+
'cutoff': cutoff
|
|
5440
|
+
})
|
|
5441
|
+
else:
|
|
5442
|
+
print("No curves were smoothed.")
|
|
5443
|
+
except ValueError:
|
|
5444
|
+
print("Invalid number.")
|
|
5445
|
+
continue
|
|
3800
5446
|
elif key == 'v':
|
|
3801
5447
|
while True:
|
|
3802
5448
|
try:
|
|
@@ -3836,6 +5482,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
3836
5482
|
|
|
3837
5483
|
print("\n--- Peak Report ---")
|
|
3838
5484
|
print(f"X range used: {x_min} .. {x_max} (relative height threshold={min_frac})")
|
|
5485
|
+
all_peak_results = [] # list of (curve_index, label, [(x, y), ...])
|
|
3839
5486
|
for i, (x_arr, y_off) in enumerate(zip(x_data_list, y_data_list)):
|
|
3840
5487
|
# Recover original curve (remove vertical offset)
|
|
3841
5488
|
if i < len(offsets_list):
|
|
@@ -3889,9 +5536,50 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
|
|
|
3889
5536
|
last_idx = pi
|
|
3890
5537
|
|
|
3891
5538
|
print(" Peaks (x, y):")
|
|
5539
|
+
peak_xy_list = []
|
|
3892
5540
|
for pi in peaks:
|
|
5541
|
+
px, py = float(x_sel[pi]), float(y_sel[pi])
|
|
5542
|
+
peak_xy_list.append((px, py))
|
|
3893
5543
|
print(f" x={x_sel[pi]:.6g}, y={y_sel[pi]:.6g}")
|
|
5544
|
+
if peak_xy_list:
|
|
5545
|
+
all_peak_results.append((i + 1, label, peak_xy_list))
|
|
3894
5546
|
print("\n--- End Peak Report ---\n")
|
|
5547
|
+
|
|
5548
|
+
# Export peaks to file
|
|
5549
|
+
if all_peak_results:
|
|
5550
|
+
export_yn = _safe_input("Export peaks to file? (y/n): ").strip().lower()
|
|
5551
|
+
if export_yn == 'y':
|
|
5552
|
+
folder = choose_save_path(source_file_paths, purpose="peak export")
|
|
5553
|
+
if folder:
|
|
5554
|
+
print(f"\nChosen path: {folder}")
|
|
5555
|
+
fname = _safe_input("Export filename (default: peaks.txt): ").strip()
|
|
5556
|
+
if not fname:
|
|
5557
|
+
fname = "peaks.txt"
|
|
5558
|
+
if not fname.endswith('.txt'):
|
|
5559
|
+
fname += '.txt'
|
|
5560
|
+
import os
|
|
5561
|
+
target = fname if os.path.isabs(fname) else os.path.join(folder, fname)
|
|
5562
|
+
do_write = not os.path.exists(target)
|
|
5563
|
+
if os.path.exists(target):
|
|
5564
|
+
ow = _safe_input(f"'{os.path.basename(target)}' exists. Overwrite? (y/n): ").strip().lower()
|
|
5565
|
+
if ow == 'y':
|
|
5566
|
+
do_write = True
|
|
5567
|
+
else:
|
|
5568
|
+
print("Export canceled.")
|
|
5569
|
+
if do_write:
|
|
5570
|
+
try:
|
|
5571
|
+
with open(target, 'w') as f:
|
|
5572
|
+
f.write("# Curve\tLabel\tPeak x\tPeak y\n")
|
|
5573
|
+
for curve_idx, label, peak_xy_list in all_peak_results:
|
|
5574
|
+
for px, py in peak_xy_list:
|
|
5575
|
+
f.write(f"{curve_idx}\t{label}\t{px:.6g}\t{py:.6g}\n")
|
|
5576
|
+
total_peaks = sum(len(pairs) for _, _, pairs in all_peak_results)
|
|
5577
|
+
print(f"Peak positions exported to {target}")
|
|
5578
|
+
print(f"Found {total_peaks} peaks across {len(all_peak_results)} curves.")
|
|
5579
|
+
except Exception as e:
|
|
5580
|
+
print(f"Error saving file: {e}")
|
|
5581
|
+
else:
|
|
5582
|
+
print("Export canceled.")
|
|
3895
5583
|
except Exception as e:
|
|
3896
5584
|
print(f"Error finding peaks: {e}")
|
|
3897
5585
|
|