batplot 1.1.0__tar.gz → 1.1.2__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.
Files changed (31) hide show
  1. {batplot-1.1.0 → batplot-1.1.2}/PKG-INFO +7 -4
  2. {batplot-1.1.0 → batplot-1.1.2}/README.md +6 -3
  3. {batplot-1.1.0 → batplot-1.1.2}/batplot/args.py +38 -22
  4. {batplot-1.1.0 → batplot-1.1.2}/batplot/batch.py +13 -9
  5. {batplot-1.1.0 → batplot-1.1.2}/batplot/batplot.py +151 -71
  6. batplot-1.1.2/batplot/batplot_new.py +0 -0
  7. {batplot-1.1.0 → batplot-1.1.2}/batplot/cpc_interactive.py +608 -71
  8. {batplot-1.1.0 → batplot-1.1.2}/batplot/electrochem_interactive.py +9 -5
  9. {batplot-1.1.0 → batplot-1.1.2}/batplot/interactive.py +2 -5
  10. {batplot-1.1.0 → batplot-1.1.2}/batplot/modes.py +9 -5
  11. {batplot-1.1.0 → batplot-1.1.2}/batplot/readers.py +234 -105
  12. {batplot-1.1.0 → batplot-1.1.2}/batplot/session.py +40 -5
  13. {batplot-1.1.0 → batplot-1.1.2}/batplot/style.py +2 -5
  14. {batplot-1.1.0 → batplot-1.1.2}/batplot.egg-info/PKG-INFO +7 -4
  15. {batplot-1.1.0 → batplot-1.1.2}/batplot.egg-info/SOURCES.txt +1 -0
  16. {batplot-1.1.0 → batplot-1.1.2}/pyproject.toml +1 -1
  17. {batplot-1.1.0 → batplot-1.1.2}/LICENSE +0 -0
  18. {batplot-1.1.0 → batplot-1.1.2}/batplot/__init__.py +0 -0
  19. {batplot-1.1.0 → batplot-1.1.2}/batplot/cif.py +0 -0
  20. {batplot-1.1.0 → batplot-1.1.2}/batplot/cli.py +0 -0
  21. {batplot-1.1.0 → batplot-1.1.2}/batplot/converters.py +0 -0
  22. {batplot-1.1.0 → batplot-1.1.2}/batplot/operando.py +0 -0
  23. {batplot-1.1.0 → batplot-1.1.2}/batplot/operando_ec_interactive.py +0 -0
  24. {batplot-1.1.0 → batplot-1.1.2}/batplot/plotting.py +0 -0
  25. {batplot-1.1.0 → batplot-1.1.2}/batplot/ui.py +0 -0
  26. {batplot-1.1.0 → batplot-1.1.2}/batplot/utils.py +0 -0
  27. {batplot-1.1.0 → batplot-1.1.2}/batplot.egg-info/dependency_links.txt +0 -0
  28. {batplot-1.1.0 → batplot-1.1.2}/batplot.egg-info/entry_points.txt +0 -0
  29. {batplot-1.1.0 → batplot-1.1.2}/batplot.egg-info/requires.txt +0 -0
  30. {batplot-1.1.0 → batplot-1.1.2}/batplot.egg-info/top_level.txt +0 -0
  31. {batplot-1.1.0 → batplot-1.1.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: batplot
3
- Version: 1.1.0
3
+ Version: 1.1.2
4
4
  Summary: Interactive plotting for XRD, PDF, and XAS data (.xye, .xy, .qye, .dat, .csv, .gr, .nor, .chik, .chir)
5
5
  Author-email: Tian Dai <tianda@uio.no>
6
6
  License: MIT License
@@ -55,10 +55,10 @@ Dynamic: license-file
55
55
 
56
56
  ## Features
57
57
 
58
- - **Electrochemistry Modes**: Galvanostatic cycling (GC), cyclic voltammetry (CV), differential capacity (dQdV), chronopotentiometry (CPC)
58
+ - **Electrochemistry Modes**: Galvanostatic cycling (GC), cyclic voltammetry (CV), differential capacity (dQdV), capacity per cycle (CPC) with multi-file support
59
59
  - **Structural Characterization**: XRD, PDF, XAS (XANES/EXAFS)
60
60
  - **Operando Analysis**: Correlate in-situ structural changes with electrochemical data
61
- - **Interactive Menu**: Real-time styling, cycle visibility control, color customization
61
+ - **Interactive Menu**: Real-time styling, cycle visibility control, individual color customization for multi-file plots
62
62
  - **Session Persistence**: Save and reload complete plot states with `.pkl` files
63
63
  - **Style Management**: Import/export plot styles as `.bpcfg` files
64
64
  - **Batch Processing**: Plot multiple files simultaneously with automatic SVG export
@@ -96,9 +96,12 @@ batplot cyclic.mpt --cv --interactive
96
96
  # Differential capacity
97
97
  batplot battery.csv --dqdv
98
98
 
99
- # Chronopotentiometry with mass normalization
99
+ # Capacity per cycle - single file
100
100
  batplot stability.mpt --cpc --mass 5.4 --interactive
101
101
 
102
+ # Capacity per cycle - multiple files with individual color control
103
+ batplot file1.csv file2.csv file3.mpt --cpc --mass 5.4 --interactive
104
+
102
105
  # Batch processing: export all EC files to SVG
103
106
  batplot --gc all # All .mpt/.csv files (.mpt needs --mass, .csv doesn't)
104
107
  batplot --cv all # All .mpt files (CV mode)
@@ -6,10 +6,10 @@
6
6
 
7
7
  ## Features
8
8
 
9
- - **Electrochemistry Modes**: Galvanostatic cycling (GC), cyclic voltammetry (CV), differential capacity (dQdV), chronopotentiometry (CPC)
9
+ - **Electrochemistry Modes**: Galvanostatic cycling (GC), cyclic voltammetry (CV), differential capacity (dQdV), capacity per cycle (CPC) with multi-file support
10
10
  - **Structural Characterization**: XRD, PDF, XAS (XANES/EXAFS)
11
11
  - **Operando Analysis**: Correlate in-situ structural changes with electrochemical data
12
- - **Interactive Menu**: Real-time styling, cycle visibility control, color customization
12
+ - **Interactive Menu**: Real-time styling, cycle visibility control, individual color customization for multi-file plots
13
13
  - **Session Persistence**: Save and reload complete plot states with `.pkl` files
14
14
  - **Style Management**: Import/export plot styles as `.bpcfg` files
15
15
  - **Batch Processing**: Plot multiple files simultaneously with automatic SVG export
@@ -47,9 +47,12 @@ batplot cyclic.mpt --cv --interactive
47
47
  # Differential capacity
48
48
  batplot battery.csv --dqdv
49
49
 
50
- # Chronopotentiometry with mass normalization
50
+ # Capacity per cycle - single file
51
51
  batplot stability.mpt --cpc --mass 5.4 --interactive
52
52
 
53
+ # Capacity per cycle - multiple files with individual color control
54
+ batplot file1.csv file2.csv file3.mpt --cpc --mass 5.4 --interactive
55
+
53
56
  # Batch processing: export all EC files to SVG
54
57
  batplot --gc all # All .mpt/.csv files (.mpt needs --mass, .csv doesn't)
55
58
  batplot --cv all # All .mpt files (CV mode)
@@ -14,23 +14,28 @@ def _print_general_help() -> None:
14
14
  " • EC: GC/CPC/dQdV/CV (from .csv or .mpt)\n"
15
15
  " • Operando: contour maps from a folder of normalized XY and .mpt files\n"
16
16
  " • Batch: export SVG plots for all files in a directory\n\n"
17
- "How to run (basics):\n"
18
- " batplot file1.xy file2.qye [option1] [option2] # XY curves\n"
19
- " batplot all # Batch mode: all XY files in current directory\n"
20
- " batplot --gc FILE.mpt --mass 7.0 # EC GC from .mpt (requires --mass mg)\n"
21
- " batplot --gc FILE.csv # EC GC from supported .csv (no mass required)\n"
22
- " batplot --gc all --mass 7.0 # Batch: all .mpt/.csv in directory (.mpt needs --mass)\n"
23
- " batplot --dqdv FILE.csv # EC dQ/dV from supported .csv\n"
24
- " batplot --dqdv all # Batch: all .csv in directory (dQdV mode)\n"
25
- " batplot --cv FILE.mpt # EC CV (cyclic voltammetry) from .mpt\n"
26
- " batplot --cv all # Batch: all .mpt in directory (CV mode)\n"
27
- " batplot --operando [FOLDER] # Operando contour from folder\n\n"
28
- " [options]\n"
17
+ " Interactive mode: --interactive flag opens a menu for styling, ranges, fonts, export, sessions\n\n"
18
+ "How to run (basics):\n"
19
+ " [XY curves]\n"
20
+ " batplot file1.xy file2.qye [option1] [option2] # XY curves\n"
21
+ " batplot all # Batch mode: all XY files in current directory\n\n"
22
+ " [Electrochemistry]\n"
23
+ " batplot --gc FILE.mpt --mass 7.0 # EC GC from .mpt (requires --mass mg)\n"
24
+ " batplot --gc FILE.csv # EC GC from supported .csv (no mass required)\n"
25
+ " batplot --gc all --mass 7.0 # Batch: all .mpt/.csv in directory (.mpt needs --mass)\n"
26
+ " batplot --dqdv FILE.csv # EC dQ/dV from supported .csv\n"
27
+ " batplot --dqdv all # Batch: all .csv in directory (dQdV mode)\n"
28
+ " batplot --cv FILE.mpt # EC CV (cyclic voltammetry) from .mpt\n"
29
+ " batplot --cv FILE.txt # EC CV (cyclic voltammetry) from .txt\n"
30
+ " batplot --cv all # Batch: all .mpt/.txt in directory (CV mode)\n\n"
31
+ " [Operando]\n"
32
+ " batplot --operando [FOLDER] # Operando contour from folder\n\n"
29
33
  "Features:\n"
30
34
  " • Quick plotting with sensible defaults, no config files needed\n"
31
35
  " • Supports many common file formats (see -h xy/ec/op)\n"
32
36
  " • Interactive menus (--interactive): styling, ranges, fonts, export, sessions\n"
33
37
  " • Batch processing: use 'all' or directory path with any mode\n\n"
38
+
34
39
  "More help:\n"
35
40
  " batplot -h xy # XY file plotting guide\n"
36
41
  " batplot -h ec # Electrochemistry (GC/dQdV/CV/CPC) guide\n"
@@ -47,12 +52,19 @@ def _print_xy_help() -> None:
47
52
  "If mixing 2θ data in Q, give wavelength per-file (file.xye:1.5406) or global --wl.\n\n"
48
53
  "Examples:\n"
49
54
  " batplot a.xye:1.5406 b.qye --stack --interactive\n"
50
- " batplot a.dat b.xy --xaxis 2theta --wl 1.54 --out fig.svg\n"
55
+ " batplot a.dat b.xy --wl 1.54 --out fig.svg\n"
51
56
  " batplot pattern.qye ticks.cif --interactive\n\n"
52
- "Tips:\n"
53
- " --delta/-d spacing between curves; --raw for raw intensity; --xrange min max; --out fig.svg\n"
54
- " Interactive menu: colors, fonts, lines, ticks, ranges, CIF hkl (z), style print/export/import, save.\n"
55
- " CIF ticks/labels for diffraction, style import/export (.bpcfg), session save/load (.pkl)\n\n"
57
+ "Tips and options:\n"
58
+ "[XY plot]\n"
59
+ " --interactive : open interactive menu for styling, ranges, fonts, export, sessions\n"
60
+ " --delta/-d <float> : spacing between curves, e.g. --delta 0.1\n"
61
+ " --raw : plot raw intensity (normalized by default)\n"
62
+ " --xrange/-r <min> <max> : set x-axis range, e.g. --xrange 0 10\n"
63
+ " --out/-o <filename> : save figure to file, e.g. --out file.svg\n"
64
+ " --xaxis <type> : set x-axis type (Q, 2theta, r, k, energy, rft, or user defined), e.g. --xaxis 2theta\n"
65
+ " --wl <float> : set wavelength for Q conversion for all files, e.g. --wl 1.5406\n"
66
+ " --fullprof <args> : FullProf overlay options\n"
67
+ " --stack : stack curves vertically\n"
56
68
  )
57
69
  print(msg)
58
70
 
@@ -60,20 +72,24 @@ def _print_xy_help() -> None:
60
72
  def _print_ec_help() -> None:
61
73
  msg = (
62
74
  "Electrochemistry (GC, dQ/dV, CV, and CPC)\n\n"
75
+ "Use --interactive for styling, colors, line widths, axis scales, etc.\n"
63
76
  "GC from .mpt: requires active mass in mg to compute mAh g⁻¹.\n"
64
77
  " batplot --gc file.mpt --mass 6.5 --interactive\n\n"
65
78
  "GC from supported .csv: specific capacity is read directly (no --mass).\n"
66
79
  " batplot --gc file.csv\n\n"
67
80
  "dQ/dV from supported .csv:\n"
68
81
  " batplot --dqdv file.csv\n\n"
69
- "Cyclic voltammetry (CV) from .mpt: plots voltage vs current for each cycle.\n"
70
- " batplot --cv file.mpt\n\n"
71
- "Capacity-per-cycle (CPC) with coulombic efficiency from .csv or .mpt (for .mpt, provide --mass mg):\n"
82
+ "Cyclic voltammetry (CV) from .mpt or .txt: plots voltage vs current for each cycle.\n"
83
+ " batplot --cv file.mpt\n"
84
+ " batplot --cv file.txt\n\n"
85
+ "Capacity-per-cycle (CPC) with coulombic efficiency from .csv or .mpt.\n"
86
+ "Supports multiple files with individual color customization:\n"
72
87
  " batplot --cpc file.csv\n"
73
- " batplot --cpc file.mpt --mass 1.2\n\n"
88
+ " batplot --cpc file.mpt --mass 1.2\n"
89
+ " batplot --cpc file1.csv file2.csv file3.mpt --mass 1.2 --interactive\n"
74
90
  "Batch mode: Process all files in a directory and export to SVG.\n"
75
91
  " batplot --gc all --mass 7.0 # All .mpt/.csv files (.mpt requires --mass)\n"
76
- " batplot --cv all # All .mpt files (CV mode)\n"
92
+ " batplot --cv all # All .mpt/.txt files (CV mode)\n"
77
93
  " batplot --dqdv all # All .csv files (dQdV mode)\n"
78
94
  " batplot --cpc all --mass 5.4 # All .mpt/.csv files (.mpt requires --mass)\n"
79
95
  " batplot --gc /path/to/folder --mass 6 # Process specific directory\n\n"
@@ -184,7 +184,7 @@ def batch_process_ec(directory: str, args):
184
184
  supported_ext = {'.mpt', '.csv'}
185
185
  elif getattr(args, 'cv', False):
186
186
  mode = 'cv'
187
- supported_ext = {'.mpt'}
187
+ supported_ext = {'.mpt', '.txt'}
188
188
  elif getattr(args, 'dqdv', False):
189
189
  mode = 'dqdv'
190
190
  supported_ext = {'.csv'}
@@ -230,11 +230,11 @@ def batch_process_ec(directory: str, args):
230
230
  specific_capacity, voltage, cycle_numbers, charge_mask, discharge_mask = \
231
231
  read_mpt_file(fpath, mode='gc', mass_mg=mass_mg)
232
232
  cap_x = specific_capacity
233
- x_label = 'Specific Capacity (mAh g⁻¹)'
233
+ x_label = r'Specific Capacity (mAh g$^{-1}$)'
234
234
  elif ext == '.csv':
235
235
  cap_x, voltage, cycle_numbers, charge_mask, discharge_mask = \
236
236
  read_ec_csv_file(fpath, prefer_specific=True)
237
- x_label = 'Specific Capacity (mAh g⁻¹)'
237
+ x_label = r'Specific Capacity (mAh g$^{-1}$)'
238
238
  else:
239
239
  raise ValueError(f"Unsupported file type for GC: {ext}")
240
240
 
@@ -274,10 +274,14 @@ def batch_process_ec(directory: str, args):
274
274
 
275
275
  # ---- CV Mode ----
276
276
  elif mode == 'cv':
277
- if ext != '.mpt':
278
- raise ValueError("CV mode requires .mpt file")
277
+ if ext == '.txt':
278
+ from .readers import read_biologic_txt_file
279
+ voltage, current, cycles = read_biologic_txt_file(fpath, mode='cv')
280
+ elif ext == '.mpt':
281
+ voltage, current, cycles = read_mpt_file(fpath, mode='cv')
282
+ else:
283
+ raise ValueError("CV mode requires .mpt or .txt file")
279
284
 
280
- voltage, current, cycles = read_mpt_file(fpath, mode='cv')
281
285
  cyc_int_raw = np.array(np.rint(cycles), dtype=int)
282
286
  if cyc_int_raw.size:
283
287
  min_c = int(np.min(cyc_int_raw))
@@ -307,7 +311,7 @@ def batch_process_ec(directory: str, args):
307
311
  voltage, dqdv = read_ec_csv_dqdv_file(fpath)
308
312
  ax_b.plot(voltage, dqdv, '-', color='#1f77b4', linewidth=1.5)
309
313
  ax_b.set_xlabel('Voltage (V)')
310
- ax_b.set_ylabel('dQ/dV (mAh g⁻¹ V⁻¹)')
314
+ ax_b.set_ylabel(r'dQ/dV (mAh g$^{-1}$ V$^{-1}$)')
311
315
  ax_b.set_title(f"{fname}")
312
316
 
313
317
  # ---- CPC Mode ----
@@ -320,7 +324,7 @@ def batch_process_ec(directory: str, args):
320
324
  continue
321
325
  cyc_nums, cap_charge, cap_discharge, eff = \
322
326
  read_mpt_file(fpath, mode='cpc', mass_mg=mass_mg)
323
- x_label = 'Specific Capacity (mAh g⁻¹)'
327
+ x_label = r'Specific Capacity (mAh g$^{-1}$)'
324
328
  elif ext == '.csv':
325
329
  # For CSV CPC, read as GC-like data
326
330
  cap_x, voltage, cycle_numbers, charge_mask, discharge_mask = \
@@ -349,7 +353,7 @@ def batch_process_ec(directory: str, args):
349
353
  cyc_nums = np.array([1])
350
354
  cap_charge = np.array([0])
351
355
  cap_discharge = np.array([0])
352
- x_label = 'Specific Capacity (mAh g⁻¹)'
356
+ x_label = r'Specific Capacity (mAh g$^{-1}$)'
353
357
  else:
354
358
  raise ValueError(f"Unsupported file type for CPC: {ext}")
355
359
 
@@ -112,14 +112,19 @@ def batplot_main() -> int:
112
112
  import os as _os
113
113
  import matplotlib.pyplot as _plt
114
114
  if len(args.files) != 1:
115
- print("CV mode: provide exactly one .mpt file.")
115
+ print("CV mode: provide exactly one file (.mpt or .txt).")
116
116
  exit(1)
117
117
  ec_file = args.files[0]
118
118
  if not _os.path.isfile(ec_file):
119
119
  print(f"File not found: {ec_file}")
120
120
  exit(1)
121
121
  try:
122
- voltage, current, cycles = read_mpt_file(ec_file, mode='cv')
122
+ # Support both .mpt and .txt formats
123
+ if ec_file.lower().endswith('.txt'):
124
+ from .readers import read_biologic_txt_file
125
+ voltage, current, cycles = read_biologic_txt_file(ec_file, mode='cv')
126
+ else:
127
+ voltage, current, cycles = read_mpt_file(ec_file, mode='cv')
123
128
  # Normalize cycle indices to start at 1
124
129
  # Find the first cycle with at least 2 data points (needed for plotting)
125
130
  cyc_int_raw = np.array(np.rint(cycles), dtype=int)
@@ -330,12 +335,12 @@ def batplot_main() -> int:
330
335
  print("Example: batplot file.mpt --gc --mass 7.0")
331
336
  exit(1)
332
337
  specific_capacity, voltage, cycle_numbers, charge_mask, discharge_mask = read_mpt_file(ec_file, mode='gc', mass_mg=mass_mg)
333
- x_label_gc = 'Specific Capacity (mAh g⁻¹)'
338
+ x_label_gc = r'Specific Capacity (mAh g$^{-1}$)'
334
339
  cap_x = specific_capacity
335
340
  elif ec_file.lower().endswith('.csv'):
336
341
  # For supported CSV export, use specific capacity directly when available (no mass required)
337
342
  cap_x, voltage, cycle_numbers, charge_mask, discharge_mask = read_ec_csv_file(ec_file, prefer_specific=True)
338
- x_label_gc = 'Specific Capacity (mAh g⁻¹)'
343
+ x_label_gc = r'Specific Capacity (mAh g$^{-1}$)'
339
344
  else:
340
345
  print("GC mode: file must be .mpt or .csv")
341
346
  exit(1)
@@ -573,81 +578,143 @@ def batplot_main() -> int:
573
578
  import os as _os
574
579
  import numpy as _np
575
580
 
576
- if len(args.files) != 1:
577
- print("CPC mode: provide exactly one file (.csv or .mpt).")
578
- exit(1)
579
- ec_file = args.files[0]
580
- if not _os.path.isfile(ec_file):
581
- print(f"File not found: {ec_file}")
581
+ if len(args.files) < 1:
582
+ print("CPC mode: provide at least one file (.csv or .mpt).")
582
583
  exit(1)
584
+
585
+ # Process multiple files
586
+ file_data = [] # List of dicts with file info and data
587
+ # Use Viridis colormap for capacity (charge/discharge) - spreads from purple to yellow
588
+ # Use Plasma colormap for efficiency - spreads from purple to yellow-pink
589
+ import matplotlib.cm as cm
590
+ import matplotlib.colors as mcolors
591
+ n_files = len(args.files)
592
+ viridis = cm.get_cmap('viridis', n_files)
593
+ plasma = cm.get_cmap('plasma', n_files)
594
+
595
+ # Generate colors from colormaps
596
+ capacity_colors = [mcolors.rgb2hex(viridis(i)[:3]) for i in range(n_files)]
597
+ efficiency_colors = [mcolors.rgb2hex(plasma(i)[:3]) for i in range(n_files)]
598
+
599
+ for file_idx, ec_file in enumerate(args.files):
600
+ if not _os.path.isfile(ec_file):
601
+ print(f"File not found: {ec_file}")
602
+ continue
583
603
 
584
- ext = _os.path.splitext(ec_file)[1].lower()
585
- if ext == '.csv':
586
- try:
587
- cap_x, voltage, cycles, chg_mask, dchg_mask = _bp_read_ec_csv(ec_file, prefer_specific=True)
588
- except Exception as e:
589
- print(f"Failed to read EC CSV: {e}")
590
- exit(1)
591
- cyc = _np.array(cycles, dtype=int)
592
- unique_cycles = _np.unique(cyc)
593
- unique_cycles = unique_cycles[_np.isfinite(unique_cycles)]
594
- unique_cycles = [int(x) for x in unique_cycles]
595
- if not unique_cycles:
596
- unique_cycles = [1]
597
- cyc_nums = []
598
- cap_charge = []
599
- cap_discharge = []
600
- eff = []
601
- for c in sorted(unique_cycles):
602
- m_c = (cyc == c)
603
- qchg = _np.nanmax(cap_x[m_c & chg_mask]) if _np.any(m_c & chg_mask) else _np.nan
604
- qdch = _np.nanmax(cap_x[m_c & dchg_mask]) if _np.any(m_c & dchg_mask) else _np.nan
605
- eta = (qdch / qchg * 100.0) if (_np.isfinite(qchg) and qchg > 0 and _np.isfinite(qdch)) else _np.nan
606
- cyc_nums.append(c)
607
- cap_charge.append(qchg)
608
- cap_discharge.append(qdch)
609
- eff.append(eta)
610
- cyc_nums = _np.array(cyc_nums, dtype=float)
611
- cap_charge = _np.array(cap_charge, dtype=float)
612
- cap_discharge = _np.array(cap_discharge, dtype=float)
613
- eff = _np.array(eff, dtype=float)
614
- elif ext == '.mpt':
615
- mass_mg = getattr(args, 'mass', None)
616
- if mass_mg is None:
617
- print("CPC mode (.mpt): --mass parameter is required (active material mass in mg).")
618
- print("Example: batplot data.mpt --cpc --mass 1.2 --interactive")
619
- exit(1)
604
+ ext = _os.path.splitext(ec_file)[1].lower()
605
+ file_basename = _os.path.basename(ec_file)
606
+
620
607
  try:
621
- cyc_nums, cap_charge, cap_discharge, eff = read_mpt_file(ec_file, mode='cpc', mass_mg=mass_mg)
608
+ if ext == '.csv':
609
+ cap_x, voltage, cycles, chg_mask, dchg_mask = read_ec_csv_file(ec_file, prefer_specific=True)
610
+ cyc = _np.array(cycles, dtype=int)
611
+ unique_cycles = _np.unique(cyc)
612
+ unique_cycles = unique_cycles[_np.isfinite(unique_cycles)]
613
+ unique_cycles = [int(x) for x in unique_cycles]
614
+ if not unique_cycles:
615
+ unique_cycles = [1]
616
+ cyc_nums = []
617
+ cap_charge = []
618
+ cap_discharge = []
619
+ eff = []
620
+ for c in sorted(unique_cycles):
621
+ m_c = (cyc == c)
622
+ qchg = _np.nanmax(cap_x[m_c & chg_mask]) if _np.any(m_c & chg_mask) else _np.nan
623
+ qdch = _np.nanmax(cap_x[m_c & dchg_mask]) if _np.any(m_c & dchg_mask) else _np.nan
624
+ eta = (qdch / qchg * 100.0) if (_np.isfinite(qchg) and qchg > 0 and _np.isfinite(qdch)) else _np.nan
625
+ cyc_nums.append(c)
626
+ cap_charge.append(qchg)
627
+ cap_discharge.append(qdch)
628
+ eff.append(eta)
629
+ cyc_nums = _np.array(cyc_nums, dtype=float)
630
+ cap_charge = _np.array(cap_charge, dtype=float)
631
+ cap_discharge = _np.array(cap_discharge, dtype=float)
632
+ eff = _np.array(eff, dtype=float)
633
+ elif ext == '.mpt':
634
+ mass_mg = getattr(args, 'mass', None)
635
+ if mass_mg is None:
636
+ print(f"Skipped {file_basename}: CPC mode (.mpt) requires --mass parameter.")
637
+ continue
638
+ cyc_nums, cap_charge, cap_discharge, eff = read_mpt_file(ec_file, mode='cpc', mass_mg=mass_mg)
639
+ else:
640
+ print(f"Skipped {file_basename}: unsupported format (must be .csv or .mpt)")
641
+ continue
642
+
643
+ # Assign colors: distinct hue for each file
644
+ capacity_color = capacity_colors[file_idx % len(capacity_colors)]
645
+ efficiency_color = efficiency_colors[file_idx % len(efficiency_colors)]
646
+
647
+ file_data.append({
648
+ 'filename': file_basename,
649
+ 'filepath': ec_file,
650
+ 'cyc_nums': cyc_nums,
651
+ 'cap_charge': cap_charge,
652
+ 'cap_discharge': cap_discharge,
653
+ 'eff': eff,
654
+ 'color': capacity_color,
655
+ 'eff_color': efficiency_color,
656
+ 'visible': True
657
+ })
658
+
622
659
  except Exception as e:
623
- print(f"Failed to read .mpt for CPC: {e}")
624
- exit(1)
625
- else:
626
- print("CPC mode: file must be .csv or .mpt")
660
+ print(f"Failed to read {file_basename}: {e}")
661
+ continue
662
+
663
+ if not file_data:
664
+ print("No valid CPC data files to plot.")
627
665
  exit(1)
628
666
 
629
667
  # Plot (same figsize as GC)
630
668
  fig, ax = plt.subplots(figsize=(10, 6))
631
- col_chg = '#1f77b4' # blue
632
- col_dch = '#d62728' # red
633
- sc_charge = ax.scatter(cyc_nums, cap_charge, color=col_chg, label='Charge capacity', s=32, zorder=3)
634
- sc_discharge = ax.scatter(cyc_nums, cap_discharge, color=col_dch, label='Discharge capacity', s=32, zorder=3)
635
669
  ax.set_xlabel('Cycle number', labelpad=8.0)
636
- ax.set_ylabel('Specific Capacity (mAh g⁻¹)', labelpad=8.0)
670
+ ax.set_ylabel(r'Specific Capacity (mAh g$^{-1}$)', labelpad=8.0)
637
671
  ax.grid(True, alpha=0.25, linestyle='--', linewidth=0.8)
638
- ax.legend(loc='best')
639
672
 
640
673
  ax2 = ax.twinx()
641
- sc_eff = ax2.scatter(cyc_nums, eff, color='#2ca02c', marker='^', label='Coulombic efficiency', s=40, alpha=0.85, zorder=3)
642
674
  ax2.set_ylabel('Efficiency (%)', labelpad=8.0)
643
- # Nice y range for efficiency if present
644
- if _np.isfinite(eff).any():
645
- lo = _np.nanmin(eff)
646
- hi = _np.nanmax(eff)
647
- pad = max(1.0, 0.05 * max(1.0, hi - lo))
648
- ax2.set_ylim(max(0.0, lo - pad), min(110.0, hi + pad))
649
-
650
- # Compose a combined legend with extra padding to avoid touching curves
675
+
676
+ # Create scatter plots for each file
677
+ for file_info in file_data:
678
+ cyc_nums = file_info['cyc_nums']
679
+ cap_charge = file_info['cap_charge']
680
+ cap_discharge = file_info['cap_discharge']
681
+ eff = file_info['eff']
682
+ color = file_info['color'] # Warm color for capacity
683
+ eff_color = file_info['eff_color'] # Cold color for efficiency
684
+ label = file_info['filename']
685
+
686
+ # For single file, use simple labels; for multiple files, prefix with filename
687
+ if len(file_data) == 1:
688
+ label_chg = 'Charge capacity'
689
+ label_dch = 'Discharge capacity'
690
+ label_eff = 'Coulombic efficiency'
691
+ else:
692
+ label_chg = f'{label} (Chg)'
693
+ label_dch = f'{label} (Dch)'
694
+ label_eff = f'{label} (Eff)'
695
+
696
+ # Use slightly different shades for charge/discharge from same file
697
+ from matplotlib.colors import to_rgb
698
+ rgb = to_rgb(color)
699
+ # Discharge: darker shade of the warm color
700
+ discharge_color = tuple(max(0, c * 0.7) for c in rgb)
701
+
702
+ sc_charge = ax.scatter(cyc_nums, cap_charge, color=color, label=label_chg,
703
+ s=32, zorder=3, alpha=0.8, marker='o')
704
+ sc_discharge = ax.scatter(cyc_nums, cap_discharge, color=discharge_color, label=label_dch,
705
+ s=32, zorder=3, alpha=0.8, marker='s')
706
+ sc_eff = ax2.scatter(cyc_nums, eff, color=eff_color, marker='^', label=label_eff,
707
+ s=40, alpha=0.7, zorder=3)
708
+
709
+ # Store scatter artists in file_info for interactive menu
710
+ file_info['sc_charge'] = sc_charge
711
+ file_info['sc_discharge'] = sc_discharge
712
+ file_info['sc_eff'] = sc_eff
713
+
714
+ # Set efficiency y-range to 0-120 by default
715
+ ax2.set_ylim(0, 120)
716
+
717
+ # Compose a combined legend
651
718
  try:
652
719
  h1, l1 = ax.get_legend_handles_labels()
653
720
  h2, l2 = ax2.get_legend_handles_labels()
@@ -655,7 +722,7 @@ def batplot_main() -> int:
655
722
  except Exception:
656
723
  pass
657
724
 
658
- # Adjust layout to ensure top and bottom labels/titles are visible (same as GC)
725
+ # Adjust layout to ensure top and bottom labels/titles are visible
659
726
  fig.subplots_adjust(left=0.12, right=0.88, top=0.88, bottom=0.15)
660
727
 
661
728
  if args.interactive and cpc_interactive_menu is not None:
@@ -679,7 +746,20 @@ def batplot_main() -> int:
679
746
  pass
680
747
  plt.show(block=False)
681
748
  try:
682
- cpc_interactive_menu(fig, ax, ax2, sc_charge, sc_discharge, sc_eff)
749
+ # Pass file_data for multi-file support, but keep backward compatibility
750
+ if len(file_data) == 1:
751
+ # Single file: use original signature
752
+ cpc_interactive_menu(fig, ax, ax2,
753
+ file_data[0]['sc_charge'],
754
+ file_data[0]['sc_discharge'],
755
+ file_data[0]['sc_eff'])
756
+ else:
757
+ # Multiple files: pass file_data list
758
+ cpc_interactive_menu(fig, ax, ax2,
759
+ file_data[0]['sc_charge'],
760
+ file_data[0]['sc_discharge'],
761
+ file_data[0]['sc_eff'],
762
+ file_data=file_data)
683
763
  except Exception as _ie:
684
764
  print(f"CPC interactive menu failed: {_ie}")
685
765
  # Keep window open after menu
@@ -1185,9 +1265,9 @@ def batplot_main() -> int:
1185
1265
  else:
1186
1266
  # Keep canvas size as current; avoid surprising resize on load
1187
1267
  pass
1188
- if 'dpi' in fig_cfg:
1189
- try: fig.set_dpi(int(fig_cfg['dpi']))
1190
- except Exception: pass
1268
+ # Don't restore saved DPI - use system default to avoid display-dependent issues
1269
+ # (Retina displays, Windows scaling, etc. can cause saved DPI to differ)
1270
+ # Keeping figure size in inches ensures consistent appearance across platforms
1191
1271
  except Exception:
1192
1272
  pass
1193
1273
  # Restore spines (linewidth, color, visibility) and subplot margins/tick widths (for CLI .pkl load)
File without changes