batplot 1.4.1__tar.gz → 1.4.3__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.4.1 → batplot-1.4.3}/PKG-INFO +4 -4
- {batplot-1.4.1 → batplot-1.4.3}/README.md +3 -3
- {batplot-1.4.1 → batplot-1.4.3}/batplot/__init__.py +1 -1
- {batplot-1.4.1 → batplot-1.4.3}/batplot/args.py +18 -14
- {batplot-1.4.1 → batplot-1.4.3}/batplot/batch.py +5 -0
- {batplot-1.4.1 → batplot-1.4.3}/batplot/batplot.py +8 -3
- {batplot-1.4.1 → batplot-1.4.3}/batplot/operando.py +90 -25
- {batplot-1.4.1 → batplot-1.4.3}/batplot/readers.py +168 -27
- {batplot-1.4.1 → batplot-1.4.3}/batplot.egg-info/PKG-INFO +4 -4
- {batplot-1.4.1 → batplot-1.4.3}/pyproject.toml +1 -1
- {batplot-1.4.1 → batplot-1.4.3}/LICENSE +0 -0
- {batplot-1.4.1 → batplot-1.4.3}/batplot/batplot_new.py +0 -0
- {batplot-1.4.1 → batplot-1.4.3}/batplot/cif.py +0 -0
- {batplot-1.4.1 → batplot-1.4.3}/batplot/cli.py +0 -0
- {batplot-1.4.1 → batplot-1.4.3}/batplot/converters.py +0 -0
- {batplot-1.4.1 → batplot-1.4.3}/batplot/cpc_interactive.py +0 -0
- {batplot-1.4.1 → batplot-1.4.3}/batplot/electrochem_interactive.py +0 -0
- {batplot-1.4.1 → batplot-1.4.3}/batplot/interactive.py +0 -0
- {batplot-1.4.1 → batplot-1.4.3}/batplot/modes.py +0 -0
- {batplot-1.4.1 → batplot-1.4.3}/batplot/operando_ec_interactive.py +0 -0
- {batplot-1.4.1 → batplot-1.4.3}/batplot/plotting.py +0 -0
- {batplot-1.4.1 → batplot-1.4.3}/batplot/session.py +0 -0
- {batplot-1.4.1 → batplot-1.4.3}/batplot/style.py +0 -0
- {batplot-1.4.1 → batplot-1.4.3}/batplot/ui.py +0 -0
- {batplot-1.4.1 → batplot-1.4.3}/batplot/utils.py +0 -0
- {batplot-1.4.1 → batplot-1.4.3}/batplot.egg-info/SOURCES.txt +0 -0
- {batplot-1.4.1 → batplot-1.4.3}/batplot.egg-info/dependency_links.txt +0 -0
- {batplot-1.4.1 → batplot-1.4.3}/batplot.egg-info/entry_points.txt +0 -0
- {batplot-1.4.1 → batplot-1.4.3}/batplot.egg-info/requires.txt +0 -0
- {batplot-1.4.1 → batplot-1.4.3}/batplot.egg-info/top_level.txt +0 -0
- {batplot-1.4.1 → batplot-1.4.3}/setup.cfg +0 -0
- {batplot-1.4.1 → batplot-1.4.3}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: batplot
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.3
|
|
4
4
|
Summary: Interactive plotting for XRD, PDF, and XAS data (.xye, .xy, .qye, .dat, .csv, .gr, .nor, .chik, .chir)
|
|
5
5
|
Author-email: Tian Dai <tianda@uio.no>
|
|
6
6
|
License: MIT License
|
|
@@ -72,7 +72,7 @@ pip install batplot
|
|
|
72
72
|
|
|
73
73
|
## Quick Start
|
|
74
74
|
|
|
75
|
-
### XRD / PDF / XAS
|
|
75
|
+
### XRD / PDF / XAS and much more
|
|
76
76
|
|
|
77
77
|
```bash
|
|
78
78
|
# Single diffraction pattern in 2theta
|
|
@@ -146,7 +146,7 @@ batplot --operando --interactive
|
|
|
146
146
|
|
|
147
147
|
| Type | Formats |
|
|
148
148
|
|------|---------|
|
|
149
|
-
| **Electrochemistry** | `.csv` (Neware), `.mpt` (Biologic) |
|
|
149
|
+
| **Electrochemistry** | `.csv` (Neware), `.mpt` (Biologic), `.xlsx` (Landt/Lanhe) CPC only |
|
|
150
150
|
| **XRD / PDF** | `.xye`, `.xy`, `.qye`, `.dat` |
|
|
151
151
|
| **XAS** | `.nor`, `.chik`, `.chir` |
|
|
152
152
|
| **Others** | `user defined` (plot first two columns as x and y) |
|
|
@@ -181,4 +181,4 @@ University of Oslo
|
|
|
181
181
|
|
|
182
182
|
**GitHub**: https://github.com/tiandai-chem/batplot
|
|
183
183
|
|
|
184
|
-
**Subscribe for Updates**: Join batplot-lab@kjemi.uio.no for updates, feature announcements, and community feedback
|
|
184
|
+
**Subscribe for Updates**: Join batplot-lab@kjemi.uio.no for updates, feature announcements, and community feedback. If you are not from UiO, send an email to sympa@kjemi.uio.no with the exact subject line with your name: "subscribe batplot-lab@kjemi.uio.no your-name"
|
|
@@ -22,7 +22,7 @@ pip install batplot
|
|
|
22
22
|
|
|
23
23
|
## Quick Start
|
|
24
24
|
|
|
25
|
-
### XRD / PDF / XAS
|
|
25
|
+
### XRD / PDF / XAS and much more
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
28
|
# Single diffraction pattern in 2theta
|
|
@@ -96,7 +96,7 @@ batplot --operando --interactive
|
|
|
96
96
|
|
|
97
97
|
| Type | Formats |
|
|
98
98
|
|------|---------|
|
|
99
|
-
| **Electrochemistry** | `.csv` (Neware), `.mpt` (Biologic) |
|
|
99
|
+
| **Electrochemistry** | `.csv` (Neware), `.mpt` (Biologic), `.xlsx` (Landt/Lanhe) CPC only |
|
|
100
100
|
| **XRD / PDF** | `.xye`, `.xy`, `.qye`, `.dat` |
|
|
101
101
|
| **XAS** | `.nor`, `.chik`, `.chir` |
|
|
102
102
|
| **Others** | `user defined` (plot first two columns as x and y) |
|
|
@@ -131,4 +131,4 @@ University of Oslo
|
|
|
131
131
|
|
|
132
132
|
**GitHub**: https://github.com/tiandai-chem/batplot
|
|
133
133
|
|
|
134
|
-
**Subscribe for Updates**: Join batplot-lab@kjemi.uio.no for updates, feature announcements, and community feedback
|
|
134
|
+
**Subscribe for Updates**: Join batplot-lab@kjemi.uio.no for updates, feature announcements, and community feedback. If you are not from UiO, send an email to sympa@kjemi.uio.no with the exact subject line with your name: "subscribe batplot-lab@kjemi.uio.no your-name"
|
|
@@ -104,15 +104,16 @@ def _print_general_help() -> None:
|
|
|
104
104
|
" • Batch styling: apply .bps/.bpsg files to all exports (use --all flag)\n"
|
|
105
105
|
" • Format option: use --format png/pdf/jpg/etc to change export format\n\n"
|
|
106
106
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
107
|
+
"More help:\n"
|
|
108
|
+
" batplot -h xy # XY file plotting guide\n"
|
|
109
|
+
" batplot -h ec # Electrochemistry (GC/dQdV/CV/CPC) guide\n"
|
|
110
|
+
" batplot -h op # Operando guide\n"
|
|
111
|
+
" Manual: https://github.com/tiandai-chem/batplot/blob/main/USER_MANUAL.md\n\n"
|
|
112
|
+
"Contact & Updates:\n"
|
|
113
|
+
" Subscribe to batplot-lab@kjemi.uio.no for updates\n"
|
|
114
|
+
" (If you are not from UiO, send an email to sympa@kjemi.uio.no with the subject line \"subscribe batplot-lab@kjemi.uio.no your-name\")\n"
|
|
115
|
+
" GitHub: https://github.com/tiandai-chem/batplot\n"
|
|
116
|
+
" Email: tianda@uio.no\n"
|
|
116
117
|
)
|
|
117
118
|
_print_help(msg)
|
|
118
119
|
|
|
@@ -180,17 +181,20 @@ def _print_ec_help() -> None:
|
|
|
180
181
|
"Cyclic voltammetry (CV) from .mpt or .txt: plots voltage vs current for each cycle.\n"
|
|
181
182
|
" batplot --cv file.mpt\n"
|
|
182
183
|
" batplot --cv file.txt\n\n"
|
|
183
|
-
"Capacity-per-cycle (CPC) with coulombic efficiency from .csv or .mpt.\n"
|
|
184
|
+
"Capacity-per-cycle (CPC) with coulombic efficiency from .csv, .xlsx, or .mpt.\n"
|
|
184
185
|
"Supports multiple files with individual color customization:\n"
|
|
185
|
-
" batplot --cpc file.csv\n"
|
|
186
|
-
" batplot --cpc file.
|
|
187
|
-
" batplot --cpc
|
|
186
|
+
" batplot --cpc file.csv # Neware CSV\n"
|
|
187
|
+
" batplot --cpc file.xlsx # Landt/Lanhe Excel (Chinese tester)\n"
|
|
188
|
+
" batplot --cpc file.mpt --mass 1.2 # Biologic MPT\n"
|
|
189
|
+
" batplot --cpc file1.csv file2.xlsx file3.mpt --mass 1.2 --interactive\n\n"
|
|
190
|
+
"Excel support: Landt/Lanhe (蓝电/蓝河) .xlsx files with Chinese headers:\n"
|
|
191
|
+
" Expected structure: Row 1=filename, Row 2=headers, Row 3+=data\n"
|
|
188
192
|
"Batch mode: Process all files and export to Figures/ subdirectory (default: .svg).\n"
|
|
189
193
|
" batplot --gc --all --mass 7.0 # All .mpt/.csv files (.mpt requires --mass)\n"
|
|
190
194
|
" batplot --gc --all --mass 7 --format png # Export as .png instead of .svg\n"
|
|
191
195
|
" batplot --cv --all # All .mpt/.txt files (CV mode)\n"
|
|
192
196
|
" batplot --dqdv --all # All .csv files (dQdV mode)\n"
|
|
193
|
-
" batplot --cpc --all --mass 5.4 # All .mpt/.csv
|
|
197
|
+
" batplot --cpc --all --mass 5.4 # All .mpt/.csv/.xlsx (.mpt requires --mass)\n"
|
|
194
198
|
" batplot --gc /path/to/folder --mass 6 # Process specific directory\n\n"
|
|
195
199
|
"Batch mode with style/geometry: Apply .bps/.bpsg files to all batch exports.\n"
|
|
196
200
|
" batplot --all --gc style.bps --mass 7 # Apply style to all GC plots\n"
|
|
@@ -344,6 +344,11 @@ def batch_process(directory: str, args):
|
|
|
344
344
|
# Transparent background for SVG exports
|
|
345
345
|
_, _ext = os.path.splitext(target)
|
|
346
346
|
if _ext.lower() == '.svg':
|
|
347
|
+
# Fix for Affinity Designer/Photo compatibility issues
|
|
348
|
+
# Use 'none' to embed fonts as text (not paths) - prevents phantom labels
|
|
349
|
+
# Set hashsalt to empty to avoid duplicate text elements
|
|
350
|
+
plt.rcParams['svg.fonttype'] = 'none'
|
|
351
|
+
plt.rcParams['svg.hashsalt'] = None
|
|
347
352
|
try:
|
|
348
353
|
_fig_fc = fig_b.get_facecolor()
|
|
349
354
|
except Exception:
|
|
@@ -538,6 +538,11 @@ def batplot_main() -> int:
|
|
|
538
538
|
# Transparent background for SVG exports
|
|
539
539
|
_, _ext = _os.path.splitext(outname)
|
|
540
540
|
if _ext.lower() == '.svg':
|
|
541
|
+
# Fix for Affinity Designer/Photo compatibility issues
|
|
542
|
+
# Use 'none' to embed fonts as text (not paths) - prevents phantom labels
|
|
543
|
+
# Set hashsalt to empty to avoid duplicate text elements
|
|
544
|
+
_plt.rcParams['svg.fonttype'] = 'none'
|
|
545
|
+
_plt.rcParams['svg.hashsalt'] = None
|
|
541
546
|
try:
|
|
542
547
|
_fig_fc = fig.get_facecolor()
|
|
543
548
|
except Exception:
|
|
@@ -625,7 +630,7 @@ def batplot_main() -> int:
|
|
|
625
630
|
import numpy as _np
|
|
626
631
|
|
|
627
632
|
if len(args.files) < 1:
|
|
628
|
-
print("CPC mode: provide at least one file (.csv or .mpt).")
|
|
633
|
+
print("CPC mode: provide at least one file (.csv, .xlsx, or .mpt).")
|
|
629
634
|
exit(1)
|
|
630
635
|
|
|
631
636
|
# Process multiple files
|
|
@@ -651,7 +656,7 @@ def batplot_main() -> int:
|
|
|
651
656
|
file_basename = _os.path.basename(ec_file)
|
|
652
657
|
|
|
653
658
|
try:
|
|
654
|
-
if ext
|
|
659
|
+
if ext in ['.csv', '.xlsx', '.xls']:
|
|
655
660
|
cap_x, voltage, cycles, chg_mask, dchg_mask = read_ec_csv_file(ec_file, prefer_specific=True)
|
|
656
661
|
cyc = _np.array(cycles, dtype=int)
|
|
657
662
|
unique_cycles = _np.unique(cyc)
|
|
@@ -683,7 +688,7 @@ def batplot_main() -> int:
|
|
|
683
688
|
continue
|
|
684
689
|
cyc_nums, cap_charge, cap_discharge, eff = read_mpt_file(ec_file, mode='cpc', mass_mg=mass_mg)
|
|
685
690
|
else:
|
|
686
|
-
print(f"Skipped {file_basename}: unsupported format (must be .csv or .mpt)")
|
|
691
|
+
print(f"Skipped {file_basename}: unsupported format (must be .csv, .xlsx, or .mpt)")
|
|
687
692
|
continue
|
|
688
693
|
|
|
689
694
|
# Assign colors: distinct hue for each file
|
|
@@ -211,33 +211,98 @@ def plot_operando_folder(folder: str, args) -> Tuple[plt.Figure, plt.Axes, Dict[
|
|
|
211
211
|
if has_ec:
|
|
212
212
|
try:
|
|
213
213
|
ec_path = mpt_files[0]
|
|
214
|
-
# Read time series from .mpt (now returns labels too)
|
|
215
|
-
result = read_mpt_file(str(ec_path), mode='time')
|
|
216
214
|
|
|
217
|
-
# Check if
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
215
|
+
# Check if user specified custom columns via --readcolmpt
|
|
216
|
+
readcol_mpt = None
|
|
217
|
+
if hasattr(args, 'readcol_by_ext') and '.mpt' in args.readcol_by_ext:
|
|
218
|
+
readcol_mpt = args.readcol_by_ext['.mpt']
|
|
219
|
+
|
|
220
|
+
if readcol_mpt:
|
|
221
|
+
# User explicitly specified columns - respect their choice
|
|
222
|
+
data = robust_loadtxt_skipheader(str(ec_path))
|
|
223
|
+
if data.ndim == 1:
|
|
224
|
+
data = data.reshape(1, -1)
|
|
225
|
+
if data.shape[1] < 2:
|
|
226
|
+
raise ValueError(f"MPT file {ec_path.name} has insufficient columns")
|
|
227
|
+
|
|
228
|
+
# Apply column selection (1-indexed -> 0-indexed)
|
|
229
|
+
x_col, y_col = readcol_mpt
|
|
230
|
+
x_col_idx = x_col - 1
|
|
231
|
+
y_col_idx = y_col - 1
|
|
232
|
+
if x_col_idx < 0 or x_col_idx >= data.shape[1]:
|
|
233
|
+
raise ValueError(f"X column {x_col} out of range in {ec_path.name} (has {data.shape[1]} columns)")
|
|
234
|
+
if y_col_idx < 0 or y_col_idx >= data.shape[1]:
|
|
235
|
+
raise ValueError(f"Y column {y_col} out of range in {ec_path.name} (has {data.shape[1]} columns)")
|
|
236
|
+
|
|
237
|
+
x_data = data[:, x_col_idx]
|
|
238
|
+
y_data = data[:, y_col_idx]
|
|
239
|
+
current_mA = None
|
|
240
|
+
# User-specified: plot exactly as specified (X on x-axis, Y on y-axis)
|
|
241
|
+
x_label = f'Column {x_col}'
|
|
242
|
+
y_label = f'Column {y_col}'
|
|
235
243
|
else:
|
|
236
|
-
#
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
244
|
+
# Auto-detect format: Read time series from .mpt
|
|
245
|
+
result = read_mpt_file(str(ec_path), mode='time')
|
|
246
|
+
|
|
247
|
+
# Check if we got labels (5 elements) or old format (3 elements)
|
|
248
|
+
if len(result) == 5:
|
|
249
|
+
x_data, y_data, current_mA, x_label, y_label = result
|
|
250
|
+
# For EC-Lab files: x_label='Time (h)', y_label='Voltage (V)'
|
|
251
|
+
# For simple files: x_label could be 'Time(h)', 'time', etc.
|
|
252
|
+
# EC-Lab returns time in seconds, needs conversion to hours
|
|
253
|
+
# operando plots with voltage on X-axis and time on Y-axis
|
|
254
|
+
|
|
255
|
+
# Check if labels indicate time/voltage data (flexible matching)
|
|
256
|
+
x_lower = x_label.lower().replace(' ', '').replace('_', '')
|
|
257
|
+
y_lower = y_label.lower().replace(' ', '').replace('_', '')
|
|
258
|
+
has_time_in_x = 'time' in x_lower
|
|
259
|
+
has_voltage_in_x = 'voltage' in x_lower or 'ewe' in x_lower
|
|
260
|
+
has_time_in_y = 'time' in y_lower
|
|
261
|
+
has_voltage_in_y = 'voltage' in y_lower or 'ewe' in y_lower
|
|
262
|
+
|
|
263
|
+
is_time_voltage = (has_time_in_x or has_time_in_y) and (has_voltage_in_x or has_voltage_in_y)
|
|
264
|
+
|
|
265
|
+
if x_label == 'Time (h)' and y_label == 'Voltage (V)':
|
|
266
|
+
# EC-Lab file: convert time to hours and swap axes
|
|
267
|
+
time_h = np.asarray(x_data, float) / 3600.0
|
|
268
|
+
voltage_v = np.asarray(y_data, float)
|
|
269
|
+
x_data = voltage_v
|
|
270
|
+
y_data = time_h
|
|
271
|
+
x_label = 'Voltage (V)'
|
|
272
|
+
y_label = 'Time (h)'
|
|
273
|
+
elif is_time_voltage:
|
|
274
|
+
# Simple file with time/voltage columns
|
|
275
|
+
# Determine which column is which, then arrange: voltage on X, time on Y
|
|
276
|
+
if has_time_in_x and has_voltage_in_y:
|
|
277
|
+
# Columns are: Time, Voltage -> swap to Voltage, Time
|
|
278
|
+
time_h = np.asarray(x_data, float)
|
|
279
|
+
voltage_v = np.asarray(y_data, float)
|
|
280
|
+
x_data = voltage_v
|
|
281
|
+
y_data = time_h
|
|
282
|
+
x_label = 'Voltage (V)'
|
|
283
|
+
y_label = 'Time (h)'
|
|
284
|
+
elif has_voltage_in_x and has_time_in_y:
|
|
285
|
+
# Columns are: Voltage, Time -> already correct order
|
|
286
|
+
voltage_v = np.asarray(x_data, float)
|
|
287
|
+
time_h = np.asarray(y_data, float)
|
|
288
|
+
x_data = voltage_v
|
|
289
|
+
y_data = time_h
|
|
290
|
+
x_label = 'Voltage (V)'
|
|
291
|
+
y_label = 'Time (h)'
|
|
292
|
+
else:
|
|
293
|
+
# Ambiguous or both in same column - default behavior
|
|
294
|
+
x_data = np.asarray(x_data, float)
|
|
295
|
+
y_data = np.asarray(y_data, float)
|
|
296
|
+
else:
|
|
297
|
+
# Generic file: use raw data as-is, keep original labels
|
|
298
|
+
x_data = np.asarray(x_data, float)
|
|
299
|
+
y_data = np.asarray(y_data, float)
|
|
300
|
+
else:
|
|
301
|
+
# Old format compatibility (shouldn't happen anymore)
|
|
302
|
+
x_data, y_data, current_mA = result
|
|
303
|
+
x_data = np.asarray(y_data, float)
|
|
304
|
+
y_data = np.asarray(x_data, float) / 3600.0
|
|
305
|
+
x_label, y_label = 'Voltage (V)', 'Time (h)'
|
|
241
306
|
|
|
242
307
|
# Add the EC axes on the right
|
|
243
308
|
ec_ax = fig.add_subplot(gs[0, 1])
|
|
@@ -6,6 +6,53 @@ import numpy as np
|
|
|
6
6
|
from typing import Tuple
|
|
7
7
|
|
|
8
8
|
|
|
9
|
+
def read_excel_to_csv_like(fname: str, header_row: int = 2, data_start_row: int = 3) -> Tuple[list, list]:
|
|
10
|
+
"""Read Excel file and convert to CSV-like structure for batplot.
|
|
11
|
+
|
|
12
|
+
This is designed for Chinese cycler data Excel files where:
|
|
13
|
+
- Row 1: File/sample name
|
|
14
|
+
- Row 2: Column headers
|
|
15
|
+
- Row 3+: Data
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
fname: Path to Excel file (.xlsx)
|
|
19
|
+
header_row: Row number containing headers (1-indexed, default=2)
|
|
20
|
+
data_start_row: First row containing data (1-indexed, default=3)
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Tuple of (header_list, rows_list) compatible with CSV processing
|
|
24
|
+
"""
|
|
25
|
+
try:
|
|
26
|
+
import openpyxl
|
|
27
|
+
except ImportError:
|
|
28
|
+
raise ImportError("openpyxl is required to read Excel files. Install with: pip install openpyxl")
|
|
29
|
+
|
|
30
|
+
wb = openpyxl.load_workbook(fname, read_only=True, data_only=True)
|
|
31
|
+
ws = wb.active
|
|
32
|
+
|
|
33
|
+
# Read header row
|
|
34
|
+
header = []
|
|
35
|
+
for cell in ws[header_row]:
|
|
36
|
+
header.append(str(cell.value) if cell.value is not None else '')
|
|
37
|
+
|
|
38
|
+
# Read data rows
|
|
39
|
+
rows = []
|
|
40
|
+
for row in ws.iter_rows(min_row=data_start_row, values_only=True):
|
|
41
|
+
# Convert row to list of strings (handle None, datetime, etc.)
|
|
42
|
+
row_data = []
|
|
43
|
+
for val in row:
|
|
44
|
+
if val is None:
|
|
45
|
+
row_data.append('')
|
|
46
|
+
elif isinstance(val, (int, float)):
|
|
47
|
+
row_data.append(str(val))
|
|
48
|
+
else:
|
|
49
|
+
row_data.append(str(val))
|
|
50
|
+
rows.append(row_data)
|
|
51
|
+
|
|
52
|
+
wb.close()
|
|
53
|
+
return header, rows
|
|
54
|
+
|
|
55
|
+
|
|
9
56
|
def read_csv_file(fname: str):
|
|
10
57
|
for delim in [",", ";", "\t"]:
|
|
11
58
|
try:
|
|
@@ -583,7 +630,7 @@ def read_biologic_txt_file(fname: str, mode: str = 'cv') -> Tuple[np.ndarray, np
|
|
|
583
630
|
|
|
584
631
|
|
|
585
632
|
def read_ec_csv_file(fname: str, prefer_specific: bool = True) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
|
586
|
-
"""Read a cycler-exported CSV (e.g., Neware-like) and extract arrays for GC plotting.
|
|
633
|
+
"""Read a cycler-exported CSV or Excel file (e.g., Neware-like) and extract arrays for GC plotting.
|
|
587
634
|
|
|
588
635
|
The CSV is expected to have a two-line header where the second header line starts
|
|
589
636
|
with an empty first cell. Columns of interest include:
|
|
@@ -594,8 +641,13 @@ def read_ec_csv_file(fname: str, prefer_specific: bool = True) -> Tuple[np.ndarr
|
|
|
594
641
|
* absolute: 'Capacity(mAh)' and optionally split 'Chg. Cap.(mAh)' / 'DChg. Cap.(mAh)'
|
|
595
642
|
* specific: 'Spec. Cap.(mAh/g)', 'Chg. Spec. Cap.(mAh/g)', 'DChg. Spec. Cap.(mAh/g)'
|
|
596
643
|
|
|
644
|
+
Excel files (.xlsx) are supported and expected to have:
|
|
645
|
+
- Row 1: File/sample name (ignored)
|
|
646
|
+
- Row 2: Column headers (Chinese or English)
|
|
647
|
+
- Row 3+: Data
|
|
648
|
+
|
|
597
649
|
Args:
|
|
598
|
-
fname: Path to the CSV file.
|
|
650
|
+
fname: Path to the CSV or Excel file.
|
|
599
651
|
prefer_specific: If True, will use specific capacities (mAh g⁻¹) if available; otherwise
|
|
600
652
|
uses absolute capacities (mAh) when present.
|
|
601
653
|
|
|
@@ -612,26 +664,54 @@ def read_ec_csv_file(fname: str, prefer_specific: bool = True) -> Tuple[np.ndarr
|
|
|
612
664
|
discharge_mask: boolean mask indicating discharging segments (inverse of charge_mask)
|
|
613
665
|
"""
|
|
614
666
|
import csv
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
667
|
+
import os
|
|
668
|
+
|
|
669
|
+
# Check if file is Excel
|
|
670
|
+
_, ext = os.path.splitext(fname)
|
|
671
|
+
if ext.lower() in ['.xlsx', '.xls']:
|
|
672
|
+
# Read Excel file
|
|
673
|
+
header, rows = read_excel_to_csv_like(fname)
|
|
674
|
+
else:
|
|
675
|
+
# Read first two rows to compose header from CSV
|
|
676
|
+
with open(fname, newline='', encoding='utf-8', errors='ignore') as f:
|
|
677
|
+
r = csv.reader(f)
|
|
678
|
+
try:
|
|
679
|
+
r1 = next(r)
|
|
680
|
+
r2 = next(r)
|
|
681
|
+
except StopIteration:
|
|
682
|
+
raise ValueError(f"CSV '{fname}' is empty or missing header rows")
|
|
683
|
+
# If second line begins with an empty first cell, treat it as a continuation of header.
|
|
684
|
+
if len(r2) > 0 and (r2[0] == '' or str(r2[0]).strip() == ''):
|
|
685
|
+
header = [c.strip() for c in r1] + [c.strip() for c in r2[1:]]
|
|
686
|
+
rows = list(r)
|
|
687
|
+
else:
|
|
688
|
+
# Single-line header: r2 is the first data row; include it back in rows
|
|
689
|
+
header = [c.strip() for c in r1]
|
|
690
|
+
rows = [r2] + list(r)
|
|
632
691
|
|
|
633
692
|
# Build fast name->index map (case-insensitive match on exact header text)
|
|
634
693
|
name_to_idx = {h: i for i, h in enumerate(header)}
|
|
694
|
+
|
|
695
|
+
# Chinese to English column name mappings
|
|
696
|
+
chinese_mappings = {
|
|
697
|
+
'循环序号': 'Cycle Index',
|
|
698
|
+
'充电比容量/mAh/g': 'Chg. Spec. Cap.(mAh/g)',
|
|
699
|
+
'放电比容量/mAh/g': 'DChg. Spec. Cap.(mAh/g)',
|
|
700
|
+
'充电容量/mAh': 'Chg. Cap.(mAh)',
|
|
701
|
+
'放电容量/mAh': 'DChg. Cap.(mAh)',
|
|
702
|
+
'效率/%': 'Efficiency(%)',
|
|
703
|
+
'充电中压/V': 'Voltage(V)',
|
|
704
|
+
'放电中压/V': 'Voltage(V)',
|
|
705
|
+
'充电均压/V': 'Voltage(V)',
|
|
706
|
+
'放电均压/V': 'Voltage(V)',
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
# Add Chinese mappings to name_to_idx
|
|
710
|
+
for i, h in enumerate(header):
|
|
711
|
+
if h in chinese_mappings:
|
|
712
|
+
eng_name = chinese_mappings[h]
|
|
713
|
+
if eng_name not in name_to_idx:
|
|
714
|
+
name_to_idx[eng_name] = i
|
|
635
715
|
|
|
636
716
|
def _find(name: str):
|
|
637
717
|
return name_to_idx.get(name, None)
|
|
@@ -641,9 +721,7 @@ def read_ec_csv_file(fname: str, prefer_specific: bool = True) -> Tuple[np.ndarr
|
|
|
641
721
|
i_idx = _find('Current(mA)')
|
|
642
722
|
cyc_idx = _find('Cycle Index')
|
|
643
723
|
step_type_idx = _find('Step Type') # Optional: explicitly indicates charge/discharge
|
|
644
|
-
|
|
645
|
-
raise ValueError("CSV missing required 'Voltage(V)' or 'Current(mA)' columns")
|
|
646
|
-
|
|
724
|
+
|
|
647
725
|
# Capacity columns (absolute preferred unless prefer_specific True)
|
|
648
726
|
cap_abs_idx = _find('Capacity(mAh)')
|
|
649
727
|
cap_abs_chg_idx = _find('Chg. Cap.(mAh)')
|
|
@@ -651,6 +729,13 @@ def read_ec_csv_file(fname: str, prefer_specific: bool = True) -> Tuple[np.ndarr
|
|
|
651
729
|
cap_spec_idx = _find('Spec. Cap.(mAh/g)')
|
|
652
730
|
cap_spec_chg_idx = _find('Chg. Spec. Cap.(mAh/g)')
|
|
653
731
|
cap_spec_dch_idx = _find('DChg. Spec. Cap.(mAh/g)')
|
|
732
|
+
|
|
733
|
+
# Check if this is a summary file (has capacity columns but no voltage/current)
|
|
734
|
+
has_capacity_cols = any([cap_abs_chg_idx, cap_abs_dch_idx, cap_spec_chg_idx, cap_spec_dch_idx])
|
|
735
|
+
is_summary_file = has_capacity_cols and (v_idx is None or i_idx is None)
|
|
736
|
+
|
|
737
|
+
if not is_summary_file and (v_idx is None or i_idx is None):
|
|
738
|
+
raise ValueError("CSV missing required 'Voltage(V)' or 'Current(mA)' columns")
|
|
654
739
|
|
|
655
740
|
use_specific = False
|
|
656
741
|
# Decide which flavor to use
|
|
@@ -664,16 +749,72 @@ def read_ec_csv_file(fname: str, prefer_specific: bool = True) -> Tuple[np.ndarr
|
|
|
664
749
|
|
|
665
750
|
# Prepare arrays
|
|
666
751
|
n = len(rows)
|
|
667
|
-
|
|
668
|
-
current = np.empty(n, dtype=float)
|
|
669
|
-
cycles = np.ones(n, dtype=int)
|
|
670
|
-
cap_x = np.full(n, np.nan, dtype=float)
|
|
671
|
-
|
|
752
|
+
|
|
672
753
|
def _to_float(val: str) -> float:
|
|
673
754
|
try:
|
|
674
755
|
return float(val.strip()) if isinstance(val, str) else float(val)
|
|
675
756
|
except Exception:
|
|
676
757
|
return np.nan
|
|
758
|
+
|
|
759
|
+
# Special handling for summary files (charge/discharge capacities per cycle, no point-by-point data)
|
|
760
|
+
if is_summary_file:
|
|
761
|
+
# For summary files, create synthetic points: one charge point and one discharge point per cycle
|
|
762
|
+
voltage = []
|
|
763
|
+
current = []
|
|
764
|
+
cycles = []
|
|
765
|
+
cap_x = []
|
|
766
|
+
is_charge_list = []
|
|
767
|
+
|
|
768
|
+
for k, row in enumerate(rows):
|
|
769
|
+
if len(row) < len(header):
|
|
770
|
+
row = row + [''] * (len(header) - len(row))
|
|
771
|
+
|
|
772
|
+
# Get cycle number
|
|
773
|
+
cycle_num = 1
|
|
774
|
+
if cyc_idx is not None:
|
|
775
|
+
cval = _to_float(row[cyc_idx])
|
|
776
|
+
cycle_num = int(cval) if not np.isnan(cval) and cval > 0 else 1
|
|
777
|
+
|
|
778
|
+
# Get charge and discharge capacities
|
|
779
|
+
if use_specific:
|
|
780
|
+
cap_chg = _to_float(row[cap_spec_chg_idx]) if cap_spec_chg_idx is not None else 0
|
|
781
|
+
cap_dch = _to_float(row[cap_spec_dch_idx]) if cap_spec_dch_idx is not None else 0
|
|
782
|
+
else:
|
|
783
|
+
cap_chg = _to_float(row[cap_abs_chg_idx]) if cap_abs_chg_idx is not None else 0
|
|
784
|
+
cap_dch = _to_float(row[cap_abs_dch_idx]) if cap_abs_dch_idx is not None else 0
|
|
785
|
+
|
|
786
|
+
# Create charge point
|
|
787
|
+
if cap_chg > 0 and not np.isnan(cap_chg):
|
|
788
|
+
voltage.append(3.5) # Synthetic voltage
|
|
789
|
+
current.append(0.1) # Synthetic current
|
|
790
|
+
cycles.append(cycle_num)
|
|
791
|
+
cap_x.append(cap_chg)
|
|
792
|
+
is_charge_list.append(True)
|
|
793
|
+
|
|
794
|
+
# Create discharge point
|
|
795
|
+
if cap_dch > 0 and not np.isnan(cap_dch):
|
|
796
|
+
voltage.append(2.5) # Synthetic voltage
|
|
797
|
+
current.append(-0.1) # Synthetic current
|
|
798
|
+
cycles.append(cycle_num)
|
|
799
|
+
cap_x.append(cap_dch)
|
|
800
|
+
is_charge_list.append(False)
|
|
801
|
+
|
|
802
|
+
voltage = np.array(voltage, dtype=float)
|
|
803
|
+
current = np.array(current, dtype=float)
|
|
804
|
+
cycles = np.array(cycles, dtype=int)
|
|
805
|
+
cap_x = np.array(cap_x, dtype=float)
|
|
806
|
+
is_charge = np.array(is_charge_list, dtype=bool)
|
|
807
|
+
|
|
808
|
+
charge_mask = is_charge
|
|
809
|
+
discharge_mask = ~is_charge
|
|
810
|
+
|
|
811
|
+
return (cap_x, voltage, cycles, charge_mask, discharge_mask)
|
|
812
|
+
|
|
813
|
+
# Normal processing for point-by-point data
|
|
814
|
+
voltage = np.empty(n, dtype=float)
|
|
815
|
+
current = np.empty(n, dtype=float)
|
|
816
|
+
cycles = np.ones(n, dtype=int)
|
|
817
|
+
cap_x = np.full(n, np.nan, dtype=float)
|
|
677
818
|
|
|
678
819
|
for k, row in enumerate(rows):
|
|
679
820
|
# Ensure row has enough columns
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: batplot
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.3
|
|
4
4
|
Summary: Interactive plotting for XRD, PDF, and XAS data (.xye, .xy, .qye, .dat, .csv, .gr, .nor, .chik, .chir)
|
|
5
5
|
Author-email: Tian Dai <tianda@uio.no>
|
|
6
6
|
License: MIT License
|
|
@@ -72,7 +72,7 @@ pip install batplot
|
|
|
72
72
|
|
|
73
73
|
## Quick Start
|
|
74
74
|
|
|
75
|
-
### XRD / PDF / XAS
|
|
75
|
+
### XRD / PDF / XAS and much more
|
|
76
76
|
|
|
77
77
|
```bash
|
|
78
78
|
# Single diffraction pattern in 2theta
|
|
@@ -146,7 +146,7 @@ batplot --operando --interactive
|
|
|
146
146
|
|
|
147
147
|
| Type | Formats |
|
|
148
148
|
|------|---------|
|
|
149
|
-
| **Electrochemistry** | `.csv` (Neware), `.mpt` (Biologic) |
|
|
149
|
+
| **Electrochemistry** | `.csv` (Neware), `.mpt` (Biologic), `.xlsx` (Landt/Lanhe) CPC only |
|
|
150
150
|
| **XRD / PDF** | `.xye`, `.xy`, `.qye`, `.dat` |
|
|
151
151
|
| **XAS** | `.nor`, `.chik`, `.chir` |
|
|
152
152
|
| **Others** | `user defined` (plot first two columns as x and y) |
|
|
@@ -181,4 +181,4 @@ University of Oslo
|
|
|
181
181
|
|
|
182
182
|
**GitHub**: https://github.com/tiandai-chem/batplot
|
|
183
183
|
|
|
184
|
-
**Subscribe for Updates**: Join batplot-lab@kjemi.uio.no for updates, feature announcements, and community feedback
|
|
184
|
+
**Subscribe for Updates**: Join batplot-lab@kjemi.uio.no for updates, feature announcements, and community feedback. If you are not from UiO, send an email to sympa@kjemi.uio.no with the exact subject line with your name: "subscribe batplot-lab@kjemi.uio.no your-name"
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "batplot"
|
|
7
|
-
version = "1.4.
|
|
7
|
+
version = "1.4.3"
|
|
8
8
|
description = "Interactive plotting for XRD, PDF, and XAS data (.xye, .xy, .qye, .dat, .csv, .gr, .nor, .chik, .chir)"
|
|
9
9
|
authors = [
|
|
10
10
|
{ name = "Tian Dai", email = "tianda@uio.no" }
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|