batplot 1.7.27__tar.gz → 1.8.0__tar.gz

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.

Potentially problematic release.


This version of batplot might be problematic. Click here for more details.

Files changed (60) hide show
  1. {batplot-1.7.27/batplot.egg-info → batplot-1.8.0}/PKG-INFO +1 -1
  2. {batplot-1.7.27 → batplot-1.8.0}/batplot/__init__.py +1 -1
  3. {batplot-1.7.27 → batplot-1.8.0}/batplot/cpc_interactive.py +3 -0
  4. {batplot-1.7.27 → batplot-1.8.0}/batplot/electrochem_interactive.py +90 -55
  5. {batplot-1.7.27 → batplot-1.8.0}/batplot/interactive.py +92 -61
  6. {batplot-1.7.27 → batplot-1.8.0}/batplot/modes.py +12 -12
  7. {batplot-1.7.27 → batplot-1.8.0}/batplot/operando_ec_interactive.py +209 -73
  8. {batplot-1.7.27 → batplot-1.8.0/batplot.egg-info}/PKG-INFO +1 -1
  9. {batplot-1.7.27 → batplot-1.8.0}/pyproject.toml +1 -1
  10. {batplot-1.7.27 → batplot-1.8.0}/LICENSE +0 -0
  11. {batplot-1.7.27 → batplot-1.8.0}/MANIFEST.in +0 -0
  12. {batplot-1.7.27 → batplot-1.8.0}/README.md +0 -0
  13. {batplot-1.7.27 → batplot-1.8.0}/USER_MANUAL.md +0 -0
  14. {batplot-1.7.27 → batplot-1.8.0}/batplot/args.py +0 -0
  15. {batplot-1.7.27 → batplot-1.8.0}/batplot/batch.py +0 -0
  16. {batplot-1.7.27 → batplot-1.8.0}/batplot/batplot.py +0 -0
  17. {batplot-1.7.27 → batplot-1.8.0}/batplot/cif.py +0 -0
  18. {batplot-1.7.27 → batplot-1.8.0}/batplot/cli.py +0 -0
  19. {batplot-1.7.27 → batplot-1.8.0}/batplot/color_utils.py +0 -0
  20. {batplot-1.7.27 → batplot-1.8.0}/batplot/config.py +0 -0
  21. {batplot-1.7.27 → batplot-1.8.0}/batplot/converters.py +0 -0
  22. {batplot-1.7.27 → batplot-1.8.0}/batplot/data/USER_MANUAL.md +0 -0
  23. {batplot-1.7.27 → batplot-1.8.0}/batplot/manual.py +0 -0
  24. {batplot-1.7.27 → batplot-1.8.0}/batplot/operando.py +0 -0
  25. {batplot-1.7.27 → batplot-1.8.0}/batplot/plotting.py +0 -0
  26. {batplot-1.7.27 → batplot-1.8.0}/batplot/readers.py +0 -0
  27. {batplot-1.7.27 → batplot-1.8.0}/batplot/session.py +0 -0
  28. {batplot-1.7.27 → batplot-1.8.0}/batplot/style.py +0 -0
  29. {batplot-1.7.27 → batplot-1.8.0}/batplot/ui.py +0 -0
  30. {batplot-1.7.27 → batplot-1.8.0}/batplot/utils.py +0 -0
  31. {batplot-1.7.27 → batplot-1.8.0}/batplot/version_check.py +0 -0
  32. {batplot-1.7.27 → batplot-1.8.0}/batplot.egg-info/SOURCES.txt +0 -0
  33. {batplot-1.7.27 → batplot-1.8.0}/batplot.egg-info/dependency_links.txt +0 -0
  34. {batplot-1.7.27 → batplot-1.8.0}/batplot.egg-info/entry_points.txt +0 -0
  35. {batplot-1.7.27 → batplot-1.8.0}/batplot.egg-info/requires.txt +0 -0
  36. {batplot-1.7.27 → batplot-1.8.0}/batplot.egg-info/top_level.txt +0 -0
  37. {batplot-1.7.27 → batplot-1.8.0}/batplot_backup_20251121_223043/__init__.py +0 -0
  38. {batplot-1.7.27 → batplot-1.8.0}/batplot_backup_20251121_223043/args.py +0 -0
  39. {batplot-1.7.27 → batplot-1.8.0}/batplot_backup_20251121_223043/batch.py +0 -0
  40. {batplot-1.7.27 → batplot-1.8.0}/batplot_backup_20251121_223043/batplot.py +0 -0
  41. {batplot-1.7.27 → batplot-1.8.0}/batplot_backup_20251121_223043/cif.py +0 -0
  42. {batplot-1.7.27 → batplot-1.8.0}/batplot_backup_20251121_223043/cli.py +0 -0
  43. {batplot-1.7.27 → batplot-1.8.0}/batplot_backup_20251121_223043/color_utils.py +0 -0
  44. {batplot-1.7.27 → batplot-1.8.0}/batplot_backup_20251121_223043/config.py +0 -0
  45. {batplot-1.7.27 → batplot-1.8.0}/batplot_backup_20251121_223043/converters.py +0 -0
  46. {batplot-1.7.27 → batplot-1.8.0}/batplot_backup_20251121_223043/cpc_interactive.py +0 -0
  47. {batplot-1.7.27 → batplot-1.8.0}/batplot_backup_20251121_223043/electrochem_interactive.py +0 -0
  48. {batplot-1.7.27 → batplot-1.8.0}/batplot_backup_20251121_223043/interactive.py +0 -0
  49. {batplot-1.7.27 → batplot-1.8.0}/batplot_backup_20251121_223043/modes.py +0 -0
  50. {batplot-1.7.27 → batplot-1.8.0}/batplot_backup_20251121_223043/operando.py +0 -0
  51. {batplot-1.7.27 → batplot-1.8.0}/batplot_backup_20251121_223043/operando_ec_interactive.py +0 -0
  52. {batplot-1.7.27 → batplot-1.8.0}/batplot_backup_20251121_223043/plotting.py +0 -0
  53. {batplot-1.7.27 → batplot-1.8.0}/batplot_backup_20251121_223043/readers.py +0 -0
  54. {batplot-1.7.27 → batplot-1.8.0}/batplot_backup_20251121_223043/session.py +0 -0
  55. {batplot-1.7.27 → batplot-1.8.0}/batplot_backup_20251121_223043/style.py +0 -0
  56. {batplot-1.7.27 → batplot-1.8.0}/batplot_backup_20251121_223043/ui.py +0 -0
  57. {batplot-1.7.27 → batplot-1.8.0}/batplot_backup_20251121_223043/utils.py +0 -0
  58. {batplot-1.7.27 → batplot-1.8.0}/batplot_backup_20251121_223043/version_check.py +0 -0
  59. {batplot-1.7.27 → batplot-1.8.0}/setup.cfg +0 -0
  60. {batplot-1.7.27 → batplot-1.8.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: batplot
3
- Version: 1.7.27
3
+ Version: 1.8.0
4
4
  Summary: Interactive plotting tool for material science (1D plot) and electrochemistry (GC, CV, dQ/dV, CPC, operando) with batch processing
5
5
  Author-email: Tian Dai <tianda@uio.no>
6
6
  License: MIT License
@@ -1,5 +1,5 @@
1
1
  """batplot: Interactive plotting for battery data visualization."""
2
2
 
3
- __version__ = "1.7.27"
3
+ __version__ = "1.8.0"
4
4
 
5
5
  __all__ = ["__version__"]
@@ -3141,6 +3141,9 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
3141
3141
  _print_menu(); continue
3142
3142
  elif key == 't':
3143
3143
  # Unified WASD toggles for spines/ticks/minor/labels/title per side
3144
+ # Import UI positioning functions locally to ensure they're accessible in nested functions
3145
+ from .ui import position_top_xlabel as _ui_position_top_xlabel, position_bottom_xlabel as _ui_position_bottom_xlabel, position_left_ylabel as _ui_position_left_ylabel, position_right_ylabel as _ui_position_right_ylabel
3146
+
3144
3147
  try:
3145
3148
  # Local WASD state stored on figure to persist across openings
3146
3149
  wasd = getattr(fig, '_cpc_wasd_state', None)
@@ -9,6 +9,7 @@ from __future__ import annotations
9
9
  from typing import Dict, Iterable, List, Optional, Tuple
10
10
  import json
11
11
  import os
12
+ import sys
12
13
 
13
14
  import matplotlib.pyplot as plt
14
15
  import matplotlib.cm as cm
@@ -42,6 +43,37 @@ from .color_utils import (
42
43
  )
43
44
 
44
45
 
46
+ class _FilterIMKWarning:
47
+ """Filter that suppresses macOS IMKCFRunLoopWakeUpReliable warnings while preserving other errors."""
48
+ def __init__(self, original_stderr):
49
+ self.original_stderr = original_stderr
50
+
51
+ def write(self, message):
52
+ # Filter out the harmless macOS IMK warning
53
+ if 'IMKCFRunLoopWakeUpReliable' not in message:
54
+ self.original_stderr.write(message)
55
+
56
+ def flush(self):
57
+ self.original_stderr.flush()
58
+
59
+
60
+ def _safe_input(prompt: str = "") -> str:
61
+ """Wrapper around input() that suppresses macOS IMKCFRunLoopWakeUpReliable warnings.
62
+
63
+ This is a harmless macOS system message that appears when using input() in terminals.
64
+ """
65
+ # Filter stderr to hide macOS IMK warnings while preserving other errors
66
+ original_stderr = sys.stderr
67
+ sys.stderr = _FilterIMKWarning(original_stderr)
68
+ try:
69
+ result = input(prompt)
70
+ return result
71
+ except (KeyboardInterrupt, EOFError):
72
+ raise
73
+ finally:
74
+ sys.stderr = original_stderr
75
+
76
+
45
77
  def _colorize_menu(text):
46
78
  """Colorize menu items: command in cyan, colon in white, description in default."""
47
79
  if ':' not in text:
@@ -937,6 +969,9 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
937
969
 
938
970
  def _title_offset_menu():
939
971
  """Allow nudging duplicate top/right titles by single-pixel increments."""
972
+ # Import UI positioning functions locally to ensure they're accessible in nested functions
973
+ from .ui import position_top_xlabel as _ui_position_top_xlabel, position_bottom_xlabel as _ui_position_bottom_xlabel, position_left_ylabel as _ui_position_left_ylabel, position_right_ylabel as _ui_position_right_ylabel
974
+
940
975
  def _dpi():
941
976
  try:
942
977
  return float(fig.dpi)
@@ -980,7 +1015,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
980
1015
  current_y_px = _px_value('_top_xlabel_manual_offset_y_pts')
981
1016
  current_x_px = _px_value('_top_xlabel_manual_offset_x_pts')
982
1017
  print(f"Top title offset: Y={current_y_px:+.2f} px (positive=up), X={current_x_px:+.2f} px (positive=right)")
983
- sub = input(_colorize_prompt("top (w=up, s=down, a=left, d=right, 0=reset, q=back): ")).strip().lower()
1018
+ sub = _safe_input(_colorize_prompt("top (w=up, s=down, a=left, d=right, 0=reset, q=back): ")).strip().lower()
984
1019
  if not sub:
985
1020
  continue
986
1021
  if sub == 'q':
@@ -1018,7 +1053,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1018
1053
  current_x_px = _px_value('_right_ylabel_manual_offset_x_pts')
1019
1054
  current_y_px = _px_value('_right_ylabel_manual_offset_y_pts')
1020
1055
  print(f"Right title offset: X={current_x_px:+.2f} px (positive=right), Y={current_y_px:+.2f} px (positive=up)")
1021
- sub = input(_colorize_prompt("right (d=right, a=left, w=up, s=down, 0=reset, q=back): ")).strip().lower()
1056
+ sub = _safe_input(_colorize_prompt("right (d=right, a=left, w=up, s=down, 0=reset, q=back): ")).strip().lower()
1022
1057
  if not sub:
1023
1058
  continue
1024
1059
  if sub == 'q':
@@ -1055,7 +1090,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1055
1090
  while True:
1056
1091
  current_y_px = _px_value('_bottom_xlabel_manual_offset_y_pts')
1057
1092
  print(f"Bottom title offset: Y={current_y_px:+.2f} px (positive=down)")
1058
- sub = input(_colorize_prompt("bottom (s=down, w=up, 0=reset, q=back): ")).strip().lower()
1093
+ sub = _safe_input(_colorize_prompt("bottom (s=down, w=up, 0=reset, q=back): ")).strip().lower()
1059
1094
  if not sub:
1060
1095
  continue
1061
1096
  if sub == 'q':
@@ -1085,7 +1120,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1085
1120
  while True:
1086
1121
  current_x_px = _px_value('_left_ylabel_manual_offset_x_pts')
1087
1122
  print(f"Left title offset: X={current_x_px:+.2f} px (positive=left)")
1088
- sub = input(_colorize_prompt("left (a=left, d=right, 0=reset, q=back): ")).strip().lower()
1123
+ sub = _safe_input(_colorize_prompt("left (a=left, d=right, 0=reset, q=back): ")).strip().lower()
1089
1124
  if not sub:
1090
1125
  continue
1091
1126
  if sub == 'q':
@@ -1116,7 +1151,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1116
1151
  print(" " + _colorize_menu('d : adjust right title (d=right, a=left, w=up, s=down)'))
1117
1152
  print(" " + _colorize_menu('r : reset all offsets'))
1118
1153
  print(" " + _colorize_menu('q : return'))
1119
- choice = input(_colorize_prompt("p> ")).strip().lower()
1154
+ choice = _safe_input(_colorize_prompt("p> ")).strip().lower()
1120
1155
  if not choice:
1121
1156
  continue
1122
1157
  if choice == 'q':
@@ -1587,7 +1622,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1587
1622
  _print_menu(len(all_cycles), is_dqdv)
1588
1623
  while True:
1589
1624
  try:
1590
- key = input("Press a key: ").strip().lower()
1625
+ key = _safe_input("Press a key: ").strip().lower()
1591
1626
  except (KeyboardInterrupt, EOFError):
1592
1627
  print("\n\nExiting interactive menu...")
1593
1628
  break
@@ -1595,7 +1630,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1595
1630
  continue
1596
1631
  if key == 'q':
1597
1632
  try:
1598
- confirm = input(_colorize_prompt("Quit EC interactive? Remember to save (e=export, s=save). Quit now? (y/n): ")).strip().lower()
1633
+ confirm = _safe_input(_colorize_prompt("Quit EC interactive? Remember to save (e=export, s=save). Quit now? (y/n): ")).strip().lower()
1599
1634
  except Exception:
1600
1635
  confirm = 'y'
1601
1636
  if confirm == 'y':
@@ -1630,9 +1665,9 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1630
1665
 
1631
1666
  last_figure_path = getattr(fig, '_last_figure_export_path', None)
1632
1667
  if last_figure_path:
1633
- fname = input("Export filename (default .svg if no extension), number to overwrite, or o to overwrite last (q=cancel): ").strip()
1668
+ fname = _safe_input("Export filename (default .svg if no extension), number to overwrite, or o to overwrite last (q=cancel): ").strip()
1634
1669
  else:
1635
- fname = input("Export filename (default .svg if no extension) or number to overwrite (q=cancel): ").strip()
1670
+ fname = _safe_input("Export filename (default .svg if no extension) or number to overwrite (q=cancel): ").strip()
1636
1671
  if not fname or fname.lower() == 'q':
1637
1672
  _print_menu(len(all_cycles), is_dqdv)
1638
1673
  continue
@@ -1648,7 +1683,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1648
1683
  print(f"Previous export file not found: {last_figure_path}")
1649
1684
  _print_menu(len(all_cycles), is_dqdv)
1650
1685
  continue
1651
- yn = input(f"Overwrite '{os.path.basename(last_figure_path)}'? (y/n): ").strip().lower()
1686
+ yn = _safe_input(f"Overwrite '{os.path.basename(last_figure_path)}'? (y/n): ").strip().lower()
1652
1687
  if yn != 'y':
1653
1688
  _print_menu(len(all_cycles), is_dqdv)
1654
1689
  continue
@@ -1660,7 +1695,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1660
1695
  idx = int(fname)
1661
1696
  if 1 <= idx <= len(files):
1662
1697
  name = files[idx-1]
1663
- yn = input(f"Overwrite '{name}'? (y/n): ").strip().lower()
1698
+ yn = _safe_input(f"Overwrite '{name}'? (y/n): ").strip().lower()
1664
1699
  if yn != 'y':
1665
1700
  _print_menu(len(all_cycles), is_dqdv)
1666
1701
  continue
@@ -1798,7 +1833,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1798
1833
  xy_in = _sanitize_legend_offset(fig, getattr(fig, '_ec_legend_xy_in', (0.0, 0.0))) or (0.0, 0.0)
1799
1834
  print(f"Legend is {'ON' if vis else 'off'}; position (inches from center): x={xy_in[0]:.2f}, y={xy_in[1]:.2f}")
1800
1835
  while True:
1801
- sub = input(_colorize_prompt("Legend: (t=toggle, p=set position, q=back): ")).strip().lower()
1836
+ sub = _safe_input(_colorize_prompt("Legend: (t=toggle, p=set position, q=back): ")).strip().lower()
1802
1837
  if not sub:
1803
1838
  continue
1804
1839
  if sub == 'q':
@@ -1822,7 +1857,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1822
1857
  while True:
1823
1858
  xy_in = _sanitize_legend_offset(fig, getattr(fig, '_ec_legend_xy_in', (0.0, 0.0))) or (0.0, 0.0)
1824
1859
  print(f"Current position: x={xy_in[0]:.2f}, y={xy_in[1]:.2f}")
1825
- pos_cmd = input(_colorize_prompt("Position: (x y) or x=x only, y=y only, q=back: ")).strip().lower()
1860
+ pos_cmd = _safe_input(_colorize_prompt("Position: (x y) or x=x only, y=y only, q=back: ")).strip().lower()
1826
1861
  if not pos_cmd or pos_cmd == 'q':
1827
1862
  break
1828
1863
  if pos_cmd == 'x':
@@ -1830,7 +1865,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1830
1865
  while True:
1831
1866
  xy_in = _sanitize_legend_offset(fig, getattr(fig, '_ec_legend_xy_in', (0.0, 0.0))) or (0.0, 0.0)
1832
1867
  print(f"Current position: x={xy_in[0]:.2f}, y={xy_in[1]:.2f}")
1833
- val = input(f"Enter new x position (current y: {xy_in[1]:.2f}, q=back): ").strip()
1868
+ val = _safe_input(f"Enter new x position (current y: {xy_in[1]:.2f}, q=back): ").strip()
1834
1869
  if not val or val.lower() == 'q':
1835
1870
  break
1836
1871
  try:
@@ -1857,7 +1892,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1857
1892
  while True:
1858
1893
  xy_in = _sanitize_legend_offset(fig, getattr(fig, '_ec_legend_xy_in', (0.0, 0.0))) or (0.0, 0.0)
1859
1894
  print(f"Current position: x={xy_in[0]:.2f}, y={xy_in[1]:.2f}")
1860
- val = input(f"Enter new y position (current x: {xy_in[0]:.2f}, q=back): ").strip()
1895
+ val = _safe_input(f"Enter new y position (current x: {xy_in[0]:.2f}, q=back): ").strip()
1861
1896
  if not val or val.lower() == 'q':
1862
1897
  break
1863
1898
  try:
@@ -1932,9 +1967,9 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1932
1967
 
1933
1968
  last_style_path = getattr(fig, '_last_style_export_path', None)
1934
1969
  if last_style_path:
1935
- sub = input(_colorize_prompt("Style submenu: (e=export, o=overwrite last, q=return, r=refresh): ")).strip().lower()
1970
+ sub = _safe_input(_colorize_prompt("Style submenu: (e=export, o=overwrite last, q=return, r=refresh): ")).strip().lower()
1936
1971
  else:
1937
- sub = input(_colorize_prompt("Style submenu: (e=export, q=return, r=refresh): ")).strip().lower()
1972
+ sub = _safe_input(_colorize_prompt("Style submenu: (e=export, q=return, r=refresh): ")).strip().lower()
1938
1973
  if sub == 'q':
1939
1974
  break
1940
1975
  if sub == 'r' or sub == '':
@@ -1947,7 +1982,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1947
1982
  if not os.path.exists(last_style_path):
1948
1983
  print(f"Previous export file not found: {last_style_path}")
1949
1984
  continue
1950
- yn = input(f"Overwrite '{os.path.basename(last_style_path)}'? (y/n): ").strip().lower()
1985
+ yn = _safe_input(f"Overwrite '{os.path.basename(last_style_path)}'? (y/n): ").strip().lower()
1951
1986
  if yn != 'y':
1952
1987
  continue
1953
1988
  # Rebuild config based on current state
@@ -1974,7 +2009,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1974
2009
  print("Export options:")
1975
2010
  print(" ps = style only (.bps)")
1976
2011
  print(" psg = style + geometry (.bpsg)")
1977
- exp_choice = input(_colorize_prompt("Export choice (ps/psg, q=cancel): ")).strip().lower()
2012
+ exp_choice = _safe_input(_colorize_prompt("Export choice (ps/psg, q=cancel): ")).strip().lower()
1978
2013
  if not exp_choice or exp_choice == 'q':
1979
2014
  print("Style export canceled.")
1980
2015
  continue
@@ -2426,13 +2461,13 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
2426
2461
  print(f" {_colorize_menu('ld : show line and dots (markers) for all curves')}")
2427
2462
  print(f" {_colorize_menu('d : show only dots (no connecting line) for all curves')}")
2428
2463
  print(f" {_colorize_menu('q : return')}")
2429
- sub = input(_colorize_prompt("Choose (c/f/g/l/ld/d/q): ")).strip().lower()
2464
+ sub = _safe_input(_colorize_prompt("Choose (c/f/g/l/ld/d/q): ")).strip().lower()
2430
2465
  if not sub:
2431
2466
  continue
2432
2467
  if sub == 'q':
2433
2468
  break
2434
2469
  if sub == 'c':
2435
- spec = input("Curve linewidth (single value for all curves, q=cancel): ").strip()
2470
+ spec = _safe_input("Curve linewidth (single value for all curves, q=cancel): ").strip()
2436
2471
  if not spec or spec.lower() == 'q':
2437
2472
  continue
2438
2473
  # Apply single width to all curves
@@ -2461,7 +2496,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
2461
2496
  except ValueError:
2462
2497
  print("Invalid width value.")
2463
2498
  elif sub == 'f':
2464
- fw_in = input("Enter frame/tick width (e.g., 1.5) or 'm M' (major minor) or q: ").strip()
2499
+ fw_in = _safe_input("Enter frame/tick width (e.g., 1.5) or 'm M' (major minor) or q: ").strip()
2465
2500
  if not fw_in or fw_in.lower() == 'q':
2466
2501
  print("Canceled.")
2467
2502
  continue
@@ -2536,7 +2571,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
2536
2571
  # Line + dots for all curves
2537
2572
  push_state("line+dots")
2538
2573
  try:
2539
- msize_in = input("Marker size (blank=auto ~3*lw): ").strip()
2574
+ msize_in = _safe_input("Marker size (blank=auto ~3*lw): ").strip()
2540
2575
  custom_msize = float(msize_in) if msize_in else None
2541
2576
  except ValueError:
2542
2577
  custom_msize = None
@@ -2566,7 +2601,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
2566
2601
  # Dots only for all curves
2567
2602
  push_state("dots-only")
2568
2603
  try:
2569
- msize_in = input("Marker size (blank=auto ~3*lw): ").strip()
2604
+ msize_in = _safe_input("Marker size (blank=auto ~3*lw): ").strip()
2570
2605
  custom_msize = float(msize_in) if msize_in else None
2571
2606
  except ValueError:
2572
2607
  custom_msize = None
@@ -2613,7 +2648,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
2613
2648
  print(f" {idx}: {color_block(color)} {color}")
2614
2649
  print("Type 'u' to edit saved colors.")
2615
2650
  print("q: back to main menu")
2616
- line = input("Enter mappings (e.g., w:red a:#4561F7) or q: ").strip()
2651
+ line = _safe_input("Enter mappings (e.g., w:red a:#4561F7) or q: ").strip()
2617
2652
  if not line or line.lower() == 'q':
2618
2653
  break
2619
2654
  if line.lower() == 'u':
@@ -2664,13 +2699,13 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
2664
2699
  print(" Bullet: $\\bullet$ → • | Greek: $\\alpha$, $\\beta$ | Angstrom: $\\AA$ → Å")
2665
2700
  while True:
2666
2701
  print("Rename axis: x, y, both, q=back")
2667
- sub = input("Rename> ").strip().lower()
2702
+ sub = _safe_input("Rename> ").strip().lower()
2668
2703
  if not sub:
2669
2704
  continue
2670
2705
  if sub == 'q':
2671
2706
  break
2672
2707
  if sub in ('x','both'):
2673
- txt = input("New X-axis label (blank=cancel): ")
2708
+ txt = _safe_input("New X-axis label (blank=cancel): ")
2674
2709
  if txt:
2675
2710
  push_state("rename-x")
2676
2711
  try:
@@ -2693,7 +2728,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
2693
2728
  except Exception:
2694
2729
  pass
2695
2730
  if sub in ('y','both'):
2696
- txt = input("New Y-axis label (blank=cancel): ")
2731
+ txt = _safe_input("New Y-axis label (blank=cancel): ")
2697
2732
  if txt:
2698
2733
  push_state("rename-y")
2699
2734
  base_ylabel = txt
@@ -2829,7 +2864,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
2829
2864
  print(_colorize_inline_commands(" 1=spine 2=ticks 3=minor ticks 4=tick labels 5=axis title"))
2830
2865
  print(_colorize_inline_commands("Type 'i' to invert tick direction, 'l' to change tick length, 'list' for state, 'q' to return."))
2831
2866
  print(_colorize_inline_commands(" p = adjust title offsets (w=top, s=bottom, a=left, d=right)"))
2832
- cmd = input(_colorize_prompt("t> ")).strip().lower()
2867
+ cmd = _safe_input(_colorize_prompt("t> ")).strip().lower()
2833
2868
  if not cmd:
2834
2869
  continue
2835
2870
  if cmd == 'q':
@@ -2856,7 +2891,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
2856
2891
  # Get current major tick length from axes
2857
2892
  current_major = ax.xaxis.get_major_ticks()[0].tick1line.get_markersize() if ax.xaxis.get_major_ticks() else 4.0
2858
2893
  print(f"Current major tick length: {current_major}")
2859
- new_length_str = input("Enter new major tick length (e.g., 6.0): ").strip()
2894
+ new_length_str = _safe_input("Enter new major tick length (e.g., 6.0): ").strip()
2860
2895
  if not new_length_str:
2861
2896
  continue
2862
2897
  new_major = float(new_length_str)
@@ -2944,7 +2979,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
2944
2979
  prompt = "Enter new filename (no ext needed), number to overwrite, or o to overwrite last (q=cancel): "
2945
2980
  else:
2946
2981
  prompt = "Enter new filename (no ext needed) or number to overwrite (q=cancel): "
2947
- choice = input(prompt).strip()
2982
+ choice = _safe_input(prompt).strip()
2948
2983
  if not choice or choice.lower() == 'q':
2949
2984
  _print_menu(len(all_cycles), is_dqdv); continue
2950
2985
  if choice.lower() == 'o':
@@ -2955,7 +2990,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
2955
2990
  if not os.path.exists(last_session_path):
2956
2991
  print(f"Previous save file not found: {last_session_path}")
2957
2992
  _print_menu(len(all_cycles), is_dqdv); continue
2958
- yn = input(f"Overwrite '{os.path.basename(last_session_path)}'? (y/n): ").strip().lower()
2993
+ yn = _safe_input(f"Overwrite '{os.path.basename(last_session_path)}'? (y/n): ").strip().lower()
2959
2994
  if yn != 'y':
2960
2995
  _print_menu(len(all_cycles), is_dqdv); continue
2961
2996
  dump_ec_session(last_session_path, fig=fig, ax=ax, cycle_lines=cycle_lines, skip_confirm=True)
@@ -2965,7 +3000,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
2965
3000
  idx = int(choice)
2966
3001
  if 1 <= idx <= len(files):
2967
3002
  name = files[idx-1]
2968
- yn = input(f"Overwrite '{name}'? (y/n): ").strip().lower()
3003
+ yn = _safe_input(f"Overwrite '{name}'? (y/n): ").strip().lower()
2969
3004
  if yn != 'y':
2970
3005
  _print_menu(len(all_cycles), is_dqdv); continue
2971
3006
  target = os.path.join(folder, name)
@@ -2982,7 +3017,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
2982
3017
  name = name + '.pkl'
2983
3018
  target = name if os.path.isabs(name) else os.path.join(folder, name)
2984
3019
  if os.path.exists(target):
2985
- yn = input(f"'{os.path.basename(target)}' exists. Overwrite? (y/n): ").strip().lower()
3020
+ yn = _safe_input(f"'{os.path.basename(target)}' exists. Overwrite? (y/n): ").strip().lower()
2986
3021
  if yn != 'y':
2987
3022
  _print_menu(len(all_cycles), is_dqdv); continue
2988
3023
  dump_ec_session(target, fig=fig, ax=ax, cycle_lines=cycle_lines, skip_confirm=True)
@@ -3018,7 +3053,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
3018
3053
  for idx, color in enumerate(user_colors, 1):
3019
3054
  print(f" {idx}: {color_block(color)} {color}")
3020
3055
  print("Type 'u' to edit saved colors before assigning.")
3021
- line = input("Selection: ").strip()
3056
+ line = _safe_input("Selection: ").strip()
3022
3057
  if not line:
3023
3058
  continue
3024
3059
  if line.lower() == 'u':
@@ -3163,14 +3198,14 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
3163
3198
  # X-axis submenu: number-of-ions vs capacity
3164
3199
  while True:
3165
3200
  print("X-axis menu: n=number of ions, c=capacity, q=back")
3166
- sub = input("X> ").strip().lower()
3201
+ sub = _safe_input("X> ").strip().lower()
3167
3202
  if not sub:
3168
3203
  continue
3169
3204
  if sub == 'q':
3170
3205
  break
3171
3206
  if sub == 'n':
3172
3207
  print("Input the theoretical capacity per 1 active ion (mAh g^-1), e.g., 125")
3173
- val = input("C_theoretical_per_ion: ").strip()
3208
+ val = _safe_input("C_theoretical_per_ion: ").strip()
3174
3209
  try:
3175
3210
  c_th = float(val)
3176
3211
  if c_th <= 0:
@@ -3307,7 +3342,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
3307
3342
  cur_size = plt.rcParams.get('font.size', None)
3308
3343
  while True:
3309
3344
  print(f"\nFont menu (current: family='{cur_family}', size={cur_size}): f=font family, s=size, q=back")
3310
- sub = input("Font> ").strip().lower()
3345
+ sub = _safe_input("Font> ").strip().lower()
3311
3346
  if not sub:
3312
3347
  continue
3313
3348
  if sub == 'q':
@@ -3320,7 +3355,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
3320
3355
  for i, font in enumerate(fonts, 1):
3321
3356
  print(f" {i}: {font}")
3322
3357
  print("Or enter custom font name directly.")
3323
- choice = input(f"Font family (current: '{cur_family}', number or name): ").strip()
3358
+ choice = _safe_input(f"Font family (current: '{cur_family}', number or name): ").strip()
3324
3359
  if not choice:
3325
3360
  continue
3326
3361
  # Check if it's a number
@@ -3352,7 +3387,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
3352
3387
  # Show current size and accept direct input
3353
3388
  import matplotlib as mpl
3354
3389
  cur_size = mpl.rcParams.get('font.size', None)
3355
- choice = input(f"Font size (current: {cur_size}): ").strip()
3390
+ choice = _safe_input(f"Font size (current: {cur_size}): ").strip()
3356
3391
  if not choice:
3357
3392
  continue
3358
3393
  try:
@@ -3377,7 +3412,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
3377
3412
  while True:
3378
3413
  current_xlim = ax.get_xlim()
3379
3414
  print(f"Current X range: {current_xlim[0]:.6g} to {current_xlim[1]:.6g}")
3380
- lim = input("Set X limits (min max), w=upper only, s=lower only, a=auto (restore original), q=back: ").strip()
3415
+ lim = _safe_input("Set X limits (min max), w=upper only, s=lower only, a=auto (restore original), q=back: ").strip()
3381
3416
  if not lim or lim.lower() == 'q':
3382
3417
  break
3383
3418
  if lim.lower() == 'a':
@@ -3399,7 +3434,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
3399
3434
  while True:
3400
3435
  current_xlim = ax.get_xlim()
3401
3436
  print(f"Current X range: {current_xlim[0]:.6g} to {current_xlim[1]:.6g}")
3402
- val = input(f"Enter new upper X limit (current lower: {current_xlim[0]:.6g}, q=back): ").strip()
3437
+ val = _safe_input(f"Enter new upper X limit (current lower: {current_xlim[0]:.6g}, q=back): ").strip()
3403
3438
  if not val or val.lower() == 'q':
3404
3439
  break
3405
3440
  try:
@@ -3425,7 +3460,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
3425
3460
  while True:
3426
3461
  current_xlim = ax.get_xlim()
3427
3462
  print(f"Current X range: {current_xlim[0]:.6g} to {current_xlim[1]:.6g}")
3428
- val = input(f"Enter new lower X limit (current upper: {current_xlim[1]:.6g}, q=back): ").strip()
3463
+ val = _safe_input(f"Enter new lower X limit (current upper: {current_xlim[1]:.6g}, q=back): ").strip()
3429
3464
  if not val or val.lower() == 'q':
3430
3465
  break
3431
3466
  try:
@@ -3461,7 +3496,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
3461
3496
  while True:
3462
3497
  current_ylim = ax.get_ylim()
3463
3498
  print(f"Current Y range: {current_ylim[0]:.6g} to {current_ylim[1]:.6g}")
3464
- lim = input("Set Y limits (min max), w=upper only, s=lower only, a=auto (restore original), q=back: ").strip()
3499
+ lim = _safe_input("Set Y limits (min max), w=upper only, s=lower only, a=auto (restore original), q=back: ").strip()
3465
3500
  if not lim or lim.lower() == 'q':
3466
3501
  break
3467
3502
  if lim.lower() == 'a':
@@ -3483,7 +3518,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
3483
3518
  while True:
3484
3519
  current_ylim = ax.get_ylim()
3485
3520
  print(f"Current Y range: {current_ylim[0]:.6g} to {current_ylim[1]:.6g}")
3486
- val = input(f"Enter new upper Y limit (current lower: {current_ylim[0]:.6g}, q=back): ").strip()
3521
+ val = _safe_input(f"Enter new upper Y limit (current lower: {current_ylim[0]:.6g}, q=back): ").strip()
3487
3522
  if not val or val.lower() == 'q':
3488
3523
  break
3489
3524
  try:
@@ -3509,7 +3544,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
3509
3544
  while True:
3510
3545
  current_ylim = ax.get_ylim()
3511
3546
  print(f"Current Y range: {current_ylim[0]:.6g} to {current_ylim[1]:.6g}")
3512
- val = input(f"Enter new lower Y limit (current upper: {current_ylim[1]:.6g}, q=back): ").strip()
3547
+ val = _safe_input(f"Enter new lower Y limit (current upper: {current_ylim[1]:.6g}, q=back): ").strip()
3513
3548
  if not val or val.lower() == 'q':
3514
3549
  break
3515
3550
  try:
@@ -3544,7 +3579,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
3544
3579
  # Geometry submenu: plot frame vs canvas (scales moved to separate keys)
3545
3580
  while True:
3546
3581
  print("Geometry menu: p=plot frame size, c=canvas size, q=back")
3547
- sub = input("Geom> ").strip().lower()
3582
+ sub = _safe_input("Geom> ").strip().lower()
3548
3583
  if not sub:
3549
3584
  continue
3550
3585
  if sub == 'q':
@@ -3583,7 +3618,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
3583
3618
  print(" o: remove outliers (removes abrupt dQ/dV spikes)")
3584
3619
  print(" r: reset to original data")
3585
3620
  print(" q: back to main menu")
3586
- sub = input("sm> ").strip().lower()
3621
+ sub = _safe_input("sm> ").strip().lower()
3587
3622
  if not sub:
3588
3623
  continue
3589
3624
  if sub == 'q':
@@ -3618,7 +3653,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
3618
3653
  if sub == 'a':
3619
3654
  try:
3620
3655
  while True:
3621
- threshold_input = input("Enter minimum voltage step in mV (default 0.5 mV, 'q'=quit, 'e'=explain): ").strip()
3656
+ threshold_input = _safe_input("Enter minimum voltage step in mV (default 0.5 mV, 'q'=quit, 'e'=explain): ").strip()
3622
3657
  if threshold_input.lower() == 'q':
3623
3658
  break
3624
3659
  if threshold_input.lower() == 'e':
@@ -3692,7 +3727,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
3692
3727
  try:
3693
3728
  print("DiffCap smoothing per Thompson et al. (2020): clean ΔV < threshold and apply Savitzky–Golay (order 3).")
3694
3729
  while True:
3695
- delta_input = input("Minimum ΔV between points (mV, default 1.0, 'q'=quit, 'e'=explain): ").strip()
3730
+ delta_input = _safe_input("Minimum ΔV between points (mV, default 1.0, 'q'=quit, 'e'=explain): ").strip()
3696
3731
  if delta_input.lower() == 'q':
3697
3732
  break
3698
3733
  if delta_input.lower() == 'e':
@@ -3715,7 +3750,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
3715
3750
  if delta_input and delta_input.lower() == 'q': # User quit at previous step
3716
3751
  continue
3717
3752
  while True:
3718
- window_input = input("Savitzky–Golay window (odd, default 9, 'q'=quit, 'e'=explain): ").strip()
3753
+ window_input = _safe_input("Savitzky–Golay window (odd, default 9, 'q'=quit, 'e'=explain): ").strip()
3719
3754
  if window_input.lower() == 'q':
3720
3755
  break
3721
3756
  if window_input.lower() == 'e':
@@ -3734,7 +3769,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
3734
3769
  if window_input and window_input.lower() == 'q': # User quit at previous step
3735
3770
  continue
3736
3771
  while True:
3737
- poly_input = input("Polynomial order (default 3, 'q'=quit, 'e'=explain): ").strip()
3772
+ poly_input = _safe_input("Polynomial order (default 3, 'q'=quit, 'e'=explain): ").strip()
3738
3773
  if poly_input.lower() == 'q':
3739
3774
  break
3740
3775
  if poly_input.lower() == 'e':
@@ -3806,7 +3841,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
3806
3841
  print(" 1: Z-score (enter standard deviation threshold, default 5.0)")
3807
3842
  print(" 2: MAD (median absolute deviation, default factor 6.0)")
3808
3843
  while True:
3809
- method = input("Method (1/2, blank=cancel, 'q'=quit, 'e'=explain): ").strip()
3844
+ method = _safe_input("Method (1/2, blank=cancel, 'q'=quit, 'e'=explain): ").strip()
3810
3845
  if not method or method.lower() == 'q':
3811
3846
  break
3812
3847
  if method.lower() == 'e':
@@ -3832,7 +3867,7 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
3832
3867
  continue
3833
3868
  try:
3834
3869
  while True:
3835
- thresh_input = input("Enter threshold (blank=default, 'q'=quit, 'e'=explain): ").strip()
3870
+ thresh_input = _safe_input("Enter threshold (blank=default, 'q'=quit, 'e'=explain): ").strip()
3836
3871
  if thresh_input.lower() == 'q':
3837
3872
  break
3838
3873
  if thresh_input.lower() == 'e':
@@ -4364,7 +4399,7 @@ def _export_style_dialog(cfg: Dict, default_ext: str = '.bpcfg', base_path: Opti
4364
4399
  for i, f in enumerate(bpcfg_files, 1):
4365
4400
  print(f" {i}: {f}")
4366
4401
 
4367
- choice = input(f"Export to file? Enter filename or number to overwrite (q=cancel): ").strip()
4402
+ choice = _safe_input(f"Export to file? Enter filename or number to overwrite (q=cancel): ").strip()
4368
4403
  if not choice or choice.lower() == 'q':
4369
4404
  return
4370
4405