batplot 1.8.2__py3-none-any.whl → 1.8.3__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.

Potentially problematic release.


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

batplot/__init__.py CHANGED
@@ -1,5 +1,5 @@
1
1
  """batplot: Interactive plotting for battery data visualization."""
2
2
 
3
- __version__ = "1.8.2"
3
+ __version__ = "1.8.3"
4
4
 
5
5
  __all__ = ["__version__"]
batplot/batch.py CHANGED
@@ -157,6 +157,18 @@ def _apply_xy_style(fig, ax, cfg: dict):
157
157
  except Exception:
158
158
  pass
159
159
 
160
+ # Enforce compatibility between style/geom ro state and current figure ro state.
161
+ # Styles saved from a plot using --ro (swapped x/y) must not be applied to a non-ro plot, and vice versa.
162
+ file_ro = bool(cfg.get('ro_active', False))
163
+ current_ro = bool(getattr(fig, '_ro_active', False))
164
+ if file_ro != current_ro:
165
+ if file_ro:
166
+ print("Warning: XY style/geometry file was saved with --ro (swapped x/y axes); batch plot is not using --ro.")
167
+ else:
168
+ print("Warning: XY style/geometry file was saved without --ro; batch plot is treated as non-ro.")
169
+ print("Skipping style/geometry in batch mode to avoid corrupting axis orientation.")
170
+ return
171
+
160
172
  # Apply WASD state (tick visibility)
161
173
  wasd_cfg = cfg.get('wasd_state', {})
162
174
  if wasd_cfg:
@@ -223,6 +235,17 @@ def _apply_ec_style(fig, ax, cfg: dict):
223
235
  cfg: Style configuration dictionary
224
236
  """
225
237
  try:
238
+ # Enforce compatibility between style/geom ro state and current figure ro state.
239
+ file_ro = bool(cfg.get('ro_active', False))
240
+ current_ro = bool(getattr(fig, '_ro_active', False))
241
+ if file_ro != current_ro:
242
+ if file_ro:
243
+ print("Warning: EC style/geometry file was saved with --ro (swapped x/y axes); batch EC plot is not using --ro.")
244
+ else:
245
+ print("Warning: EC style/geometry file was saved without --ro; batch EC plot is treated as non-ro.")
246
+ print("Skipping EC style/geometry in batch mode to avoid corrupting axis orientation.")
247
+ return
248
+
226
249
  # Apply fonts
227
250
  font_cfg = cfg.get('font', {})
228
251
  if font_cfg:
batplot/batplot.py CHANGED
@@ -507,6 +507,11 @@ def batplot_main() -> int:
507
507
  except Exception:
508
508
  pass
509
509
  _plt.show(block=False)
510
+ # Track whether data axes were swapped via --ro for this EC figure
511
+ try:
512
+ fig._ro_active = bool(getattr(args, "ro", False))
513
+ except Exception:
514
+ pass
510
515
  try:
511
516
  fig._bp_source_paths = [_os.path.abspath(ec_file)]
512
517
  except Exception:
@@ -1785,9 +1790,40 @@ def batplot_main() -> int:
1785
1790
  if not isinstance(sess, dict) or 'version' not in sess:
1786
1791
  print("Not a valid batplot session file.")
1787
1792
  exit(1)
1793
+ except ModuleNotFoundError as e:
1794
+ # Handle numpy._core and other module import errors
1795
+ if '_core' in str(e) or 'numpy' in str(e).lower():
1796
+ print(f"\nERROR: NumPy version mismatch detected when loading: {sess_path}")
1797
+ print("This session was saved with a different NumPy version.")
1798
+ print("The error 'No module named numpy._core' indicates:")
1799
+ print(" - Session saved with NumPy 2.0+ but loading with NumPy <2.0, OR")
1800
+ print(" - Session saved with NumPy <2.0 but loading with NumPy 2.0+")
1801
+ print("\nSolutions:")
1802
+ print(" 1. Check NumPy version: python3 -c 'import numpy; print(numpy.__version__)'")
1803
+ print(" 2. Install matching version:")
1804
+ print(" - If session was saved with NumPy 2.0+: pip install 'numpy>=2.0'")
1805
+ print(" - If session was saved with NumPy <2.0: pip install 'numpy<2.0'")
1806
+ print(" 3. Recreate the session from original data files")
1807
+ else:
1808
+ print(f"\nERROR: Module import error when loading: {sess_path}")
1809
+ print(f"Error: {e}")
1810
+ print("This usually indicates a package version mismatch.")
1811
+ print("Try installing matching package versions or recreate the session.")
1812
+ exit(1)
1788
1813
  except Exception as e:
1789
1814
  print(f"Failed to load session: {e}")
1790
1815
  exit(1)
1816
+
1817
+ # Check package version compatibility (if version info is available)
1818
+ try:
1819
+ from .session import _get_package_versions, _check_package_compatibility
1820
+ saved_versions = sess.get('package_versions', {})
1821
+ current_versions = _get_package_versions()
1822
+ if saved_versions and not _check_package_compatibility(saved_versions, current_versions, sess_path):
1823
+ exit(1)
1824
+ except Exception:
1825
+ # If compatibility checking fails, continue anyway (backward compatibility)
1826
+ pass
1791
1827
  # If it's an EC GC session, load and open EC interactive menu directly
1792
1828
  if isinstance(sess, dict) and sess.get('kind') == 'ec_gc':
1793
1829
  try:
@@ -1882,6 +1918,11 @@ def batplot_main() -> int:
1882
1918
  # Reconstruct minimal state and go to interactive if requested
1883
1919
  plt.ion() if args.interactive else None
1884
1920
  fig, ax = plt.subplots(figsize=(8,6))
1921
+ # Restore ro flag from session (if present) so style/geom imports can enforce compatibility
1922
+ try:
1923
+ fig._ro_active = bool(sess.get('ro_active', False))
1924
+ except Exception:
1925
+ pass
1885
1926
  y_data_list = []
1886
1927
  x_data_list = []
1887
1928
  labels_list = []
@@ -2973,19 +3014,21 @@ def batplot_main() -> int:
2973
3014
  offset += increment
2974
3015
 
2975
3016
  # ---- Plot curve ----
2976
- # Swap x and y if --ro flag is set
3017
+ # Swap x and y if --ro flag is set (and keep lists aligned once)
2977
3018
  if getattr(args, 'ro', False):
2978
- ax.plot(y_plot_offset, x_plot, "-", lw=1, alpha=0.8)
2979
- y_data_list.append(x_plot.copy())
2980
- x_data_list.append(y_plot_offset)
3019
+ x_plotted = y_plot_offset # goes on x-axis when rotated
3020
+ y_plotted = x_plot # goes on y-axis when rotated
2981
3021
  else:
3022
+ x_plotted = x_plot
3023
+ y_plotted = y_plot_offset
2982
3024
 
2983
- ax.plot(x_plot, y_plot_offset, "-", lw=1, alpha=0.8)
2984
- y_data_list.append(y_plot_offset.copy())
2985
- x_data_list.append(x_plot)
3025
+ ax.plot(x_plotted, y_plotted, "-", lw=1, alpha=0.8)
3026
+ x_data_list.append(x_plotted)
3027
+ y_data_list.append(y_plotted.copy())
2986
3028
  labels_list.append(label)
2987
3029
  # Store current normalized (subset) (used by rearrange logic)
2988
- orig_y.append(y_norm.copy())
3030
+ # Keep orig_y aligned with the plotted y data to avoid length mismatch on undo/relabel.
3031
+ orig_y.append(y_plotted.copy())
2989
3032
 
2990
3033
  # ---------------- Force axis to fit all data before labels ----------------
2991
3034
  ax.relim()
@@ -3498,6 +3541,12 @@ def batplot_main() -> int:
3498
3541
  fig.set_tight_layout(False)
3499
3542
  except Exception:
3500
3543
  pass
3544
+
3545
+ # Track whether data axes were swapped via --ro for this figure
3546
+ try:
3547
+ fig._ro_active = bool(getattr(args, "ro", False))
3548
+ except Exception:
3549
+ pass
3501
3550
 
3502
3551
  # Build CIF globals dict for explicit passing
3503
3552
  cif_globals = {
@@ -564,6 +564,8 @@ def _style_snapshot(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_data=Non
564
564
  'frame_size': [frame_w_in, frame_h_in],
565
565
  'axes_fraction': [ax_bbox.x0, ax_bbox.y0, ax_bbox.width, ax_bbox.height]
566
566
  },
567
+ # Track whether data axes were swapped via --ro when this style was saved
568
+ 'ro_active': bool(getattr(fig, '_ro_active', False)),
567
569
  'font': {'family': fam0, 'size': fsize},
568
570
  'legend': {
569
571
  'visible': legend_visible,
@@ -1533,9 +1535,8 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
1533
1535
  ax2.tick_params(axis='y', which='minor', right=True, labelright=False)
1534
1536
  else:
1535
1537
  ax2.tick_params(axis='y', which='minor', right=False, labelright=False)
1536
- # Position label spacings (bottom/left) for consistency
1537
- _ui_position_bottom_xlabel(ax, fig, tick_state)
1538
- _ui_position_left_ylabel(ax, fig, tick_state)
1538
+ # Note: Do NOT call position functions during undo restore as it causes title drift
1539
+ # Title offsets are already restored from snapshot in restore_state()
1539
1540
  try:
1540
1541
  for spine_name, color in getattr(fig, '_cpc_spine_colors', {}).items():
1541
1542
  _set_spine_color(spine_name, color)
@@ -2683,6 +2684,17 @@ def cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff, file_dat
2683
2684
  kind = cfg.get('kind', '')
2684
2685
  if kind not in ('cpc_style', 'cpc_style_geom'):
2685
2686
  print("Not a CPC style file."); _print_menu(); continue
2687
+
2688
+ # Enforce compatibility between style/geom ro state and current figure ro state
2689
+ file_ro = bool(cfg.get('ro_active', False))
2690
+ current_ro = bool(getattr(fig, '_ro_active', False))
2691
+ if file_ro != current_ro:
2692
+ if file_ro:
2693
+ print("Warning: Style/geometry file was saved with --ro (swapped x/y axes); current plot is not using --ro.")
2694
+ else:
2695
+ print("Warning: Style/geometry file was saved without --ro; current plot was created with --ro.")
2696
+ print("Not applying CPC style/geometry to avoid corrupting axis orientation.")
2697
+ _print_menu(); continue
2686
2698
 
2687
2699
  has_geometry = (kind == 'cpc_style_geom' and 'geometry' in cfg)
2688
2700
 
@@ -1551,10 +1551,8 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
1551
1551
  ax._right_ylabel_manual_offset_y_pts = 0.0
1552
1552
  ax._top_xlabel_on = bool(snap.get('titles',{}).get('top_x', False))
1553
1553
  ax._right_ylabel_on = bool(snap.get('titles',{}).get('right_y', False))
1554
- _ui_position_top_xlabel(ax, fig, tick_state)
1555
- _ui_position_bottom_xlabel(ax, fig, tick_state)
1556
- _ui_position_left_ylabel(ax, fig, tick_state)
1557
- _ui_position_right_ylabel(ax, fig, tick_state)
1554
+ # Note: Do NOT call position functions during undo restore as it causes title drift
1555
+ # Title offsets are already restored from snapshot above
1558
1556
  except Exception:
1559
1557
  pass
1560
1558
  # Restore labelpads (for title positioning)
@@ -2091,6 +2089,18 @@ def electrochem_interactive_menu(fig, ax, cycle_lines: Dict[int, Dict[str, Optio
2091
2089
  print("Not an EC style file.")
2092
2090
  _print_menu(len(all_cycles), is_dqdv)
2093
2091
  continue
2092
+
2093
+ # Enforce compatibility between style/geom ro state and current figure ro state
2094
+ file_ro = bool(cfg.get('ro_active', False))
2095
+ current_ro = bool(getattr(fig, '_ro_active', False))
2096
+ if file_ro != current_ro:
2097
+ if file_ro:
2098
+ print("Warning: EC style/geometry file was saved with --ro (swapped x/y axes); current plot is not using --ro.")
2099
+ else:
2100
+ print("Warning: EC style/geometry file was saved without --ro; current plot was created with --ro.")
2101
+ print("Not applying EC style/geometry to avoid corrupting axis orientation.")
2102
+ _print_menu(len(all_cycles), is_dqdv)
2103
+ continue
2094
2104
 
2095
2105
  has_geometry = (kind == 'ec_style_geom' and 'geometry' in cfg)
2096
2106
 
@@ -4231,6 +4241,8 @@ def _get_style_snapshot(fig, ax, cycle_lines: Dict, tick_state: Dict) -> Dict:
4231
4241
  'curve_markers': curve_marker_props,
4232
4242
  'rotation_angle': getattr(fig, '_ec_rotation_angle', 0),
4233
4243
  'cycle_styles': cycle_styles,
4244
+ # Track whether data axes were swapped via --ro when this style was saved
4245
+ 'ro_active': bool(getattr(fig, '_ro_active', False)),
4234
4246
  }
4235
4247
 
4236
4248
 
@@ -4322,6 +4334,10 @@ def _print_style_snapshot(cfg: Dict):
4322
4334
  if rotation_angle != 0:
4323
4335
  print(f"Rotation angle: {rotation_angle}°")
4324
4336
 
4337
+ # ro / axis-swap state
4338
+ ro_active = bool(cfg.get('ro_active', False))
4339
+ print(f"Data axes swapped via --ro: {'YES' if ro_active else 'no'}")
4340
+
4325
4341
  # Per-side matrix summary (spine, major, minor, labels, title)
4326
4342
  def _onoff(v):
4327
4343
  return 'ON ' if bool(v) else 'off'
batplot/interactive.py CHANGED
@@ -1218,8 +1218,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
1218
1218
  fig.set_size_inches(snap["fig_size"][0], snap["fig_size"][1], forward=True)
1219
1219
  except Exception:
1220
1220
  pass
1221
- else:
1222
- print("(Canvas fixed) Ignoring undo figure size restore.")
1221
+ # No message needed - canvas size is managed by system
1223
1222
  # Don't restore DPI from undo - use system default to avoid display-dependent issues
1224
1223
 
1225
1224
  # Restore axes (plot frame) via stored bbox if present
@@ -1287,15 +1286,9 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
1287
1286
  position_right_ylabel()
1288
1287
  except Exception:
1289
1288
  pass
1290
- # Also reposition bottom/left titles to consume pending pads and match tick label visibility
1291
- try:
1292
- position_bottom_xlabel()
1293
- except Exception:
1294
- pass
1295
- try:
1296
- position_left_ylabel()
1297
- except Exception:
1298
- pass
1289
+ # Note: Do NOT call position_bottom_xlabel() / position_left_ylabel() here
1290
+ # as it causes title drift when combined with fig.canvas.draw() below.
1291
+ # Title offsets are already restored from snapshot above.
1299
1292
 
1300
1293
  # Spines (linewidth, color, visibility)
1301
1294
  for name, spec in snap.get("spines", {}).items():
@@ -1737,7 +1730,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
1737
1730
  show_cif_titles=(bool(getattr(_bp,'show_cif_titles', True)) if _bp is not None else True),
1738
1731
  skip_confirm=skip_confirm,
1739
1732
  )
1740
- print(f"Saved session to {target_path}")
1733
+ # Message already printed by dump_session
1741
1734
  fig._last_session_save_path = target_path
1742
1735
  continue
1743
1736
  else:
@@ -1777,7 +1770,7 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
1777
1770
  show_cif_titles=(bool(getattr(_bp,'show_cif_titles', True)) if _bp is not None else True),
1778
1771
  skip_confirm=skip_confirm,
1779
1772
  )
1780
- print(f"Saved session to {target_path}")
1773
+ # Message already printed by dump_session
1781
1774
  fig._last_session_save_path = target_path
1782
1775
  except Exception as e:
1783
1776
  print(f"Error saving session: {e}")
@@ -2697,10 +2690,11 @@ def interactive_menu(fig, ax, y_data_list, x_data_list, labels, orig_y,
2697
2690
  eps = abs(y_min)*1e-6 if y_min != 0 else 1e-6
2698
2691
  y_min -= eps
2699
2692
  y_max += eps
2700
- ax.set_ylim(y_min, y_max)
2693
+ ax.set_ylim(y_min, y_max)
2701
2694
  update_labels(ax, y_data_list, label_text_objects, args.stack, getattr(fig, '_stack_label_at_bottom', False))
2702
2695
  fig.canvas.draw_idle()
2703
- print(f"Y range set to {ax.get_ylim()}")
2696
+ ymin, ymax = ax.get_ylim()
2697
+ print(f"Y range set to ({float(ymin)}, {float(ymax)})")
2704
2698
  except Exception as e:
2705
2699
  print(f"Error setting Y-axis range: {e}")
2706
2700
  elif key == 'd': # <-- DELTA / OFFSET HANDLER (now only reachable if not args.stack)
batplot/session.py CHANGED
@@ -36,11 +36,116 @@ import os
36
36
 
37
37
  import matplotlib.pyplot as plt
38
38
  import numpy as np
39
+ import sys
39
40
 
40
41
  from .utils import _confirm_overwrite
41
42
  from .color_utils import ensure_colormap
42
43
 
43
44
 
45
+ def _get_package_versions():
46
+ """Get versions of critical packages for compatibility checking.
47
+
48
+ Returns:
49
+ dict: Package name -> version string mapping
50
+ """
51
+ versions = {}
52
+ critical_packages = ['numpy', 'matplotlib', 'scipy']
53
+
54
+ for pkg_name in critical_packages:
55
+ try:
56
+ mod = __import__(pkg_name)
57
+ versions[pkg_name] = getattr(mod, '__version__', 'unknown')
58
+ except ImportError:
59
+ versions[pkg_name] = 'not_installed'
60
+ except Exception:
61
+ versions[pkg_name] = 'unknown'
62
+
63
+ # Also track Python version
64
+ versions['python'] = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
65
+
66
+ return versions
67
+
68
+
69
+ def _check_package_compatibility(saved_versions, current_versions, filename):
70
+ """Check if package versions are compatible and warn/error if not.
71
+
72
+ Args:
73
+ saved_versions: dict of package -> version from saved session
74
+ current_versions: dict of package -> version currently installed
75
+ filename: session filename for error messages
76
+
77
+ Returns:
78
+ bool: True if compatible (or can proceed), False if should abort
79
+ """
80
+ if not saved_versions:
81
+ # Old session file without version info - allow but warn
82
+ print("Warning: Session file does not contain package version information.")
83
+ print("This may cause compatibility issues. Consider recreating the session.")
84
+ return True
85
+
86
+ mismatches = []
87
+ critical_mismatches = []
88
+
89
+ for pkg in ['numpy', 'matplotlib', 'python']:
90
+ saved_ver = saved_versions.get(pkg, 'unknown')
91
+ current_ver = current_versions.get(pkg, 'unknown')
92
+
93
+ if saved_ver == 'unknown' or current_ver == 'unknown':
94
+ continue
95
+
96
+ if saved_ver != current_ver:
97
+ # Check for critical incompatibilities
98
+ if pkg == 'numpy':
99
+ # numpy 2.0+ uses _core, <2.0 doesn't - this is a breaking change
100
+ try:
101
+ saved_major = int(saved_ver.split('.')[0])
102
+ current_major = int(current_ver.split('.')[0])
103
+ if saved_major != current_major:
104
+ critical_mismatches.append((pkg, saved_ver, current_ver))
105
+ else:
106
+ mismatches.append((pkg, saved_ver, current_ver))
107
+ except (ValueError, IndexError):
108
+ mismatches.append((pkg, saved_ver, current_ver))
109
+ elif pkg == 'python':
110
+ # Python major version changes are critical
111
+ try:
112
+ saved_major = int(saved_ver.split('.')[0])
113
+ current_major = int(current_ver.split('.')[0])
114
+ if saved_major != current_major:
115
+ critical_mismatches.append((pkg, saved_ver, current_ver))
116
+ else:
117
+ mismatches.append((pkg, saved_ver, current_ver))
118
+ except (ValueError, IndexError):
119
+ mismatches.append((pkg, saved_ver, current_ver))
120
+ else:
121
+ mismatches.append((pkg, saved_ver, current_ver))
122
+
123
+ if critical_mismatches:
124
+ print(f"\nERROR: Critical package version mismatch detected for session: {filename}")
125
+ print("The following packages have incompatible major version changes:")
126
+ for pkg, saved, current in critical_mismatches:
127
+ print(f" {pkg}: saved with {saved}, current is {current}")
128
+ print("\nThis will likely cause pickle loading to fail.")
129
+ print("Solutions:")
130
+ print(" 1. Install matching package versions:")
131
+ for pkg, saved, current in critical_mismatches:
132
+ print(f" pip install '{pkg}=={saved}' # or use conda")
133
+ print(" 2. Recreate the session from original data files")
134
+ return False
135
+
136
+ if mismatches:
137
+ print(f"\nWarning: Package version differences detected for session: {filename}")
138
+ print("Minor version differences may be okay, but proceed with caution:")
139
+ for pkg, saved, current in mismatches:
140
+ print(f" {pkg}: saved with {saved}, current is {current}")
141
+ print("If you encounter errors, try matching the saved versions.")
142
+ response = input("Continue loading session? (y/n): ").strip().lower()
143
+ if response != 'y':
144
+ return False
145
+
146
+ return True
147
+
148
+
44
149
  def _current_tick_width(axis_obj, which: str):
45
150
  """
46
151
  Return the configured tick width for the given X/Y axis.
@@ -431,8 +536,12 @@ def dump_session(
431
536
  wasd_state = _capture_wasd_state(ax)
432
537
 
433
538
  try:
539
+ # Get current package versions for compatibility checking
540
+ package_versions = _get_package_versions()
541
+
434
542
  sess = {
435
543
  'version': 3,
544
+ 'package_versions': package_versions, # Track versions for compatibility checking
436
545
  'x_data': [np.array(a) for a in x_data_list],
437
546
  'y_data': [np.array(a) for a in y_data_list],
438
547
  'orig_y': [np.array(a) for a in orig_y],
@@ -507,6 +616,8 @@ def dump_session(
507
616
  }
508
617
  # Save curve names visibility
509
618
  sess['curve_names_visible'] = bool(getattr(fig, '_curve_names_visible', True))
619
+ # Save whether data were plotted with swapped axes via --ro
620
+ sess['ro_active'] = bool(getattr(fig, '_ro_active', False))
510
621
  # Save stack/legend anchor preferences
511
622
  sess['stack_label_at_bottom'] = bool(getattr(fig, '_stack_label_at_bottom', False))
512
623
  sess['label_anchor_left'] = bool(getattr(fig, '_label_anchor_left', False))
@@ -752,9 +863,13 @@ def dump_operando_session(
752
863
  cb_h_offset = getattr(cbar.ax, '_cb_h_offset_in', 0.0)
753
864
  ec_h_offset = getattr(ec_ax, '_ec_h_offset_in', 0.0) if ec_ax is not None else None
754
865
 
866
+ # Get current package versions for compatibility checking
867
+ package_versions = _get_package_versions()
868
+
755
869
  sess = {
756
870
  'kind': 'operando_ec',
757
871
  'version': 2,
872
+ 'package_versions': package_versions, # Track versions for compatibility checking
758
873
  'figure': {'size': (fig_w, fig_h), 'dpi': dpi},
759
874
  'layout_inches': {
760
875
  'cb_w_in': cb_w_in,
@@ -819,6 +934,26 @@ def load_operando_session(filename: str):
819
934
  try:
820
935
  with open(filename, 'rb') as f:
821
936
  sess = pickle.load(f)
937
+ except ModuleNotFoundError as e:
938
+ # Handle numpy._core and other module import errors
939
+ if '_core' in str(e) or 'numpy' in str(e).lower():
940
+ print(f"\nERROR: NumPy version mismatch detected when loading: {filename}")
941
+ print("This session was saved with a different NumPy version.")
942
+ print("The error 'No module named numpy._core' indicates:")
943
+ print(" - Session saved with NumPy 2.0+ but loading with NumPy <2.0, OR")
944
+ print(" - Session saved with NumPy <2.0 but loading with NumPy 2.0+")
945
+ print("\nSolutions:")
946
+ print(" 1. Check NumPy version: python3 -c 'import numpy; print(numpy.__version__)'")
947
+ print(" 2. Install matching version:")
948
+ print(" - If session was saved with NumPy 2.0+: pip install 'numpy>=2.0'")
949
+ print(" - If session was saved with NumPy <2.0: pip install 'numpy<2.0'")
950
+ print(" 3. Recreate the session from original data files")
951
+ else:
952
+ print(f"\nERROR: Module import error when loading: {filename}")
953
+ print(f"Error: {e}")
954
+ print("This usually indicates a package version mismatch.")
955
+ print("Try installing matching package versions or recreate the session.")
956
+ return None
822
957
  except Exception as e:
823
958
  print(f"Failed to load session: {e}")
824
959
  return None
@@ -826,6 +961,12 @@ def load_operando_session(filename: str):
826
961
  if not isinstance(sess, dict) or sess.get('kind') != 'operando_ec':
827
962
  print("Not an operando+EC session file.")
828
963
  return None
964
+
965
+ # Check package version compatibility
966
+ saved_versions = sess.get('package_versions', {})
967
+ current_versions = _get_package_versions()
968
+ if not _check_package_compatibility(saved_versions, current_versions, filename):
969
+ return None
829
970
 
830
971
  # Use standard DPI of 100 instead of saved DPI to avoid display-dependent issues
831
972
  # (Retina displays, Windows scaling, etc. can cause saved DPI to differ)
@@ -1605,9 +1746,13 @@ def dump_ec_session(
1605
1746
  legend_xy_in = (float(xy[0]), float(xy[1]))
1606
1747
  except Exception:
1607
1748
  legend_xy_in = None
1749
+ # Get current package versions for compatibility checking
1750
+ package_versions = _get_package_versions()
1751
+
1608
1752
  sess = {
1609
1753
  'kind': 'ec_gc',
1610
1754
  'version': 2,
1755
+ 'package_versions': package_versions, # Track versions for compatibility checking
1611
1756
  'figure': {'size': (fig_w, fig_h), 'dpi': dpi},
1612
1757
  'axis': axis,
1613
1758
  'subplot_margins': subplot_margins,
@@ -1657,6 +1802,26 @@ def load_ec_session(filename: str):
1657
1802
  try:
1658
1803
  with open(filename, 'rb') as f:
1659
1804
  sess = pickle.load(f)
1805
+ except ModuleNotFoundError as e:
1806
+ # Handle numpy._core and other module import errors
1807
+ if '_core' in str(e) or 'numpy' in str(e).lower():
1808
+ print(f"\nERROR: NumPy version mismatch detected when loading: {filename}")
1809
+ print("This session was saved with a different NumPy version.")
1810
+ print("The error 'No module named numpy._core' indicates:")
1811
+ print(" - Session saved with NumPy 2.0+ but loading with NumPy <2.0, OR")
1812
+ print(" - Session saved with NumPy <2.0 but loading with NumPy 2.0+")
1813
+ print("\nSolutions:")
1814
+ print(" 1. Check NumPy version: python3 -c 'import numpy; print(numpy.__version__)'")
1815
+ print(" 2. Install matching version:")
1816
+ print(" - If session was saved with NumPy 2.0+: pip install 'numpy>=2.0'")
1817
+ print(" - If session was saved with NumPy <2.0: pip install 'numpy<2.0'")
1818
+ print(" 3. Recreate the session from original data files")
1819
+ else:
1820
+ print(f"\nERROR: Module import error when loading: {filename}")
1821
+ print(f"Error: {e}")
1822
+ print("This usually indicates a package version mismatch.")
1823
+ print("Try installing matching package versions or recreate the session.")
1824
+ return None
1660
1825
  except Exception as e:
1661
1826
  print(f"Failed to load EC session: {e}")
1662
1827
  return None
@@ -1664,6 +1829,12 @@ def load_ec_session(filename: str):
1664
1829
  if not isinstance(sess, dict) or sess.get('kind') != 'ec_gc':
1665
1830
  print("Not an EC GC session file.")
1666
1831
  return None
1832
+
1833
+ # Check package version compatibility
1834
+ saved_versions = sess.get('package_versions', {})
1835
+ current_versions = _get_package_versions()
1836
+ if not _check_package_compatibility(saved_versions, current_versions, filename):
1837
+ return None
1667
1838
 
1668
1839
  # Use standard DPI of 100 instead of saved DPI to avoid display-dependent issues
1669
1840
  # (Retina displays, Windows scaling, etc. can cause saved DPI to differ)
@@ -2368,9 +2539,13 @@ def dump_cpc_session(
2368
2539
  'right_y': float(getattr(ax2, '_right_ylabel_manual_offset_y_pts', 0.0) or 0.0),
2369
2540
  }
2370
2541
 
2542
+ # Get current package versions for compatibility checking
2543
+ package_versions = _get_package_versions()
2544
+
2371
2545
  meta = {
2372
2546
  'kind': 'cpc',
2373
2547
  'version': 2, # Incremented version for new format
2548
+ 'package_versions': package_versions, # Track versions for compatibility checking
2374
2549
  'figure': {
2375
2550
  'size': (fig_w, fig_h),
2376
2551
  'dpi': dpi,
@@ -2526,12 +2701,38 @@ def load_cpc_session(filename: str):
2526
2701
  try:
2527
2702
  with open(filename, 'rb') as f:
2528
2703
  sess = pickle.load(f)
2704
+ except ModuleNotFoundError as e:
2705
+ # Handle numpy._core and other module import errors
2706
+ if '_core' in str(e) or 'numpy' in str(e).lower():
2707
+ print(f"\nERROR: NumPy version mismatch detected when loading: {filename}")
2708
+ print("This session was saved with a different NumPy version.")
2709
+ print("The error 'No module named numpy._core' indicates:")
2710
+ print(" - Session saved with NumPy 2.0+ but loading with NumPy <2.0, OR")
2711
+ print(" - Session saved with NumPy <2.0 but loading with NumPy 2.0+")
2712
+ print("\nSolutions:")
2713
+ print(" 1. Check NumPy version: python3 -c 'import numpy; print(numpy.__version__)'")
2714
+ print(" 2. Install matching version:")
2715
+ print(" - If session was saved with NumPy 2.0+: pip install 'numpy>=2.0'")
2716
+ print(" - If session was saved with NumPy <2.0: pip install 'numpy<2.0'")
2717
+ print(" 3. Recreate the session from original data files")
2718
+ else:
2719
+ print(f"\nERROR: Module import error when loading: {filename}")
2720
+ print(f"Error: {e}")
2721
+ print("This usually indicates a package version mismatch.")
2722
+ print("Try installing matching package versions or recreate the session.")
2723
+ return None
2529
2724
  except Exception as e:
2530
2725
  print(f"Failed to load session: {e}")
2531
2726
  return None
2532
2727
  if not isinstance(sess, dict) or sess.get('kind') != 'cpc':
2533
2728
  print("Not a CPC session file.")
2534
2729
  return None
2730
+
2731
+ # Check package version compatibility
2732
+ saved_versions = sess.get('package_versions', {})
2733
+ current_versions = _get_package_versions()
2734
+ if not _check_package_compatibility(saved_versions, current_versions, filename):
2735
+ return None
2535
2736
  try:
2536
2737
  # Use standard DPI of 100 instead of saved DPI to avoid display-dependent issues
2537
2738
  # (Retina displays, Windows scaling, etc. can cause saved DPI to differ)
batplot/style.py CHANGED
@@ -358,6 +358,13 @@ def print_style_info(
358
358
  print(f"Font family chain (rcParams['font.sans-serif']): {plt.rcParams.get('font.sans-serif')}")
359
359
  print(f"Mathtext fontset: {plt.rcParams.get('mathtext.fontset')}")
360
360
 
361
+ # Report whether data axes were swapped via --ro when this figure was created
362
+ try:
363
+ ro_active = bool(getattr(fig, "_ro_active", False))
364
+ except Exception:
365
+ ro_active = False
366
+ print(f"Data axes swapped via --ro: {'YES' if ro_active else 'no'}")
367
+
361
368
  # Rotation angle
362
369
  rotation_angle = getattr(ax, '_rotation_angle', 0)
363
370
  if rotation_angle != 0:
@@ -646,6 +653,8 @@ def export_style_config(
646
653
  }
647
654
  # Save rotation angle
648
655
  cfg["rotation_angle"] = getattr(ax, '_rotation_angle', 0)
656
+ # Track whether data axes were swapped via --ro when this style was saved
657
+ cfg["ro_active"] = bool(getattr(fig, '_ro_active', False))
649
658
 
650
659
  # Save curve names visibility
651
660
  cfg["curve_names_visible"] = True # Default to visible
@@ -853,6 +862,17 @@ def apply_style_config(
853
862
  except Exception as e:
854
863
  print(f"Could not read config: {e}")
855
864
  return
865
+ # Enforce compatibility between style/geometry ro state and current figure ro state.
866
+ # Styles saved from a plot using --ro (swapped x/y) must not be applied to a non-ro plot, and vice versa.
867
+ file_ro = bool(cfg.get("ro_active", False))
868
+ current_ro = bool(getattr(fig, "_ro_active", False))
869
+ if file_ro != current_ro:
870
+ if file_ro:
871
+ print("Warning: Style/geometry file was saved with --ro (swapped x/y axes); current plot is not using --ro.")
872
+ else:
873
+ print("Warning: Style/geometry file was saved without --ro; current plot was created with --ro.")
874
+ print("Not applying style/geometry to avoid corrupting axis orientation.")
875
+ return
856
876
  # Save current labelpad values BEFORE any style changes
857
877
  saved_xlabelpad = None
858
878
  saved_ylabelpad = None
@@ -878,8 +898,7 @@ def apply_style_config(
878
898
  if not keep_canvas_fixed:
879
899
  # Use forward=False to prevent automatic subplot adjustment that can shift the plot
880
900
  fig.set_size_inches(fw, fh, forward=False)
881
- else:
882
- print("(Canvas fixed) Ignoring style figure size request.")
901
+ # No message needed when canvas is fixed - this is normal behavior
883
902
  except Exception as e:
884
903
  print(f"Warning: could not parse figure size: {e}")
885
904
  try:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: batplot
3
- Version: 1.8.2
3
+ Version: 1.8.3
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,28 +1,28 @@
1
- batplot/__init__.py,sha256=cmYE6SKId-NqANj2k5EUEXFUnzP58FNBG7XRF6CLfgQ,118
1
+ batplot/__init__.py,sha256=RAScNSaXpWIGw1hbfjWc2RDcKAf-ftvoSfY-3DOZTF0,118
2
2
  batplot/args.py,sha256=mrDjMURp_OQnXrAwl9WnE_FW4HJu940O7NmL9QWQnD4,35189
3
- batplot/batch.py,sha256=YQ7obCIqLCObwDbM7TXpOBh7g7BO95wZNsa2Fy84c6o,53858
4
- batplot/batplot.py,sha256=Avb0Txs-VaG1UU-bL-7HWnZf_8RS_n-KTnZz9O_3GPA,174374
3
+ batplot/batch.py,sha256=P2HNBYqrSzDr2fruYz8BFKwNFX6vdJpHDZpaG_hmEc8,55253
4
+ batplot/batplot.py,sha256=Yd6wyMTRgu65BY9ousd_3Zxoauce99vYqzeSgqhxv8U,177164
5
5
  batplot/cif.py,sha256=JfHwNf3SHrcpALc_F5NjJmQ3lg71MBRSaIUJjGYPTx8,30120
6
6
  batplot/cli.py,sha256=ScDb2je8VQ0mz_z0SLCHEigiTuFPY5pb1snnzCouKms,5828
7
7
  batplot/color_utils.py,sha256=7InQLVo1XTg7sgAbltM2KeDSFJgr787YEaV9vJbIoWY,20460
8
8
  batplot/config.py,sha256=6nGY7fKN4T5KZUGQS2ArUBgEkLAL0j37XwG5SCVQgKA,6420
9
9
  batplot/converters.py,sha256=rR2WMPM0nR5E3eZI3gWbaJf_AfbdQx3urVSbJmZXNzo,8237
10
- batplot/cpc_interactive.py,sha256=r70RVu75QAPEWkK_bR4c3MWDXNSwHYxOAYGEVf0TgUg,238755
11
- batplot/electrochem_interactive.py,sha256=ti7V8BoAxUk4BD_vDRKAu5ydlHMl75htLvdVYFUUVsw,221778
12
- batplot/interactive.py,sha256=2hQrvV-7VEz0cscIb3Bc7qQ9MpRxP4hjZ6raEnpl5yg,206779
10
+ batplot/cpc_interactive.py,sha256=qadWV2PaQMsqM16mSp5r1-WP7di0JCnzNI4RJy27alo,239616
11
+ batplot/electrochem_interactive.py,sha256=8mFr5vtWb_ZDiJmZWXjkARq21D2GflT8-J2xvCGvDIc,222769
12
+ batplot/interactive.py,sha256=5u2ulhTzRr7fA-INc5hfcz7xaHObnnigiXjrgdtE7XE,206728
13
13
  batplot/manual.py,sha256=pbRI6G4Pm12pOW8LrOLWWu7IEOtqWN3tRHtgge50LlA,11556
14
14
  batplot/modes.py,sha256=qE2OsOQQKhwOWene5zxJeuuewTrZxubtahQuz5je7ok,37252
15
15
  batplot/operando.py,sha256=p2Ug1mFUQxaU702cTBGgJKb3_v1C2p3LLUwfXaVBpPY,28311
16
16
  batplot/operando_ec_interactive.py,sha256=8GQ47-I8SLTS88sFEk8m3vDxFEjSfD3hao62Qke7SxA,305137
17
17
  batplot/plotting.py,sha256=hG2_EdDhF1Qpn1XfZKdCQ5-w_m9gUYFbr804UQ5QjsU,10841
18
18
  batplot/readers.py,sha256=kAI0AvYrdfGRZkvADJ4riN96IWtrH24aAoZpBtONTbw,112960
19
- batplot/session.py,sha256=WuBZR-HKDr4VlqjwynyK8gCkx6xLoHzNaGBH948p9jU,135638
20
- batplot/style.py,sha256=ig1ozX4dhEsXf5JKaPZOvgVS3CWx-BTFSc3vfAH3Y-E,62274
19
+ batplot/session.py,sha256=5eZLJHE66wtZPwNsnR1I2iyEvxY3cLTl3aY7HNxpX6U,145772
20
+ batplot/style.py,sha256=jXtFaJR1aa6vIHupmDNqY2NY5Rgtw49UxF7cS4y8fCA,63375
21
21
  batplot/ui.py,sha256=ifpbK74juUzLMCt-sJGVaWtpDb1NMRJzs2YyiwwafzY,35302
22
22
  batplot/utils.py,sha256=LY2-Axr3DsQMTxuXe48vSjrLJKEnkzkZjdSFdQizbpg,37599
23
23
  batplot/version_check.py,sha256=--U_74DKgHbGtVdBsg9DfUJ10S5-OMXT-rzaYjK0JBc,9997
24
24
  batplot/data/USER_MANUAL.md,sha256=VYPvNZt3Fy8Z4Izr2FnQBw9vEaFTPkybhHDnF-OuKws,17694
25
- batplot-1.8.2.dist-info/licenses/LICENSE,sha256=2PAnHeCiTfgI7aKZLWr0G56HI9fGKQ0CEbQ02H-yExQ,1065
25
+ batplot-1.8.3.dist-info/licenses/LICENSE,sha256=2PAnHeCiTfgI7aKZLWr0G56HI9fGKQ0CEbQ02H-yExQ,1065
26
26
  batplot_backup_20251121_223043/__init__.py,sha256=3s2DUQuTbWs65hoN9cQQ8IiJbaFJY8fNxiCpwRBYoOA,118
27
27
  batplot_backup_20251121_223043/args.py,sha256=OH-h84QhN-IhMS8sPAsSEqccHD3wpeMgmXa_fqv5xtg,21215
28
28
  batplot_backup_20251121_223043/batch.py,sha256=oI7PONJyciHDOqNPq-8fnOQMyn9CpAdVznKaEdsy0ig,48650
@@ -68,8 +68,8 @@ batplot_backup_20251221_101150/style.py,sha256=ig1ozX4dhEsXf5JKaPZOvgVS3CWx-BTFS
68
68
  batplot_backup_20251221_101150/ui.py,sha256=ifpbK74juUzLMCt-sJGVaWtpDb1NMRJzs2YyiwwafzY,35302
69
69
  batplot_backup_20251221_101150/utils.py,sha256=LY2-Axr3DsQMTxuXe48vSjrLJKEnkzkZjdSFdQizbpg,37599
70
70
  batplot_backup_20251221_101150/version_check.py,sha256=ztTHwqgWd8OlS9PLLY5A_TabWxBASDA_-5yyN15PZC8,9996
71
- batplot-1.8.2.dist-info/METADATA,sha256=ZNkBWCjEFRJVKuq1FtQ9JCovRWnEm3lmqWs8eYsvMzU,7406
72
- batplot-1.8.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
73
- batplot-1.8.2.dist-info/entry_points.txt,sha256=73GgH3Zs-qGIvgiyQLgGsSW-ryOwPPKHveOW6TDIR5Q,82
74
- batplot-1.8.2.dist-info/top_level.txt,sha256=Z5Q4sAiT_FDqZqhlLsYn9avRTuFAEEf3AVfkswxOb18,70
75
- batplot-1.8.2.dist-info/RECORD,,
71
+ batplot-1.8.3.dist-info/METADATA,sha256=BNWy96koh0Pcj9CDJYuJ6Xp44krYa8Wg9SM1T4qhYhA,7406
72
+ batplot-1.8.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
73
+ batplot-1.8.3.dist-info/entry_points.txt,sha256=73GgH3Zs-qGIvgiyQLgGsSW-ryOwPPKHveOW6TDIR5Q,82
74
+ batplot-1.8.3.dist-info/top_level.txt,sha256=Z5Q4sAiT_FDqZqhlLsYn9avRTuFAEEf3AVfkswxOb18,70
75
+ batplot-1.8.3.dist-info/RECORD,,