batplot 1.8.4__py3-none-any.whl → 1.8.6__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.
batplot/converters.py CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  This module provides functions to convert X-ray diffraction data between
4
4
  different representations, primarily from angle-based (2θ) to momentum
5
- transfer (Q) space.
5
+ transfer (Q) space, and between different wavelengths.
6
6
 
7
7
  WHY CONVERT BETWEEN 2θ AND Q?
8
8
  -----------------------------
@@ -24,8 +24,8 @@ Converting to Q-space is useful for:
24
24
  - Direct comparison with theoretical calculations (often in Q-space)
25
25
  - Combining datasets from different experiments
26
26
 
27
- CONVERSION FORMULA:
28
- ------------------
27
+ CONVERSION FORMULAS:
28
+ -------------------
29
29
  The conversion uses Bragg's law:
30
30
  Q = (4π sin(θ)) / λ
31
31
 
@@ -34,6 +34,9 @@ Where:
34
34
  - θ: Half of the diffraction angle (2θ/2) in radians
35
35
  - λ: X-ray wavelength in Angstroms
36
36
 
37
+ To convert from Q back to 2θ:
38
+ 2θ = 2 × arcsin(Q × λ / (4π)) × (180/π)
39
+
37
40
  Physical meaning:
38
41
  - Q represents the momentum transferred from X-ray to sample
39
42
  - Higher Q = smaller d-spacing (shorter distances in crystal)
@@ -46,159 +49,204 @@ import os
46
49
  import numpy as np
47
50
 
48
51
 
49
- def convert_to_qye(filenames, wavelength: float):
52
+ def convert_xrd_data(filenames, from_param: str, to_param: str):
50
53
  """
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:
54
+ Convert XRD data files between different representations.
58
55
 
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 = sin(θ_rad) / λ
63
- 5. Save output as .qye file (Q, intensity, [error])
56
+ This function handles three conversion modes:
57
+ 1. Wavelength-to-wavelength: Convert 2θ values from one wavelength to another
58
+ 2. Wavelength-to-Q: Convert 2θ (with given wavelength) to Q space
59
+ 3. Q-to-wavelength: Convert Q space to 2θ (with given wavelength)
64
60
 
65
- INPUT FORMAT:
61
+ HOW IT WORKS:
66
62
  ------------
67
- Expected columns in input file:
68
- - Column 1: 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
63
+ The function reads input files, determines the conversion type based on
64
+ the from/to parameters, applies the appropriate conversion formula, and
65
+ saves the converted data to a new 'converted' subfolder.
79
66
 
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 Å
67
+ CONVERSION MODES:
68
+ ----------------
69
+ 1. Wavelength-to-wavelength (e.g., --convert 1.54 0.25):
70
+ - Input: values measured with wavelength1
71
+ - Process: Convert Q using wavelength1, then Q → 2θ using wavelength2
72
+ - Output: values for wavelength2
73
+
74
+ 2. Wavelength-to-Q (e.g., --convert 1.54 q):
75
+ - Input: 2θ values measured with given wavelength
76
+ - Process: Convert 2θ → Q using the wavelength
77
+ - Output: Q values (Å⁻¹)
78
+
79
+ 3. Q-to-wavelength (e.g., --convert q 1.54):
80
+ - Input: Q values (Å⁻¹)
81
+ - Process: Convert Q → 2θ using the given wavelength
82
+ - Output: 2θ values for the given wavelength
88
83
 
89
84
  Args:
90
85
  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
86
+ from_param: Source parameter - either a wavelength (float as string) or 'q'
87
+ to_param: Target parameter - either a wavelength (float as string) or 'q'
96
88
 
97
89
  Output:
98
- Creates .qye files alongside input files with same basename.
99
- Example: data.xy data.qye
90
+ Creates converted files in a 'converted' subfolder within the directory
91
+ containing the input files. Files keep their original names but may have
92
+ different extensions (.qye for Q-space, original extension for 2θ).
100
93
 
101
94
  Example:
102
- >>> # Convert Cu Kα data to Q-space
103
- >>> convert_to_qye(['pattern.xy'], wavelength=1.5406)
104
- Saved pattern.qye
95
+ >>> # Convert 2θ from Cu Kα (1.54 Å) to Mo Kα (0.709 Å)
96
+ >>> convert_xrd_data(['pattern.xy'], '1.54', '0.709')
97
+ Saved converted/pattern.xy
98
+
99
+ >>> # Convert 2θ to Q space
100
+ >>> convert_xrd_data(['pattern.xy'], '1.54', 'q')
101
+ Saved converted/pattern.qye
105
102
 
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
103
+ >>> # Convert Q to
104
+ >>> convert_xrd_data(['pattern.qye'], 'q', '1.54')
105
+ Saved converted/pattern.xy
110
106
  """
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
- # ====================================================================
107
+ # Parse parameters
108
+ try:
109
+ from_is_q = (from_param.lower() == 'q')
110
+ to_is_q = (to_param.lower() == 'q')
111
+
112
+ if from_is_q:
113
+ from_wl = None
114
+ else:
115
+ from_wl = float(from_param)
116
+
117
+ if to_is_q:
118
+ to_wl = None
119
+ else:
120
+ to_wl = float(to_param)
121
+ except ValueError:
122
+ print(f"Error: Invalid conversion parameters. Expected wavelengths (numbers) or 'q', got '{from_param}' and '{to_param}'")
123
+ return
124
+
125
+ # Determine conversion type
126
+ if not from_is_q and not to_is_q:
127
+ # Wavelength-to-wavelength conversion
128
+ conversion_type = "wavelength_to_wavelength"
129
+ elif not from_is_q and to_is_q:
130
+ # Wavelength-to-Q conversion
131
+ conversion_type = "wavelength_to_q"
132
+ elif from_is_q and not to_is_q:
133
+ # Q-to-wavelength conversion
134
+ conversion_type = "q_to_wavelength"
135
+ else:
136
+ print("Error: Cannot convert Q to Q (no conversion needed)")
137
+ return
138
+
139
+ # Process each file
117
140
  for fname in filenames:
118
- # STEP 1: Validate file exists
141
+ # Validate file exists
119
142
  if not os.path.isfile(fname):
120
143
  print(f"File not found: {fname}")
121
- continue # Skip this file, continue with next
144
+ continue
122
145
 
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
146
+ # Read data from file
126
147
  try:
127
148
  data = np.loadtxt(fname, comments="#")
128
149
  except Exception as e:
129
150
  print(f"Error reading {fname}: {e}")
130
- continue # Skip if file can't be read
151
+ continue
131
152
 
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
153
+ # Ensure data is 2D array
135
154
  if data.ndim == 1:
136
- data = data.reshape(1, -1) # Reshape to (1, n_columns)
155
+ data = data.reshape(1, -1)
137
156
 
138
- # STEP 4: Validate data format
139
- # Need at least 2 columns: x (2θ) and y (intensity)
157
+ # Validate data format
140
158
  if data.shape[1] < 2:
141
159
  print(f"Invalid data format in {fname}: need at least 2 columns (x, y)")
142
160
  continue
143
161
 
144
- # STEP 5: Extract columns
145
- x = data[:, 0] # 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
- # ====================================================================
162
+ # Extract columns
163
+ x = data[:, 0] # X values (2θ or Q)
164
+ y = data[:, 1] # Intensity values
165
+ e = data[:, 2] if data.shape[1] >= 3 else None # Error bars (optional)
170
166
 
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
167
+ # Perform conversion based on type
168
+ if conversion_type == "wavelength_to_wavelength":
169
+ # Step 1: Convert 2θ (from_wl) Q
170
+ theta_rad = np.radians(x / 2) # Convert 2θ to θ in radians
171
+ q = 4 * np.pi * np.sin(theta_rad) / from_wl
172
+
173
+ # Step 2: Convert Q → 2θ (to_wl)
174
+ # Q = 4π sin(θ) / λ, so sin(θ) = Q × λ / (4π)
175
+ # θ = arcsin(Q × λ / (4π))
176
+ # 2θ = 2 × θ × (180/π)
177
+ sin_theta = q * to_wl / (4 * np.pi)
178
+ # Clamp sin_theta to valid range [-1, 1] to avoid domain errors
179
+ sin_theta = np.clip(sin_theta, -1.0, 1.0)
180
+ theta_rad_new = np.arcsin(sin_theta)
181
+ x_new = 2 * np.degrees(theta_rad_new) # Convert to 2θ in degrees
182
+ output_ext = os.path.splitext(fname)[1] # Keep original extension
183
+
184
+ elif conversion_type == "wavelength_to_q":
185
+ # Convert 2θ → Q
186
+ theta_rad = np.radians(x / 2) # Convert 2θ to θ in radians
187
+ x_new = 4 * np.pi * np.sin(theta_rad) / from_wl
188
+ output_ext = ".qye"
189
+
190
+ elif conversion_type == "q_to_wavelength":
191
+ # Convert Q → 2θ
192
+ # Q = 4π sin(θ) / λ, so sin(θ) = Q × λ / (4π)
193
+ sin_theta = x * to_wl / (4 * np.pi)
194
+ # Clamp sin_theta to valid range [-1, 1] to avoid domain errors
195
+ sin_theta = np.clip(sin_theta, -1.0, 1.0)
196
+ theta_rad = np.arcsin(sin_theta)
197
+ x_new = 2 * np.degrees(theta_rad) # Convert to 2θ in degrees
198
+ output_ext = ".xy"
174
199
 
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
200
+ # Prepare output data
181
201
  if e is None:
182
- # No error bars: output has 2 columns (Q, intensity)
183
- out_data = np.column_stack((q, y))
202
+ out_data = np.column_stack((x_new, y))
184
203
  else:
185
- # With error bars: output has 3 columns (Q, intensity, error)
186
- out_data = np.column_stack((q, y, e))
204
+ out_data = np.column_stack((x_new, y, e))
205
+
206
+ # Create output directory (converted subfolder)
207
+ input_dir = os.path.dirname(os.path.abspath(fname))
208
+ if not input_dir:
209
+ input_dir = os.getcwd()
210
+ output_dir = os.path.join(input_dir, "converted")
211
+ os.makedirs(output_dir, exist_ok=True)
212
+
213
+ # Generate output filename
214
+ base = os.path.basename(os.path.splitext(fname)[0])
215
+ output_fname = os.path.join(output_dir, f"{base}{output_ext}")
187
216
 
188
- # STEP 8: Generate output filename
189
- # Replace input extension with .qye
190
- # Example: data.xy data.qye, pattern.xyepattern.qye
191
- base, _ = os.path.splitext(fname)
192
- out_fname = f"{base}.qye"
217
+ # Create header comment
218
+ if conversion_type == "wavelength_to_wavelength":
219
+ header = f"# Converted from {os.path.basename(fname)}: (λ={from_wl} Å)Q → 2θ (λ={to_wl} Å)"
220
+ elif conversion_type == "wavelength_to_q":
221
+ header = f"# Converted from {os.path.basename(fname)}: 2θ (λ={from_wl} Å) → Q"
222
+ else: # q_to_wavelength
223
+ header = f"# Converted from {os.path.basename(fname)}: Q → 2θ (λ={to_wl} Å)"
193
224
 
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}")
225
+ # Save converted data
226
+ try:
227
+ np.savetxt(output_fname, out_data, fmt="% .6f", header=header)
228
+ print(f"Saved {output_fname}")
229
+ except Exception as e:
230
+ print(f"Error saving {output_fname}: {e}")
231
+
232
+
233
+ def convert_to_qye(filenames, wavelength: float):
234
+ """
235
+ Convert 2θ-based XRD files to Q-based .qye files.
236
+
237
+ This is a legacy function maintained for backward compatibility.
238
+ For new code, use convert_xrd_data() instead.
239
+
240
+ Args:
241
+ filenames: List of file paths to convert (e.g., ['data.xy', 'pattern.xye'])
242
+ wavelength: X-ray wavelength in Angstroms (Å)
243
+
244
+ Output:
245
+ Creates .qye files alongside input files with same basename.
246
+ Example: data.xy → data.qye
247
+ """
248
+ # Convert to new format: wavelength to Q
249
+ convert_xrd_data(filenames, str(wavelength), 'q')
202
250
 
203
251
 
204
- __all__ = ["convert_to_qye"]
252
+ __all__ = ["convert_xrd_data", "convert_to_qye"]