batplot 1.8.1__py3-none-any.whl → 1.8.3__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 +2 -0
- batplot/batch.py +23 -0
- batplot/batplot.py +101 -12
- batplot/cpc_interactive.py +25 -3
- batplot/electrochem_interactive.py +20 -4
- batplot/interactive.py +19 -15
- batplot/modes.py +12 -12
- batplot/operando_ec_interactive.py +4 -4
- batplot/session.py +218 -0
- batplot/style.py +21 -2
- batplot/version_check.py +1 -1
- {batplot-1.8.1.dist-info → batplot-1.8.3.dist-info}/METADATA +1 -1
- batplot-1.8.3.dist-info/RECORD +75 -0
- {batplot-1.8.1.dist-info → batplot-1.8.3.dist-info}/top_level.txt +1 -0
- batplot_backup_20251221_101150/__init__.py +5 -0
- batplot_backup_20251221_101150/args.py +625 -0
- batplot_backup_20251221_101150/batch.py +1176 -0
- batplot_backup_20251221_101150/batplot.py +3589 -0
- batplot_backup_20251221_101150/cif.py +823 -0
- batplot_backup_20251221_101150/cli.py +149 -0
- batplot_backup_20251221_101150/color_utils.py +547 -0
- batplot_backup_20251221_101150/config.py +198 -0
- batplot_backup_20251221_101150/converters.py +204 -0
- batplot_backup_20251221_101150/cpc_interactive.py +4409 -0
- batplot_backup_20251221_101150/electrochem_interactive.py +4520 -0
- batplot_backup_20251221_101150/interactive.py +3894 -0
- batplot_backup_20251221_101150/manual.py +323 -0
- batplot_backup_20251221_101150/modes.py +799 -0
- batplot_backup_20251221_101150/operando.py +603 -0
- batplot_backup_20251221_101150/operando_ec_interactive.py +5487 -0
- batplot_backup_20251221_101150/plotting.py +228 -0
- batplot_backup_20251221_101150/readers.py +2607 -0
- batplot_backup_20251221_101150/session.py +2951 -0
- batplot_backup_20251221_101150/style.py +1441 -0
- batplot_backup_20251221_101150/ui.py +790 -0
- batplot_backup_20251221_101150/utils.py +1046 -0
- batplot_backup_20251221_101150/version_check.py +253 -0
- batplot-1.8.1.dist-info/RECORD +0 -52
- {batplot-1.8.1.dist-info → batplot-1.8.3.dist-info}/WHEEL +0 -0
- {batplot-1.8.1.dist-info → batplot-1.8.3.dist-info}/entry_points.txt +0 -0
- {batplot-1.8.1.dist-info → batplot-1.8.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,823 @@
|
|
|
1
|
+
"""CIF parsing and simple powder pattern simulation utilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _atomic_number_table():
|
|
10
|
+
"""
|
|
11
|
+
Create a lookup table mapping element symbols to atomic numbers.
|
|
12
|
+
|
|
13
|
+
HOW IT WORKS:
|
|
14
|
+
------------
|
|
15
|
+
This function creates a dictionary where:
|
|
16
|
+
- Key: Element symbol (e.g., 'H', 'He', 'Li')
|
|
17
|
+
- Value: Atomic number (e.g., 1, 2, 3)
|
|
18
|
+
|
|
19
|
+
Example:
|
|
20
|
+
{'H': 1, 'He': 2, 'Li': 3, 'Be': 4, ...}
|
|
21
|
+
|
|
22
|
+
WHY NEEDED?
|
|
23
|
+
----------
|
|
24
|
+
CIF files store atoms by element symbol (e.g., 'Li', 'Fe', 'O').
|
|
25
|
+
Some calculations need atomic numbers instead (e.g., for scattering factors).
|
|
26
|
+
This table lets us convert symbols to numbers quickly.
|
|
27
|
+
|
|
28
|
+
DICTIONARY COMPREHENSION:
|
|
29
|
+
------------------------
|
|
30
|
+
The return statement uses a dictionary comprehension:
|
|
31
|
+
{el: i + 1 for i, el in enumerate(elements)}
|
|
32
|
+
|
|
33
|
+
This is equivalent to:
|
|
34
|
+
result = {}
|
|
35
|
+
for i, el in enumerate(elements):
|
|
36
|
+
result[el] = i + 1
|
|
37
|
+
return result
|
|
38
|
+
|
|
39
|
+
enumerate() gives us both index (i) and element (el):
|
|
40
|
+
- i = 0, el = 'H' → {'H': 1}
|
|
41
|
+
- i = 1, el = 'He' → {'He': 2}
|
|
42
|
+
- etc.
|
|
43
|
+
|
|
44
|
+
We use i + 1 because atomic numbers start at 1 (not 0).
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Dictionary mapping element symbols to atomic numbers
|
|
48
|
+
"""
|
|
49
|
+
# Periodic table elements in order (by atomic number)
|
|
50
|
+
elements = [
|
|
51
|
+
'H', 'He', 'Li', 'Be', 'B', 'C', 'N', 'O', 'F', 'Ne',
|
|
52
|
+
'Na', 'Mg', 'Al', 'Si', 'P', 'S', 'Cl', 'Ar', 'K', 'Ca',
|
|
53
|
+
'Sc', 'Ti', 'V', 'Cr', 'Mn', 'Fe', 'Co', 'Ni', 'Cu', 'Zn',
|
|
54
|
+
'Ga', 'Ge', 'As', 'Se', 'Br', 'Kr', 'Rb', 'Sr', 'Y', 'Zr',
|
|
55
|
+
'Nb', 'Mo', 'Tc', 'Ru', 'Rh', 'Pd', 'Ag', 'Cd', 'In', 'Sn',
|
|
56
|
+
'Sb', 'Te', 'I', 'Xe', 'Cs', 'Ba', 'La', 'Ce', 'Pr', 'Nd',
|
|
57
|
+
'Pm', 'Sm', 'Eu', 'Gd', 'Tb', 'Dy', 'Ho', 'Er', 'Tm', 'Yb',
|
|
58
|
+
'Lu', 'Hf', 'Ta', 'W', 'Re', 'Os', 'Ir', 'Pt', 'Au', 'Hg',
|
|
59
|
+
'Tl', 'Pb', 'Bi', 'Po', 'At', 'Rn'
|
|
60
|
+
]
|
|
61
|
+
# Dictionary comprehension: create dict from list with index as value
|
|
62
|
+
# enumerate() gives (index, element) pairs: (0, 'H'), (1, 'He'), ...
|
|
63
|
+
# i + 1 converts 0-based index to 1-based atomic number
|
|
64
|
+
return {el: i + 1 for i, el in enumerate(elements)}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _parse_cif_basic(fname):
|
|
68
|
+
"""
|
|
69
|
+
Parse a CIF (Crystallographic Information File) to extract crystal structure data.
|
|
70
|
+
|
|
71
|
+
WHAT IS A CIF FILE?
|
|
72
|
+
-------------------
|
|
73
|
+
CIF (Crystallographic Information File) is a standard format for storing
|
|
74
|
+
crystal structure information. It contains:
|
|
75
|
+
- Unit cell parameters (a, b, c, α, β, γ)
|
|
76
|
+
- Space group information
|
|
77
|
+
- Atom positions (fractional coordinates)
|
|
78
|
+
- Symmetry operations
|
|
79
|
+
|
|
80
|
+
HOW PARSING WORKS:
|
|
81
|
+
-----------------
|
|
82
|
+
CIF files are text-based with a specific format:
|
|
83
|
+
- Lines starting with '_' are data names (keys)
|
|
84
|
+
- Lines starting with 'loop_' begin a data loop (table)
|
|
85
|
+
- Values follow on the same line or next lines
|
|
86
|
+
- Comments start with '#'
|
|
87
|
+
|
|
88
|
+
Example CIF structure:
|
|
89
|
+
_cell_length_a 5.0
|
|
90
|
+
_cell_length_b 5.0
|
|
91
|
+
_cell_length_c 5.0
|
|
92
|
+
loop_
|
|
93
|
+
_atom_site_fract_x
|
|
94
|
+
_atom_site_fract_y
|
|
95
|
+
_atom_site_fract_z
|
|
96
|
+
_atom_site_type_symbol
|
|
97
|
+
0.0 0.0 0.0 Li
|
|
98
|
+
0.5 0.5 0.5 O
|
|
99
|
+
|
|
100
|
+
This function reads through the file line by line, identifying and
|
|
101
|
+
extracting the relevant information.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
fname: Path to CIF file
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Dictionary with keys:
|
|
108
|
+
- 'cell': Dictionary with unit cell parameters (a, b, c, alpha, beta, gamma, space_group)
|
|
109
|
+
- 'atoms': List of atom dictionaries (each with x, y, z, symbol, etc.)
|
|
110
|
+
- 'sym_ops': List of symmetry operation strings
|
|
111
|
+
"""
|
|
112
|
+
# Initialize data structures to store parsed information
|
|
113
|
+
# cell: Unit cell parameters (lengths and angles)
|
|
114
|
+
cell = {
|
|
115
|
+
'a': None, 'b': None, 'c': None, # Unit cell lengths (in Angstroms)
|
|
116
|
+
'alpha': None, 'beta': None, 'gamma': None, # Unit cell angles (in degrees)
|
|
117
|
+
'space_group': None # Space group symbol (e.g., "Fm-3m")
|
|
118
|
+
}
|
|
119
|
+
atoms = [] # List to store atom information (positions, types, etc.)
|
|
120
|
+
sym_ops = [] # List to store symmetry operations (for generating equivalent positions)
|
|
121
|
+
atom_headers = [] # List of column headers in atom loop (e.g., "_atom_site_fract_x")
|
|
122
|
+
in_atom_loop = False # Flag: are we currently reading atom data rows?
|
|
123
|
+
|
|
124
|
+
def _clean_num(tok: str):
|
|
125
|
+
"""
|
|
126
|
+
Clean a numeric token by removing quotes and uncertainty values.
|
|
127
|
+
|
|
128
|
+
CIF files sometimes have numbers like:
|
|
129
|
+
- "5.0" (with quotes)
|
|
130
|
+
- 5.0(2) (with uncertainty in parentheses)
|
|
131
|
+
|
|
132
|
+
This function removes quotes and uncertainty to get just the number.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
tok: String token that should contain a number
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Cleaned string (ready to convert to float)
|
|
139
|
+
"""
|
|
140
|
+
# Remove whitespace and quotes (single or double)
|
|
141
|
+
t = tok.strip().strip("'\"")
|
|
142
|
+
# Remove uncertainty notation: "5.0(2)" → "5.0"
|
|
143
|
+
# r"\([0-9]+\)$" matches parentheses with digits at end of string
|
|
144
|
+
t = re.sub(r"\([0-9]+\)$", "", t)
|
|
145
|
+
return t
|
|
146
|
+
|
|
147
|
+
# Open file and read line by line
|
|
148
|
+
# encoding='utf-8': Handle international characters properly
|
|
149
|
+
# errors='ignore': Skip invalid characters instead of crashing
|
|
150
|
+
with open(fname, 'r', encoding='utf-8', errors='ignore') as f:
|
|
151
|
+
# Process each line in the file
|
|
152
|
+
for raw in f:
|
|
153
|
+
# Remove leading/trailing whitespace
|
|
154
|
+
line = raw.strip()
|
|
155
|
+
# Skip empty lines and comments
|
|
156
|
+
if not line or line.startswith('#'):
|
|
157
|
+
continue
|
|
158
|
+
|
|
159
|
+
# Convert to lowercase for case-insensitive matching
|
|
160
|
+
# CIF keywords are case-insensitive, so we normalize for comparison
|
|
161
|
+
low = line.lower()
|
|
162
|
+
|
|
163
|
+
# Parse space group
|
|
164
|
+
if (low.startswith('_space_group_name_h-m_alt') or
|
|
165
|
+
low.startswith('_symmetry_space_group_name_h-m')):
|
|
166
|
+
parts = line.split()
|
|
167
|
+
if len(parts) >= 2:
|
|
168
|
+
cell['space_group'] = parts[1].strip("'\"")
|
|
169
|
+
|
|
170
|
+
# Parse cell parameters
|
|
171
|
+
if low.startswith('_cell_length_a'):
|
|
172
|
+
try:
|
|
173
|
+
cell['a'] = float(_clean_num(line.split()[1]))
|
|
174
|
+
except Exception:
|
|
175
|
+
pass
|
|
176
|
+
elif low.startswith('_cell_length_b'):
|
|
177
|
+
try:
|
|
178
|
+
cell['b'] = float(_clean_num(line.split()[1]))
|
|
179
|
+
except Exception:
|
|
180
|
+
pass
|
|
181
|
+
elif low.startswith('_cell_length_c'):
|
|
182
|
+
try:
|
|
183
|
+
cell['c'] = float(_clean_num(line.split()[1]))
|
|
184
|
+
except Exception:
|
|
185
|
+
pass
|
|
186
|
+
elif low.startswith('_cell_angle_alpha'):
|
|
187
|
+
try:
|
|
188
|
+
cell['alpha'] = float(_clean_num(line.split()[1]))
|
|
189
|
+
except Exception:
|
|
190
|
+
pass
|
|
191
|
+
elif low.startswith('_cell_angle_beta'):
|
|
192
|
+
try:
|
|
193
|
+
cell['beta'] = float(_clean_num(line.split()[1]))
|
|
194
|
+
except Exception:
|
|
195
|
+
pass
|
|
196
|
+
elif low.startswith('_cell_angle_gamma'):
|
|
197
|
+
try:
|
|
198
|
+
cell['gamma'] = float(_clean_num(line.split()[1]))
|
|
199
|
+
except Exception:
|
|
200
|
+
pass
|
|
201
|
+
|
|
202
|
+
# ====================================================================
|
|
203
|
+
# HANDLE LOOP STRUCTURES
|
|
204
|
+
# ====================================================================
|
|
205
|
+
# CIF files use 'loop_' to indicate the start of a data table.
|
|
206
|
+
# After 'loop_', there are column headers (lines starting with '_'),
|
|
207
|
+
# followed by data rows (values separated by spaces).
|
|
208
|
+
#
|
|
209
|
+
# Example:
|
|
210
|
+
# loop_
|
|
211
|
+
# _atom_site_fract_x
|
|
212
|
+
# _atom_site_fract_y
|
|
213
|
+
# _atom_site_fract_z
|
|
214
|
+
# _atom_site_type_symbol
|
|
215
|
+
# 0.0 0.0 0.0 Li
|
|
216
|
+
# 0.5 0.5 0.5 O
|
|
217
|
+
#
|
|
218
|
+
# When we see 'loop_', we reset our parsing state.
|
|
219
|
+
# ====================================================================
|
|
220
|
+
if line.lower().startswith('loop_'):
|
|
221
|
+
in_atom_loop = False # Reset flag - we're starting a new loop
|
|
222
|
+
atom_headers = [] # Clear previous headers
|
|
223
|
+
continue # Move to next line
|
|
224
|
+
|
|
225
|
+
# Skip symmetry operation header (we'll collect operations separately)
|
|
226
|
+
if line.lower().startswith('_space_group_symop_operation_xyz'):
|
|
227
|
+
continue
|
|
228
|
+
|
|
229
|
+
# ====================================================================
|
|
230
|
+
# COLLECT ATOM SITE HEADERS
|
|
231
|
+
# ====================================================================
|
|
232
|
+
# Atom site headers define what each column in the atom data table means.
|
|
233
|
+
# Common headers:
|
|
234
|
+
# _atom_site_fract_x: Fractional x-coordinate (0.0 to 1.0)
|
|
235
|
+
# _atom_site_fract_y: Fractional y-coordinate (0.0 to 1.0)
|
|
236
|
+
# _atom_site_fract_z: Fractional z-coordinate (0.0 to 1.0)
|
|
237
|
+
# _atom_site_type_symbol: Element symbol (e.g., 'Li', 'O', 'Fe')
|
|
238
|
+
# _atom_site_label: Atom label (e.g., 'Li1', 'O1')
|
|
239
|
+
#
|
|
240
|
+
# We need x, y, z coordinates to know where atoms are located.
|
|
241
|
+
# Once we have all three, we know we're ready to parse atom data rows.
|
|
242
|
+
# ====================================================================
|
|
243
|
+
if line.lower().startswith('_atom_site_'):
|
|
244
|
+
atom_headers.append(line) # Store this header
|
|
245
|
+
# Check if we have all required coordinate columns
|
|
246
|
+
# any() returns True if at least one header matches
|
|
247
|
+
has_x = any(h.lower().startswith('_atom_site_fract_x')
|
|
248
|
+
for h in atom_headers)
|
|
249
|
+
has_y = any(h.lower().startswith('_atom_site_fract_y')
|
|
250
|
+
for h in atom_headers)
|
|
251
|
+
has_z = any(h.lower().startswith('_atom_site_fract_z')
|
|
252
|
+
for h in atom_headers)
|
|
253
|
+
# If we have x, y, z columns, we're ready to parse atom data
|
|
254
|
+
if has_x and has_y and has_z:
|
|
255
|
+
in_atom_loop = True # Set flag: next non-header lines are atom data
|
|
256
|
+
continue # Move to next line
|
|
257
|
+
|
|
258
|
+
# Parse symmetry operations
|
|
259
|
+
if (len(atom_headers) == 1 and
|
|
260
|
+
atom_headers[0].lower().startswith('_space_group_symop_operation_xyz') and
|
|
261
|
+
not line.startswith('_') and ',' in line):
|
|
262
|
+
sym_ops.append(line.strip().strip("'\""))
|
|
263
|
+
continue
|
|
264
|
+
|
|
265
|
+
# ====================================================================
|
|
266
|
+
# PARSE ATOM POSITIONS
|
|
267
|
+
# ====================================================================
|
|
268
|
+
# When we're in an atom loop and the line doesn't start with '_',
|
|
269
|
+
# it's a data row containing atom information.
|
|
270
|
+
#
|
|
271
|
+
# Example data row:
|
|
272
|
+
# 0.0 0.0 0.0 Li 1.0 0.05
|
|
273
|
+
# ↑ ↑ ↑ ↑ ↑ ↑
|
|
274
|
+
# x y z sym occ uiso
|
|
275
|
+
#
|
|
276
|
+
# The order of values matches the order of headers we collected earlier.
|
|
277
|
+
# ====================================================================
|
|
278
|
+
if in_atom_loop and not line.startswith('_'):
|
|
279
|
+
# Split line into tokens (values separated by whitespace)
|
|
280
|
+
toks = line.split()
|
|
281
|
+
# Need at least 4 tokens (x, y, z, symbol)
|
|
282
|
+
if len(toks) < 4:
|
|
283
|
+
continue # Skip malformed lines
|
|
284
|
+
|
|
285
|
+
# ================================================================
|
|
286
|
+
# CREATE HEADER-TO-COLUMN-INDEX MAPPING
|
|
287
|
+
# ================================================================
|
|
288
|
+
# We collected headers in order: ['_atom_site_fract_x', '_atom_site_fract_y', ...]
|
|
289
|
+
# Now we create a dictionary mapping header name → column index
|
|
290
|
+
#
|
|
291
|
+
# Example:
|
|
292
|
+
# headers = ['_atom_site_fract_x', '_atom_site_fract_y', '_atom_site_type_symbol']
|
|
293
|
+
# header_map = {
|
|
294
|
+
# '_atom_site_fract_x': 0,
|
|
295
|
+
# '_atom_site_fract_y': 1,
|
|
296
|
+
# '_atom_site_type_symbol': 2
|
|
297
|
+
# }
|
|
298
|
+
#
|
|
299
|
+
# This lets us find which column contains which data.
|
|
300
|
+
# ================================================================
|
|
301
|
+
header_map = {h.lower(): i for i, h in enumerate(atom_headers)}
|
|
302
|
+
|
|
303
|
+
def gidx(prefix):
|
|
304
|
+
"""
|
|
305
|
+
Get column index for a header that starts with given prefix.
|
|
306
|
+
|
|
307
|
+
This helper function searches through headers to find which
|
|
308
|
+
column contains a particular type of data.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
prefix: Header prefix to search for (e.g., '_atom_site_fract_x')
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
Column index (0-based), or None if not found
|
|
315
|
+
"""
|
|
316
|
+
for h, i in header_map.items():
|
|
317
|
+
if h.startswith(prefix):
|
|
318
|
+
return i
|
|
319
|
+
return None
|
|
320
|
+
|
|
321
|
+
# Find column indices for each piece of atom data
|
|
322
|
+
ix = gidx('_atom_site_fract_x') # X coordinate column
|
|
323
|
+
iy = gidx('_atom_site_fract_y') # Y coordinate column
|
|
324
|
+
iz = gidx('_atom_site_fract_z') # Z coordinate column
|
|
325
|
+
isym = gidx('_atom_site_type_symbol') # Element symbol column
|
|
326
|
+
ilab = gidx('_atom_site_label') # Atom label column (optional)
|
|
327
|
+
iocc = gidx('_atom_site_occupancy') # Occupancy column (optional, usually 1.0)
|
|
328
|
+
# Thermal displacement parameter (B-factor) - try multiple possible header names
|
|
329
|
+
iuiso = (gidx('_atom_site_u_iso') or
|
|
330
|
+
gidx('_atom_site_u_iso_or_equiv') or
|
|
331
|
+
gidx('_atom_site_u_equiv'))
|
|
332
|
+
|
|
333
|
+
try:
|
|
334
|
+
x = float(_clean_num(toks[ix])) if ix is not None else 0.0
|
|
335
|
+
y = float(_clean_num(toks[iy])) if iy is not None else 0.0
|
|
336
|
+
z = float(_clean_num(toks[iz])) if iz is not None else 0.0
|
|
337
|
+
except Exception:
|
|
338
|
+
continue
|
|
339
|
+
|
|
340
|
+
if isym is not None and isym < len(toks):
|
|
341
|
+
sym = re.sub(r'[^A-Za-z].*', '', toks[isym])
|
|
342
|
+
elif ilab is not None and ilab < len(toks):
|
|
343
|
+
sym = re.sub(r'[^A-Za-z].*', '', toks[ilab])
|
|
344
|
+
else:
|
|
345
|
+
sym = 'X'
|
|
346
|
+
|
|
347
|
+
if iocc is not None and iocc < len(toks):
|
|
348
|
+
try:
|
|
349
|
+
occ = float(_clean_num(toks[iocc]))
|
|
350
|
+
except Exception:
|
|
351
|
+
occ = 1.0
|
|
352
|
+
else:
|
|
353
|
+
occ = 1.0
|
|
354
|
+
|
|
355
|
+
if iuiso is not None and iuiso < len(toks):
|
|
356
|
+
try:
|
|
357
|
+
Uiso = float(_clean_num(toks[iuiso]))
|
|
358
|
+
except Exception:
|
|
359
|
+
Uiso = None
|
|
360
|
+
else:
|
|
361
|
+
Uiso = None
|
|
362
|
+
|
|
363
|
+
atoms.append((sym, x, y, z, occ, Uiso))
|
|
364
|
+
|
|
365
|
+
# Validate parsed data
|
|
366
|
+
if any(v is None for v in cell.values()):
|
|
367
|
+
raise ValueError(f"Incomplete cell parameters in CIF {fname}")
|
|
368
|
+
|
|
369
|
+
if not atoms:
|
|
370
|
+
raise ValueError(f"No atoms parsed from CIF {fname}")
|
|
371
|
+
|
|
372
|
+
# Apply symmetry operations if present
|
|
373
|
+
if sym_ops:
|
|
374
|
+
seen = set()
|
|
375
|
+
expanded = []
|
|
376
|
+
if not any(op.replace(' ', '') in ('x,y,z', 'x,y,z,') for op in sym_ops):
|
|
377
|
+
sym_ops.append('x, y, z')
|
|
378
|
+
|
|
379
|
+
def eval_coord(expr, x, y, z):
|
|
380
|
+
expr = expr.strip().lower().replace(' ', '')
|
|
381
|
+
if not re.match(r'^[xyz0-9+\-*/().,/]*$', expr):
|
|
382
|
+
return x
|
|
383
|
+
try:
|
|
384
|
+
return eval(expr, {"__builtins__": {}}, {'x': x, 'y': y, 'z': z}) % 1.0
|
|
385
|
+
except Exception:
|
|
386
|
+
return x
|
|
387
|
+
|
|
388
|
+
for sym, x, y, z, occ, Uiso in atoms:
|
|
389
|
+
for op in sym_ops:
|
|
390
|
+
parts = op.strip().strip("'\"").split(',')
|
|
391
|
+
if len(parts) != 3:
|
|
392
|
+
continue
|
|
393
|
+
|
|
394
|
+
nx = eval_coord(parts[0], x, y, z)
|
|
395
|
+
ny = eval_coord(parts[1], x, y, z)
|
|
396
|
+
nz = eval_coord(parts[2], x, y, z)
|
|
397
|
+
|
|
398
|
+
key = (round(nx, 4), round(ny, 4), round(nz, 4), sym)
|
|
399
|
+
if key in seen:
|
|
400
|
+
continue
|
|
401
|
+
|
|
402
|
+
seen.add(key)
|
|
403
|
+
expanded.append((sym, nx, ny, nz, occ, Uiso))
|
|
404
|
+
|
|
405
|
+
if expanded:
|
|
406
|
+
atoms = expanded
|
|
407
|
+
|
|
408
|
+
return cell, atoms
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def simulate_cif_pattern_Q(fname, Qmax=10.0, dQ=0.002, peak_width=0.01,
|
|
412
|
+
wavelength=1.5406, space_group_hint=None):
|
|
413
|
+
"""Simulate powder diffraction pattern from CIF file in Q-space."""
|
|
414
|
+
cell, atoms = _parse_cif_basic(fname)
|
|
415
|
+
|
|
416
|
+
if space_group_hint is None:
|
|
417
|
+
space_group_hint = cell.get('space_group')
|
|
418
|
+
|
|
419
|
+
a, b, c = cell['a'], cell['b'], cell['c']
|
|
420
|
+
alpha = np.deg2rad(cell['alpha'])
|
|
421
|
+
beta = np.deg2rad(cell['beta'])
|
|
422
|
+
gamma = np.deg2rad(cell['gamma'])
|
|
423
|
+
|
|
424
|
+
# Build unit cell vectors
|
|
425
|
+
a_vec = np.array([a, 0, 0], dtype=float)
|
|
426
|
+
b_vec = np.array([b * np.cos(gamma), b * np.sin(gamma), 0], dtype=float)
|
|
427
|
+
|
|
428
|
+
c_x = c * np.cos(beta)
|
|
429
|
+
c_y = c * (np.cos(alpha) - np.cos(beta) * np.cos(gamma)) / np.sin(gamma)
|
|
430
|
+
c_z = np.sqrt(max(c ** 2 - c_x ** 2 - c_y ** 2, 1e-12))
|
|
431
|
+
c_vec = np.array([c_x, c_y, c_z], dtype=float)
|
|
432
|
+
|
|
433
|
+
# Calculate reciprocal lattice
|
|
434
|
+
A = np.column_stack([a_vec, b_vec, c_vec])
|
|
435
|
+
V = np.dot(a_vec, np.cross(b_vec, c_vec))
|
|
436
|
+
|
|
437
|
+
if abs(V) < 1e-10:
|
|
438
|
+
raise ValueError('Invalid cell volume')
|
|
439
|
+
|
|
440
|
+
B = 2 * np.pi * np.linalg.inv(A).T
|
|
441
|
+
b1, b2, b3 = B[:, 0], B[:, 1], B[:, 2]
|
|
442
|
+
|
|
443
|
+
a_star = np.linalg.norm(b1)
|
|
444
|
+
b_star = np.linalg.norm(b2)
|
|
445
|
+
c_star = np.linalg.norm(b3)
|
|
446
|
+
|
|
447
|
+
hmax = max(1, int(np.ceil(Qmax / a_star)))
|
|
448
|
+
kmax = max(1, int(np.ceil(Qmax / b_star)))
|
|
449
|
+
lmax = max(1, int(np.ceil(Qmax / c_star)))
|
|
450
|
+
|
|
451
|
+
Zmap = _atomic_number_table()
|
|
452
|
+
|
|
453
|
+
# Cromer-Mann coefficients for form factors
|
|
454
|
+
CM_COEFFS = {
|
|
455
|
+
'C': ([2.3100, 1.0200, 1.5886, 0.8650],
|
|
456
|
+
[20.8439, 10.2075, 0.5687, 51.6512], 0.2156),
|
|
457
|
+
'N': ([12.2126, 3.1322, 2.0125, 1.1663],
|
|
458
|
+
[0.0057, 9.8933, 28.9975, 0.5826], -11.5290),
|
|
459
|
+
'O': ([3.0485, 2.2868, 1.5463, 0.8670],
|
|
460
|
+
[13.2771, 5.7011, 0.3239, 32.9089], 0.2508),
|
|
461
|
+
'Si': ([6.2915, 3.0353, 1.9891, 1.5410],
|
|
462
|
+
[2.4386, 32.3337, 0.6785, 81.6937], 1.1407),
|
|
463
|
+
'Fe': ([11.7695, 7.3573, 3.5222, 2.3045],
|
|
464
|
+
[4.7611, 0.3072, 15.3535, 76.8805], 1.0369),
|
|
465
|
+
'Ni': ([12.8376, 7.2920, 4.4438, 2.3800],
|
|
466
|
+
[3.8785, 0.2565, 13.5290, 71.1692], 1.0341),
|
|
467
|
+
'Cu': ([13.3380, 7.1676, 5.6158, 1.6735],
|
|
468
|
+
[3.5828, 0.2470, 11.3966, 64.8126], 1.1910),
|
|
469
|
+
'Se': ([19.3319, 8.8752, 2.6959, 1.2199],
|
|
470
|
+
[6.4000, 1.4838, 19.9887, 55.4486], 1.1053),
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
def form_factor(sym, Q):
|
|
474
|
+
s2 = (Q / (4 * np.pi)) ** 2
|
|
475
|
+
if sym in CM_COEFFS:
|
|
476
|
+
a, b, c = CM_COEFFS[sym]
|
|
477
|
+
f = c
|
|
478
|
+
for ai, bi in zip(a, b):
|
|
479
|
+
f += ai * np.exp(-bi * s2)
|
|
480
|
+
return max(f, 0.0)
|
|
481
|
+
Z = Zmap.get(sym, 10)
|
|
482
|
+
return Z * np.exp(-0.002 * Q * Q)
|
|
483
|
+
|
|
484
|
+
# Prepare atom data
|
|
485
|
+
atom_data = []
|
|
486
|
+
for sym, x, y, z, occ, Uiso in _parse_cif_basic(fname)[1]:
|
|
487
|
+
sym_cap = sym.capitalize()
|
|
488
|
+
Biso = 8 * np.pi ** 2 * Uiso if Uiso is not None else 0.0
|
|
489
|
+
atom_data.append((sym_cap, x, y, z, occ, Biso))
|
|
490
|
+
|
|
491
|
+
def extinct(h, k, l, sg):
|
|
492
|
+
"""Check if reflection is extinct due to space group symmetry."""
|
|
493
|
+
if not sg:
|
|
494
|
+
return False
|
|
495
|
+
|
|
496
|
+
sg0 = sg.lower()[0]
|
|
497
|
+
|
|
498
|
+
if sg0 == 'p':
|
|
499
|
+
return False
|
|
500
|
+
if sg0 == 'i':
|
|
501
|
+
return (h + k + l) % 2 != 0
|
|
502
|
+
if sg0 == 'f':
|
|
503
|
+
all_even = (h % 2 == 0) and (k % 2 == 0) and (l % 2 == 0)
|
|
504
|
+
all_odd = (h % 2 != 0) and (k % 2 != 0) and (l % 2 != 0)
|
|
505
|
+
return not (all_even or all_odd)
|
|
506
|
+
if sg0 == 'c':
|
|
507
|
+
return (h + k) % 2 != 0
|
|
508
|
+
if sg0 == 'r':
|
|
509
|
+
return ((-h + k + l) % 3) != 0
|
|
510
|
+
|
|
511
|
+
return False
|
|
512
|
+
|
|
513
|
+
# Calculate reflections
|
|
514
|
+
refl_map = {}
|
|
515
|
+
lam = wavelength if wavelength else 1.5406
|
|
516
|
+
|
|
517
|
+
for h in range(-hmax, hmax + 1):
|
|
518
|
+
for k in range(-kmax, kmax + 1):
|
|
519
|
+
for l in range(-lmax, lmax + 1):
|
|
520
|
+
if h == 0 and k == 0 and l == 0:
|
|
521
|
+
continue
|
|
522
|
+
if extinct(h, k, l, space_group_hint):
|
|
523
|
+
continue
|
|
524
|
+
|
|
525
|
+
G = h * b1 + k * b2 + l * b3
|
|
526
|
+
Q = np.linalg.norm(G)
|
|
527
|
+
|
|
528
|
+
if Q <= 0 or Q > Qmax:
|
|
529
|
+
continue
|
|
530
|
+
|
|
531
|
+
s = (Q * lam) / (4 * np.pi)
|
|
532
|
+
if s <= 0 or s >= 1:
|
|
533
|
+
continue
|
|
534
|
+
|
|
535
|
+
theta = np.arcsin(s)
|
|
536
|
+
s2 = (Q / (4 * np.pi)) ** 2
|
|
537
|
+
|
|
538
|
+
phases = []
|
|
539
|
+
weights = []
|
|
540
|
+
|
|
541
|
+
for sym_cap, ax, ay, az, occ, Biso in atom_data:
|
|
542
|
+
phase = 2 * np.pi * (h * ax + k * ay + l * az)
|
|
543
|
+
f0 = form_factor(sym_cap, Q)
|
|
544
|
+
|
|
545
|
+
if f0 <= 1e-8:
|
|
546
|
+
continue
|
|
547
|
+
|
|
548
|
+
dw = np.exp(-Biso * s2) if Biso > 0 else 1.0
|
|
549
|
+
w = f0 * occ * dw
|
|
550
|
+
|
|
551
|
+
if w <= 0:
|
|
552
|
+
continue
|
|
553
|
+
|
|
554
|
+
phases.append(phase)
|
|
555
|
+
weights.append(w)
|
|
556
|
+
|
|
557
|
+
if not weights:
|
|
558
|
+
continue
|
|
559
|
+
|
|
560
|
+
weights = np.array(weights)
|
|
561
|
+
phases = np.array(phases)
|
|
562
|
+
|
|
563
|
+
F = np.sum(weights * np.exp(1j * phases))
|
|
564
|
+
I = (F.real ** 2 + F.imag ** 2)
|
|
565
|
+
|
|
566
|
+
if I <= 1e-14:
|
|
567
|
+
continue
|
|
568
|
+
|
|
569
|
+
cos_2theta = np.cos(2 * theta)
|
|
570
|
+
sin_theta_sq = np.sin(theta) ** 2
|
|
571
|
+
sin_2theta = np.sin(2 * theta)
|
|
572
|
+
|
|
573
|
+
if sin_theta_sq <= 0 or sin_2theta <= 1e-12:
|
|
574
|
+
continue
|
|
575
|
+
|
|
576
|
+
lp = (1 + cos_2theta ** 2) / (sin_theta_sq * sin_2theta)
|
|
577
|
+
qkey = round(Q, 5)
|
|
578
|
+
refl_map[qkey] = refl_map.get(qkey, 0.0) + I * lp
|
|
579
|
+
|
|
580
|
+
if not refl_map:
|
|
581
|
+
raise ValueError('No reflections in range')
|
|
582
|
+
|
|
583
|
+
# Convert to arrays and apply peak broadening
|
|
584
|
+
refl_items = sorted(refl_map.items())
|
|
585
|
+
refl_Q = np.array([k for k, _ in refl_items])
|
|
586
|
+
refl_I = np.array([v for _, v in refl_items])
|
|
587
|
+
|
|
588
|
+
Q_grid = np.arange(0, Qmax + dQ * 0.5, dQ)
|
|
589
|
+
intens = np.zeros_like(Q_grid)
|
|
590
|
+
|
|
591
|
+
for q, I in zip(refl_Q, refl_I):
|
|
592
|
+
sigma = peak_width * (0.6 + 0.4 * q / Qmax)
|
|
593
|
+
intens += I * np.exp(-0.5 * ((Q_grid - q) / sigma) ** 2)
|
|
594
|
+
|
|
595
|
+
if intens.max() > 0:
|
|
596
|
+
intens /= intens.max()
|
|
597
|
+
|
|
598
|
+
return Q_grid, intens
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def cif_reflection_positions(fname, Qmax=10.0, wavelength=1.5406,
|
|
602
|
+
space_group_hint=None):
|
|
603
|
+
"""Get list of reflection Q positions from CIF file."""
|
|
604
|
+
cell, atoms = _parse_cif_basic(fname)
|
|
605
|
+
|
|
606
|
+
if space_group_hint is None:
|
|
607
|
+
space_group_hint = cell.get('space_group')
|
|
608
|
+
|
|
609
|
+
a, b, c = cell['a'], cell['b'], cell['c']
|
|
610
|
+
alpha = np.deg2rad(cell['alpha'])
|
|
611
|
+
beta = np.deg2rad(cell['beta'])
|
|
612
|
+
gamma = np.deg2rad(cell['gamma'])
|
|
613
|
+
|
|
614
|
+
# Build unit cell vectors
|
|
615
|
+
a_vec = np.array([a, 0, 0])
|
|
616
|
+
b_vec = np.array([b * np.cos(gamma), b * np.sin(gamma), 0])
|
|
617
|
+
|
|
618
|
+
c_x = c * np.cos(beta)
|
|
619
|
+
c_y = c * (np.cos(alpha) - np.cos(beta) * np.cos(gamma)) / np.sin(gamma)
|
|
620
|
+
c_z = np.sqrt(max(c ** 2 - c_x ** 2 - c_y ** 2, 1e-12))
|
|
621
|
+
c_vec = np.array([c_x, c_y, c_z])
|
|
622
|
+
|
|
623
|
+
A = np.column_stack([a_vec, b_vec, c_vec])
|
|
624
|
+
V = np.dot(a_vec, np.cross(b_vec, c_vec))
|
|
625
|
+
|
|
626
|
+
if abs(V) < 1e-10:
|
|
627
|
+
return []
|
|
628
|
+
|
|
629
|
+
B = 2 * np.pi * np.linalg.inv(A).T
|
|
630
|
+
b1, b2, b3 = B[:, 0], B[:, 1], B[:, 2]
|
|
631
|
+
|
|
632
|
+
a_star = np.linalg.norm(b1)
|
|
633
|
+
b_star = np.linalg.norm(b2)
|
|
634
|
+
c_star = np.linalg.norm(b3)
|
|
635
|
+
|
|
636
|
+
hmax = max(1, int(np.ceil(Qmax / a_star)))
|
|
637
|
+
kmax = max(1, int(np.ceil(Qmax / b_star)))
|
|
638
|
+
lmax = max(1, int(np.ceil(Qmax / c_star)))
|
|
639
|
+
|
|
640
|
+
def extinct(h, k, l, sg):
|
|
641
|
+
"""Check if reflection is extinct due to space group symmetry."""
|
|
642
|
+
if not sg:
|
|
643
|
+
return False
|
|
644
|
+
|
|
645
|
+
c0 = sg.lower()[0]
|
|
646
|
+
|
|
647
|
+
if c0 == 'i':
|
|
648
|
+
return (h + k + l) % 2 != 0
|
|
649
|
+
if c0 == 'f':
|
|
650
|
+
all_even = (h % 2 == 0 and k % 2 == 0 and l % 2 == 0)
|
|
651
|
+
all_odd = (h % 2 != 0 and k % 2 != 0 and l % 2 != 0)
|
|
652
|
+
return not (all_even or all_odd)
|
|
653
|
+
if c0 == 'c':
|
|
654
|
+
return (h + k) % 2 != 0
|
|
655
|
+
if c0 == 'r':
|
|
656
|
+
return ((-h + k + l) % 3) != 0
|
|
657
|
+
|
|
658
|
+
return False
|
|
659
|
+
|
|
660
|
+
lam = wavelength
|
|
661
|
+
refl = set()
|
|
662
|
+
|
|
663
|
+
for h in range(-hmax, hmax + 1):
|
|
664
|
+
for k in range(-kmax, kmax + 1):
|
|
665
|
+
for l in range(-lmax, lmax + 1):
|
|
666
|
+
if h == k == l == 0:
|
|
667
|
+
continue
|
|
668
|
+
if extinct(h, k, l, space_group_hint):
|
|
669
|
+
continue
|
|
670
|
+
|
|
671
|
+
G = h * b1 + k * b2 + l * b3
|
|
672
|
+
Q = np.linalg.norm(G)
|
|
673
|
+
|
|
674
|
+
if Q <= 0 or Q > Qmax:
|
|
675
|
+
continue
|
|
676
|
+
|
|
677
|
+
if lam is not None:
|
|
678
|
+
s = (Q * lam) / (4 * np.pi)
|
|
679
|
+
if s <= 0 or s >= 1:
|
|
680
|
+
continue
|
|
681
|
+
|
|
682
|
+
q_round = round(Q, 6)
|
|
683
|
+
refl.add(q_round)
|
|
684
|
+
|
|
685
|
+
return sorted(refl)
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
# --- New helpers for hkl labeling ---
|
|
689
|
+
|
|
690
|
+
def list_reflections_with_hkl(fname, Qmax=10.0, wavelength=1.5406,
|
|
691
|
+
space_group_hint=None):
|
|
692
|
+
"""Return a list of (Q_rounded, h, k, l) for reflections up to Qmax.
|
|
693
|
+
|
|
694
|
+
When wavelength is None, do not apply Bragg cutoff (enumerate by Q only).
|
|
695
|
+
Q values are rounded to 6 decimals to group symmetrically equivalent sets.
|
|
696
|
+
"""
|
|
697
|
+
cell, _atoms = _parse_cif_basic(fname)
|
|
698
|
+
|
|
699
|
+
if space_group_hint is None:
|
|
700
|
+
space_group_hint = cell.get('space_group')
|
|
701
|
+
|
|
702
|
+
a, b, c = cell['a'], cell['b'], cell['c']
|
|
703
|
+
alpha = np.deg2rad(cell['alpha'])
|
|
704
|
+
beta = np.deg2rad(cell['beta'])
|
|
705
|
+
gamma = np.deg2rad(cell['gamma'])
|
|
706
|
+
|
|
707
|
+
# Build unit cell vectors
|
|
708
|
+
a_vec = np.array([a, 0, 0])
|
|
709
|
+
b_vec = np.array([b * np.cos(gamma), b * np.sin(gamma), 0])
|
|
710
|
+
|
|
711
|
+
c_x = c * np.cos(beta)
|
|
712
|
+
c_y = c * (np.cos(alpha) - np.cos(beta) * np.cos(gamma)) / np.sin(gamma)
|
|
713
|
+
c_z = np.sqrt(max(c ** 2 - c_x ** 2 - c_y ** 2, 1e-12))
|
|
714
|
+
c_vec = np.array([c_x, c_y, c_z])
|
|
715
|
+
|
|
716
|
+
A = np.column_stack([a_vec, b_vec, c_vec])
|
|
717
|
+
V = np.dot(a_vec, np.cross(b_vec, c_vec))
|
|
718
|
+
|
|
719
|
+
if abs(V) < 1e-10:
|
|
720
|
+
return []
|
|
721
|
+
|
|
722
|
+
B = 2 * np.pi * np.linalg.inv(A).T
|
|
723
|
+
b1, b2, b3 = B[:, 0], B[:, 1], B[:, 2]
|
|
724
|
+
|
|
725
|
+
a_star = np.linalg.norm(b1)
|
|
726
|
+
b_star = np.linalg.norm(b2)
|
|
727
|
+
c_star = np.linalg.norm(b3)
|
|
728
|
+
|
|
729
|
+
hmax = max(1, int(np.ceil(Qmax / a_star)))
|
|
730
|
+
kmax = max(1, int(np.ceil(Qmax / b_star)))
|
|
731
|
+
lmax = max(1, int(np.ceil(Qmax / c_star)))
|
|
732
|
+
|
|
733
|
+
def extinct(h, k, l, sg):
|
|
734
|
+
"""Check if reflection is extinct due to space group symmetry."""
|
|
735
|
+
if not sg:
|
|
736
|
+
return False
|
|
737
|
+
|
|
738
|
+
c0 = sg.lower()[0]
|
|
739
|
+
|
|
740
|
+
if c0 == 'i':
|
|
741
|
+
return (h + k + l) % 2 != 0
|
|
742
|
+
if c0 == 'f':
|
|
743
|
+
all_even = (h % 2 == 0 and k % 2 == 0 and l % 2 == 0)
|
|
744
|
+
all_odd = (h % 2 != 0 and k % 2 != 0 and l % 2 != 0)
|
|
745
|
+
return not (all_even or all_odd)
|
|
746
|
+
if c0 == 'c':
|
|
747
|
+
return (h + k) % 2 != 0
|
|
748
|
+
if c0 == 'r':
|
|
749
|
+
return ((-h + k + l) % 3) != 0
|
|
750
|
+
|
|
751
|
+
return False
|
|
752
|
+
|
|
753
|
+
lam = wavelength
|
|
754
|
+
hkl_list = []
|
|
755
|
+
|
|
756
|
+
for h in range(-hmax, hmax + 1):
|
|
757
|
+
for k in range(-kmax, kmax + 1):
|
|
758
|
+
for l in range(-lmax, lmax + 1):
|
|
759
|
+
if h == k == l == 0:
|
|
760
|
+
continue
|
|
761
|
+
if extinct(h, k, l, space_group_hint):
|
|
762
|
+
continue
|
|
763
|
+
|
|
764
|
+
G = h * b1 + k * b2 + l * b3
|
|
765
|
+
Q = np.linalg.norm(G)
|
|
766
|
+
|
|
767
|
+
if Q <= 0 or Q > Qmax:
|
|
768
|
+
continue
|
|
769
|
+
|
|
770
|
+
if lam is not None:
|
|
771
|
+
s = (Q * lam) / (4 * np.pi)
|
|
772
|
+
if s <= 0 or s >= 1:
|
|
773
|
+
continue
|
|
774
|
+
|
|
775
|
+
q_round = round(Q, 6)
|
|
776
|
+
hkl_list.append((q_round, h, k, l))
|
|
777
|
+
|
|
778
|
+
# De-duplicate identical entries
|
|
779
|
+
seen = set()
|
|
780
|
+
uniq = []
|
|
781
|
+
|
|
782
|
+
for item in hkl_list:
|
|
783
|
+
if item in seen:
|
|
784
|
+
continue
|
|
785
|
+
seen.add(item)
|
|
786
|
+
uniq.append(item)
|
|
787
|
+
|
|
788
|
+
return uniq
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
def build_hkl_label_map_from_list(hkl_list):
|
|
792
|
+
"""Build a dict Q-> "(h k l), (h k l), ..." using canonical positive indices if present.
|
|
793
|
+
|
|
794
|
+
This mirrors the prior UI labeling convention.
|
|
795
|
+
"""
|
|
796
|
+
by_q = {}
|
|
797
|
+
|
|
798
|
+
for q, h, k, l in hkl_list:
|
|
799
|
+
# Canonicalize sign: prefer non-negative if possible for readability
|
|
800
|
+
if h < 0 or (h == 0 and k < 0) or (h == 0 and k == 0 and l < 0):
|
|
801
|
+
h, k, l = -h, -k, -l
|
|
802
|
+
by_q.setdefault(q, set()).add((h, k, l))
|
|
803
|
+
|
|
804
|
+
label_map = {}
|
|
805
|
+
|
|
806
|
+
for q, triples in by_q.items():
|
|
807
|
+
ordered = sorted(triples)
|
|
808
|
+
nonneg_all = [t for t in ordered
|
|
809
|
+
if t[0] >= 0 and t[1] >= 0 and t[2] >= 0]
|
|
810
|
+
use_list = nonneg_all if nonneg_all else ordered
|
|
811
|
+
label_map[q] = ", ".join(f"({h} {k} {l})" for h, k, l in use_list)
|
|
812
|
+
|
|
813
|
+
return label_map
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
def build_hkl_label_map(fname, Qmax=10.0, wavelength=1.5406,
|
|
817
|
+
space_group_hint=None):
|
|
818
|
+
"""Convenience: compute label map directly from a CIF file."""
|
|
819
|
+
hkl_list = list_reflections_with_hkl(
|
|
820
|
+
fname, Qmax=Qmax, wavelength=wavelength,
|
|
821
|
+
space_group_hint=space_group_hint
|
|
822
|
+
)
|
|
823
|
+
return build_hkl_label_map_from_list(hkl_list)
|