batplot 1.8.38__tar.gz → 1.8.40__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.
- {batplot-1.8.38/batplot.egg-info → batplot-1.8.40}/PKG-INFO +1 -1
- {batplot-1.8.38 → batplot-1.8.40}/batplot/__init__.py +1 -1
- {batplot-1.8.38 → batplot-1.8.40}/batplot/args.py +20 -7
- {batplot-1.8.38 → batplot-1.8.40}/batplot/batch.py +40 -19
- {batplot-1.8.38 → batplot-1.8.40}/batplot/batplot.py +70 -11
- {batplot-1.8.38 → batplot-1.8.40}/batplot/color_utils.py +33 -14
- {batplot-1.8.38 → batplot-1.8.40}/batplot/config.py +44 -0
- {batplot-1.8.38 → batplot-1.8.40}/batplot/cpc_interactive.py +168 -86
- {batplot-1.8.38 → batplot-1.8.40}/batplot/data/CHANGELOG.md +8 -0
- {batplot-1.8.38 → batplot-1.8.40}/batplot/electrochem_interactive.py +839 -69
- {batplot-1.8.38 → batplot-1.8.40}/batplot/interactive.py +161 -109
- {batplot-1.8.38 → batplot-1.8.40}/batplot/operando.py +106 -55
- {batplot-1.8.38 → batplot-1.8.40}/batplot/operando_ec_interactive.py +574 -181
- {batplot-1.8.38 → batplot-1.8.40}/batplot/readers.py +2 -2
- {batplot-1.8.38 → batplot-1.8.40}/batplot/session.py +255 -183
- {batplot-1.8.38 → batplot-1.8.40}/batplot/style.py +17 -51
- {batplot-1.8.38 → batplot-1.8.40}/batplot/ui.py +150 -1
- {batplot-1.8.38 → batplot-1.8.40}/batplot/utils.py +52 -0
- {batplot-1.8.38 → batplot-1.8.40/batplot.egg-info}/PKG-INFO +1 -1
- {batplot-1.8.38 → batplot-1.8.40}/pyproject.toml +1 -1
- {batplot-1.8.38 → batplot-1.8.40}/LICENSE +0 -0
- {batplot-1.8.38 → batplot-1.8.40}/MANIFEST.in +0 -0
- {batplot-1.8.38 → batplot-1.8.40}/NOTICE +0 -0
- {batplot-1.8.38 → batplot-1.8.40}/README.md +0 -0
- {batplot-1.8.38 → batplot-1.8.40}/batplot/canvas_interactive.py +0 -0
- {batplot-1.8.38 → batplot-1.8.40}/batplot/cif.py +0 -0
- {batplot-1.8.38 → batplot-1.8.40}/batplot/cli.py +0 -0
- {batplot-1.8.38 → batplot-1.8.40}/batplot/converters.py +0 -0
- {batplot-1.8.38 → batplot-1.8.40}/batplot/data/USER_MANUAL.md +0 -0
- {batplot-1.8.38 → batplot-1.8.40}/batplot/dev_upgrade.py +0 -0
- {batplot-1.8.38 → batplot-1.8.40}/batplot/manual.py +0 -0
- {batplot-1.8.38 → batplot-1.8.40}/batplot/modes.py +0 -0
- {batplot-1.8.38 → batplot-1.8.40}/batplot/plotting.py +0 -0
- {batplot-1.8.38 → batplot-1.8.40}/batplot/showcol.py +0 -0
- {batplot-1.8.38 → batplot-1.8.40}/batplot/version_check.py +0 -0
- {batplot-1.8.38 → batplot-1.8.40}/batplot.egg-info/SOURCES.txt +0 -0
- {batplot-1.8.38 → batplot-1.8.40}/batplot.egg-info/dependency_links.txt +0 -0
- {batplot-1.8.38 → batplot-1.8.40}/batplot.egg-info/entry_points.txt +0 -0
- {batplot-1.8.38 → batplot-1.8.40}/batplot.egg-info/requires.txt +0 -0
- {batplot-1.8.38 → batplot-1.8.40}/batplot.egg-info/top_level.txt +0 -0
- {batplot-1.8.38 → batplot-1.8.40}/setup.cfg +0 -0
- {batplot-1.8.38 → batplot-1.8.40}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: batplot
|
|
3
|
-
Version: 1.8.
|
|
3
|
+
Version: 1.8.40
|
|
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
|
|
@@ -37,6 +37,8 @@ import re
|
|
|
37
37
|
#
|
|
38
38
|
# If rich is not installed, we fall back to plain text (still works fine).
|
|
39
39
|
# ====================================================================
|
|
40
|
+
from .utils import parse_mass_mg_from_cli
|
|
41
|
+
|
|
40
42
|
try:
|
|
41
43
|
from rich.console import Console # type: ignore[import]
|
|
42
44
|
from rich.markup import escape # type: ignore[import]
|
|
@@ -234,6 +236,8 @@ def _print_xy_help() -> None:
|
|
|
234
236
|
" batplot f1.txt --readcol 2 3 f2.txt --readcol 5 6 --convert 1.54 q\n"
|
|
235
237
|
" Directory: pass a folder to convert all .xy/.xye/.qye/.dat/.csv/.txt files:\n"
|
|
236
238
|
" batplot /path/to/folder --convert 0.25448 1.54\n"
|
|
239
|
+
" Batch in current folder: use allfiles token (non-convertible files are skipped):\n"
|
|
240
|
+
" batplot allfiles --convert q 1.54\n"
|
|
237
241
|
" Examples:\n"
|
|
238
242
|
" batplot file.xye --convert 1.54 0.25\n"
|
|
239
243
|
" batplot file.xye --convert 1.54 q\n"
|
|
@@ -276,8 +280,9 @@ def _print_ec_help() -> None:
|
|
|
276
280
|
" • Neware: Customized report — check all boxes\n"
|
|
277
281
|
" • Biologic: Export all info to .mpt file\n\n"
|
|
278
282
|
"Use --i for styling, colors, line widths, axis scales, etc.\n"
|
|
279
|
-
"GC from .mpt: requires active mass
|
|
280
|
-
" batplot --gc file.mpt --mass 6.5 --i\n
|
|
283
|
+
"GC from .mpt or .npt: requires active mass to compute mAh g⁻¹ (default unit: mg; use a ``g`` suffix for grams, e.g. --mass 0.0065g).\n"
|
|
284
|
+
" batplot --gc file.mpt --mass 6.5 --i\n"
|
|
285
|
+
" batplot --gc file.npt --mass 10g --i\n\n"
|
|
281
286
|
"GC from supported .csv: specific capacity read directly when available; use --mass for\n"
|
|
282
287
|
" Neware absolute-capacity files (Cycle Index / Step Index / DataPoint format).\n"
|
|
283
288
|
" batplot --gc file.csv\n"
|
|
@@ -287,9 +292,11 @@ def _print_ec_help() -> None:
|
|
|
287
292
|
" batplot f1.csv --mass 3.52 f2.mpt --mass 5.0 --cpc\n"
|
|
288
293
|
" # Files without --mass between them use the global --mass value (or none)\n"
|
|
289
294
|
" # Single --mass applies to all files: batplot f1.mpt f2.mpt --gc --mass 7.0\n\n"
|
|
290
|
-
"dQ/dV from supported .csv (pre-calculated column or computed from GC data):\n"
|
|
295
|
+
"dQ/dV from supported .csv (pre-calculated column or computed from GC data), or from Biologic .mpt/.npt (numerical dQ/dV from GC; requires --mass):\n"
|
|
291
296
|
" batplot --dqdv file.csv\n"
|
|
292
|
-
" batplot --dqdv file.csv --mass 3.52 # Neware absolute-capacity CSV\n
|
|
297
|
+
" batplot --dqdv file.csv --mass 3.52 # Neware absolute-capacity CSV\n"
|
|
298
|
+
" batplot --dqdv file.mpt --mass 6.5\n"
|
|
299
|
+
" batplot --dqdv file.npt --mass 0.01g\n\n"
|
|
293
300
|
"Cyclic voltammetry (CV) from .mpt or .txt: plots potential vs current for each cycle.\n"
|
|
294
301
|
" batplot --cv file.mpt\n"
|
|
295
302
|
" batplot --cv file.txt\n\n"
|
|
@@ -298,7 +305,7 @@ def _print_ec_help() -> None:
|
|
|
298
305
|
" batplot --cpc file.csv # Neware CSV (specific capacity)\n"
|
|
299
306
|
" batplot --cpc file.csv --mass 3.52 # Neware absolute-capacity CSV\n"
|
|
300
307
|
" batplot --cpc file.xlsx # Landt/Lanhe Excel (Chinese tester)\n"
|
|
301
|
-
" batplot --cpc file.mpt --mass 1.2 # Biologic
|
|
308
|
+
" batplot --cpc file.mpt --mass 1.2 # Biologic .mpt / .npt\n"
|
|
302
309
|
" batplot file1.csv --mass 3.52 file2.mpt --mass 1.2 --cpc # Per-file mass\n"
|
|
303
310
|
" batplot --cpc file1.csv file2.xlsx file3.mpt --mass 1.2 --i\n\n"
|
|
304
311
|
"Excel support: Landt/Lanhe (蓝电/蓝河) .xlsx files with Chinese headers:\n"
|
|
@@ -340,6 +347,8 @@ def _print_op_help() -> None:
|
|
|
340
347
|
" batplot --operando --xaxis 2theta # Using 2theta axis\n"
|
|
341
348
|
" batplot --operando --1d --i # Plot derivatives as contour with interactive menu\n"
|
|
342
349
|
" batplot --operando --2d --i # Plot derivatives (alias for --1d)\n\n"
|
|
350
|
+
" batplot --operando --average 2 --i # Average every 2 scans before contouring\n\n"
|
|
351
|
+
" batplot --operando --sum 2 --i # Sum every 2 scans to boost intensity\n\n"
|
|
343
352
|
"Bruker operando (.brml):\n"
|
|
344
353
|
" • Place .brml files (e.g. XX_cyc1.brml, XX_cyc2.brml) in the folder.\n"
|
|
345
354
|
" • Each .brml is expanded into per-scan rows; files sorted by cyc1/cyc2/cyc3.\n"
|
|
@@ -353,6 +362,8 @@ def _print_op_help() -> None:
|
|
|
353
362
|
" • If a .mpt file is present, a side panel is added for dual-panel mode (time/potential/temp/etc.).\n"
|
|
354
363
|
" • Without a .mpt file, operando-only mode shows the contour plot alone.\n"
|
|
355
364
|
" • --1d / --2d: plot the first derivative (dy/dx) of each scan as a contour plot.\n\n"
|
|
365
|
+
" • --average N: average every N consecutive scans to improve S/N (e.g., N=2 averages scans 1+2, 3+4, ...).\n\n"
|
|
366
|
+
" • --sum N: sum every N consecutive scans (same binning as --average, but without division by N).\n\n"
|
|
356
367
|
"Column selection (operando-specific):\n"
|
|
357
368
|
" --readcolc <x> <y> : columns for contour plot (x,y in .xy/.xye/.qye/.dat files)\n"
|
|
358
369
|
" --readcols <x> <y> : columns for side panel (x,y in .mpt file)\n"
|
|
@@ -382,7 +393,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
382
393
|
--------------
|
|
383
394
|
- Positional arguments: 'files' - list of file paths (can be 0 or more)
|
|
384
395
|
- Flags (boolean): '--i' - True if present, False if absent
|
|
385
|
-
- Options with values: '--mass 7.0'
|
|
396
|
+
- Options with values: '--mass 7.0' or '--mass 0.01g' - mass in mg, or grams with a ``g`` suffix
|
|
386
397
|
- Optional arguments: '--help xy' - can have optional value
|
|
387
398
|
|
|
388
399
|
WHY add_help=False?
|
|
@@ -459,9 +470,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
459
470
|
parser.add_argument("--ry", action="store_true", help=argparse.SUPPRESS)
|
|
460
471
|
parser.add_argument("--txaxis", action="store_true", help=argparse.SUPPRESS)
|
|
461
472
|
parser.add_argument("--operando", "--contour", action="store_true", dest="operando", help=argparse.SUPPRESS)
|
|
473
|
+
parser.add_argument("--average", type=int, help=argparse.SUPPRESS)
|
|
474
|
+
parser.add_argument("--sum", dest="scan_sum", type=int, help=argparse.SUPPRESS)
|
|
462
475
|
parser.add_argument("--debug", action="store_true", help=argparse.SUPPRESS)
|
|
463
476
|
parser.add_argument("--gc", action="store_true", help=argparse.SUPPRESS)
|
|
464
|
-
parser.add_argument("--mass", type=
|
|
477
|
+
parser.add_argument("--mass", type=parse_mass_mg_from_cli, action='append', help=argparse.SUPPRESS)
|
|
465
478
|
parser.add_argument("--dqdv", action="store_true", help=argparse.SUPPRESS)
|
|
466
479
|
parser.add_argument("--cv", action="store_true", help=argparse.SUPPRESS)
|
|
467
480
|
parser.add_argument("--cpc", action="store_true", help=argparse.SUPPRESS)
|
|
@@ -28,6 +28,13 @@ from .readers import (
|
|
|
28
28
|
read_biologic_txt_file,
|
|
29
29
|
)
|
|
30
30
|
|
|
31
|
+
# BioLogic EC-Lab ASCII exports use ``.mpt``; ``.npt`` is accepted as the same format.
|
|
32
|
+
_MPT_LIKE_EXTS = frozenset({'.mpt', '.npt'})
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _is_mpt_like_ext(ext: str) -> bool:
|
|
36
|
+
return (ext or '').lower() in _MPT_LIKE_EXTS
|
|
37
|
+
|
|
31
38
|
|
|
32
39
|
def _resolve_mass(mass_arg, file_idx: int = 0):
|
|
33
40
|
"""Return mass (mg) for file at file_idx from a --mass list or single value."""
|
|
@@ -488,7 +495,7 @@ def batch_process(directory: str, args):
|
|
|
488
495
|
known_ext = {'.xye', '.xy', '.qye', '.dat', '.csv', '.gr', '.nor', '.chik', '.chir', '.txt', '.brml', '.raw', '.xrdml', '.rasx'}
|
|
489
496
|
|
|
490
497
|
# Extensions to exclude (not data files, or require special handling)
|
|
491
|
-
excluded_ext = {'.cif', '.pkl', '.py', '.md', '.json', '.yml', '.yaml', '.sh', '.bat', '.mpt'}
|
|
498
|
+
excluded_ext = {'.cif', '.pkl', '.py', '.md', '.json', '.yml', '.yaml', '.sh', '.bat', '.mpt', '.npt'}
|
|
492
499
|
|
|
493
500
|
# Create output directory for saved plots
|
|
494
501
|
out_dir = ensure_subdirectory('Figures', directory)
|
|
@@ -842,7 +849,7 @@ def batch_process(directory: str, args):
|
|
|
842
849
|
def batch_process_ec(directory: str, args):
|
|
843
850
|
"""Batch process electrochemistry files in a directory.
|
|
844
851
|
|
|
845
|
-
Supports GC (.mpt/.csv), CV (.mpt),
|
|
852
|
+
Supports GC (.mpt/.npt/.csv), CV (.mpt/.npt/.txt), dQ/dV (.mpt/.npt/.csv), and CPC (.mpt/.npt/.csv) modes.
|
|
846
853
|
Exports SVG plots to batplot_svg subdirectory.
|
|
847
854
|
|
|
848
855
|
Can apply style/geometry from .bps/.bpsg files using --all flag:
|
|
@@ -852,7 +859,7 @@ def batch_process_ec(directory: str, args):
|
|
|
852
859
|
batplot --all --cpc config.bpsg # Apply to all CPC files
|
|
853
860
|
|
|
854
861
|
Note: For GC and CPC modes with .csv files, --mass is not required as the
|
|
855
|
-
capacity data is already in the file. For .mpt files, --mass is required.
|
|
862
|
+
capacity data is already in the file. For .mpt/.npt files, --mass is required (mg by default; use a ``g`` suffix for grams, e.g. ``--mass 0.01g``).
|
|
856
863
|
|
|
857
864
|
Args:
|
|
858
865
|
directory: Directory containing EC files
|
|
@@ -906,19 +913,19 @@ def batch_process_ec(directory: str, args):
|
|
|
906
913
|
mode = None
|
|
907
914
|
if getattr(args, 'gc', False):
|
|
908
915
|
mode = 'gc'
|
|
909
|
-
supported_ext = {'.mpt', '.csv'}
|
|
916
|
+
supported_ext = {'.mpt', '.npt', '.csv'}
|
|
910
917
|
elif getattr(args, 'cv', False):
|
|
911
918
|
mode = 'cv'
|
|
912
|
-
supported_ext = {'.mpt', '.txt'}
|
|
919
|
+
supported_ext = {'.mpt', '.npt', '.txt'}
|
|
913
920
|
elif getattr(args, 'dqdv', False):
|
|
914
921
|
mode = 'dqdv'
|
|
915
|
-
supported_ext = {'.csv'}
|
|
922
|
+
supported_ext = {'.mpt', '.npt', '.csv'}
|
|
916
923
|
elif getattr(args, 'cpc', False):
|
|
917
924
|
mode = 'cpc'
|
|
918
|
-
supported_ext = {'.mpt', '.csv'}
|
|
925
|
+
supported_ext = {'.mpt', '.npt', '.csv'}
|
|
919
926
|
elif getattr(args, 'epc', False):
|
|
920
927
|
mode = 'epc'
|
|
921
|
-
supported_ext = {'.mpt', '.csv'}
|
|
928
|
+
supported_ext = {'.mpt', '.npt', '.csv'}
|
|
922
929
|
else:
|
|
923
930
|
print("EC batch mode requires one of: --gc, --cv, --dqdv, or --cpc")
|
|
924
931
|
return
|
|
@@ -1007,9 +1014,9 @@ def batch_process_ec(directory: str, args):
|
|
|
1007
1014
|
|
|
1008
1015
|
# ---- GC Mode ----
|
|
1009
1016
|
if mode == 'gc':
|
|
1010
|
-
if ext
|
|
1017
|
+
if _is_mpt_like_ext(ext):
|
|
1011
1018
|
if mass_mg is None:
|
|
1012
|
-
print(f" Skipped {fname}: GC mode (.mpt) requires --mass parameter")
|
|
1019
|
+
print(f" Skipped {fname}: GC mode (.mpt/.npt) requires --mass parameter")
|
|
1013
1020
|
plt.close(fig_b)
|
|
1014
1021
|
continue
|
|
1015
1022
|
specific_capacity, voltage, cycle_numbers, charge_mask, discharge_mask = cast(
|
|
@@ -1089,13 +1096,13 @@ def batch_process_ec(directory: str, args):
|
|
|
1089
1096
|
elif mode == 'cv':
|
|
1090
1097
|
if ext == '.txt':
|
|
1091
1098
|
voltage, current, cycles = read_biologic_txt_file(fpath, mode='cv')
|
|
1092
|
-
elif ext
|
|
1099
|
+
elif _is_mpt_like_ext(ext):
|
|
1093
1100
|
voltage, current, cycles = cast(
|
|
1094
1101
|
Tuple[np.ndarray, np.ndarray, np.ndarray],
|
|
1095
1102
|
read_mpt_file(fpath, mode='cv'),
|
|
1096
1103
|
)
|
|
1097
1104
|
else:
|
|
1098
|
-
raise ValueError("CV mode requires .mpt or .txt file")
|
|
1105
|
+
raise ValueError("CV mode requires .mpt, .npt, or .txt file")
|
|
1099
1106
|
|
|
1100
1107
|
cyc_int_raw = np.array(np.rint(cycles), dtype=int)
|
|
1101
1108
|
if cyc_int_raw.size:
|
|
@@ -1125,13 +1132,27 @@ def batch_process_ec(directory: str, args):
|
|
|
1125
1132
|
|
|
1126
1133
|
# ---- dQdV Mode ----
|
|
1127
1134
|
elif mode == 'dqdv':
|
|
1128
|
-
if ext != '.csv':
|
|
1129
|
-
raise ValueError("dQdV mode requires .csv file")
|
|
1130
|
-
|
|
1131
1135
|
# Try to load pre-calculated dQ/dV columns; fall back to numerical computation
|
|
1132
1136
|
_b_dqdv_header = None
|
|
1133
1137
|
_b_loaded = False
|
|
1134
|
-
if
|
|
1138
|
+
if _is_mpt_like_ext(ext):
|
|
1139
|
+
if mass_mg is None or mass_mg <= 0:
|
|
1140
|
+
print(f" Skipped {fname}: dQ/dV (.mpt/.npt) requires --mass parameter")
|
|
1141
|
+
plt.close(fig_b)
|
|
1142
|
+
continue
|
|
1143
|
+
_b_gc_cap, _b_gc_volt, _b_gc_cyc, _b_gc_chgm, _b_gc_dchm = cast(
|
|
1144
|
+
Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray],
|
|
1145
|
+
read_mpt_file(fpath, mode='gc', mass_mg=mass_mg),
|
|
1146
|
+
)
|
|
1147
|
+
voltage, dqdv, cycles, charge_mask, discharge_mask, y_label = \
|
|
1148
|
+
compute_dqdv_numerical(
|
|
1149
|
+
_b_gc_cap, _b_gc_volt, _b_gc_cyc, _b_gc_chgm, _b_gc_dchm,
|
|
1150
|
+
)
|
|
1151
|
+
_b_loaded = True
|
|
1152
|
+
print(f"dQ/dV batch: computing numerically from GC data for {fname!r}.")
|
|
1153
|
+
elif ext != '.csv':
|
|
1154
|
+
raise ValueError(f"dQ/dV mode requires .csv or Biologic .mpt/.npt file, got {ext}")
|
|
1155
|
+
elif is_biologic_datalogger_csv(fpath):
|
|
1135
1156
|
if mass_mg is None or mass_mg <= 0:
|
|
1136
1157
|
print(f" Skipped {fname}: dQ/dV (Biologic DataLogger CSV) requires --mass parameter")
|
|
1137
1158
|
plt.close(fig_b)
|
|
@@ -1164,7 +1185,7 @@ def batch_process_ec(directory: str, args):
|
|
|
1164
1185
|
if _b_mass and _b_mass > 0:
|
|
1165
1186
|
_b_gc_cap = _b_gc_cap * (1000.0 / float(_b_mass))
|
|
1166
1187
|
else:
|
|
1167
|
-
print(f"dQ/dV batch: {fname!r} — pass --mass
|
|
1188
|
+
print(f"dQ/dV batch: {fname!r} — pass --mass (mg, or e.g. 0.01g) for specific dQ/dV.")
|
|
1168
1189
|
voltage, dqdv, cycles, charge_mask, discharge_mask, y_label = \
|
|
1169
1190
|
compute_dqdv_numerical(_b_gc_cap, _b_gc_volt, _b_gc_cyc, _b_gc_chgm, _b_gc_dchm)
|
|
1170
1191
|
print(f"dQ/dV batch: computing numerically from GC data for {fname!r}.")
|
|
@@ -1219,9 +1240,9 @@ def batch_process_ec(directory: str, args):
|
|
|
1219
1240
|
|
|
1220
1241
|
# ---- CPC / EPC Mode ----
|
|
1221
1242
|
elif mode in ('cpc', 'epc'):
|
|
1222
|
-
if ext
|
|
1243
|
+
if _is_mpt_like_ext(ext):
|
|
1223
1244
|
if mass_mg is None:
|
|
1224
|
-
print(f" Skipped {fname}: {mode.upper()} mode (.mpt) requires --mass parameter")
|
|
1245
|
+
print(f" Skipped {fname}: {mode.upper()} mode (.mpt/.npt) requires --mass parameter")
|
|
1225
1246
|
plt.close(fig_b)
|
|
1226
1247
|
continue
|
|
1227
1248
|
if mode == 'cpc':
|
|
@@ -106,6 +106,39 @@ try:
|
|
|
106
106
|
except ImportError:
|
|
107
107
|
operando_ec_interactive_menu = None
|
|
108
108
|
|
|
109
|
+
|
|
110
|
+
def _run_saved_dqdv_2d_companion(fig, sess_path: str) -> None:
|
|
111
|
+
"""If load_ec_session attached a saved dQ/dV 2D bundle, run its operando menu after the EC menu."""
|
|
112
|
+
b = getattr(fig, "_dqdv_2d_companion_bundle", None)
|
|
113
|
+
if not b or len(b) < 4:
|
|
114
|
+
return
|
|
115
|
+
cfig, cax, im, cbar = b[0], b[1], b[2], b[3]
|
|
116
|
+
if operando_ec_interactive_menu is None:
|
|
117
|
+
print("Operando interactive not available; skipping saved dQ/dV 2D map.")
|
|
118
|
+
return
|
|
119
|
+
paths = list(getattr(fig, "_bp_source_paths", []) or [])
|
|
120
|
+
if not paths:
|
|
121
|
+
paths = [sess_path]
|
|
122
|
+
print("\nSession includes a saved dQ/dV 2D map — opening contour interactive menu (q exits to finish).")
|
|
123
|
+
try:
|
|
124
|
+
plt.show(block=False)
|
|
125
|
+
except Exception:
|
|
126
|
+
pass
|
|
127
|
+
try:
|
|
128
|
+
operando_ec_interactive_menu(cfig, cax, im, cbar, None, file_paths=paths, canvas_mode=False)
|
|
129
|
+
except Exception as e:
|
|
130
|
+
print(f"Saved dQ/dV 2D map menu failed: {e}")
|
|
131
|
+
finally:
|
|
132
|
+
try:
|
|
133
|
+
plt.close(cfig)
|
|
134
|
+
except Exception:
|
|
135
|
+
pass
|
|
136
|
+
try:
|
|
137
|
+
delattr(fig, "_dqdv_2d_companion_bundle")
|
|
138
|
+
except Exception:
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
|
|
109
142
|
try:
|
|
110
143
|
from .cpc_interactive import cpc_interactive_menu, _generate_similar_color, _build_compact_cpc_legend
|
|
111
144
|
except ImportError:
|
|
@@ -2624,7 +2657,11 @@ def batplot_main() -> int: # type: ignore
|
|
|
2624
2657
|
expanded = []
|
|
2625
2658
|
for p in args.files:
|
|
2626
2659
|
if os.path.isfile(p):
|
|
2627
|
-
|
|
2660
|
+
ext = os.path.splitext(p)[1].lower()
|
|
2661
|
+
if ext in convert_ext:
|
|
2662
|
+
expanded.append(p)
|
|
2663
|
+
else:
|
|
2664
|
+
print(f"Warning: Skipping non-convertible file: {p}")
|
|
2628
2665
|
elif os.path.isdir(p):
|
|
2629
2666
|
for f in sorted(os.listdir(p), key=natural_sort_key):
|
|
2630
2667
|
fp = os.path.join(p, f)
|
|
@@ -2786,6 +2823,10 @@ def batplot_main() -> int: # type: ignore
|
|
|
2786
2823
|
electrochem_interactive_menu(fig, ax, cycle_lines, file_path=sess_path)
|
|
2787
2824
|
except Exception as _ie:
|
|
2788
2825
|
print(f"Interactive menu failed: {_ie}")
|
|
2826
|
+
try:
|
|
2827
|
+
_run_saved_dqdv_2d_companion(fig, sess_path)
|
|
2828
|
+
except Exception as _c2d:
|
|
2829
|
+
print(f"Saved dQ/dV 2D companion: {_c2d}")
|
|
2789
2830
|
plt.show()
|
|
2790
2831
|
exit()
|
|
2791
2832
|
except Exception as e:
|
|
@@ -3351,6 +3392,10 @@ def batplot_main() -> int: # type: ignore
|
|
|
3351
3392
|
stack_label_bottom = bool(sess.get('stack_label_at_bottom', False))
|
|
3352
3393
|
update_labels(ax, y_data_list, label_text_objects, saved_stack, stack_label_bottom)
|
|
3353
3394
|
if cif_tick_series:
|
|
3395
|
+
try:
|
|
3396
|
+
fig._batplot_cif_tick_series = cif_tick_series
|
|
3397
|
+
except Exception:
|
|
3398
|
+
pass
|
|
3354
3399
|
# Provide draw/extend helpers compatible with interactive menu using original placement logic
|
|
3355
3400
|
def _session_q_to_2theta(peaksQ, wl):
|
|
3356
3401
|
if wl is None:
|
|
@@ -3364,7 +3409,8 @@ def batplot_main() -> int: # type: ignore
|
|
|
3364
3409
|
|
|
3365
3410
|
def _session_ensure_wavelength(default_wl=1.5406):
|
|
3366
3411
|
# Prefer any stored wl, else args.wl, else provided default
|
|
3367
|
-
|
|
3412
|
+
_ser = getattr(fig, '_batplot_cif_tick_series', None) or cif_tick_series
|
|
3413
|
+
for _lab,_fname,_peaks,_wl,_qmax,_color in _ser:
|
|
3368
3414
|
if _wl is not None:
|
|
3369
3415
|
return _wl
|
|
3370
3416
|
return getattr(args, 'wl', None) or default_wl
|
|
@@ -3374,7 +3420,10 @@ def batplot_main() -> int: # type: ignore
|
|
|
3374
3420
|
return
|
|
3375
3421
|
|
|
3376
3422
|
def _session_cif_draw():
|
|
3377
|
-
|
|
3423
|
+
cif_series_draw = getattr(fig, '_batplot_cif_tick_series', None)
|
|
3424
|
+
if cif_series_draw is None:
|
|
3425
|
+
cif_series_draw = cif_tick_series
|
|
3426
|
+
if not cif_series_draw:
|
|
3378
3427
|
return
|
|
3379
3428
|
try:
|
|
3380
3429
|
# Preserve current limits before drawing - use actual current limits
|
|
@@ -3435,7 +3484,7 @@ def batplot_main() -> int: # type: ignore
|
|
|
3435
3484
|
stacked_or_multi_y=_stacked_s,
|
|
3436
3485
|
)
|
|
3437
3486
|
_cif_bottom_m = xy_cif_stack_bottom_margin_yr(fixed_yr, show_titles=show_titles_local)
|
|
3438
|
-
needed_min = base - (len(
|
|
3487
|
+
needed_min = base - (len(cif_series_draw) - 1) * spacing - _cif_bottom_m
|
|
3439
3488
|
if not show_titles_local:
|
|
3440
3489
|
ylim_draw = tuple(prev_ylim)
|
|
3441
3490
|
elif needed_min >= prev_ylim[0]:
|
|
@@ -3457,7 +3506,7 @@ def batplot_main() -> int: # type: ignore
|
|
|
3457
3506
|
wl_any = _session_ensure_wavelength()
|
|
3458
3507
|
|
|
3459
3508
|
# Draw each series
|
|
3460
|
-
for i,(lab,fname,peaksQ,wl,qmax_sim,color) in enumerate(
|
|
3509
|
+
for i,(lab,fname,peaksQ,wl,qmax_sim,color) in enumerate(cif_series_draw):
|
|
3461
3510
|
y_line = base - i * spacing + xy_cif_stack_y_offset(fig, i)
|
|
3462
3511
|
tick_h, hkl_y = xy_cif_tick_stack_layout(y_line, yr)
|
|
3463
3512
|
# Convert peaks to axis domain
|
|
@@ -3621,7 +3670,7 @@ def batplot_main() -> int: # type: ignore
|
|
|
3621
3670
|
cif_globals_dict = None
|
|
3622
3671
|
if cif_tick_series:
|
|
3623
3672
|
cif_globals_dict = {
|
|
3624
|
-
'cif_tick_series':
|
|
3673
|
+
'cif_tick_series': cif_tick_series,
|
|
3625
3674
|
'cif_hkl_map': cif_hkl_map,
|
|
3626
3675
|
'cif_hkl_label_map': cif_hkl_label_map,
|
|
3627
3676
|
'show_cif_hkl': bool(show_cif_hkl),
|
|
@@ -4457,7 +4506,13 @@ def batplot_main() -> int: # type: ignore
|
|
|
4457
4506
|
draw_cif_ticks()
|
|
4458
4507
|
|
|
4459
4508
|
def draw_cif_ticks():
|
|
4460
|
-
|
|
4509
|
+
# Interactive menu mutates _bp.cif_tick_series; session/menu paths may use a
|
|
4510
|
+
# different list than this closure. fig._batplot_cif_tick_series stays synced
|
|
4511
|
+
# from interactive_menu so redraw sees renames (r→t), reorder, colors, etc.
|
|
4512
|
+
cif_series_draw = getattr(fig, '_batplot_cif_tick_series', None)
|
|
4513
|
+
if cif_series_draw is None:
|
|
4514
|
+
cif_series_draw = cif_tick_series
|
|
4515
|
+
if not cif_series_draw:
|
|
4461
4516
|
return
|
|
4462
4517
|
# Preserve current limits before drawing - use actual current limits
|
|
4463
4518
|
# to prevent any movement when toggling
|
|
@@ -4492,13 +4547,13 @@ def batplot_main() -> int: # type: ignore
|
|
|
4492
4547
|
_bp_module = sys.modules.get('__main__')
|
|
4493
4548
|
if _bp_module is not None and hasattr(_bp_module, 'cif_set_visible'):
|
|
4494
4549
|
vis = list(getattr(_bp_module, 'cif_set_visible') or [])
|
|
4495
|
-
if len(vis) == len(
|
|
4550
|
+
if len(vis) == len(cif_series_draw):
|
|
4496
4551
|
set_visible = [bool(v) for v in vis]
|
|
4497
4552
|
except Exception:
|
|
4498
4553
|
pass
|
|
4499
4554
|
# Effective number of visible CIF rows (for spacing and y-limit expansion)
|
|
4500
4555
|
if set_visible is None:
|
|
4501
|
-
n_rows = len(
|
|
4556
|
+
n_rows = len(cif_series_draw)
|
|
4502
4557
|
else:
|
|
4503
4558
|
n_rows = max(1, sum(1 for v in set_visible if v))
|
|
4504
4559
|
|
|
@@ -4571,7 +4626,7 @@ def batplot_main() -> int: # type: ignore
|
|
|
4571
4626
|
except Exception:
|
|
4572
4627
|
pass
|
|
4573
4628
|
visible_idx = 0
|
|
4574
|
-
for i,(lab, fname, peaksQ, wl, qmax_sim, color) in enumerate(
|
|
4629
|
+
for i,(lab, fname, peaksQ, wl, qmax_sim, color) in enumerate(cif_series_draw):
|
|
4575
4630
|
if set_visible is not None and i < len(set_visible) and not set_visible[i]:
|
|
4576
4631
|
continue
|
|
4577
4632
|
y_line = base - visible_idx * spacing + xy_cif_stack_y_offset(fig, i)
|
|
@@ -4651,7 +4706,7 @@ def batplot_main() -> int: # type: ignore
|
|
|
4651
4706
|
hover_meta = []
|
|
4652
4707
|
show_hkl = globals().get('show_cif_hkl', False)
|
|
4653
4708
|
# Build mapping from Q to label text if available
|
|
4654
|
-
for i,(lab, fname, peaksQ, wl, qmax_sim, color) in enumerate(
|
|
4709
|
+
for i,(lab, fname, peaksQ, wl, qmax_sim, color) in enumerate(cif_series_draw):
|
|
4655
4710
|
if use_2th and wl is None:
|
|
4656
4711
|
wl = getattr(ax, '_cif_hover_wl', None)
|
|
4657
4712
|
# Recreate domain peaks consistent with those drawn (limit to view)
|
|
@@ -4738,6 +4793,10 @@ def batplot_main() -> int: # type: ignore
|
|
|
4738
4793
|
ax._cif_hover_cid = cid
|
|
4739
4794
|
|
|
4740
4795
|
if cif_tick_series:
|
|
4796
|
+
try:
|
|
4797
|
+
fig._batplot_cif_tick_series = cif_tick_series
|
|
4798
|
+
except Exception:
|
|
4799
|
+
pass
|
|
4741
4800
|
# Auto-assign distinct colors for CIF tick series.
|
|
4742
4801
|
# For multiple CIF series:
|
|
4743
4802
|
# - If <= 10 files, use 'tab10' but in a re-ordered sequence to
|
|
@@ -104,6 +104,37 @@ def ensure_colormap(name: Optional[str]) -> bool:
|
|
|
104
104
|
"""
|
|
105
105
|
if not name:
|
|
106
106
|
return False
|
|
107
|
+
|
|
108
|
+
def _register_cmap_safe(cmap_name: str, cmap_obj) -> bool:
|
|
109
|
+
"""Register cmap across matplotlib API variants."""
|
|
110
|
+
# matplotlib >= 3.5 style registry API
|
|
111
|
+
try:
|
|
112
|
+
reg = getattr(plt, "colormaps", None)
|
|
113
|
+
if reg is not None and hasattr(reg, "register"):
|
|
114
|
+
try:
|
|
115
|
+
reg.register(cmap_obj, name=cmap_name, force=True)
|
|
116
|
+
except TypeError:
|
|
117
|
+
# Older signature may not support force kwarg
|
|
118
|
+
reg.register(cmap_obj, name=cmap_name)
|
|
119
|
+
return True
|
|
120
|
+
except Exception:
|
|
121
|
+
pass
|
|
122
|
+
# Older pyplot API fallback
|
|
123
|
+
try:
|
|
124
|
+
if hasattr(plt, "register_cmap"):
|
|
125
|
+
plt.register_cmap(name=cmap_name, cmap=cmap_obj)
|
|
126
|
+
return True
|
|
127
|
+
except ValueError:
|
|
128
|
+
# Already registered
|
|
129
|
+
return True
|
|
130
|
+
except Exception:
|
|
131
|
+
pass
|
|
132
|
+
# As a last resort, accept cmap object usability even if registration fails.
|
|
133
|
+
try:
|
|
134
|
+
_ = cmap_obj(0.5)
|
|
135
|
+
return True
|
|
136
|
+
except Exception:
|
|
137
|
+
return False
|
|
107
138
|
|
|
108
139
|
# Handle reversed colormaps (remove '_r' suffix to get base name)
|
|
109
140
|
# Example: 'viridis_r' → base = 'viridis', we'll reverse it later if needed
|
|
@@ -120,13 +151,7 @@ def ensure_colormap(name: Optional[str]) -> bool:
|
|
|
120
151
|
import cmcrameri.cm as cmc
|
|
121
152
|
if hasattr(cmc, base_lower):
|
|
122
153
|
cmap_obj = getattr(cmc, base_lower)
|
|
123
|
-
|
|
124
|
-
# Register it with matplotlib so it can be used like built-in colormaps
|
|
125
|
-
plt.register_cmap(name=base_lower, cmap=cmap_obj)
|
|
126
|
-
except ValueError:
|
|
127
|
-
# Already registered, that's fine
|
|
128
|
-
pass
|
|
129
|
-
return True
|
|
154
|
+
return _register_cmap_safe(base_lower, cmap_obj)
|
|
130
155
|
except Exception:
|
|
131
156
|
# cmcrameri not installed or colormap not found, continue to next step
|
|
132
157
|
pass
|
|
@@ -139,13 +164,7 @@ def ensure_colormap(name: Optional[str]) -> bool:
|
|
|
139
164
|
# N=256 means create 256 intermediate colors by interpolating between the given colors
|
|
140
165
|
# This creates a smooth gradient
|
|
141
166
|
cmap_obj = LinearSegmentedColormap.from_list(base_lower, custom, N=256)
|
|
142
|
-
|
|
143
|
-
# Register with matplotlib
|
|
144
|
-
plt.register_cmap(name=base_lower, cmap=cmap_obj)
|
|
145
|
-
except ValueError:
|
|
146
|
-
# Already registered, that's fine
|
|
147
|
-
pass
|
|
148
|
-
return True
|
|
167
|
+
return _register_cmap_safe(base_lower, cmap_obj)
|
|
149
168
|
except Exception:
|
|
150
169
|
return False
|
|
151
170
|
|
|
@@ -18,6 +18,10 @@ Example config.json structure:
|
|
|
18
18
|
"#0000FF",
|
|
19
19
|
"red",
|
|
20
20
|
"blue"
|
|
21
|
+
],
|
|
22
|
+
"recent_axis_names": [
|
|
23
|
+
"Potential (V)",
|
|
24
|
+
"dQ/dV (mAh V$^{-1}$)"
|
|
21
25
|
]
|
|
22
26
|
}
|
|
23
27
|
|
|
@@ -192,7 +196,47 @@ def save_user_colors(colors: List[str]) -> None:
|
|
|
192
196
|
save_config(config) # Save entire config back to file
|
|
193
197
|
|
|
194
198
|
|
|
199
|
+
_RECENT_AXIS_NAMES_KEY = 'recent_axis_names'
|
|
200
|
+
RECENT_AXIS_NAMES_MAX = 20
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def get_recent_axis_names() -> List[str]:
|
|
204
|
+
"""Return up to :data:`RECENT_AXIS_NAMES_MAX` recently typed axis labels (newest first)."""
|
|
205
|
+
raw = load_config().get(_RECENT_AXIS_NAMES_KEY, [])
|
|
206
|
+
if not isinstance(raw, list):
|
|
207
|
+
return []
|
|
208
|
+
out: List[str] = []
|
|
209
|
+
for item in raw:
|
|
210
|
+
s = str(item).strip()
|
|
211
|
+
if s and s not in out:
|
|
212
|
+
out.append(s)
|
|
213
|
+
if len(out) >= RECENT_AXIS_NAMES_MAX:
|
|
214
|
+
break
|
|
215
|
+
return out
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def record_recent_axis_name(name: str) -> None:
|
|
219
|
+
"""Add an axis label to the shared recent list (dedupe, newest first, max 20)."""
|
|
220
|
+
s = str(name or '').strip()
|
|
221
|
+
if not s:
|
|
222
|
+
return
|
|
223
|
+
config = load_config()
|
|
224
|
+
raw = config.get(_RECENT_AXIS_NAMES_KEY, [])
|
|
225
|
+
names: List[str] = []
|
|
226
|
+
if isinstance(raw, list):
|
|
227
|
+
for item in raw:
|
|
228
|
+
t = str(item).strip()
|
|
229
|
+
if t and t != s:
|
|
230
|
+
names.append(t)
|
|
231
|
+
names.insert(0, s)
|
|
232
|
+
config[_RECENT_AXIS_NAMES_KEY] = names[:RECENT_AXIS_NAMES_MAX]
|
|
233
|
+
save_config(config)
|
|
234
|
+
|
|
235
|
+
|
|
195
236
|
__all__ = [
|
|
196
237
|
'get_user_colors',
|
|
197
238
|
'save_user_colors',
|
|
239
|
+
'get_recent_axis_names',
|
|
240
|
+
'record_recent_axis_name',
|
|
241
|
+
'RECENT_AXIS_NAMES_MAX',
|
|
198
242
|
]
|