batplot 1.8.25__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 (42) hide show
  1. {batplot-1.8.25/batplot.egg-info → batplot-1.8.27}/PKG-INFO +1 -1
  2. {batplot-1.8.25 → batplot-1.8.27}/batplot/__init__.py +1 -1
  3. {batplot-1.8.25 → batplot-1.8.27}/batplot/args.py +18 -7
  4. {batplot-1.8.25 → batplot-1.8.27}/batplot/batch.py +145 -40
  5. {batplot-1.8.25 → batplot-1.8.27}/batplot/batplot.py +345 -76
  6. {batplot-1.8.25 → batplot-1.8.27}/batplot/cif.py +40 -23
  7. {batplot-1.8.25 → batplot-1.8.27}/batplot/color_utils.py +9 -4
  8. {batplot-1.8.25 → batplot-1.8.27}/batplot/cpc_interactive.py +915 -358
  9. {batplot-1.8.25 → batplot-1.8.27}/batplot/data/CHANGELOG.md +18 -0
  10. {batplot-1.8.25 → batplot-1.8.27}/batplot/data/USER_MANUAL.md +37 -1
  11. {batplot-1.8.25 → batplot-1.8.27}/batplot/electrochem_interactive.py +693 -341
  12. {batplot-1.8.25 → batplot-1.8.27}/batplot/interactive.py +159 -147
  13. {batplot-1.8.25 → batplot-1.8.27}/batplot/modes.py +53 -13
  14. {batplot-1.8.25 → batplot-1.8.27}/batplot/operando.py +10 -10
  15. {batplot-1.8.25 → batplot-1.8.27}/batplot/operando_ec_interactive.py +243 -227
  16. {batplot-1.8.25 → batplot-1.8.27}/batplot/plotting.py +1 -1
  17. {batplot-1.8.25 → batplot-1.8.27}/batplot/readers.py +114 -12
  18. {batplot-1.8.25 → batplot-1.8.27}/batplot/session.py +161 -111
  19. {batplot-1.8.25 → batplot-1.8.27}/batplot/style.py +22 -12
  20. {batplot-1.8.25 → batplot-1.8.27}/batplot/utils.py +42 -15
  21. {batplot-1.8.25 → batplot-1.8.27}/batplot/version_check.py +3 -2
  22. {batplot-1.8.25 → batplot-1.8.27/batplot.egg-info}/PKG-INFO +1 -1
  23. batplot-1.8.27/batplot.egg-info/top_level.txt +1 -0
  24. {batplot-1.8.25 → batplot-1.8.27}/pyproject.toml +1 -1
  25. batplot-1.8.25/batplot.egg-info/top_level.txt +0 -2
  26. {batplot-1.8.25 → batplot-1.8.27}/LICENSE +0 -0
  27. {batplot-1.8.25 → batplot-1.8.27}/MANIFEST.in +0 -0
  28. {batplot-1.8.25 → batplot-1.8.27}/NOTICE +0 -0
  29. {batplot-1.8.25 → batplot-1.8.27}/README.md +0 -0
  30. {batplot-1.8.25 → batplot-1.8.27}/USER_MANUAL.md +0 -0
  31. {batplot-1.8.25 → batplot-1.8.27}/batplot/cli.py +0 -0
  32. {batplot-1.8.25 → batplot-1.8.27}/batplot/config.py +0 -0
  33. {batplot-1.8.25 → batplot-1.8.27}/batplot/converters.py +0 -0
  34. {batplot-1.8.25 → batplot-1.8.27}/batplot/dev_upgrade.py +0 -0
  35. {batplot-1.8.25 → batplot-1.8.27}/batplot/manual.py +0 -0
  36. {batplot-1.8.25 → batplot-1.8.27}/batplot/ui.py +0 -0
  37. {batplot-1.8.25 → batplot-1.8.27}/batplot.egg-info/SOURCES.txt +0 -0
  38. {batplot-1.8.25 → batplot-1.8.27}/batplot.egg-info/dependency_links.txt +0 -0
  39. {batplot-1.8.25 → batplot-1.8.27}/batplot.egg-info/entry_points.txt +0 -0
  40. {batplot-1.8.25 → batplot-1.8.27}/batplot.egg-info/requires.txt +0 -0
  41. {batplot-1.8.25 → batplot-1.8.27}/setup.cfg +0 -0
  42. {batplot-1.8.25 → 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.25
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.25"
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)
@@ -4,9 +4,11 @@ from __future__ import annotations
4
4
 
5
5
  import os
6
6
  import json
7
- import matplotlib.cm as cm
8
- import numpy as np
9
- import matplotlib.pyplot as plt
7
+ from typing import Tuple, cast
8
+
9
+ import matplotlib.cm as cm # type: ignore[import]
10
+ import numpy as np # type: ignore[import]
11
+ import matplotlib.pyplot as plt # type: ignore[import]
10
12
 
11
13
  from .readers import (
12
14
  read_gr_file,
@@ -16,8 +18,26 @@ from .readers import (
16
18
  read_mpt_file,
17
19
  read_ec_csv_file,
18
20
  read_ec_csv_dqdv_file,
21
+ compute_dqdv_numerical,
22
+ is_cs_b_format,
23
+ _load_csv_header_and_rows,
19
24
  read_biologic_txt_file,
20
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
21
41
  from .utils import _confirm_overwrite, natural_sort_key, ensure_subdirectory
22
42
 
23
43
 
@@ -886,6 +906,9 @@ def batch_process_ec(directory: str, args):
886
906
  elif getattr(args, 'cpc', False):
887
907
  mode = 'cpc'
888
908
  supported_ext = {'.mpt', '.csv'}
909
+ elif getattr(args, 'epc', False):
910
+ mode = 'epc'
911
+ supported_ext = {'.mpt', '.csv'}
889
912
  else:
890
913
  print("EC batch mode requires one of: --gc, --cv, --dqdv, or --cpc")
891
914
  return
@@ -964,9 +987,10 @@ def batch_process_ec(directory: str, args):
964
987
 
965
988
  return colors[:n_colors] # Ensure exact count
966
989
 
967
- for fname in files:
990
+ for _batch_file_idx, fname in enumerate(files):
968
991
  fpath = os.path.join(directory, fname)
969
992
  ext = os.path.splitext(fname)[1].lower()
993
+ mass_mg = _resolve_mass(getattr(args, 'mass', None), _batch_file_idx)
970
994
 
971
995
  try:
972
996
  fig_b, ax_b = plt.subplots(figsize=(6, 4))
@@ -974,13 +998,14 @@ def batch_process_ec(directory: str, args):
974
998
  # ---- GC Mode ----
975
999
  if mode == 'gc':
976
1000
  if ext == '.mpt':
977
- mass_mg = getattr(args, 'mass', None)
978
1001
  if mass_mg is None:
979
1002
  print(f" Skipped {fname}: GC mode (.mpt) requires --mass parameter")
980
1003
  plt.close(fig_b)
981
1004
  continue
982
- specific_capacity, voltage, cycle_numbers, charge_mask, discharge_mask = \
983
- read_mpt_file(fpath, mode='gc', mass_mg=mass_mg)
1005
+ specific_capacity, voltage, cycle_numbers, charge_mask, discharge_mask = cast(
1006
+ Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray],
1007
+ read_mpt_file(fpath, mode='gc', mass_mg=mass_mg),
1008
+ )
984
1009
  cap_x = specific_capacity
985
1010
  x_label = r'Specific Capacity (mAh g$^{-1}$)'
986
1011
  elif ext == '.csv':
@@ -1005,14 +1030,19 @@ def batch_process_ec(directory: str, args):
1005
1030
 
1006
1031
  # Generate color palette for the number of cycles
1007
1032
  cycle_colors = get_color_palette(len(cycles_present))
1033
+
1034
+ # Ensure masks are boolean numpy arrays for safe logical operations
1035
+ charge_mask_arr = np.asarray(charge_mask, dtype=bool)
1036
+ discharge_mask_arr = np.asarray(discharge_mask, dtype=bool)
1008
1037
 
1009
1038
  for idx, cyc in enumerate(cycles_present): # Plot all cycles
1010
1039
  if cycle_numbers is not None:
1011
- mask_c = (cyc_int == cyc) & charge_mask
1012
- mask_d = (cyc_int == cyc) & discharge_mask
1040
+ cyc_eq = (cyc_int == cyc)
1041
+ mask_c = cyc_eq & charge_mask_arr
1042
+ mask_d = cyc_eq & discharge_mask_arr
1013
1043
  else:
1014
- mask_c = charge_mask
1015
- mask_d = discharge_mask
1044
+ mask_c = charge_mask_arr
1045
+ mask_d = discharge_mask_arr
1016
1046
 
1017
1047
  color = cycle_colors[idx]
1018
1048
 
@@ -1042,7 +1072,10 @@ def batch_process_ec(directory: str, args):
1042
1072
  if ext == '.txt':
1043
1073
  voltage, current, cycles = read_biologic_txt_file(fpath, mode='cv')
1044
1074
  elif ext == '.mpt':
1045
- voltage, current, cycles = read_mpt_file(fpath, mode='cv')
1075
+ voltage, current, cycles = cast(
1076
+ Tuple[np.ndarray, np.ndarray, np.ndarray],
1077
+ read_mpt_file(fpath, mode='cv'),
1078
+ )
1046
1079
  else:
1047
1080
  raise ValueError("CV mode requires .mpt or .txt file")
1048
1081
 
@@ -1076,10 +1109,39 @@ def batch_process_ec(directory: str, args):
1076
1109
  elif mode == 'dqdv':
1077
1110
  if ext != '.csv':
1078
1111
  raise ValueError("dQdV mode requires .csv file")
1079
-
1080
- # Read dQdV data with cycle information
1081
- voltage, dqdv, cycles, charge_mask, discharge_mask, y_label = \
1082
- 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}.")
1083
1145
 
1084
1146
  # Process cycles similar to GC mode
1085
1147
  if cycles is not None and cycles.size > 0:
@@ -1129,48 +1191,91 @@ def batch_process_ec(directory: str, args):
1129
1191
  legend = ax_b.legend(loc='best', fontsize='small', framealpha=0.8, title='Cycle')
1130
1192
  legend.get_title().set_fontsize('small')
1131
1193
 
1132
- # ---- CPC Mode ----
1133
- elif mode == 'cpc':
1194
+ # ---- CPC / EPC Mode ----
1195
+ elif mode in ('cpc', 'epc'):
1134
1196
  if ext == '.mpt':
1135
- mass_mg = getattr(args, 'mass', None)
1136
1197
  if mass_mg is None:
1137
- print(f" Skipped {fname}: CPC mode (.mpt) requires --mass parameter")
1198
+ print(f" Skipped {fname}: {mode.upper()} mode (.mpt) requires --mass parameter")
1138
1199
  plt.close(fig_b)
1139
1200
  continue
1140
- cyc_nums, cap_charge, cap_discharge, eff = \
1141
- read_mpt_file(fpath, mode='cpc', mass_mg=mass_mg)
1142
- 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}$)'
1143
1238
  elif ext == '.csv':
1144
1239
  # For CSV CPC, read as GC-like data
1145
1240
  cap_x, voltage, cycle_numbers, charge_mask, discharge_mask = \
1146
1241
  read_ec_csv_file(fpath, prefer_specific=True)
1147
- # Plot capacity vs cycle number
1148
1242
  if cycle_numbers is not None:
1149
1243
  cyc_int_raw = np.array(np.rint(cycle_numbers), dtype=int)
1150
1244
  if cyc_int_raw.size:
1151
1245
  cycles_present = sorted(int(c) for c in np.unique(cyc_int_raw))
1152
- # Calculate capacity per cycle
1153
- cap_charge = []
1154
- cap_discharge = []
1246
+ cap_charge_list = []
1247
+ cap_discharge_list = []
1155
1248
  for cyc in cycles_present:
1156
1249
  mask_c = (cyc_int_raw == cyc) & charge_mask
1157
1250
  mask_d = (cyc_int_raw == cyc) & discharge_mask
1158
- cap_charge.append(np.max(cap_x[mask_c]) if np.any(mask_c) else 0)
1159
- 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)
1160
1265
  cyc_nums = np.array(cycles_present)
1161
- cap_charge = np.array(cap_charge)
1162
- cap_discharge = np.array(cap_discharge)
1266
+ cap_charge = np.array(cap_charge_list)
1267
+ cap_discharge = np.array(cap_discharge_list)
1163
1268
  else:
1164
- cyc_nums = np.array([1])
1165
- cap_charge = np.array([0])
1166
- 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])
1167
1272
  else:
1168
- cyc_nums = np.array([1])
1169
- cap_charge = np.array([0])
1170
- cap_discharge = np.array([0])
1171
- 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}$)'
1172
1277
  else:
1173
- raise ValueError(f"Unsupported file type for CPC: {ext}")
1278
+ raise ValueError(f"Unsupported file type for {mode.upper()}: {ext}")
1174
1279
 
1175
1280
  # Plot CPC data
1176
1281
  ax_b.plot(cyc_nums, cap_charge, 'o-', color='#1f77b4',
@@ -1178,7 +1283,7 @@ def batch_process_ec(directory: str, args):
1178
1283
  ax_b.plot(cyc_nums, cap_discharge, 's-', color='#ff7f0e',
1179
1284
  linewidth=1.5, markersize=4, label='Discharge', alpha=0.8)
1180
1285
  ax_b.set_xlabel('Cycle Number')
1181
- ax_b.set_ylabel(x_label)
1286
+ ax_b.set_ylabel(y_label)
1182
1287
  ax_b.legend()
1183
1288
  ax_b.set_title(f"{fname}")
1184
1289