batplot 1.8.26__tar.gz → 1.8.27__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 (41) hide show
  1. {batplot-1.8.26/batplot.egg-info → batplot-1.8.27}/PKG-INFO +1 -1
  2. {batplot-1.8.26 → batplot-1.8.27}/batplot/__init__.py +1 -1
  3. {batplot-1.8.26 → batplot-1.8.27}/batplot/args.py +18 -7
  4. {batplot-1.8.26 → batplot-1.8.27}/batplot/batch.py +123 -32
  5. {batplot-1.8.26 → batplot-1.8.27}/batplot/batplot.py +343 -79
  6. {batplot-1.8.26 → batplot-1.8.27}/batplot/cpc_interactive.py +858 -312
  7. {batplot-1.8.26 → batplot-1.8.27}/batplot/data/CHANGELOG.md +14 -1
  8. {batplot-1.8.26 → batplot-1.8.27}/batplot/data/USER_MANUAL.md +26 -1
  9. {batplot-1.8.26 → batplot-1.8.27}/batplot/electrochem_interactive.py +580 -331
  10. {batplot-1.8.26 → batplot-1.8.27}/batplot/interactive.py +111 -99
  11. {batplot-1.8.26 → batplot-1.8.27}/batplot/modes.py +16 -1
  12. {batplot-1.8.26 → batplot-1.8.27}/batplot/operando_ec_interactive.py +225 -215
  13. {batplot-1.8.26 → batplot-1.8.27}/batplot/readers.py +114 -12
  14. {batplot-1.8.26 → batplot-1.8.27}/batplot/session.py +142 -90
  15. {batplot-1.8.26 → batplot-1.8.27}/batplot/style.py +16 -7
  16. {batplot-1.8.26 → batplot-1.8.27}/batplot/utils.py +39 -12
  17. {batplot-1.8.26 → batplot-1.8.27}/batplot/version_check.py +3 -2
  18. {batplot-1.8.26 → batplot-1.8.27/batplot.egg-info}/PKG-INFO +1 -1
  19. {batplot-1.8.26 → batplot-1.8.27}/pyproject.toml +1 -1
  20. {batplot-1.8.26 → batplot-1.8.27}/LICENSE +0 -0
  21. {batplot-1.8.26 → batplot-1.8.27}/MANIFEST.in +0 -0
  22. {batplot-1.8.26 → batplot-1.8.27}/NOTICE +0 -0
  23. {batplot-1.8.26 → batplot-1.8.27}/README.md +0 -0
  24. {batplot-1.8.26 → batplot-1.8.27}/USER_MANUAL.md +0 -0
  25. {batplot-1.8.26 → batplot-1.8.27}/batplot/cif.py +0 -0
  26. {batplot-1.8.26 → batplot-1.8.27}/batplot/cli.py +0 -0
  27. {batplot-1.8.26 → batplot-1.8.27}/batplot/color_utils.py +0 -0
  28. {batplot-1.8.26 → batplot-1.8.27}/batplot/config.py +0 -0
  29. {batplot-1.8.26 → batplot-1.8.27}/batplot/converters.py +0 -0
  30. {batplot-1.8.26 → batplot-1.8.27}/batplot/dev_upgrade.py +0 -0
  31. {batplot-1.8.26 → batplot-1.8.27}/batplot/manual.py +0 -0
  32. {batplot-1.8.26 → batplot-1.8.27}/batplot/operando.py +0 -0
  33. {batplot-1.8.26 → batplot-1.8.27}/batplot/plotting.py +0 -0
  34. {batplot-1.8.26 → batplot-1.8.27}/batplot/ui.py +0 -0
  35. {batplot-1.8.26 → batplot-1.8.27}/batplot.egg-info/SOURCES.txt +0 -0
  36. {batplot-1.8.26 → batplot-1.8.27}/batplot.egg-info/dependency_links.txt +0 -0
  37. {batplot-1.8.26 → batplot-1.8.27}/batplot.egg-info/entry_points.txt +0 -0
  38. {batplot-1.8.26 → batplot-1.8.27}/batplot.egg-info/requires.txt +0 -0
  39. {batplot-1.8.26 → batplot-1.8.27}/batplot.egg-info/top_level.txt +0 -0
  40. {batplot-1.8.26 → batplot-1.8.27}/setup.cfg +0 -0
  41. {batplot-1.8.26 → batplot-1.8.27}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: batplot
3
- Version: 1.8.26
3
+ Version: 1.8.27
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.8.26"
3
+ __version__ = "1.8.27"
4
4
 
5
5
  __all__ = ["__version__"]
@@ -296,18 +296,28 @@ def _print_ec_help() -> None:
296
296
  "Use --interactive for styling, colors, line widths, axis scales, etc.\n"
297
297
  "GC from .mpt: requires active mass in mg to compute mAh g⁻¹.\n"
298
298
  " batplot --gc file.mpt --mass 6.5 --interactive\n\n"
299
- "GC from supported .csv: specific capacity is read directly (no --mass).\n"
300
- " batplot --gc file.csv\n\n"
301
- "dQ/dV from supported .csv:\n"
302
- " batplot --dqdv file.csv\n\n"
299
+ "GC from supported .csv: specific capacity read directly when available; use --mass for\n"
300
+ " Neware absolute-capacity files (Cycle Index / Step Index / DataPoint format).\n"
301
+ " batplot --gc file.csv\n"
302
+ " batplot --gc file.csv --mass 3.52 # Neware absolute-capacity CSV\n\n"
303
+ "Per-file mass: repeat --mass once per file that needs it, in file order.\n"
304
+ " batplot f1.mpt --mass 6.5 f2.csv f3.mpt --mass 7.0 --gc\n"
305
+ " batplot f1.csv --mass 3.52 f2.mpt --mass 5.0 --cpc\n"
306
+ " # Files without --mass between them use the global --mass value (or none)\n"
307
+ " # Single --mass applies to all files: batplot f1.mpt f2.mpt --gc --mass 7.0\n\n"
308
+ "dQ/dV from supported .csv (pre-calculated column or computed from GC data):\n"
309
+ " batplot --dqdv file.csv\n"
310
+ " batplot --dqdv file.csv --mass 3.52 # Neware absolute-capacity CSV\n\n"
303
311
  "Cyclic voltammetry (CV) from .mpt or .txt: plots voltage vs current for each cycle.\n"
304
312
  " batplot --cv file.mpt\n"
305
313
  " batplot --cv file.txt\n\n"
306
314
  "Capacity-per-cycle (CPC) with coulombic efficiency from .csv, .xlsx, or .mpt.\n"
307
315
  "Supports multiple files with individual color customization:\n"
308
- " batplot --cpc file.csv # Neware CSV\n"
316
+ " batplot --cpc file.csv # Neware CSV (specific capacity)\n"
317
+ " batplot --cpc file.csv --mass 3.52 # Neware absolute-capacity CSV\n"
309
318
  " batplot --cpc file.xlsx # Landt/Lanhe Excel (Chinese tester)\n"
310
- " batplot --cpc file.mpt --mass 1.2 # Biologic MPT\n"
319
+ " batplot --cpc file.mpt --mass 1.2 # Biologic MPT\n"
320
+ " batplot file1.csv --mass 3.52 file2.mpt --mass 1.2 --cpc # Per-file mass\n"
311
321
  " batplot --cpc file1.csv file2.xlsx file3.mpt --mass 1.2 --interactive\n\n"
312
322
  "Excel support: Landt/Lanhe (蓝电/蓝河) .xlsx files with Chinese headers:\n"
313
323
  " Expected structure: Row 1=filename, Row 2=headers, Row 3+=data\n"
@@ -457,10 +467,11 @@ def build_parser() -> argparse.ArgumentParser:
457
467
  parser.add_argument("--operando", "--contour", action="store_true", dest="operando", help=argparse.SUPPRESS)
458
468
  parser.add_argument("--debug", action="store_true", help=argparse.SUPPRESS)
459
469
  parser.add_argument("--gc", action="store_true", help=argparse.SUPPRESS)
460
- parser.add_argument("--mass", type=float, help=argparse.SUPPRESS)
470
+ parser.add_argument("--mass", type=float, action='append', help=argparse.SUPPRESS)
461
471
  parser.add_argument("--dqdv", action="store_true", help=argparse.SUPPRESS)
462
472
  parser.add_argument("--cv", action="store_true", help=argparse.SUPPRESS)
463
473
  parser.add_argument("--cpc", action="store_true", help=argparse.SUPPRESS)
474
+ parser.add_argument("--epc", action="store_true", help=argparse.SUPPRESS)
464
475
  parser.add_argument("--pw", nargs=2, type=float, metavar=('V_MIN', 'V_MAX'),
465
476
  help=argparse.SUPPRESS)
466
477
  parser.add_argument("--cd", type=float, help=argparse.SUPPRESS)
@@ -18,8 +18,26 @@ from .readers import (
18
18
  read_mpt_file,
19
19
  read_ec_csv_file,
20
20
  read_ec_csv_dqdv_file,
21
+ compute_dqdv_numerical,
22
+ is_cs_b_format,
23
+ _load_csv_header_and_rows,
21
24
  read_biologic_txt_file,
22
25
  )
26
+
27
+
28
+ def _resolve_mass(mass_arg, file_idx: int = 0):
29
+ """Return mass (mg) for file at file_idx from a --mass list or single value."""
30
+ if mass_arg is None:
31
+ return None
32
+ if isinstance(mass_arg, (int, float)):
33
+ return float(mass_arg)
34
+ if isinstance(mass_arg, list):
35
+ if len(mass_arg) == 1:
36
+ return float(mass_arg[0])
37
+ if file_idx < len(mass_arg):
38
+ return float(mass_arg[file_idx])
39
+ return float(mass_arg[-1])
40
+ return None
23
41
  from .utils import _confirm_overwrite, natural_sort_key, ensure_subdirectory
24
42
 
25
43
 
@@ -888,6 +906,9 @@ def batch_process_ec(directory: str, args):
888
906
  elif getattr(args, 'cpc', False):
889
907
  mode = 'cpc'
890
908
  supported_ext = {'.mpt', '.csv'}
909
+ elif getattr(args, 'epc', False):
910
+ mode = 'epc'
911
+ supported_ext = {'.mpt', '.csv'}
891
912
  else:
892
913
  print("EC batch mode requires one of: --gc, --cv, --dqdv, or --cpc")
893
914
  return
@@ -966,9 +987,10 @@ def batch_process_ec(directory: str, args):
966
987
 
967
988
  return colors[:n_colors] # Ensure exact count
968
989
 
969
- for fname in files:
990
+ for _batch_file_idx, fname in enumerate(files):
970
991
  fpath = os.path.join(directory, fname)
971
992
  ext = os.path.splitext(fname)[1].lower()
993
+ mass_mg = _resolve_mass(getattr(args, 'mass', None), _batch_file_idx)
972
994
 
973
995
  try:
974
996
  fig_b, ax_b = plt.subplots(figsize=(6, 4))
@@ -976,7 +998,6 @@ def batch_process_ec(directory: str, args):
976
998
  # ---- GC Mode ----
977
999
  if mode == 'gc':
978
1000
  if ext == '.mpt':
979
- mass_mg = getattr(args, 'mass', None)
980
1001
  if mass_mg is None:
981
1002
  print(f" Skipped {fname}: GC mode (.mpt) requires --mass parameter")
982
1003
  plt.close(fig_b)
@@ -1088,10 +1109,39 @@ def batch_process_ec(directory: str, args):
1088
1109
  elif mode == 'dqdv':
1089
1110
  if ext != '.csv':
1090
1111
  raise ValueError("dQdV mode requires .csv file")
1091
-
1092
- # Read dQdV data with cycle information
1093
- voltage, dqdv, cycles, charge_mask, discharge_mask, y_label = \
1094
- read_ec_csv_dqdv_file(fpath, prefer_specific=True)
1112
+
1113
+ # Try to load pre-calculated dQ/dV columns; fall back to numerical computation
1114
+ _b_dqdv_header = None
1115
+ try:
1116
+ _b_dqdv_header, _, _ = _load_csv_header_and_rows(fpath)
1117
+ except Exception:
1118
+ pass
1119
+
1120
+ _b_loaded = False
1121
+ if not _b_loaded:
1122
+ try:
1123
+ voltage, dqdv, cycles, charge_mask, discharge_mask, y_label = \
1124
+ read_ec_csv_dqdv_file(fpath, prefer_specific=True)
1125
+ _b_loaded = True
1126
+ except ValueError:
1127
+ pass
1128
+
1129
+ if not _b_loaded:
1130
+ _b_gc_cap, _b_gc_volt, _b_gc_cyc, _b_gc_chgm, _b_gc_dchm = \
1131
+ read_ec_csv_file(fpath, prefer_specific=True)
1132
+ _b_mass = mass_mg
1133
+ if _b_dqdv_header is not None:
1134
+ _b_hdrs = [h.strip().replace('\t', '') for h in _b_dqdv_header]
1135
+ _b_has_spec = any('Spec. Cap.(mAh/g)' in h for h in _b_hdrs)
1136
+ _b_has_abs = any(h == 'Capacity(mAh)' for h in _b_hdrs)
1137
+ if _b_has_abs and not _b_has_spec:
1138
+ if _b_mass and _b_mass > 0:
1139
+ _b_gc_cap = _b_gc_cap * (1000.0 / float(_b_mass))
1140
+ else:
1141
+ print(f"dQ/dV batch: {fname!r} — pass --mass <mg> for specific dQ/dV.")
1142
+ voltage, dqdv, cycles, charge_mask, discharge_mask, y_label = \
1143
+ compute_dqdv_numerical(_b_gc_cap, _b_gc_volt, _b_gc_cyc, _b_gc_chgm, _b_gc_dchm)
1144
+ print(f"dQ/dV batch: computing numerically from GC data for {fname!r}.")
1095
1145
 
1096
1146
  # Process cycles similar to GC mode
1097
1147
  if cycles is not None and cycles.size > 0:
@@ -1141,50 +1191,91 @@ def batch_process_ec(directory: str, args):
1141
1191
  legend = ax_b.legend(loc='best', fontsize='small', framealpha=0.8, title='Cycle')
1142
1192
  legend.get_title().set_fontsize('small')
1143
1193
 
1144
- # ---- CPC Mode ----
1145
- elif mode == 'cpc':
1194
+ # ---- CPC / EPC Mode ----
1195
+ elif mode in ('cpc', 'epc'):
1146
1196
  if ext == '.mpt':
1147
- mass_mg = getattr(args, 'mass', None)
1148
1197
  if mass_mg is None:
1149
- print(f" Skipped {fname}: CPC mode (.mpt) requires --mass parameter")
1198
+ print(f" Skipped {fname}: {mode.upper()} mode (.mpt) requires --mass parameter")
1150
1199
  plt.close(fig_b)
1151
1200
  continue
1152
- cyc_nums, cap_charge, cap_discharge, eff = cast(
1153
- Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray],
1154
- read_mpt_file(fpath, mode='cpc', mass_mg=mass_mg),
1155
- )
1156
- x_label = r'Specific Capacity (mAh g$^{-1}$)'
1201
+ if mode == 'cpc':
1202
+ cyc_nums, cap_charge, cap_discharge, _eff_dummy = cast(
1203
+ Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray],
1204
+ read_mpt_file(fpath, mode='cpc', mass_mg=mass_mg),
1205
+ )
1206
+ y_label = r'Specific Capacity (mAh g$^{-1}$)'
1207
+ else:
1208
+ cap_x, voltage, cycle_numbers, charge_mask, discharge_mask = cast(
1209
+ Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray],
1210
+ read_mpt_file(fpath, mode='gc', mass_mg=mass_mg),
1211
+ )
1212
+ cyc_int_raw = np.array(np.rint(cycle_numbers), dtype=int)
1213
+ if cyc_int_raw.size:
1214
+ cycles_present = sorted(int(c) for c in np.unique(cyc_int_raw))
1215
+ en_charge = []
1216
+ en_discharge = []
1217
+ for cyc in cycles_present:
1218
+ mask_c = (cyc_int_raw == cyc) & charge_mask
1219
+ mask_d = (cyc_int_raw == cyc) & discharge_mask
1220
+ if np.count_nonzero(mask_c) >= 2:
1221
+ en_c = float(np.trapz(voltage[mask_c], cap_x[mask_c]))
1222
+ else:
1223
+ en_c = 0.0
1224
+ if np.count_nonzero(mask_d) >= 2:
1225
+ en_d = float(np.trapz(voltage[mask_d], cap_x[mask_d]))
1226
+ else:
1227
+ en_d = 0.0
1228
+ en_charge.append(en_c)
1229
+ en_discharge.append(en_d)
1230
+ cyc_nums = np.array(cycles_present)
1231
+ cap_charge = np.array(en_charge)
1232
+ cap_discharge = np.array(en_discharge)
1233
+ else:
1234
+ cyc_nums = np.array([1.0])
1235
+ cap_charge = np.array([0.0])
1236
+ cap_discharge = np.array([0.0])
1237
+ y_label = r'Specific Energy (mWh g$^{-1}$)'
1157
1238
  elif ext == '.csv':
1158
1239
  # For CSV CPC, read as GC-like data
1159
1240
  cap_x, voltage, cycle_numbers, charge_mask, discharge_mask = \
1160
1241
  read_ec_csv_file(fpath, prefer_specific=True)
1161
- # Plot capacity vs cycle number
1162
1242
  if cycle_numbers is not None:
1163
1243
  cyc_int_raw = np.array(np.rint(cycle_numbers), dtype=int)
1164
1244
  if cyc_int_raw.size:
1165
1245
  cycles_present = sorted(int(c) for c in np.unique(cyc_int_raw))
1166
- # Calculate capacity per cycle
1167
- cap_charge = []
1168
- cap_discharge = []
1246
+ cap_charge_list = []
1247
+ cap_discharge_list = []
1169
1248
  for cyc in cycles_present:
1170
1249
  mask_c = (cyc_int_raw == cyc) & charge_mask
1171
1250
  mask_d = (cyc_int_raw == cyc) & discharge_mask
1172
- cap_charge.append(np.max(cap_x[mask_c]) if np.any(mask_c) else 0)
1173
- cap_discharge.append(np.max(cap_x[mask_d]) if np.any(mask_d) else 0)
1251
+ if mode == 'cpc':
1252
+ val_c = np.max(cap_x[mask_c]) if np.any(mask_c) else 0.0
1253
+ val_d = np.max(cap_x[mask_d]) if np.any(mask_d) else 0.0
1254
+ else:
1255
+ if np.count_nonzero(mask_c) >= 2:
1256
+ val_c = float(np.trapz(voltage[mask_c], cap_x[mask_c]))
1257
+ else:
1258
+ val_c = 0.0
1259
+ if np.count_nonzero(mask_d) >= 2:
1260
+ val_d = float(np.trapz(voltage[mask_d], cap_x[mask_d]))
1261
+ else:
1262
+ val_d = 0.0
1263
+ cap_charge_list.append(val_c)
1264
+ cap_discharge_list.append(val_d)
1174
1265
  cyc_nums = np.array(cycles_present)
1175
- cap_charge = np.array(cap_charge)
1176
- cap_discharge = np.array(cap_discharge)
1266
+ cap_charge = np.array(cap_charge_list)
1267
+ cap_discharge = np.array(cap_discharge_list)
1177
1268
  else:
1178
- cyc_nums = np.array([1])
1179
- cap_charge = np.array([0])
1180
- cap_discharge = np.array([0])
1269
+ cyc_nums = np.array([1.0])
1270
+ cap_charge = np.array([0.0])
1271
+ cap_discharge = np.array([0.0])
1181
1272
  else:
1182
- cyc_nums = np.array([1])
1183
- cap_charge = np.array([0])
1184
- cap_discharge = np.array([0])
1185
- x_label = r'Specific Capacity (mAh g$^{-1}$)'
1273
+ cyc_nums = np.array([1.0])
1274
+ cap_charge = np.array([0.0])
1275
+ cap_discharge = np.array([0.0])
1276
+ y_label = r'Specific Capacity (mAh g$^{-1}$)' if mode == 'cpc' else r'Specific Energy (mWh g$^{-1}$)'
1186
1277
  else:
1187
- raise ValueError(f"Unsupported file type for CPC: {ext}")
1278
+ raise ValueError(f"Unsupported file type for {mode.upper()}: {ext}")
1188
1279
 
1189
1280
  # Plot CPC data
1190
1281
  ax_b.plot(cyc_nums, cap_charge, 'o-', color='#1f77b4',
@@ -1192,7 +1283,7 @@ def batch_process_ec(directory: str, args):
1192
1283
  ax_b.plot(cyc_nums, cap_discharge, 's-', color='#ff7f0e',
1193
1284
  linewidth=1.5, markersize=4, label='Discharge', alpha=0.8)
1194
1285
  ax_b.set_xlabel('Cycle Number')
1195
- ax_b.set_ylabel(x_label)
1286
+ ax_b.set_ylabel(y_label)
1196
1287
  ax_b.legend()
1197
1288
  ax_b.set_title(f"{fname}")
1198
1289