batplot 1.8.0__py3-none-any.whl → 1.8.2__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.

Files changed (42) hide show
  1. batplot/__init__.py +1 -1
  2. batplot/args.py +5 -3
  3. batplot/batplot.py +44 -4
  4. batplot/cpc_interactive.py +96 -3
  5. batplot/electrochem_interactive.py +28 -0
  6. batplot/interactive.py +18 -2
  7. batplot/modes.py +12 -12
  8. batplot/operando.py +2 -0
  9. batplot/operando_ec_interactive.py +112 -11
  10. batplot/session.py +35 -1
  11. batplot/utils.py +40 -0
  12. batplot/version_check.py +85 -6
  13. {batplot-1.8.0.dist-info → batplot-1.8.2.dist-info}/METADATA +1 -1
  14. batplot-1.8.2.dist-info/RECORD +75 -0
  15. {batplot-1.8.0.dist-info → batplot-1.8.2.dist-info}/top_level.txt +1 -0
  16. batplot_backup_20251221_101150/__init__.py +5 -0
  17. batplot_backup_20251221_101150/args.py +625 -0
  18. batplot_backup_20251221_101150/batch.py +1176 -0
  19. batplot_backup_20251221_101150/batplot.py +3589 -0
  20. batplot_backup_20251221_101150/cif.py +823 -0
  21. batplot_backup_20251221_101150/cli.py +149 -0
  22. batplot_backup_20251221_101150/color_utils.py +547 -0
  23. batplot_backup_20251221_101150/config.py +198 -0
  24. batplot_backup_20251221_101150/converters.py +204 -0
  25. batplot_backup_20251221_101150/cpc_interactive.py +4409 -0
  26. batplot_backup_20251221_101150/electrochem_interactive.py +4520 -0
  27. batplot_backup_20251221_101150/interactive.py +3894 -0
  28. batplot_backup_20251221_101150/manual.py +323 -0
  29. batplot_backup_20251221_101150/modes.py +799 -0
  30. batplot_backup_20251221_101150/operando.py +603 -0
  31. batplot_backup_20251221_101150/operando_ec_interactive.py +5487 -0
  32. batplot_backup_20251221_101150/plotting.py +228 -0
  33. batplot_backup_20251221_101150/readers.py +2607 -0
  34. batplot_backup_20251221_101150/session.py +2951 -0
  35. batplot_backup_20251221_101150/style.py +1441 -0
  36. batplot_backup_20251221_101150/ui.py +790 -0
  37. batplot_backup_20251221_101150/utils.py +1046 -0
  38. batplot_backup_20251221_101150/version_check.py +253 -0
  39. batplot-1.8.0.dist-info/RECORD +0 -52
  40. {batplot-1.8.0.dist-info → batplot-1.8.2.dist-info}/WHEEL +0 -0
  41. {batplot-1.8.0.dist-info → batplot-1.8.2.dist-info}/entry_points.txt +0 -0
  42. {batplot-1.8.0.dist-info → batplot-1.8.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,198 @@
1
+ """Configuration management for batplot.
2
+
3
+ This module handles persistent user preferences that are saved between sessions.
4
+ Currently, it manages user-defined color lists, but can be extended for other
5
+ preferences like default styles, font settings, etc.
6
+
7
+ HOW CONFIGURATION WORKS:
8
+ -----------------------
9
+ User preferences are stored in a JSON file at ~/.batplot/config.json.
10
+ This file persists between batplot sessions, so your custom colors are
11
+ remembered the next time you run batplot.
12
+
13
+ Example config.json structure:
14
+ {
15
+ "user_colors": [
16
+ "#FF0000",
17
+ "#00FF00",
18
+ "#0000FF",
19
+ "red",
20
+ "blue"
21
+ ]
22
+ }
23
+
24
+ The config file is created automatically the first time you save a preference.
25
+ If the file doesn't exist or is corrupted, we return empty defaults (graceful degradation).
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import os
31
+ import json
32
+ from pathlib import Path
33
+ from typing import List, Optional, Dict, Any
34
+
35
+
36
+ def get_config_dir() -> Path:
37
+ """
38
+ Get batplot configuration directory, creating it if needed.
39
+
40
+ HOW IT WORKS:
41
+ ------------
42
+ Returns the path to ~/.batplot directory (user's home directory + .batplot).
43
+ If the directory doesn't exist, it's created automatically.
44
+
45
+ WHY ~/.batplot?
46
+ --------------
47
+ The tilde (~) represents the user's home directory. This is a standard
48
+ location for application configuration files on Unix-like systems (Linux, macOS).
49
+ It keeps user data separate from system files and other users' data.
50
+
51
+ Examples:
52
+ Linux/macOS: /home/username/.batplot or /Users/username/.batplot
53
+ Windows: C:\\Users\\username\\.batplot
54
+
55
+ Returns:
56
+ Path object pointing to ~/.batplot directory
57
+ """
58
+ config_dir = Path.home() / '.batplot'
59
+ # mkdir(exist_ok=True) creates directory if it doesn't exist,
60
+ # but doesn't raise error if it already exists
61
+ config_dir.mkdir(exist_ok=True)
62
+ return config_dir
63
+
64
+
65
+ def get_config_file() -> Path:
66
+ """
67
+ Get path to main configuration file.
68
+
69
+ Returns:
70
+ Path object pointing to ~/.batplot/config.json
71
+ """
72
+ return get_config_dir() / 'config.json'
73
+
74
+
75
+ def load_config() -> Dict[str, Any]:
76
+ """
77
+ Load configuration from JSON file.
78
+
79
+ HOW IT WORKS:
80
+ ------------
81
+ 1. Check if config file exists
82
+ 2. If not, return empty dictionary (defaults)
83
+ 3. If exists, read JSON and parse it
84
+ 4. If file is corrupted or unreadable, return empty dictionary (graceful failure)
85
+
86
+ WHY GRACEFUL FAILURE?
87
+ --------------------
88
+ If the config file is corrupted (invalid JSON) or can't be read (permissions),
89
+ we don't want to crash the program. Instead, we return empty defaults and
90
+ let the user continue. The next time they save a preference, a new valid
91
+ file will be created.
92
+
93
+ Returns:
94
+ Dictionary with configuration values, or empty dict if:
95
+ - File doesn't exist (first run)
96
+ - File is corrupted (invalid JSON)
97
+ - File can't be read (permissions error)
98
+ """
99
+ config_file = get_config_file()
100
+
101
+ # If file doesn't exist, return empty defaults (first run)
102
+ if not config_file.exists():
103
+ return {}
104
+
105
+ # Try to read and parse JSON file
106
+ try:
107
+ with open(config_file, 'r') as f:
108
+ return json.load(f) # Parse JSON string into Python dictionary
109
+ except (json.JSONDecodeError, IOError):
110
+ # File exists but is corrupted or unreadable
111
+ # Return empty defaults instead of crashing
112
+ return {}
113
+
114
+
115
+ def save_config(config: Dict[str, Any]) -> None:
116
+ """
117
+ Save configuration dictionary to JSON file.
118
+
119
+ HOW IT WORKS:
120
+ ------------
121
+ 1. Get path to config file (~/.batplot/config.json)
122
+ 2. Write dictionary as formatted JSON (indented for readability)
123
+ 3. If write fails (permissions, disk full, etc.), silently ignore
124
+
125
+ WHY SILENT FAILURE?
126
+ ------------------
127
+ Configuration saving is not critical for program operation. If it fails,
128
+ the program should continue working (just won't remember preferences).
129
+ We don't want to interrupt the user's workflow with error messages about
130
+ config file issues.
131
+
132
+ Args:
133
+ config: Dictionary with configuration values to save.
134
+ Example: {'user_colors': ['#FF0000', '#00FF00']}
135
+ """
136
+ config_file = get_config_file()
137
+ try:
138
+ with open(config_file, 'w') as f:
139
+ # indent=2 makes JSON file human-readable (pretty-printed)
140
+ json.dump(config, f, indent=2)
141
+ except IOError:
142
+ # Silently ignore write errors (permissions, disk full, etc.)
143
+ pass
144
+
145
+
146
+ def get_user_colors() -> List[str]:
147
+ """
148
+ Get user-defined color list from configuration file.
149
+
150
+ HOW IT WORKS:
151
+ ------------
152
+ 1. Load entire config file
153
+ 2. Extract 'user_colors' key (list of color codes)
154
+ 3. Return list, or empty list if not found
155
+
156
+ WHAT ARE USER COLORS?
157
+ --------------------
158
+ Users can save custom color codes (hex like '#FF0000' or named like 'red')
159
+ for quick access in interactive menus. These colors are stored persistently
160
+ and can be referenced by index (e.g., 'u1' for first user color).
161
+
162
+ Returns:
163
+ List of color codes (hex strings like '#FF0000' or named colors like 'red').
164
+ Empty list if no user colors have been saved yet.
165
+ """
166
+ config = load_config()
167
+ # .get('user_colors', []) returns the list if key exists, or [] if not
168
+ return config.get('user_colors', [])
169
+
170
+
171
+ def save_user_colors(colors: List[str]) -> None:
172
+ """
173
+ Save user-defined color list to configuration file.
174
+
175
+ HOW IT WORKS:
176
+ ------------
177
+ 1. Load existing config (to preserve other settings)
178
+ 2. Update 'user_colors' key with new list
179
+ 3. Save entire config back to file
180
+
181
+ WHY UPDATE ENTIRE CONFIG?
182
+ ------------------------
183
+ The config file might contain other settings in the future (font preferences,
184
+ default styles, etc.). We want to preserve those when updating colors.
185
+
186
+ Args:
187
+ colors: List of color codes to save.
188
+ Example: ['#FF0000', '#00FF00', 'red', 'blue']
189
+ """
190
+ config = load_config() # Load existing config (preserves other settings)
191
+ config['user_colors'] = colors # Update user_colors key
192
+ save_config(config) # Save entire config back to file
193
+
194
+
195
+ __all__ = [
196
+ 'get_user_colors',
197
+ 'save_user_colors',
198
+ ]
@@ -0,0 +1,204 @@
1
+ """Data conversion utilities for batplot.
2
+
3
+ This module provides functions to convert X-ray diffraction data between
4
+ different representations, primarily from angle-based (2θ) to momentum
5
+ transfer (Q) space.
6
+
7
+ WHY CONVERT BETWEEN 2θ AND Q?
8
+ -----------------------------
9
+ X-ray diffraction data can be represented in two ways:
10
+
11
+ 1. **2θ space (angle-based)**: Traditional representation in degrees
12
+ - Pros: Directly matches experimental setup (detector angle)
13
+ - Cons: Depends on X-ray wavelength (different wavelengths = different scales)
14
+ - Example: Peak at 2θ = 20° for Cu Kα (λ=1.5406 Å)
15
+
16
+ 2. **Q space (momentum transfer)**: Physical quantity independent of wavelength
17
+ - Pros: Universal scale (same Q value regardless of wavelength)
18
+ - Cons: Requires wavelength to convert
19
+ - Example: Peak at Q = 2.0 Å⁻¹ (same for any wavelength)
20
+
21
+ Converting to Q-space is useful for:
22
+ - Comparing data from different X-ray sources (synchrotron vs lab)
23
+ - Pair Distribution Function (PDF) analysis (requires Q-space)
24
+ - Direct comparison with theoretical calculations (often in Q-space)
25
+ - Combining datasets from different experiments
26
+
27
+ CONVERSION FORMULA:
28
+ ------------------
29
+ The conversion uses Bragg's law:
30
+ Q = (4π sin(θ)) / λ
31
+
32
+ Where:
33
+ - Q: Momentum transfer in Å⁻¹
34
+ - θ: Half of the diffraction angle (2θ/2) in radians
35
+ - λ: X-ray wavelength in Angstroms
36
+
37
+ Physical meaning:
38
+ - Q represents the momentum transferred from X-ray to sample
39
+ - Higher Q = smaller d-spacing (shorter distances in crystal)
40
+ - Q is proportional to sin(θ), so it increases non-linearly with angle
41
+ """
42
+
43
+ from __future__ import annotations
44
+
45
+ import os
46
+ import numpy as np
47
+
48
+
49
+ def convert_to_qye(filenames, wavelength: float):
50
+ """
51
+ Convert 2θ-based XRD files to Q-based .qye files.
52
+
53
+ HOW IT WORKS:
54
+ ------------
55
+ This function reads XRD data files that have 2θ (two-theta) angles as the
56
+ x-axis and converts them to Q-space (momentum transfer). The conversion
57
+ process:
58
+
59
+ 1. Read input file (2θ in degrees, intensity, optional error bars)
60
+ 2. Convert 2θ to θ (half-angle): θ = 2θ / 2
61
+ 3. Convert θ from degrees to radians: θ_rad = θ × π/180
62
+ 4. Apply conversion formula: Q = 4π sin(θ_rad) / λ
63
+ 5. Save output as .qye file (Q, intensity, [error])
64
+
65
+ INPUT FORMAT:
66
+ ------------
67
+ Expected columns in input file:
68
+ - Column 1: 2θ values in degrees (e.g., 10.0, 10.1, 10.2, ...)
69
+ - Column 2: Intensity values (e.g., 100, 150, 200, ...)
70
+ - Column 3 (optional): Error bars (e.g., 5, 7, 10, ...)
71
+
72
+ OUTPUT FORMAT:
73
+ -------------
74
+ Saved .qye file contains:
75
+ - Column 1: Q values in Å⁻¹ (momentum transfer)
76
+ - Column 2: Intensity values (unchanged)
77
+ - Column 3 (if present): Error bars (unchanged)
78
+ - Header comment: Documents conversion parameters
79
+
80
+ WAVELENGTH VALUES:
81
+ -----------------
82
+ Common X-ray wavelengths:
83
+ - Cu Kα: 1.5406 Å (most common lab source)
84
+ - Mo Kα: 0.7093 Å (higher energy, shorter wavelength)
85
+ - Synchrotron: 0.6199 Å (or other, depends on beamline)
86
+ - Co Kα: 1.7889 Å
87
+ - Fe Kα: 1.9360 Å
88
+
89
+ Args:
90
+ filenames: List of file paths to convert (e.g., ['data.xy', 'pattern.xye'])
91
+ wavelength: X-ray wavelength in Angstroms (Å)
92
+ Examples:
93
+ - 1.5406 for Cu Kα (most common)
94
+ - 0.7093 for Mo Kα
95
+ - 0.6199 for synchrotron
96
+
97
+ Output:
98
+ Creates .qye files alongside input files with same basename.
99
+ Example: data.xy → data.qye
100
+
101
+ Example:
102
+ >>> # Convert Cu Kα data to Q-space
103
+ >>> convert_to_qye(['pattern.xy'], wavelength=1.5406)
104
+ Saved pattern.qye
105
+
106
+ >>> # Convert multiple files from synchrotron
107
+ >>> convert_to_qye(['scan1.xy', 'scan2.xy'], wavelength=0.6199)
108
+ Saved scan1.qye
109
+ Saved scan2.qye
110
+ """
111
+ # ====================================================================
112
+ # PROCESS EACH FILE
113
+ # ====================================================================
114
+ # Loop through each input file and convert it to Q-space.
115
+ # Each file is processed independently (errors in one don't stop others).
116
+ # ====================================================================
117
+ for fname in filenames:
118
+ # STEP 1: Validate file exists
119
+ if not os.path.isfile(fname):
120
+ print(f"File not found: {fname}")
121
+ continue # Skip this file, continue with next
122
+
123
+ # STEP 2: Read data from file
124
+ # np.loadtxt() reads numeric data, skipping lines starting with '#'
125
+ # This handles comment lines in data files
126
+ try:
127
+ data = np.loadtxt(fname, comments="#")
128
+ except Exception as e:
129
+ print(f"Error reading {fname}: {e}")
130
+ continue # Skip if file can't be read
131
+
132
+ # STEP 3: Ensure data is 2D array (handle edge cases)
133
+ # Some files might have only one row, which numpy reads as 1D array
134
+ # We reshape to 2D so indexing works consistently
135
+ if data.ndim == 1:
136
+ data = data.reshape(1, -1) # Reshape to (1, n_columns)
137
+
138
+ # STEP 4: Validate data format
139
+ # Need at least 2 columns: x (2θ) and y (intensity)
140
+ if data.shape[1] < 2:
141
+ print(f"Invalid data format in {fname}: need at least 2 columns (x, y)")
142
+ continue
143
+
144
+ # STEP 5: Extract columns
145
+ x = data[:, 0] # 2θ values in degrees (first column)
146
+ y = data[:, 1] # Intensity values (second column)
147
+ e = data[:, 2] if data.shape[1] >= 3 else None # Error bars (third column, optional)
148
+
149
+ # ====================================================================
150
+ # STEP 6: CONVERT 2θ TO Q
151
+ # ====================================================================
152
+ # This is the core conversion using Bragg's law.
153
+ #
154
+ # Mathematical steps:
155
+ # 1. Get θ (half-angle): θ = 2θ / 2
156
+ # Example: 2θ = 20° → θ = 10°
157
+ #
158
+ # 2. Convert to radians: θ_rad = θ × (π/180)
159
+ # Example: θ = 10° → θ_rad = 0.1745 radians
160
+ # (NumPy's np.radians() does this conversion)
161
+ #
162
+ # 3. Apply conversion formula: Q = 4π sin(θ_rad) / λ
163
+ # Example: θ_rad = 0.1745, λ = 1.5406 Å
164
+ # Q = 4π sin(0.1745) / 1.5406 = 1.42 Å⁻¹
165
+ #
166
+ # Why sin(θ)?
167
+ # The momentum transfer Q is proportional to sin(θ), not θ itself.
168
+ # This is because diffraction follows a sine relationship (Bragg's law).
169
+ # ====================================================================
170
+
171
+ # Get θ (half of 2θ) and convert to radians
172
+ # x contains 2θ values in degrees, so x/2 gives θ in degrees
173
+ theta_rad = np.radians(x / 2) # Convert degrees to radians
174
+
175
+ # Apply conversion formula: Q = 4π sin(θ) / λ
176
+ # This gives Q in units of Å⁻¹ (inverse Angstroms)
177
+ q = 4 * np.pi * np.sin(theta_rad) / wavelength
178
+
179
+ # STEP 7: Prepare output data
180
+ # Combine Q, intensity, and optional error bars into output array
181
+ if e is None:
182
+ # No error bars: output has 2 columns (Q, intensity)
183
+ out_data = np.column_stack((q, y))
184
+ else:
185
+ # With error bars: output has 3 columns (Q, intensity, error)
186
+ out_data = np.column_stack((q, y, e))
187
+
188
+ # STEP 8: Generate output filename
189
+ # Replace input extension with .qye
190
+ # Example: data.xy → data.qye, pattern.xye → pattern.qye
191
+ base, _ = os.path.splitext(fname)
192
+ out_fname = f"{base}.qye"
193
+
194
+ # STEP 9: Save converted data
195
+ # Save with:
196
+ # - Header comment documenting the conversion (wavelength used)
197
+ # - 6 decimal places precision (sufficient for most XRD data)
198
+ # - Space-padded format for readability
199
+ np.savetxt(out_fname, out_data, fmt="% .6f",
200
+ header=f"# Converted from {fname} using λ={wavelength} Å")
201
+ print(f"Saved {out_fname}")
202
+
203
+
204
+ __all__ = ["convert_to_qye"]