batplot 1.8.39__tar.gz → 1.8.41__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 (135) hide show
  1. batplot-1.8.41/MANIFEST.in +3 -0
  2. {batplot-1.8.39/batplot.egg-info → batplot-1.8.41}/PKG-INFO +10 -8
  3. {batplot-1.8.39 → batplot-1.8.41}/README.md +6 -7
  4. batplot-1.8.41/batplot/__init__.py +74 -0
  5. {batplot-1.8.39 → batplot-1.8.41}/batplot/args.py +115 -89
  6. {batplot-1.8.39 → batplot-1.8.41}/batplot/batch.py +40 -19
  7. batplot-1.8.41/batplot/batplot.py +642 -0
  8. {batplot-1.8.39 → batplot-1.8.41}/batplot/canvas_interactive.py +7 -7
  9. {batplot-1.8.39 → batplot-1.8.41}/batplot/color_utils.py +33 -14
  10. {batplot-1.8.39 → batplot-1.8.41}/batplot/config.py +44 -0
  11. {batplot-1.8.39 → batplot-1.8.41}/batplot/data/CHANGELOG.md +13 -0
  12. {batplot-1.8.39 → batplot-1.8.41}/batplot/dev_upgrade.py +350 -103
  13. batplot-1.8.41/batplot/ec_common.py +103 -0
  14. batplot-1.8.41/batplot/modes.py +37 -0
  15. batplot-1.8.41/batplot/plot_modes/__init__.py +1 -0
  16. batplot-1.8.41/batplot/plot_modes/common/__init__.py +1 -0
  17. batplot-1.8.41/batplot/plot_modes/common/axis_state.py +128 -0
  18. batplot-1.8.41/batplot/plot_modes/common/files.py +42 -0
  19. batplot-1.8.41/batplot/plot_modes/common/fonts.py +132 -0
  20. batplot-1.8.41/batplot/plot_modes/common/interactive_state.py +99 -0
  21. batplot-1.8.41/batplot/plot_modes/common/menu_rendering.py +84 -0
  22. batplot-1.8.41/batplot/plot_modes/common/menus.py +437 -0
  23. batplot-1.8.41/batplot/plot_modes/common/palettes.py +215 -0
  24. batplot-1.8.41/batplot/plot_modes/common/smoothing.py +38 -0
  25. batplot-1.8.41/batplot/plot_modes/common/sources.py +68 -0
  26. batplot-1.8.41/batplot/plot_modes/common/spines.py +758 -0
  27. batplot-1.8.41/batplot/plot_modes/common/terminal.py +128 -0
  28. batplot-1.8.41/batplot/plot_modes/common/title_offsets.py +65 -0
  29. batplot-1.8.41/batplot/plot_modes/cpc/__init__.py +5 -0
  30. batplot-1.8.41/batplot/plot_modes/cpc/actions.py +963 -0
  31. batplot-1.8.41/batplot/plot_modes/cpc/colors.py +363 -0
  32. batplot-1.8.41/batplot/plot_modes/cpc/interactive.py +2822 -0
  33. batplot-1.8.41/batplot/plot_modes/cpc/labels.py +257 -0
  34. batplot-1.8.41/batplot/plot_modes/cpc/legend.py +480 -0
  35. batplot-1.8.41/batplot/plot_modes/cpc/menu.py +74 -0
  36. batplot-1.8.41/batplot/plot_modes/cpc/routing.py +516 -0
  37. batplot-1.8.41/batplot/plot_modes/cpc/session.py +15 -0
  38. batplot-1.8.41/batplot/plot_modes/cpc/snapshots.py +119 -0
  39. batplot-1.8.41/batplot/plot_modes/electrochem/__init__.py +5 -0
  40. batplot-1.8.41/batplot/plot_modes/electrochem/actions.py +1246 -0
  41. batplot-1.8.41/batplot/plot_modes/electrochem/colors.py +722 -0
  42. batplot-1.8.41/batplot/plot_modes/electrochem/dqdv_2d.py +689 -0
  43. batplot-1.8.41/batplot/plot_modes/electrochem/export.py +65 -0
  44. batplot-1.8.41/batplot/plot_modes/electrochem/interactive.py +3220 -0
  45. batplot-1.8.41/batplot/plot_modes/electrochem/labels.py +224 -0
  46. batplot-1.8.41/batplot/plot_modes/electrochem/legend.py +269 -0
  47. batplot-1.8.41/batplot/plot_modes/electrochem/legend_order.py +56 -0
  48. batplot-1.8.41/batplot/plot_modes/electrochem/line_style.py +269 -0
  49. batplot-1.8.41/batplot/plot_modes/electrochem/menu.py +111 -0
  50. batplot-1.8.41/batplot/plot_modes/electrochem/routing.py +1540 -0
  51. batplot-1.8.41/batplot/plot_modes/electrochem/session.py +15 -0
  52. batplot-1.8.41/batplot/plot_modes/electrochem/spine_colors.py +108 -0
  53. batplot-1.8.41/batplot/plot_modes/electrochem/style.py +651 -0
  54. batplot-1.8.41/batplot/plot_modes/operando/__init__.py +5 -0
  55. batplot-1.8.41/batplot/plot_modes/operando/actions.py +1565 -0
  56. batplot-1.8.41/batplot/plot_modes/operando/colors.py +174 -0
  57. batplot-1.8.41/batplot/plot_modes/operando/grid.py +127 -0
  58. batplot-1.8.41/batplot/plot_modes/operando/interactive.py +3761 -0
  59. batplot-1.8.41/batplot/plot_modes/operando/ions_axis.py +141 -0
  60. batplot-1.8.41/batplot/plot_modes/operando/labels.py +173 -0
  61. batplot-1.8.41/batplot/plot_modes/operando/layout.py +409 -0
  62. batplot-1.8.41/batplot/plot_modes/operando/line_style.py +103 -0
  63. batplot-1.8.41/batplot/plot_modes/operando/menu.py +114 -0
  64. batplot-1.8.41/batplot/plot_modes/operando/peaks.py +289 -0
  65. batplot-1.8.39/batplot/operando.py → batplot-1.8.41/batplot/plot_modes/operando/plot.py +58 -8
  66. batplot-1.8.41/batplot/plot_modes/operando/routing.py +148 -0
  67. batplot-1.8.41/batplot/plot_modes/operando/session.py +15 -0
  68. batplot-1.8.41/batplot/plot_modes/operando/style.py +389 -0
  69. batplot-1.8.41/batplot/plot_modes/operando/visibility.py +285 -0
  70. batplot-1.8.41/batplot/plot_modes/session_routing.py +1072 -0
  71. batplot-1.8.41/batplot/plot_modes/xy/__init__.py +5 -0
  72. batplot-1.8.41/batplot/plot_modes/xy/actions.py +483 -0
  73. batplot-1.8.41/batplot/plot_modes/xy/arrange.py +131 -0
  74. batplot-1.8.41/batplot/plot_modes/xy/axis_range.py +624 -0
  75. batplot-1.8.41/batplot/plot_modes/xy/cif.py +465 -0
  76. batplot-1.8.41/batplot/plot_modes/xy/colors.py +336 -0
  77. batplot-1.8.41/batplot/plot_modes/xy/data_ops.py +114 -0
  78. batplot-1.8.41/batplot/plot_modes/xy/derivative.py +128 -0
  79. batplot-1.8.41/batplot/plot_modes/xy/game.py +125 -0
  80. batplot-1.8.41/batplot/plot_modes/xy/interactive.py +2441 -0
  81. batplot-1.8.41/batplot/plot_modes/xy/labels.py +160 -0
  82. batplot-1.8.41/batplot/plot_modes/xy/line_style.py +257 -0
  83. batplot-1.8.41/batplot/plot_modes/xy/menu.py +49 -0
  84. batplot-1.8.41/batplot/plot_modes/xy/peaks.py +174 -0
  85. batplot-1.8.41/batplot/plot_modes/xy/pipeline.py +1470 -0
  86. batplot-1.8.41/batplot/plot_modes/xy/session.py +15 -0
  87. batplot-1.8.41/batplot/plot_modes/xy/smoothing.py +716 -0
  88. {batplot-1.8.39/batplot → batplot-1.8.41/batplot/plot_modes/xy}/style.py +76 -110
  89. {batplot-1.8.39 → batplot-1.8.41}/batplot/plotting.py +32 -1
  90. {batplot-1.8.39 → batplot-1.8.41}/batplot/readers.py +50 -2
  91. {batplot-1.8.39 → batplot-1.8.41}/batplot/session.py +773 -433
  92. {batplot-1.8.39 → batplot-1.8.41}/batplot/showcol.py +8 -7
  93. batplot-1.8.41/batplot/style.py +19 -0
  94. {batplot-1.8.39 → batplot-1.8.41}/batplot/ui.py +150 -3
  95. {batplot-1.8.39 → batplot-1.8.41}/batplot/utils.py +52 -0
  96. {batplot-1.8.39 → batplot-1.8.41}/batplot/version_check.py +3 -2
  97. {batplot-1.8.39 → batplot-1.8.41/batplot.egg-info}/PKG-INFO +10 -8
  98. batplot-1.8.41/batplot.egg-info/SOURCES.txt +121 -0
  99. {batplot-1.8.39 → batplot-1.8.41}/batplot.egg-info/entry_points.txt +0 -1
  100. {batplot-1.8.39 → batplot-1.8.41}/batplot.egg-info/requires.txt +4 -0
  101. batplot-1.8.41/batplot.egg-info/top_level.txt +1 -0
  102. {batplot-1.8.39 → batplot-1.8.41}/pyproject.toml +24 -2
  103. batplot-1.8.41/tests/test_cli_smoke.py +183 -0
  104. batplot-1.8.41/tests/test_common_files.py +60 -0
  105. batplot-1.8.41/tests/test_common_palettes.py +209 -0
  106. batplot-1.8.41/tests/test_contracts.py +402 -0
  107. batplot-1.8.41/tests/test_cpc_roundtrip.py +458 -0
  108. batplot-1.8.41/tests/test_csv_readers.py +38 -0
  109. batplot-1.8.41/tests/test_dev_upgrade.py +163 -0
  110. batplot-1.8.41/tests/test_ec_roundtrip.py +553 -0
  111. batplot-1.8.41/tests/test_interactive_menu_smoke.py +236 -0
  112. batplot-1.8.41/tests/test_interactive_state.py +822 -0
  113. batplot-1.8.41/tests/test_operando_roundtrip.py +696 -0
  114. batplot-1.8.41/tests/test_xy_modules.py +246 -0
  115. batplot-1.8.41/tests/test_xy_roundtrip.py +267 -0
  116. batplot-1.8.39/MANIFEST.in +0 -2
  117. batplot-1.8.39/batplot/__init__.py +0 -5
  118. batplot-1.8.39/batplot/batplot.py +0 -5070
  119. batplot-1.8.39/batplot/cpc_interactive.py +0 -5677
  120. batplot-1.8.39/batplot/data/USER_MANUAL.md +0 -624
  121. batplot-1.8.39/batplot/electrochem_interactive.py +0 -6831
  122. batplot-1.8.39/batplot/interactive.py +0 -6353
  123. batplot-1.8.39/batplot/manual.py +0 -342
  124. batplot-1.8.39/batplot/modes.py +0 -862
  125. batplot-1.8.39/batplot/operando_ec_interactive.py +0 -7104
  126. batplot-1.8.39/batplot.egg-info/SOURCES.txt +0 -40
  127. batplot-1.8.39/batplot.egg-info/top_level.txt +0 -2
  128. {batplot-1.8.39 → batplot-1.8.41}/LICENSE +0 -0
  129. {batplot-1.8.39 → batplot-1.8.41}/NOTICE +0 -0
  130. {batplot-1.8.39 → batplot-1.8.41}/batplot/cif.py +0 -0
  131. {batplot-1.8.39 → batplot-1.8.41}/batplot/cli.py +0 -0
  132. {batplot-1.8.39 → batplot-1.8.41}/batplot/converters.py +0 -0
  133. {batplot-1.8.39 → batplot-1.8.41}/batplot.egg-info/dependency_links.txt +0 -0
  134. {batplot-1.8.39 → batplot-1.8.41}/setup.cfg +0 -0
  135. {batplot-1.8.39 → batplot-1.8.41}/setup.py +0 -0
@@ -0,0 +1,3 @@
1
+ include README.md LICENSE
2
+ recursive-include batplot/data CHANGELOG.md
3
+ exclude batplot/data/USER_MANUAL.md
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: batplot
3
- Version: 1.8.39
3
+ Version: 1.8.41
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
@@ -47,6 +47,9 @@ Requires-Dist: numpy>=1.21.0
47
47
  Requires-Dist: matplotlib>=3.5.0
48
48
  Requires-Dist: rich>=10.0.0
49
49
  Requires-Dist: openpyxl>=3.0.0
50
+ Provides-Extra: test
51
+ Requires-Dist: pytest>=7.0; extra == "test"
52
+ Requires-Dist: basedpyright>=1.28.0; extra == "test"
50
53
  Dynamic: license-file
51
54
 
52
55
  # batplot
@@ -213,7 +216,6 @@ batplot battery.mpt --gc --mass 7.0 --i
213
216
 
214
217
  ```bash
215
218
  batplot cyclic.mpt --cv --i
216
- batplot cyclic.mpt --cv --i
217
219
  ```
218
220
 
219
221
  ### Differential capacity (dQ/dV)
@@ -356,12 +358,12 @@ With `--interactive`:
356
358
  ## Help & Documentation
357
359
 
358
360
  ```bash
359
- batplot --help # General help
360
- batplot --help xy # XY mode guide
361
- batplot --help ec # Electrochemistry guide
362
- batplot --help op # Operando guide
363
- batplot --version # Version and release notes
364
- batplot --manual # Open illustrated manual
361
+ batplot --h # General help
362
+ batplot --h xy # XY mode guide
363
+ batplot --h ec # Electrochemistry guide
364
+ batplot --h op # Operando guide
365
+ batplot --v # Version and release notes
366
+ batplot --m # Open illustrated manual
365
367
  ```
366
368
 
367
369
  - [USER_MANUAL.md](USER_MANUAL.md) — Detailed usage and workflows
@@ -162,7 +162,6 @@ batplot battery.mpt --gc --mass 7.0 --i
162
162
 
163
163
  ```bash
164
164
  batplot cyclic.mpt --cv --i
165
- batplot cyclic.mpt --cv --i
166
165
  ```
167
166
 
168
167
  ### Differential capacity (dQ/dV)
@@ -305,12 +304,12 @@ With `--interactive`:
305
304
  ## Help & Documentation
306
305
 
307
306
  ```bash
308
- batplot --help # General help
309
- batplot --help xy # XY mode guide
310
- batplot --help ec # Electrochemistry guide
311
- batplot --help op # Operando guide
312
- batplot --version # Version and release notes
313
- batplot --manual # Open illustrated manual
307
+ batplot --h # General help
308
+ batplot --h xy # XY mode guide
309
+ batplot --h ec # Electrochemistry guide
310
+ batplot --h op # Operando guide
311
+ batplot --v # Version and release notes
312
+ batplot --m # Open illustrated manual
314
313
  ```
315
314
 
316
315
  - [USER_MANUAL.md](USER_MANUAL.md) — Detailed usage and workflows
@@ -0,0 +1,74 @@
1
+ """batplot: Interactive plotting for battery data visualization."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ import importlib.abc
7
+ import importlib.machinery
8
+ import sys
9
+
10
+ __version__ = "1.8.41"
11
+
12
+
13
+ _LEGACY_MODULE_ALIASES = {
14
+ "batplot.interactive": "batplot.plot_modes.xy.interactive",
15
+ "batplot.electrochem_interactive": "batplot.plot_modes.electrochem.interactive",
16
+ "batplot.cpc_interactive": "batplot.plot_modes.cpc.interactive",
17
+ "batplot.cpc_menu": "batplot.plot_modes.cpc.menu",
18
+ "batplot.operando_ec_interactive": "batplot.plot_modes.operando.interactive",
19
+ "batplot.operando_layout": "batplot.plot_modes.operando.layout",
20
+ "batplot.operando_menu": "batplot.plot_modes.operando.menu",
21
+ "batplot.operando_style": "batplot.plot_modes.operando.style",
22
+ "batplot.interactive_state": "batplot.plot_modes.common.interactive_state",
23
+ }
24
+
25
+
26
+ class _LegacyModuleAliasLoader(importlib.abc.Loader):
27
+ """Lazy compatibility loader for mode modules moved under ``plot_modes``."""
28
+
29
+ def __init__(self, fullname: str, target_name: str):
30
+ self.fullname = fullname
31
+ self.target_name = target_name
32
+
33
+ def create_module(self, spec):
34
+ module = importlib.import_module(self.target_name)
35
+ self._apply_extra_exports(module)
36
+ sys.modules[self.fullname] = module
37
+ parent_name, _, child_name = self.fullname.rpartition(".")
38
+ parent = sys.modules.get(parent_name)
39
+ if parent is not None:
40
+ setattr(parent, child_name, module)
41
+ return module
42
+
43
+ def exec_module(self, module) -> None:
44
+ return None
45
+
46
+ def _apply_extra_exports(self, module) -> None:
47
+ if self.fullname == "batplot.cpc_interactive":
48
+ menu = importlib.import_module("batplot.plot_modes.cpc.menu")
49
+ module.print_cpc_menu = menu.print_cpc_menu
50
+ module.build_cpc_menu_columns = menu.build_cpc_menu_columns
51
+ elif self.fullname == "batplot.operando_ec_interactive":
52
+ style = importlib.import_module("batplot.plot_modes.operando.style")
53
+ module.build_operando_ec_style_config_v2 = style.build_operando_ec_style_config_v2
54
+
55
+
56
+ class _LegacyModuleAliasFinder(importlib.abc.MetaPathFinder):
57
+ def find_spec(self, fullname, path=None, target=None):
58
+ target_name = _LEGACY_MODULE_ALIASES.get(fullname)
59
+ if target_name is None:
60
+ return None
61
+ loader = _LegacyModuleAliasLoader(fullname, target_name)
62
+ spec = importlib.machinery.ModuleSpec(fullname, loader)
63
+ spec.origin = f"legacy-alias:{target_name}"
64
+ return spec
65
+
66
+
67
+ def _install_legacy_alias_finder() -> None:
68
+ if not any(isinstance(finder, _LegacyModuleAliasFinder) for finder in sys.meta_path):
69
+ sys.meta_path.insert(0, _LegacyModuleAliasFinder())
70
+
71
+
72
+ _install_legacy_alias_finder()
73
+
74
+ __all__ = ["__version__"]
@@ -24,6 +24,7 @@ from __future__ import annotations
24
24
  import argparse
25
25
  import sys
26
26
  import re
27
+ import webbrowser
27
28
 
28
29
  # ====================================================================
29
30
  # HELP OUTPUT
@@ -37,6 +38,8 @@ import re
37
38
  #
38
39
  # If rich is not installed, we fall back to plain text (still works fine).
39
40
  # ====================================================================
41
+ from .utils import parse_mass_mg_from_cli
42
+
40
43
  try:
41
44
  from rich.console import Console # type: ignore[import]
42
45
  from rich.markup import escape # type: ignore[import]
@@ -167,13 +170,13 @@ def _print_general_help() -> None:
167
170
  " • More: --help xy / --help ec / --help op\n\n"
168
171
 
169
172
  "More help:\n"
170
- " batplot --version # Version and release info (with option to show full release notes)\n"
173
+ " batplot --v # Version and release info (with option to show full release notes)\n"
171
174
  " batplot --showcol FILE [FILE...] # Preview column names + first 10 values per column\n"
172
- " batplot --help # This help\n"
173
- " batplot --help xy # XY file plotting guide\n"
174
- " batplot --help ec # Electrochemistry (GC/dQdV/CV/CPC) guide\n"
175
- " batplot --help op # Operando contour guide (also: batplot --help contour)\n"
176
- " batplot --manual # Open the illustrated txt manual with highlights\n\n"
175
+ " batplot --h # This help\n"
176
+ " batplot --h xy # XY file plotting guide\n"
177
+ " batplot --h ec # Electrochemistry (GC/dQdV/CV/CPC) guide\n"
178
+ " batplot --h op # Operando contour guide (also: batplot --help contour)\n"
179
+ " batplot --m # Open the illustrated PDF manual\n\n"
177
180
 
178
181
  "Contact & Updates:\n"
179
182
  " Subscribe to batplot-lab@kjemi.uio.no for updates\n"
@@ -244,10 +247,14 @@ def _print_xy_help() -> None:
244
247
  " - file:wl : single wavelength (for Q conversion or CIF 2theta calculation)\n"
245
248
  " - file:wl1:wl2 : dual wavelength (convert 2theta→Q using wl1, then Q→2theta using wl2)\n"
246
249
  " - file.cif:wl : CIF file with wavelength for 2theta tick calculation\n"
250
+ " - file:q : mark file as already in Q (no conversion; implies Q axis)\n"
251
+ " Any file:wl or file:q suffix implies Q mode automatically (no --xaxis q needed);\n"
252
+ " files without wavelength info are then assumed to be already in Q.\n"
247
253
  " Examples:\n"
248
254
  " batplot data.xye:1.5406 --xaxis 2theta\n"
249
255
  " batplot data.xye:0.25:1.54 --xaxis 2theta\n"
250
256
  " batplot data.xye pattern.cif:0.25448 --xaxis 2theta\n"
257
+ " batplot scan.xy:0.709 sim.csv:q --stack --i\n"
251
258
  " --readcol <x_col> <y_col> : specify which columns to read as x and y (1-indexed)\n"
252
259
  " Per-file: file1.xy --readcol 2 3 file2.xy --readcol 4 5 (different cols per file)\n"
253
260
  " Multi-curve: file.xy --readcol 1 2 1 3 (plot cols 1,2 and 1,3 as two curves)\n"
@@ -278,8 +285,9 @@ def _print_ec_help() -> None:
278
285
  " • Neware: Customized report — check all boxes\n"
279
286
  " • Biologic: Export all info to .mpt file\n\n"
280
287
  "Use --i for styling, colors, line widths, axis scales, etc.\n"
281
- "GC from .mpt: requires active mass in mg to compute mAh g⁻¹.\n"
282
- " batplot --gc file.mpt --mass 6.5 --i\n\n"
288
+ "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"
289
+ " batplot --gc file.mpt --mass 6.5 --i\n"
290
+ " batplot --gc file.npt --mass 10g --i\n\n"
283
291
  "GC from supported .csv: specific capacity read directly when available; use --mass for\n"
284
292
  " Neware absolute-capacity files (Cycle Index / Step Index / DataPoint format).\n"
285
293
  " batplot --gc file.csv\n"
@@ -289,9 +297,11 @@ def _print_ec_help() -> None:
289
297
  " batplot f1.csv --mass 3.52 f2.mpt --mass 5.0 --cpc\n"
290
298
  " # Files without --mass between them use the global --mass value (or none)\n"
291
299
  " # Single --mass applies to all files: batplot f1.mpt f2.mpt --gc --mass 7.0\n\n"
292
- "dQ/dV from supported .csv (pre-calculated column or computed from GC data):\n"
300
+ "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"
293
301
  " batplot --dqdv file.csv\n"
294
- " batplot --dqdv file.csv --mass 3.52 # Neware absolute-capacity CSV\n\n"
302
+ " batplot --dqdv file.csv --mass 3.52 # Neware absolute-capacity CSV\n"
303
+ " batplot --dqdv file.mpt --mass 6.5\n"
304
+ " batplot --dqdv file.npt --mass 0.01g\n\n"
295
305
  "Cyclic voltammetry (CV) from .mpt or .txt: plots potential vs current for each cycle.\n"
296
306
  " batplot --cv file.mpt\n"
297
307
  " batplot --cv file.txt\n\n"
@@ -300,7 +310,7 @@ def _print_ec_help() -> None:
300
310
  " batplot --cpc file.csv # Neware CSV (specific capacity)\n"
301
311
  " batplot --cpc file.csv --mass 3.52 # Neware absolute-capacity CSV\n"
302
312
  " batplot --cpc file.xlsx # Landt/Lanhe Excel (Chinese tester)\n"
303
- " batplot --cpc file.mpt --mass 1.2 # Biologic MPT\n"
313
+ " batplot --cpc file.mpt --mass 1.2 # Biologic .mpt / .npt\n"
304
314
  " batplot file1.csv --mass 3.52 file2.mpt --mass 1.2 --cpc # Per-file mass\n"
305
315
  " batplot --cpc file1.csv file2.xlsx file3.mpt --mass 1.2 --i\n\n"
306
316
  "Excel support: Landt/Lanhe (蓝电/蓝河) .xlsx files with Chinese headers:\n"
@@ -342,6 +352,8 @@ def _print_op_help() -> None:
342
352
  " batplot --operando --xaxis 2theta # Using 2theta axis\n"
343
353
  " batplot --operando --1d --i # Plot derivatives as contour with interactive menu\n"
344
354
  " batplot --operando --2d --i # Plot derivatives (alias for --1d)\n\n"
355
+ " batplot --operando --average 2 --i # Average every 2 scans before contouring\n\n"
356
+ " batplot --operando --sum 2 --i # Sum every 2 scans to boost intensity\n\n"
345
357
  "Bruker operando (.brml):\n"
346
358
  " • Place .brml files (e.g. XX_cyc1.brml, XX_cyc2.brml) in the folder.\n"
347
359
  " • Each .brml is expanded into per-scan rows; files sorted by cyc1/cyc2/cyc3.\n"
@@ -355,6 +367,8 @@ def _print_op_help() -> None:
355
367
  " • If a .mpt file is present, a side panel is added for dual-panel mode (time/potential/temp/etc.).\n"
356
368
  " • Without a .mpt file, operando-only mode shows the contour plot alone.\n"
357
369
  " • --1d / --2d: plot the first derivative (dy/dx) of each scan as a contour plot.\n\n"
370
+ " • --average N: average every N consecutive scans to improve S/N (e.g., N=2 averages scans 1+2, 3+4, ...).\n\n"
371
+ " • --sum N: sum every N consecutive scans (same binning as --average, but without division by N).\n\n"
358
372
  "Column selection (operando-specific):\n"
359
373
  " --readcolc <x> <y> : columns for contour plot (x,y in .xy/.xye/.qye/.dat files)\n"
360
374
  " --readcols <x> <y> : columns for side panel (x,y in .mpt file)\n"
@@ -366,58 +380,10 @@ def _print_op_help() -> None:
366
380
  _print_help(msg)
367
381
 
368
382
 
369
- def build_parser() -> argparse.ArgumentParser:
370
- """
371
- Build the argument parser for batplot command-line interface.
372
-
373
- HOW ARGUMENT PARSING WORKS:
374
- --------------------------
375
- This function creates an ArgumentParser object that defines all valid
376
- command-line arguments for batplot. When you run 'batplot file.xy --i',
377
- argparse uses this parser to:
378
- 1. Recognize which arguments are valid
379
- 2. Extract values from the command line
380
- 3. Convert them to appropriate Python types (int, float, bool, etc.)
381
- 4. Store them in a namespace object (args.files, args.interactive, etc.)
382
-
383
- ARGUMENT TYPES:
384
- --------------
385
- - Positional arguments: 'files' - list of file paths (can be 0 or more)
386
- - Flags (boolean): '--i' - True if present, False if absent
387
- - Options with values: '--mass 7.0' - requires a value (float in this case)
388
- - Optional arguments: '--help xy' - can have optional value
389
-
390
- WHY add_help=False?
391
- -------------------
392
- We use a custom help system that supports topic-specific help:
393
- - 'batplot --help' → general help
394
- - 'batplot --help xy' → XY mode help
395
- - 'batplot --help ec' → EC mode help
396
- - 'batplot --help op' → Operando mode help
397
-
398
- This gives users more targeted help instead of one giant help page.
399
-
400
- Returns:
401
- Configured ArgumentParser object ready to parse command-line arguments
402
- """
403
- # Create parser with custom help system (we handle help ourselves)
404
- parser = argparse.ArgumentParser(add_help=False)
405
-
406
- # ====================================================================
407
- # TOPIC-AWARE HELP SYSTEM
408
- # ====================================================================
409
- # Instead of standard --help, we support topic-specific help:
410
- # batplot --help → general help
411
- # batplot --help xy → XY mode help
412
- # batplot --help ec → EC mode help
413
- # batplot --help op → Operando mode help
414
- #
415
- # nargs="?" means the argument is optional:
416
- # - If not provided: const="" (empty string)
417
- # - If provided: uses the value (e.g., "xy", "ec", "op")
418
- # ====================================================================
383
+ def _add_help_and_entry_arguments(parser: argparse.ArgumentParser) -> None:
384
+ """Register help/version/manual flags and positional file inputs."""
419
385
  parser.add_argument("--help", nargs="?", const="", metavar="topic",
420
- help=argparse.SUPPRESS) # SUPPRESS hides from auto-generated help
386
+ help=argparse.SUPPRESS)
421
387
  parser.add_argument("--version", action="store_true", dest="version",
422
388
  help="Show version and current release info, then exit.")
423
389
  parser.add_argument(
@@ -426,18 +392,11 @@ def build_parser() -> argparse.ArgumentParser:
426
392
  help=argparse.SUPPRESS,
427
393
  )
428
394
  parser.add_argument("--manual", action="store_true", help=argparse.SUPPRESS)
429
-
430
- # ====================================================================
431
- # POSITIONAL ARGUMENTS (FILE PATHS)
432
- # ====================================================================
433
- # 'files' is a positional argument, meaning it doesn't need a flag.
434
- # nargs="*" means it accepts 0 or more values (list).
435
- # Examples:
436
- # batplot file1.xy file2.xy → args.files = ['file1.xy', 'file2.xy']
437
- # batplot allfiles → args.files = ['allfiles']
438
- # batplot --i → args.files = [] (empty list)
439
- # ====================================================================
440
395
  parser.add_argument("files", nargs="*", help=argparse.SUPPRESS)
396
+
397
+
398
+ def _add_xy_arguments(parser: argparse.ArgumentParser) -> None:
399
+ """Register XY/general plotting arguments."""
441
400
  parser.add_argument("--delta", type=float, default=None, help=argparse.SUPPRESS)
442
401
  parser.add_argument("--autoscale", action="store_true", help=argparse.SUPPRESS)
443
402
  parser.add_argument("--xrange", nargs=2, type=float, help=argparse.SUPPRESS)
@@ -455,15 +414,31 @@ def build_parser() -> argparse.ArgumentParser:
455
414
  parser.add_argument("--kchik", action="store_true", help=argparse.SUPPRESS)
456
415
  parser.add_argument("--k2chik", action="store_true", help=argparse.SUPPRESS)
457
416
  parser.add_argument("--k3chik", action="store_true", help=argparse.SUPPRESS)
417
+ parser.add_argument("--1d", action="store_true", dest="derivative_1d", help=argparse.SUPPRESS)
418
+ parser.add_argument("--2d", action="store_true", dest="derivative_2d", help=argparse.SUPPRESS)
419
+
420
+
421
+ def _add_interactive_export_arguments(parser: argparse.ArgumentParser) -> None:
422
+ """Register interactive, export, and batch-output arguments."""
458
423
  parser.add_argument("--i", "--interactive", action="store_true", dest="interactive", help=argparse.SUPPRESS)
459
424
  parser.add_argument("--savefig", type=str, help=argparse.SUPPRESS)
460
425
  parser.add_argument("--stack", action="store_true", help=argparse.SUPPRESS)
461
426
  parser.add_argument("--ry", action="store_true", help=argparse.SUPPRESS)
462
427
  parser.add_argument("--txaxis", action="store_true", help=argparse.SUPPRESS)
463
- parser.add_argument("--operando", "--contour", action="store_true", dest="operando", help=argparse.SUPPRESS)
464
428
  parser.add_argument("--debug", action="store_true", help=argparse.SUPPRESS)
429
+ parser.add_argument("--ro", action="store_true", help=argparse.SUPPRESS)
430
+ parser.add_argument("--all", type=str, nargs='?', const='all', help=argparse.SUPPRESS)
431
+ parser.add_argument("--format", type=str, default='svg',
432
+ choices=['svg', 'png', 'pdf', 'jpg', 'jpeg', 'eps', 'tif', 'tiff'],
433
+ help=argparse.SUPPRESS)
434
+ parser.add_argument("--canvas", action="store_true", dest="canvas",
435
+ help="Canvas mode: combine multiple .pkl sessions into one layout. Use numbers to edit each panel.")
436
+
437
+
438
+ def _add_electrochem_arguments(parser: argparse.ArgumentParser) -> None:
439
+ """Register electrochemistry mode arguments."""
465
440
  parser.add_argument("--gc", action="store_true", help=argparse.SUPPRESS)
466
- parser.add_argument("--mass", type=float, action='append', help=argparse.SUPPRESS)
441
+ parser.add_argument("--mass", type=parse_mass_mg_from_cli, action='append', help=argparse.SUPPRESS)
467
442
  parser.add_argument("--dqdv", action="store_true", help=argparse.SUPPRESS)
468
443
  parser.add_argument("--cv", action="store_true", help=argparse.SUPPRESS)
469
444
  parser.add_argument("--cpc", action="store_true", help=argparse.SUPPRESS)
@@ -475,14 +450,19 @@ def build_parser() -> argparse.ArgumentParser:
475
450
  help=argparse.SUPPRESS)
476
451
  parser.add_argument("--anode", action="store_true", help=argparse.SUPPRESS)
477
452
  parser.add_argument("--cathode", action="store_true", help=argparse.SUPPRESS)
478
- parser.add_argument("--ro", action="store_true", help=argparse.SUPPRESS)
479
- parser.add_argument("--all", type=str, nargs='?', const='all', help=argparse.SUPPRESS)
480
- parser.add_argument("--format", type=str, default='svg',
481
- choices=['svg', 'png', 'pdf', 'jpg', 'jpeg', 'eps', 'tif', 'tiff'],
482
- help=argparse.SUPPRESS)
453
+
454
+
455
+ def _add_operando_arguments(parser: argparse.ArgumentParser) -> None:
456
+ """Register operando contour arguments."""
457
+ parser.add_argument("--operando", "--contour", action="store_true", dest="operando", help=argparse.SUPPRESS)
458
+ parser.add_argument("--average", type=int, help=argparse.SUPPRESS)
459
+ parser.add_argument("--sum", dest="scan_sum", type=int, help=argparse.SUPPRESS)
460
+
461
+
462
+ def _add_read_column_arguments(parser: argparse.ArgumentParser) -> None:
463
+ """Register column-selection arguments."""
483
464
  parser.add_argument("--readcol", nargs=2, type=int, metavar=('X_COL', 'Y_COL'),
484
465
  help=argparse.SUPPRESS)
485
- # Add extension-specific readcol arguments
486
466
  parser.add_argument("--readcolxy", nargs=2, type=int, metavar=('X_COL', 'Y_COL'),
487
467
  help=argparse.SUPPRESS)
488
468
  parser.add_argument("--readcolxye", nargs=2, type=int, metavar=('X_COL', 'Y_COL'),
@@ -499,10 +479,51 @@ def build_parser() -> argparse.ArgumentParser:
499
479
  help=argparse.SUPPRESS)
500
480
  parser.add_argument("--readcols", nargs=2, type=int, metavar=('X_COL', 'Y_COL'),
501
481
  help=argparse.SUPPRESS)
502
- parser.add_argument("--1d", action="store_true", dest="derivative_1d", help=argparse.SUPPRESS)
503
- parser.add_argument("--2d", action="store_true", dest="derivative_2d", help=argparse.SUPPRESS)
504
- parser.add_argument("--canvas", action="store_true", dest="canvas",
505
- help="Canvas mode: combine multiple .pkl sessions into one layout. Use numbers to edit each panel.")
482
+
483
+
484
+ def build_parser() -> argparse.ArgumentParser:
485
+ """
486
+ Build the argument parser for batplot command-line interface.
487
+
488
+ HOW ARGUMENT PARSING WORKS:
489
+ --------------------------
490
+ This function creates an ArgumentParser object that defines all valid
491
+ command-line arguments for batplot. When you run 'batplot file.xy --i',
492
+ argparse uses this parser to:
493
+ 1. Recognize which arguments are valid
494
+ 2. Extract values from the command line
495
+ 3. Convert them to appropriate Python types (int, float, bool, etc.)
496
+ 4. Store them in a namespace object (args.files, args.interactive, etc.)
497
+
498
+ ARGUMENT TYPES:
499
+ --------------
500
+ - Positional arguments: 'files' - list of file paths (can be 0 or more)
501
+ - Flags (boolean): '--i' - True if present, False if absent
502
+ - Options with values: '--mass 7.0' or '--mass 0.01g' - mass in mg, or grams with a ``g`` suffix
503
+ - Optional arguments: '--help xy' - can have optional value
504
+
505
+ WHY add_help=False?
506
+ -------------------
507
+ We use a custom help system that supports topic-specific help:
508
+ - 'batplot --help' → general help
509
+ - 'batplot --help xy' → XY mode help
510
+ - 'batplot --help ec' → EC mode help
511
+ - 'batplot --help op' → Operando mode help
512
+
513
+ This gives users more targeted help instead of one giant help page.
514
+
515
+ Returns:
516
+ Configured ArgumentParser object ready to parse command-line arguments
517
+ """
518
+ # Create parser with custom help system (we handle help ourselves)
519
+ parser = argparse.ArgumentParser(add_help=False)
520
+
521
+ _add_help_and_entry_arguments(parser)
522
+ _add_xy_arguments(parser)
523
+ _add_interactive_export_arguments(parser)
524
+ _add_operando_arguments(parser)
525
+ _add_electrochem_arguments(parser)
526
+ _add_read_column_arguments(parser)
506
527
  return parser
507
528
 
508
529
 
@@ -700,18 +721,23 @@ def parse_args(argv=None):
700
721
  # weren't in the parser yet when we built it
701
722
  ns, _unknown = parser.parse_known_args(argv)
702
723
  if getattr(ns, "manual", False):
724
+ manual_url = "https://github.com/chem-plot/batplot/blob/main/batplot_user_manual.pdf"
703
725
  try:
704
- from .manual import open_manual_url # Lazy import avoids matplotlib startup unless needed
705
- open_manual_url()
726
+ opened = webbrowser.open(manual_url)
706
727
  if _HAS_RICH and _console:
707
- _console.print("\n[green]Opened manual in browser[/green]")
728
+ if opened:
729
+ _console.print("\n[green]Opened PDF manual in browser[/green]")
730
+ else:
731
+ _console.print(f"\n[yellow]Manual PDF:[/yellow] {manual_url}")
708
732
  else:
709
- print("\nOpened manual in browser")
733
+ print("\nOpened PDF manual in browser" if opened else f"\nManual PDF: {manual_url}")
710
734
  except Exception as exc: # pragma: no cover - best effort
711
735
  if _HAS_RICH and _console:
712
736
  _console.print(f"\n[red]Failed to open manual:[/red] {exc}")
737
+ _console.print(f"[yellow]Manual PDF:[/yellow] {manual_url}")
713
738
  else:
714
739
  print(f"\nFailed to open manual: {exc}")
740
+ print(f"Manual PDF: {manual_url}")
715
741
  sys.exit(0)
716
742
 
717
743
  topic = getattr(ns, 'help', None)
@@ -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), dQdV (.csv), and CPC (.mpt/.csv) modes.
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 == '.mpt':
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 == '.mpt':
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 is_biologic_datalogger_csv(fpath):
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 <mg> for specific dQ/dV.")
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 == '.mpt':
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':