batplot 1.8.3__py3-none-any.whl → 1.8.5__py3-none-any.whl
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.
Potentially problematic release.
This version of batplot might be problematic. Click here for more details.
- batplot/__init__.py +1 -1
- batplot/args.py +22 -4
- batplot/batch.py +12 -0
- batplot/batplot.py +234 -91
- batplot/cli.py +14 -0
- batplot/converters.py +170 -122
- batplot/cpc_interactive.py +33 -21
- batplot/data/USER_MANUAL.md +49 -0
- batplot/electrochem_interactive.py +63 -36
- batplot/interactive.py +1547 -73
- batplot/operando.py +22 -0
- batplot/operando_ec_interactive.py +232 -2
- batplot/session.py +128 -148
- batplot/style.py +89 -2
- {batplot-1.8.3.dist-info → batplot-1.8.5.dist-info}/METADATA +4 -9
- {batplot-1.8.3.dist-info → batplot-1.8.5.dist-info}/RECORD +20 -20
- {batplot-1.8.3.dist-info → batplot-1.8.5.dist-info}/WHEEL +1 -1
- {batplot-1.8.3.dist-info → batplot-1.8.5.dist-info}/licenses/LICENSE +0 -0
- {batplot-1.8.3.dist-info → batplot-1.8.5.dist-info}/entry_points.txt +0 -0
- {batplot-1.8.3.dist-info → batplot-1.8.5.dist-info}/top_level.txt +0 -0
batplot/__init__.py
CHANGED
batplot/args.py
CHANGED
|
@@ -141,6 +141,7 @@ def _print_general_help() -> None:
|
|
|
141
141
|
"How to run (basics):\n"
|
|
142
142
|
" [1D(XY) curves]\n"
|
|
143
143
|
" batplot file1.xy file2.qye [option1] [option2] # 1D curves, read the first two columns as X and Y axis by default\n"
|
|
144
|
+
" batplot file1.xy file2.xy --1d --stack --i # Plot 1st derivatives with interactive menu\n"
|
|
144
145
|
" batplot allfiles # Plot all files in current directory on same figure\n"
|
|
145
146
|
" batplot allfiles /path/to/dir # Plot all files in specified directory\n"
|
|
146
147
|
" batplot allfiles --i # Plot all files with interactive menu\n"
|
|
@@ -208,7 +209,8 @@ def _print_xy_help() -> None:
|
|
|
208
209
|
"Examples:\n"
|
|
209
210
|
" batplot a.xye:1.5406 b.qye --stack --i\n"
|
|
210
211
|
" batplot a.dat b.xy --wl 1.54 --i\n"
|
|
211
|
-
" batplot pattern.qye ticks.cif:1.54 --i\n
|
|
212
|
+
" batplot pattern.qye ticks.cif:1.54 --i\n"
|
|
213
|
+
" batplot file1.xy file2.xy --1d --stack --i # Plot 1st derivatives with interactive menu\n\n"
|
|
212
214
|
"Plot all files together:\n"
|
|
213
215
|
" batplot allfiles # Plot all XY files on same figure\n"
|
|
214
216
|
" batplot allfiles /path/to/dir # Plot all XY files in specified directory\n"
|
|
@@ -241,6 +243,8 @@ def _print_xy_help() -> None:
|
|
|
241
243
|
" --kchik : multiply y by x for EXAFS kχ(k) plots (sets labels to k (Å⁻¹) vs kχ(k) (Å⁻¹))\n"
|
|
242
244
|
" --k2chik : multiply y by x² for EXAFS k²χ(k) plots (sets labels to k (Å⁻¹) vs k²χ(k) (Å⁻²))\n"
|
|
243
245
|
" --k3chik : multiply y by x³ for EXAFS k³χ(k) plots (sets labels to k (Å⁻¹) vs k³χ(k) (Å⁻³))\n"
|
|
246
|
+
" --1d : plot the first derivative (dy/dx) of the datasets\n"
|
|
247
|
+
" --2d : plot the first derivative (dy/dx) of the datasets (alias for --1d)\n"
|
|
244
248
|
" --xrange/-r <min> <max> : set x-axis range, e.g. --xrange 0 10\n"
|
|
245
249
|
" --out/-o <filename> : save figure to file, e.g. --out file.svg\n"
|
|
246
250
|
" --xaxis <type> : set x-axis type (Q, 2theta, r, k, energy, rft, time, or user defined)\n"
|
|
@@ -248,6 +252,14 @@ def _print_xy_help() -> None:
|
|
|
248
252
|
" --ro : swap x and y axes (exchange x and y values before plotting)\n"
|
|
249
253
|
" e.g. --xaxis time --ro plots time as y-axis and voltage as x-axis\n"
|
|
250
254
|
" --wl <float> : set wavelength for Q conversion for all files, e.g. --wl 1.5406\n"
|
|
255
|
+
" --convert/-c <from> <to> : convert XRD data and export to 'converted' subfolder:\n"
|
|
256
|
+
" - <wl1> <wl2> : convert 2θ from wavelength1 to wavelength2\n"
|
|
257
|
+
" - <wl> q : convert 2θ (with wavelength) to Q space\n"
|
|
258
|
+
" - q <wl> : convert Q space to 2θ (with wavelength)\n"
|
|
259
|
+
" Examples:\n"
|
|
260
|
+
" batplot file.xye --convert 1.54 0.25\n"
|
|
261
|
+
" batplot file.xye --convert 1.54 q\n"
|
|
262
|
+
" batplot file.qye --convert q 1.54\n"
|
|
251
263
|
" File wavelength syntax : specify wavelength(s) per file using colon syntax:\n"
|
|
252
264
|
" - file:wl : single wavelength (for Q conversion or CIF 2theta calculation)\n"
|
|
253
265
|
" - file:wl1:wl2 : dual wavelength (convert 2theta→Q using wl1, then Q→2theta using wl2)\n"
|
|
@@ -332,12 +344,15 @@ def _print_op_help() -> None:
|
|
|
332
344
|
"Operando contour plots\n\n"
|
|
333
345
|
"Example usage:\n"
|
|
334
346
|
" batplot --operando --interactive --wl 0.25995 # Interactive mode with Q conversion\n"
|
|
335
|
-
" batplot --operando --xaxis 2theta # Using 2theta axis\n
|
|
347
|
+
" batplot --operando --xaxis 2theta # Using 2theta axis\n"
|
|
348
|
+
" batplot --operando --1d --interactive # Plot derivatives as contour with interactive menu\n"
|
|
349
|
+
" batplot --operando --2d --interactive # Plot derivatives (alias for --1d)\n\n"
|
|
336
350
|
" • Folder should contain XY files (.xy/.xye/.qye/.dat).\n"
|
|
337
351
|
" • Intensity scale is auto-adjusted between min/max values.\n"
|
|
338
352
|
" • If no .qye present, provide --xaxis 2theta or set --wl for Q conversion.\n"
|
|
339
353
|
" • If a .mpt file is present, an EC side panel is added for dual-panel mode.\n"
|
|
340
|
-
" • Without a .mpt file, operando-only mode shows the contour plot alone.\n
|
|
354
|
+
" • Without a .mpt file, operando-only mode shows the contour plot alone.\n"
|
|
355
|
+
" • --1d / --2d: plot the first derivative (dy/dx) of each scan as a contour plot.\n\n"
|
|
341
356
|
"Interactive (--interactive): resize axes/canvas, change colormap, set intensity range (oz),\n"
|
|
342
357
|
"EC y-axis options (time ↔ ions), geometry tweaks, toggle spines/ticks/labels,\n"
|
|
343
358
|
"print/export/import style, save session.\n"
|
|
@@ -416,7 +431,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
416
431
|
parser.add_argument("--out", "-o", type=str, help=argparse.SUPPRESS)
|
|
417
432
|
parser.add_argument("--errors", action="store_true", help=argparse.SUPPRESS)
|
|
418
433
|
parser.add_argument("--xaxis", type=str, help=argparse.SUPPRESS)
|
|
419
|
-
parser.add_argument("--convert", "-c", nargs="
|
|
434
|
+
parser.add_argument("--convert", "-c", nargs=2, metavar=("FROM", "TO"),
|
|
435
|
+
help="Convert XRD data: wavelength-to-wavelength (e.g., 1.54 0.25), wavelength-to-Q (e.g., 1.54 q), or Q-to-wavelength (e.g., q 1.54). Exports to 'converted' subfolder.")
|
|
420
436
|
parser.add_argument("--wl", type=float, help=argparse.SUPPRESS)
|
|
421
437
|
parser.add_argument("--fullprof", nargs="+", type=float, help=argparse.SUPPRESS)
|
|
422
438
|
parser.add_argument("--norm", action="store_true", help=argparse.SUPPRESS)
|
|
@@ -453,6 +469,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
453
469
|
help=argparse.SUPPRESS)
|
|
454
470
|
parser.add_argument("--readcolcsv", nargs=2, type=int, metavar=('X_COL', 'Y_COL'),
|
|
455
471
|
help=argparse.SUPPRESS)
|
|
472
|
+
parser.add_argument("--1d", action="store_true", dest="derivative_1d", help=argparse.SUPPRESS)
|
|
473
|
+
parser.add_argument("--2d", action="store_true", dest="derivative_2d", help=argparse.SUPPRESS)
|
|
456
474
|
return parser
|
|
457
475
|
|
|
458
476
|
|
batplot/batch.py
CHANGED
|
@@ -646,6 +646,18 @@ def batch_process(directory: str, args):
|
|
|
646
646
|
else:
|
|
647
647
|
x_plot = x
|
|
648
648
|
|
|
649
|
+
# Calculate first derivative if requested (after axis conversion)
|
|
650
|
+
if getattr(args, 'derivative_1d', False) or getattr(args, 'derivative_2d', False):
|
|
651
|
+
# Calculate dy/dx using numpy gradient
|
|
652
|
+
# numpy.gradient handles non-uniform spacing automatically
|
|
653
|
+
if len(y) > 1:
|
|
654
|
+
dy_dx = np.gradient(y, x_plot)
|
|
655
|
+
y = dy_dx
|
|
656
|
+
else:
|
|
657
|
+
# Single point or empty - cannot calculate derivative
|
|
658
|
+
print(f"Warning: Cannot calculate derivative for {fname}: insufficient data points")
|
|
659
|
+
continue
|
|
660
|
+
|
|
649
661
|
# Normalize if --norm flag is set
|
|
650
662
|
if getattr(args, 'norm', False):
|
|
651
663
|
if y.size:
|
batplot/batplot.py
CHANGED
|
@@ -9,7 +9,7 @@ from .electrochem_interactive import electrochem_interactive_menu
|
|
|
9
9
|
from .args import parse_args as _bp_parse_args
|
|
10
10
|
from .interactive import interactive_menu
|
|
11
11
|
from .batch import batch_process, batch_process_ec
|
|
12
|
-
from .converters import
|
|
12
|
+
from .converters import convert_xrd_data
|
|
13
13
|
from .session import (
|
|
14
14
|
dump_session as _bp_dump_session,
|
|
15
15
|
load_ec_session,
|
|
@@ -1793,37 +1793,38 @@ def batplot_main() -> int:
|
|
|
1793
1793
|
except ModuleNotFoundError as e:
|
|
1794
1794
|
# Handle numpy._core and other module import errors
|
|
1795
1795
|
if '_core' in str(e) or 'numpy' in str(e).lower():
|
|
1796
|
+
# Try to extract version info before the error
|
|
1797
|
+
from .session import _try_extract_version_from_pickle, _get_current_numpy_version
|
|
1798
|
+
saved_versions = _try_extract_version_from_pickle(sess_path)
|
|
1799
|
+
current_numpy = _get_current_numpy_version()
|
|
1800
|
+
|
|
1801
|
+
saved_numpy = saved_versions.get('numpy', 'unknown')
|
|
1802
|
+
|
|
1796
1803
|
print(f"\nERROR: NumPy version mismatch detected when loading: {sess_path}")
|
|
1797
1804
|
print("This session was saved with a different NumPy version.")
|
|
1805
|
+
print()
|
|
1806
|
+
print(f"Session was saved with: NumPy {saved_numpy}")
|
|
1807
|
+
print(f"Currently installed: NumPy {current_numpy}")
|
|
1808
|
+
print()
|
|
1798
1809
|
print("The error 'No module named numpy._core' indicates:")
|
|
1799
1810
|
print(" - Session saved with NumPy 2.0+ but loading with NumPy <2.0, OR")
|
|
1800
1811
|
print(" - Session saved with NumPy <2.0 but loading with NumPy 2.0+")
|
|
1801
|
-
print(
|
|
1802
|
-
print("
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1812
|
+
print()
|
|
1813
|
+
print("Solutions:")
|
|
1814
|
+
if saved_numpy != 'unknown':
|
|
1815
|
+
print(f" 1. Install matching version: pip install 'numpy=={saved_numpy}'")
|
|
1816
|
+
else:
|
|
1817
|
+
print(" 1. Try installing NumPy <2.0: pip install 'numpy<2.0'")
|
|
1818
|
+
print(" OR try installing NumPy 2.0+: pip install 'numpy>=2.0'")
|
|
1819
|
+
print(" 2. Recreate the session from original data files")
|
|
1807
1820
|
else:
|
|
1808
1821
|
print(f"\nERROR: Module import error when loading: {sess_path}")
|
|
1809
1822
|
print(f"Error: {e}")
|
|
1810
1823
|
print("This usually indicates a package version mismatch.")
|
|
1811
|
-
print("Try installing matching package versions or recreate the session.")
|
|
1812
1824
|
exit(1)
|
|
1813
1825
|
except Exception as e:
|
|
1814
1826
|
print(f"Failed to load session: {e}")
|
|
1815
1827
|
exit(1)
|
|
1816
|
-
|
|
1817
|
-
# Check package version compatibility (if version info is available)
|
|
1818
|
-
try:
|
|
1819
|
-
from .session import _get_package_versions, _check_package_compatibility
|
|
1820
|
-
saved_versions = sess.get('package_versions', {})
|
|
1821
|
-
current_versions = _get_package_versions()
|
|
1822
|
-
if saved_versions and not _check_package_compatibility(saved_versions, current_versions, sess_path):
|
|
1823
|
-
exit(1)
|
|
1824
|
-
except Exception:
|
|
1825
|
-
# If compatibility checking fails, continue anyway (backward compatibility)
|
|
1826
|
-
pass
|
|
1827
1828
|
# If it's an EC GC session, load and open EC interactive menu directly
|
|
1828
1829
|
if isinstance(sess, dict) and sess.get('kind') == 'ec_gc':
|
|
1829
1830
|
try:
|
|
@@ -1963,16 +1964,58 @@ def batplot_main() -> int:
|
|
|
1963
1964
|
y_loaded = sess.get('y_data', []) # stored plotted (baseline+offset) values
|
|
1964
1965
|
orig_loaded = sess.get('orig_y', []) # stored baseline (normalized/raw w/out offsets)
|
|
1965
1966
|
offsets_saved = sess.get('offsets', [])
|
|
1967
|
+
# Restore processed data (for smooth/reduce operations)
|
|
1968
|
+
original_x_data_list = sess.get('original_x_data_list')
|
|
1969
|
+
original_y_data_list = sess.get('original_y_data_list')
|
|
1970
|
+
smooth_settings = sess.get('smooth_settings')
|
|
1971
|
+
if original_x_data_list is not None:
|
|
1972
|
+
fig._original_x_data_list = [np.array(a) for a in original_x_data_list]
|
|
1973
|
+
if original_y_data_list is not None:
|
|
1974
|
+
fig._original_y_data_list = [np.array(a) for a in original_y_data_list]
|
|
1975
|
+
full_processed_x_data_list = sess.get('full_processed_x_data_list')
|
|
1976
|
+
full_processed_y_data_list = sess.get('full_processed_y_data_list')
|
|
1977
|
+
if full_processed_x_data_list is not None:
|
|
1978
|
+
fig._full_processed_x_data_list = [np.array(a) for a in full_processed_x_data_list]
|
|
1979
|
+
if full_processed_y_data_list is not None:
|
|
1980
|
+
fig._full_processed_y_data_list = [np.array(a) for a in full_processed_y_data_list]
|
|
1981
|
+
if smooth_settings is not None:
|
|
1982
|
+
fig._smooth_settings = dict(smooth_settings)
|
|
1983
|
+
last_smooth_settings = sess.get('last_smooth_settings')
|
|
1984
|
+
if last_smooth_settings is not None:
|
|
1985
|
+
fig._last_smooth_settings = dict(last_smooth_settings)
|
|
1986
|
+
# Restore derivative data (for derivative operations)
|
|
1987
|
+
pre_derivative_x_data_list = sess.get('pre_derivative_x_data_list')
|
|
1988
|
+
pre_derivative_y_data_list = sess.get('pre_derivative_y_data_list')
|
|
1989
|
+
pre_derivative_ylabel = sess.get('pre_derivative_ylabel')
|
|
1990
|
+
derivative_order = sess.get('derivative_order')
|
|
1991
|
+
if pre_derivative_x_data_list is not None:
|
|
1992
|
+
fig._pre_derivative_x_data_list = [np.array(a) for a in pre_derivative_x_data_list]
|
|
1993
|
+
if pre_derivative_y_data_list is not None:
|
|
1994
|
+
fig._pre_derivative_y_data_list = [np.array(a) for a in pre_derivative_y_data_list]
|
|
1995
|
+
if pre_derivative_ylabel is not None:
|
|
1996
|
+
fig._pre_derivative_ylabel = str(pre_derivative_ylabel)
|
|
1997
|
+
if derivative_order is not None:
|
|
1998
|
+
fig._derivative_order = int(derivative_order)
|
|
1999
|
+
derivative_reversed = sess.get('derivative_reversed')
|
|
2000
|
+
if derivative_reversed is not None:
|
|
2001
|
+
fig._derivative_reversed = bool(derivative_reversed)
|
|
1966
2002
|
n_curves = len(x_loaded)
|
|
1967
2003
|
for i in range(n_curves):
|
|
1968
|
-
|
|
2004
|
+
# Ensure arrays are 1D and have matching shapes
|
|
2005
|
+
x_arr = np.asarray(x_loaded[i], dtype=float).flatten()
|
|
1969
2006
|
off = offsets_saved[i] if i < len(offsets_saved) else 0.0
|
|
1970
2007
|
if orig_loaded and i < len(orig_loaded):
|
|
1971
|
-
base = np.
|
|
2008
|
+
base = np.asarray(orig_loaded[i], dtype=float).flatten()
|
|
1972
2009
|
else:
|
|
1973
2010
|
# Fallback: derive baseline by subtracting offset from stored y (handles legacy sessions)
|
|
1974
|
-
y_arr_full = np.
|
|
2011
|
+
y_arr_full = np.asarray(y_loaded[i], dtype=float).flatten() if i < len(y_loaded) else np.array([], dtype=float)
|
|
1975
2012
|
base = y_arr_full - off
|
|
2013
|
+
# Ensure x and y have matching lengths
|
|
2014
|
+
if x_arr.size != base.size:
|
|
2015
|
+
print(f"Warning: Curve {i+1} has mismatched x/y lengths ({x_arr.size} vs {base.size}). Trimming to match.")
|
|
2016
|
+
min_len = min(x_arr.size, base.size)
|
|
2017
|
+
x_arr = x_arr[:min_len]
|
|
2018
|
+
base = base[:min_len]
|
|
1976
2019
|
y_plot = base + off
|
|
1977
2020
|
x_data_list.append(x_arr)
|
|
1978
2021
|
orig_y.append(base)
|
|
@@ -2276,8 +2319,24 @@ def batplot_main() -> int:
|
|
|
2276
2319
|
cif_hkl_label_map = {k: dict(v) for k,v in sess.get('cif_hkl_label_map', {}).items()}
|
|
2277
2320
|
cif_numbering_enabled = True
|
|
2278
2321
|
cif_extend_suspended = False
|
|
2279
|
-
|
|
2280
|
-
|
|
2322
|
+
# Restore CIF visibility flags - default to False for hkl (labels hidden by default)
|
|
2323
|
+
# and True for titles (shown by default)
|
|
2324
|
+
show_cif_hkl = bool(sess.get('show_cif_hkl', False))
|
|
2325
|
+
show_cif_titles = bool(sess.get('show_cif_titles', True))
|
|
2326
|
+
|
|
2327
|
+
# Store CIF state in __main__ module for interactive menu to access
|
|
2328
|
+
# This ensures CIF commands (z, hkl, j) are available in the menu
|
|
2329
|
+
try:
|
|
2330
|
+
_bp_module = sys.modules.get('__main__')
|
|
2331
|
+
if _bp_module is not None and cif_tick_series:
|
|
2332
|
+
setattr(_bp_module, 'cif_tick_series', list(cif_tick_series))
|
|
2333
|
+
setattr(_bp_module, 'cif_hkl_map', cif_hkl_map)
|
|
2334
|
+
setattr(_bp_module, 'cif_hkl_label_map', cif_hkl_label_map)
|
|
2335
|
+
setattr(_bp_module, 'show_cif_hkl', bool(show_cif_hkl))
|
|
2336
|
+
setattr(_bp_module, 'show_cif_titles', bool(show_cif_titles))
|
|
2337
|
+
setattr(_bp_module, 'cif_extend_suspended', False)
|
|
2338
|
+
except Exception:
|
|
2339
|
+
pass
|
|
2281
2340
|
# Provide minimal stubs to satisfy interactive menu dependencies
|
|
2282
2341
|
# Axis mode restoration informs downstream toggles (e.g., CIF conversions, crosshair availability)
|
|
2283
2342
|
axis_mode_restored = sess.get('axis_mode', 'unknown')
|
|
@@ -2336,13 +2395,20 @@ def batplot_main() -> int:
|
|
|
2336
2395
|
if not cif_tick_series:
|
|
2337
2396
|
return
|
|
2338
2397
|
try:
|
|
2339
|
-
# Preserve
|
|
2398
|
+
# Preserve current limits before drawing - use actual current limits
|
|
2399
|
+
# to prevent any movement when toggling
|
|
2340
2400
|
prev_xlim = ax.get_xlim()
|
|
2341
2401
|
prev_ylim = ax.get_ylim()
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2402
|
+
|
|
2403
|
+
# Use current ylim as fixed reference to prevent incremental movement
|
|
2404
|
+
# This ensures that repeated 'z' commands don't cause drift
|
|
2405
|
+
# Store it only once on first call, then reuse
|
|
2406
|
+
if not hasattr(ax, '_cif_initial_ylim'):
|
|
2407
|
+
ax._cif_initial_ylim = tuple(prev_ylim)
|
|
2408
|
+
fixed_ylim = ax._cif_initial_ylim
|
|
2409
|
+
fixed_yr = fixed_ylim[1] - fixed_ylim[0]
|
|
2410
|
+
if fixed_yr <= 0: fixed_yr = 1.0
|
|
2411
|
+
|
|
2346
2412
|
# Check visibility flag first
|
|
2347
2413
|
show_titles_local = bool(show_cif_titles) # Use closure variable from outer scope
|
|
2348
2414
|
# Also check figure attribute and module attribute as fallback
|
|
@@ -2356,38 +2422,53 @@ def batplot_main() -> int:
|
|
|
2356
2422
|
show_titles_local = bool(getattr(_bp_module, 'show_cif_titles', show_titles_local))
|
|
2357
2423
|
except Exception:
|
|
2358
2424
|
pass
|
|
2359
|
-
|
|
2425
|
+
|
|
2426
|
+
# Check hkl visibility - check __main__ module first (where interactive menu stores it)
|
|
2427
|
+
# then fall back to closure variable
|
|
2428
|
+
show_hkl_local = False
|
|
2429
|
+
try:
|
|
2430
|
+
_bp_module = sys.modules.get('__main__')
|
|
2431
|
+
if _bp_module is not None and hasattr(_bp_module, 'show_cif_hkl'):
|
|
2432
|
+
show_hkl_local = bool(getattr(_bp_module, 'show_cif_hkl', False))
|
|
2433
|
+
except Exception:
|
|
2434
|
+
pass
|
|
2435
|
+
# Fall back to closure variable if not found in module
|
|
2436
|
+
if not show_hkl_local:
|
|
2437
|
+
try:
|
|
2438
|
+
show_hkl_local = bool(show_cif_hkl)
|
|
2439
|
+
except Exception:
|
|
2440
|
+
pass
|
|
2441
|
+
|
|
2442
|
+
# Calculate base and spacing based on FIXED y-axis limits (not current)
|
|
2443
|
+
# This prevents incremental movement when toggling
|
|
2360
2444
|
if saved_stack or len(y_data_list) > 1:
|
|
2361
|
-
global_min = min(float(a.min()) for a in y_data_list if len(a)) if y_data_list else
|
|
2362
|
-
base = global_min - 0.08*
|
|
2445
|
+
global_min = min(float(a.min()) for a in y_data_list if len(a)) if y_data_list else fixed_ylim[0]
|
|
2446
|
+
base = global_min - 0.08*fixed_yr; spacing = 0.05*fixed_yr
|
|
2363
2447
|
else:
|
|
2364
2448
|
global_min = min(float(a.min()) for a in y_data_list if len(a)) if y_data_list else 0.0
|
|
2365
|
-
base = global_min - 0.06*
|
|
2449
|
+
base = global_min - 0.06*fixed_yr; spacing = 0.04*fixed_yr
|
|
2450
|
+
|
|
2366
2451
|
# Only adjust y-axis limits if titles are visible
|
|
2367
|
-
needed_min = base - (len(cif_tick_series)-1)*spacing - 0.04*
|
|
2452
|
+
needed_min = base - (len(cif_tick_series)-1)*spacing - 0.04*fixed_yr
|
|
2453
|
+
if show_titles_local and needed_min < fixed_ylim[0]:
|
|
2454
|
+
# Expand y-axis only if needed, using fixed limits as reference
|
|
2455
|
+
ax.set_ylim(needed_min, fixed_ylim[1])
|
|
2456
|
+
else:
|
|
2457
|
+
# Restore to fixed limits if no expansion needed
|
|
2458
|
+
ax.set_ylim(fixed_ylim)
|
|
2459
|
+
|
|
2460
|
+
# Get current limits for drawing (after potential expansion)
|
|
2368
2461
|
cur_ylim = ax.get_ylim()
|
|
2369
2462
|
yr = cur_ylim[1] - cur_ylim[0]
|
|
2370
2463
|
if yr <= 0: yr = 1.0
|
|
2371
|
-
|
|
2372
|
-
# Expand y-axis only if needed, using original limits as reference
|
|
2373
|
-
ax.set_ylim(needed_min, orig_ylim[1])
|
|
2374
|
-
cur_ylim = ax.get_ylim()
|
|
2375
|
-
yr = cur_ylim[1] - cur_ylim[0]
|
|
2376
|
-
if yr <= 0: yr = 1.0
|
|
2377
|
-
# Recalculate base with new limits if we expanded
|
|
2378
|
-
if saved_stack or len(y_data_list) > 1:
|
|
2379
|
-
global_min = min(float(a.min()) for a in y_data_list if len(a)) if y_data_list else cur_ylim[0]
|
|
2380
|
-
base = global_min - 0.08*yr; spacing = 0.05*yr
|
|
2381
|
-
else:
|
|
2382
|
-
global_min = min(float(a.min()) for a in y_data_list if len(a)) if y_data_list else 0.0
|
|
2383
|
-
base = global_min - 0.06*yr; spacing = 0.04*yr
|
|
2464
|
+
|
|
2384
2465
|
# Clear previous artifacts
|
|
2385
2466
|
for art in getattr(ax, '_cif_tick_art', []):
|
|
2386
2467
|
try: art.remove()
|
|
2387
2468
|
except Exception: pass
|
|
2388
2469
|
new_art = []
|
|
2389
|
-
show_hkl_local = bool(show_cif_hkl)
|
|
2390
2470
|
wl_any = _session_ensure_wavelength()
|
|
2471
|
+
|
|
2391
2472
|
# Draw each series
|
|
2392
2473
|
for i,(lab,fname,peaksQ,wl,qmax_sim,color) in enumerate(cif_tick_series):
|
|
2393
2474
|
y_line = base - i*spacing
|
|
@@ -2401,12 +2482,18 @@ def batplot_main() -> int:
|
|
|
2401
2482
|
xlow,xhigh = ax.get_xlim()
|
|
2402
2483
|
domain_peaks = [p for p in domain_peaks if xlow <= p <= xhigh]
|
|
2403
2484
|
# Build hkl label map (keys are Q values, not 2θ)
|
|
2404
|
-
|
|
2485
|
+
# Only use label_map if hkl labels are enabled
|
|
2486
|
+
label_map = {}
|
|
2487
|
+
if show_hkl_local:
|
|
2488
|
+
label_map = cif_hkl_label_map.get(fname, {})
|
|
2405
2489
|
if show_hkl_local and len(domain_peaks) > 4000:
|
|
2406
2490
|
show_hkl_local = False # safety
|
|
2491
|
+
label_map = {} # Clear label map if too many peaks
|
|
2407
2492
|
for p in domain_peaks:
|
|
2493
|
+
# Use color from tuple (preserved from session)
|
|
2408
2494
|
ln, = ax.plot([p,p],[y_line, y_line+0.02*yr], color=color, lw=1.0, alpha=0.9, zorder=3)
|
|
2409
2495
|
new_art.append(ln)
|
|
2496
|
+
# Only show hkl labels if explicitly enabled
|
|
2410
2497
|
if show_hkl_local:
|
|
2411
2498
|
# When axis is 2θ convert back to Q to look up hkl label
|
|
2412
2499
|
if use_2th and (wl or wl_any):
|
|
@@ -2414,28 +2501,35 @@ def batplot_main() -> int:
|
|
|
2414
2501
|
Qp = 4*np.pi*np.sin(theta)/(wl if wl is not None else wl_any)
|
|
2415
2502
|
else:
|
|
2416
2503
|
Qp = p
|
|
2417
|
-
|
|
2504
|
+
Qp_rounded = round(Qp, 6)
|
|
2505
|
+
lbl = label_map.get(Qp_rounded)
|
|
2418
2506
|
if lbl:
|
|
2507
|
+
# Use same color as tick line
|
|
2419
2508
|
t_hkl = ax.text(p, y_line+0.022*yr, lbl, ha='center', va='bottom', fontsize=7, rotation=90, color=color)
|
|
2420
2509
|
new_art.append(t_hkl)
|
|
2421
|
-
# Removed numbering prefix; keep one leading space for padding from axis
|
|
2422
2510
|
# Only add title label if show_cif_titles is True
|
|
2423
2511
|
if show_titles_local:
|
|
2424
2512
|
label_text = f" {lab}"
|
|
2513
|
+
# Use color from tuple (preserved from session)
|
|
2425
2514
|
txt = ax.text(prev_xlim[0], y_line+0.005*yr, label_text,
|
|
2426
2515
|
ha='left', va='bottom', fontsize=max(8,int(0.55*plt.rcParams.get('font.size',16))), color=color)
|
|
2427
2516
|
new_art.append(txt)
|
|
2428
2517
|
ax._cif_tick_art = new_art
|
|
2429
|
-
# Restore
|
|
2518
|
+
# Restore x-axis limits
|
|
2430
2519
|
ax.set_xlim(prev_xlim)
|
|
2431
2520
|
# Restore y-axis: if titles are hidden, always restore; if titles are shown, only restore if we didn't need to expand
|
|
2521
|
+
# Use prev_ylim (current limits before drawing) to prevent any movement
|
|
2432
2522
|
if not show_titles_local:
|
|
2433
2523
|
# Titles hidden: always restore original limits
|
|
2434
2524
|
ax.set_ylim(prev_ylim)
|
|
2435
2525
|
elif needed_min >= prev_ylim[0]:
|
|
2436
2526
|
# Titles shown but no expansion needed: restore original limits
|
|
2437
2527
|
ax.set_ylim(prev_ylim)
|
|
2438
|
-
|
|
2528
|
+
else:
|
|
2529
|
+
# Expansion needed: use the minimum of needed_min and prev_ylim[0] to prevent incremental growth
|
|
2530
|
+
# This ensures that repeated toggles don't cause drift
|
|
2531
|
+
new_ymin = min(needed_min, prev_ylim[0])
|
|
2532
|
+
ax.set_ylim(new_ymin, prev_ylim[1])
|
|
2439
2533
|
fig.canvas.draw_idle()
|
|
2440
2534
|
except Exception:
|
|
2441
2535
|
pass
|
|
@@ -2547,20 +2641,34 @@ def batplot_main() -> int:
|
|
|
2547
2641
|
except Exception:
|
|
2548
2642
|
pass
|
|
2549
2643
|
|
|
2644
|
+
# Prepare CIF globals for interactive menu (ensures CIF commands are available)
|
|
2645
|
+
cif_globals_dict = None
|
|
2646
|
+
if cif_tick_series:
|
|
2647
|
+
cif_globals_dict = {
|
|
2648
|
+
'cif_tick_series': list(cif_tick_series),
|
|
2649
|
+
'cif_hkl_map': cif_hkl_map,
|
|
2650
|
+
'cif_hkl_label_map': cif_hkl_label_map,
|
|
2651
|
+
'show_cif_hkl': bool(show_cif_hkl),
|
|
2652
|
+
'show_cif_titles': bool(show_cif_titles),
|
|
2653
|
+
'cif_extend_suspended': False,
|
|
2654
|
+
'keep_canvas_fixed': True,
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2550
2657
|
interactive_menu(fig, ax, y_data_list, x_data_list, labels_list,
|
|
2551
2658
|
orig_y, label_text_objects, delta, x_label, args,
|
|
2552
2659
|
x_full_list, raw_y_full_list, offsets_list,
|
|
2553
|
-
use_Q, use_r, use_E, use_k, use_rft
|
|
2660
|
+
use_Q, use_r, use_E, use_k, use_rft,
|
|
2661
|
+
cif_globals=cif_globals_dict)
|
|
2554
2662
|
plt.show()
|
|
2555
2663
|
exit()
|
|
2556
2664
|
|
|
2557
2665
|
# ---------------- Handle conversion ----------------
|
|
2558
2666
|
if args.convert:
|
|
2559
|
-
if args.
|
|
2560
|
-
print("Error: --
|
|
2561
|
-
|
|
2667
|
+
if not args.files:
|
|
2668
|
+
print("Error: --convert requires file(s) to convert")
|
|
2562
2669
|
exit(1)
|
|
2563
|
-
|
|
2670
|
+
from_param, to_param = args.convert
|
|
2671
|
+
convert_xrd_data(args.files, from_param, to_param)
|
|
2564
2672
|
exit()
|
|
2565
2673
|
|
|
2566
2674
|
# ---------------- Plotting ----------------
|
|
@@ -2947,6 +3055,19 @@ def batplot_main() -> int:
|
|
|
2947
3055
|
# ---- Store full (converted) arrays BEFORE cropping ----
|
|
2948
3056
|
x_full = x_plot.copy()
|
|
2949
3057
|
y_full_raw = y.copy()
|
|
3058
|
+
|
|
3059
|
+
# ---- Calculate first derivative if requested ----
|
|
3060
|
+
if getattr(args, 'derivative_1d', False) or getattr(args, 'derivative_2d', False):
|
|
3061
|
+
# Calculate dy/dx using numpy gradient
|
|
3062
|
+
# numpy.gradient handles non-uniform spacing automatically
|
|
3063
|
+
if len(y_full_raw) > 1:
|
|
3064
|
+
dy_dx = np.gradient(y_full_raw, x_full)
|
|
3065
|
+
y_full_raw = dy_dx
|
|
3066
|
+
else:
|
|
3067
|
+
# Single point or empty - cannot calculate derivative
|
|
3068
|
+
print(f"Warning: Cannot calculate derivative for {fname}: insufficient data points")
|
|
3069
|
+
continue
|
|
3070
|
+
|
|
2950
3071
|
raw_y_full_list.append(y_full_raw)
|
|
2951
3072
|
x_full_list.append(x_full)
|
|
2952
3073
|
|
|
@@ -3180,13 +3301,20 @@ def batplot_main() -> int:
|
|
|
3180
3301
|
def draw_cif_ticks():
|
|
3181
3302
|
if not cif_tick_series:
|
|
3182
3303
|
return
|
|
3183
|
-
# Preserve
|
|
3304
|
+
# Preserve current limits before drawing - use actual current limits
|
|
3305
|
+
# to prevent any movement when toggling
|
|
3184
3306
|
prev_xlim = ax.get_xlim()
|
|
3185
3307
|
prev_ylim = ax.get_ylim()
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3308
|
+
|
|
3309
|
+
# Store initial limits as fixed reference point to prevent incremental movement
|
|
3310
|
+
# This ensures that repeated 'z' commands don't cause drift
|
|
3311
|
+
# Only set once on first call, then reuse to prevent drift
|
|
3312
|
+
if not hasattr(ax, '_cif_initial_ylim'):
|
|
3313
|
+
ax._cif_initial_ylim = tuple(prev_ylim)
|
|
3314
|
+
fixed_ylim = ax._cif_initial_ylim
|
|
3315
|
+
fixed_yr = fixed_ylim[1] - fixed_ylim[0]
|
|
3316
|
+
if fixed_yr <= 0: fixed_yr = 1.0
|
|
3317
|
+
|
|
3190
3318
|
# Check visibility flag first to decide if we need to adjust y-axis
|
|
3191
3319
|
show_titles = show_cif_titles # Use closure variable
|
|
3192
3320
|
try:
|
|
@@ -3199,38 +3327,50 @@ def batplot_main() -> int:
|
|
|
3199
3327
|
show_titles = bool(getattr(fig, '_bp_show_cif_titles', True))
|
|
3200
3328
|
except Exception:
|
|
3201
3329
|
pass
|
|
3202
|
-
|
|
3330
|
+
|
|
3331
|
+
# Calculate base and spacing based on FIXED y-axis limits (not current)
|
|
3332
|
+
# This prevents incremental movement when toggling
|
|
3203
3333
|
if args.stack or len(y_data_list) > 1:
|
|
3204
|
-
global_min = min(float(a.min()) for a in y_data_list if len(a)) if y_data_list else
|
|
3205
|
-
base = global_min - 0.08*
|
|
3334
|
+
global_min = min(float(a.min()) for a in y_data_list if len(a)) if y_data_list else fixed_ylim[0]
|
|
3335
|
+
base = global_min - 0.08*fixed_yr; spacing = 0.05*fixed_yr
|
|
3206
3336
|
else:
|
|
3207
3337
|
global_min = min(float(a.min()) for a in y_data_list if len(a)) if y_data_list else 0.0
|
|
3208
|
-
base = global_min - 0.06*
|
|
3338
|
+
base = global_min - 0.06*fixed_yr; spacing = 0.04*fixed_yr
|
|
3339
|
+
|
|
3209
3340
|
# Only adjust y-axis limits if titles are visible
|
|
3210
|
-
needed_min = base - (len(cif_tick_series)-1)*spacing - 0.04*
|
|
3341
|
+
needed_min = base - (len(cif_tick_series)-1)*spacing - 0.04*fixed_yr
|
|
3342
|
+
if show_titles and needed_min < fixed_ylim[0]:
|
|
3343
|
+
# Expand y-axis only if needed, using fixed limits as reference
|
|
3344
|
+
ax.set_ylim(needed_min, fixed_ylim[1])
|
|
3345
|
+
else:
|
|
3346
|
+
# Restore to fixed limits if no expansion needed
|
|
3347
|
+
ax.set_ylim(fixed_ylim)
|
|
3348
|
+
|
|
3349
|
+
# Get current limits for drawing (after potential expansion)
|
|
3211
3350
|
cur_ylim = ax.get_ylim()
|
|
3212
3351
|
yr = cur_ylim[1] - cur_ylim[0]
|
|
3213
3352
|
if yr <= 0: yr = 1.0
|
|
3214
|
-
if show_titles and needed_min < orig_ylim[0]:
|
|
3215
|
-
# Expand y-axis only if needed, using original limits as reference
|
|
3216
|
-
ax.set_ylim(needed_min, orig_ylim[1])
|
|
3217
|
-
cur_ylim = ax.get_ylim()
|
|
3218
|
-
yr = cur_ylim[1] - cur_ylim[0]
|
|
3219
|
-
if yr <= 0: yr = 1.0
|
|
3220
|
-
# Recalculate base with new limits if we expanded
|
|
3221
|
-
if args.stack or len(y_data_list) > 1:
|
|
3222
|
-
global_min = min(float(a.min()) for a in y_data_list if len(a)) if y_data_list else cur_ylim[0]
|
|
3223
|
-
base = global_min - 0.08*yr; spacing = 0.05*yr
|
|
3224
|
-
else:
|
|
3225
|
-
global_min = min(float(a.min()) for a in y_data_list if len(a)) if y_data_list else 0.0
|
|
3226
|
-
base = global_min - 0.06*yr; spacing = 0.04*yr
|
|
3227
3353
|
# Clear previous
|
|
3228
3354
|
for art in getattr(ax, '_cif_tick_art', []):
|
|
3229
3355
|
try: art.remove()
|
|
3230
3356
|
except Exception: pass
|
|
3231
3357
|
new_art = []
|
|
3232
3358
|
mixed_mode = (not cif_only) # cif_only variable defined earlier in script context
|
|
3233
|
-
|
|
3359
|
+
# Check hkl visibility - check __main__ module first (where interactive menu stores it)
|
|
3360
|
+
# then fall back to closure variable
|
|
3361
|
+
show_hkl = False
|
|
3362
|
+
try:
|
|
3363
|
+
_bp_module = sys.modules.get('__main__')
|
|
3364
|
+
if _bp_module is not None and hasattr(_bp_module, 'show_cif_hkl'):
|
|
3365
|
+
show_hkl = bool(getattr(_bp_module, 'show_cif_hkl', False))
|
|
3366
|
+
except Exception:
|
|
3367
|
+
pass
|
|
3368
|
+
# Fall back to closure variable if not found in module
|
|
3369
|
+
if not show_hkl:
|
|
3370
|
+
try:
|
|
3371
|
+
show_hkl = bool(globals().get('show_cif_hkl', False))
|
|
3372
|
+
except Exception:
|
|
3373
|
+
pass
|
|
3234
3374
|
for i,(lab, fname, peaksQ, wl, qmax_sim, color) in enumerate(cif_tick_series):
|
|
3235
3375
|
y_line = base - i*spacing
|
|
3236
3376
|
if use_2th:
|
|
@@ -3252,14 +3392,12 @@ def batplot_main() -> int:
|
|
|
3252
3392
|
ha='left', va='bottom', fontsize=max(8,int(0.55*plt.rcParams.get('font.size',12))), color=color)
|
|
3253
3393
|
new_art.append(txt)
|
|
3254
3394
|
continue
|
|
3255
|
-
# Build map for quick hkl lookup by Q
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
for qval,h,k,l in hkl_entries:
|
|
3260
|
-
hkl_by_q.setdefault(qval, []).append((h,k,l))
|
|
3261
|
-
label_map = cif_hkl_label_map.get(fname, {})
|
|
3395
|
+
# Build map for quick hkl lookup by Q (only if hkl labels are enabled)
|
|
3396
|
+
label_map = {}
|
|
3397
|
+
if show_hkl:
|
|
3398
|
+
label_map = cif_hkl_label_map.get(fname, {})
|
|
3262
3399
|
# --- Optimized tick & hkl label drawing ---
|
|
3400
|
+
# Check if we should show hkl labels: need show_hkl, peaks, AND a non-empty label_map
|
|
3263
3401
|
if show_hkl and peaksQ and label_map:
|
|
3264
3402
|
# Guard against pathological large peak lists (can freeze UI)
|
|
3265
3403
|
if len(peaksQ) > 4000 or len(domain_peaks) > 4000:
|
|
@@ -3282,7 +3420,8 @@ def batplot_main() -> int:
|
|
|
3282
3420
|
Qp = 4*np.pi*np.sin(theta)/wl
|
|
3283
3421
|
else:
|
|
3284
3422
|
Qp = p
|
|
3285
|
-
|
|
3423
|
+
Qp_rounded = round(Qp, 6)
|
|
3424
|
+
lbl = label_map.get(Qp_rounded)
|
|
3286
3425
|
if lbl:
|
|
3287
3426
|
t_hkl = ax.text(p, y_line+0.022*yr, lbl, ha='center', va='bottom', fontsize=7, rotation=90, color=color)
|
|
3288
3427
|
new_art.append(t_hkl)
|
|
@@ -3308,7 +3447,11 @@ def batplot_main() -> int:
|
|
|
3308
3447
|
elif needed_min >= prev_ylim[0]:
|
|
3309
3448
|
# Titles shown but no expansion needed: restore original limits
|
|
3310
3449
|
ax.set_ylim(prev_ylim)
|
|
3311
|
-
|
|
3450
|
+
else:
|
|
3451
|
+
# Expansion needed: use the minimum of needed_min and prev_ylim[0] to prevent incremental growth
|
|
3452
|
+
# This ensures that repeated toggles don't cause drift
|
|
3453
|
+
new_ymin = min(needed_min, prev_ylim[0])
|
|
3454
|
+
ax.set_ylim(new_ymin, prev_ylim[1])
|
|
3312
3455
|
# Store simplified metadata for hover: list of dicts with 'x','y','label'
|
|
3313
3456
|
hover_meta = []
|
|
3314
3457
|
show_hkl = globals().get('show_cif_hkl', False)
|
batplot/cli.py
CHANGED
|
@@ -59,6 +59,20 @@ def main(argv: Optional[list] = None) -> int:
|
|
|
59
59
|
>>> main()
|
|
60
60
|
0
|
|
61
61
|
"""
|
|
62
|
+
# ====================================================================
|
|
63
|
+
# STEP 0: PYTHON VERSION CHECK
|
|
64
|
+
# ====================================================================
|
|
65
|
+
# Check if Python version is 3.13 (required for batplot)
|
|
66
|
+
# ====================================================================
|
|
67
|
+
if sys.version_info.major != 3 or sys.version_info.minor != 13:
|
|
68
|
+
current_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
|
69
|
+
print(f"\n⚠️ WARNING: Python version mismatch detected!")
|
|
70
|
+
print(f" batplot requires Python 3.13")
|
|
71
|
+
print(f" Currently running: Python {current_version}")
|
|
72
|
+
print(f"\n This may cause compatibility issues.")
|
|
73
|
+
print(f" Please install Python 3.13 and use it to run batplot.")
|
|
74
|
+
print(f" Continuing anyway, but expect potential issues...\n")
|
|
75
|
+
|
|
62
76
|
# ====================================================================
|
|
63
77
|
# STEP 1: VERSION CHECK (NON-BLOCKING)
|
|
64
78
|
# ====================================================================
|