Last active 1742233591

texture_proc_real_1step_not_aligned_2step_aligned.py Raw
1import json
2import numpy as np
3import os
4import scipy
5from scipy.optimize import minimize
6from scipy.optimize import curve_fit
7from scipy.signal import argrelextrema
8from pystog import Pre_Proc
9import matplotlib.pyplot as plt
10import random
11
12rebin = Pre_Proc.rebin
13
14# ================
15# Input parameters
16# vvvvvvvvvvvvvvvv
17
18# ----------
19# Some flags
20# ----------
21testing = False # flag for testing
22
23# ------------------------------------------------------------------
24# Directory and config files definition
25#
26# run_dir => directory for storing the reduced data files
27# according to the the two theta and azimuthal
28# angle bins.
29# out_dir => directory for storing the outputs.
30# det_map_file => detector group information for all the two
31# theta and azimuthal bins.
32# ttheta_group_file => config file defining groups for second stage
33# processing.
34# ------------------------------------------------------------------
35run_dir = os.path.join(
36 ".",
37 "texture_proc"
38)
39out_dir = os.path.join(
40 run_dir,
41 "texture_proc_output"
42)
43det_map_file = os.path.join(
44 "/SNS/NOM/shared/scripts/texture",
45 "nom_texture_pointer.json"
46)
47ttheta_group_file = os.path.join(
48 ".",
49 "ttheta_group_params_new_new_new.json"
50)
51
52# ---------------------
53# Processing parameters
54# ---------------------
55stem_name = "NOM_Si_640e_"
56ttheta_binw = 5. # in degrees
57azm_binw = 10. # in degrees
58header_num = 2
59sph_l_max = 1 # for first stage
60tt_sph_l_max = 2 # for second stage
61amp_thresh = .9 # Threshold for peak intensity -- in a certain two theta
62 # bins, all patterns with the fitted peak intensity
63 # different from the median intensity by more than this
64 # percentage threshold will be discarded.
65
66# ^^^^^^^^^^^^^^^^
67# Input parameters
68# ================
69
70if testing:
71 print("\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
72 print("[Warning] Testing mode is on.")
73 print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n")
74
75
76def gaussian(x, a, b, c):
77 """Gaussian function.
78 Parameters
79 ----------
80 x : ndarray Independent variable.
81 a : float Amplitude.
82 b : float Center.
83 c : flaot Width.
84
85 Returns
86 -------
87 float Gaussian function value.
88 """
89 return a * np.exp(-(x - b)**2 / (2 * c**2))
90
91
92def bkg_expo(x, a, b, c):
93 """Background exponential function.
94 Parameters
95 ----------
96 x : ndarray Independent variable.
97 a : float offset.
98 b : float coefficient.
99 c : flaot exponent.
100
101 Returns
102 -------
103 float Exponential background value.
104 """
105 return a - b * np.exp(-c * x**2.)
106
107
108def sph_harm_real(m, l, theta, phi):
109 """Real spherical harmonics. Refer to the Wikipedia,
110 https://en.wikipedia.org/wiki/Table_of_spherical_harmonics
111 We could follow the standard definition of the spherical harmonics to
112 define the function here, but for the purpose of this script, we only
113 need the first few terms so we just implemented the function directly.
114
115 Parameters
116 ----------
117 m : order of spherical harmonics
118 l : degree of spherical harmonics
119 theta : polar angle in radian
120 phi : azimuthal angle in radian
121
122 Returns
123 -------
124 float Spherical harmonics real value
125 """
126 if l == 0 and m == 0:
127 return np.sqrt(1. / (4. * np.pi))
128 if l == 1 and m == -1:
129 return np.sqrt(3. / (4. * np.pi)) * np.sin(phi) * np.sin(theta)
130 if l == 1 and m == 0:
131 return np.sqrt(3. / (4. * np.pi)) * np.cos(theta)
132 if l == 1 and m == 1:
133 return np.sqrt(3. / (4. * np.pi)) * np.cos(phi) * np.sin(theta)
134 if l == 2 and m == -2:
135 part = np.sin(phi) * np.cos(phi) * (np.sin(theta))**2.
136 return np.sqrt(15. / (4. * np.pi)) * part
137 if l == 2 and m == -1:
138 part = np.sin(phi) * np.sin(theta) * np.cos(theta)
139 return np.sqrt(15. / (4. * np.pi)) * part
140 if l == 2 and m == 0:
141 part = 3. * (np.cos(theta))**2. - 1.
142 return np.sqrt(5. / (16. * np.pi)) * part
143 if l == 2 and m == 1:
144 part = np.cos(phi) * np.sin(theta) * np.cos(theta)
145 return np.sqrt(15. / (4. * np.pi)) * part
146 if l == 2 and m == 2:
147 part = ((np.cos(phi))**2. - (np.sin(phi))**2.) * (np.sin(theta))**2.
148 return np.sqrt(15. / (16. * np.pi)) * part
149
150
151def target_function(params, sph_mat, txt_int):
152 """Target function for the optimization of the spherical harmonic
153 coefficients.
154
155 Parameters
156 ----------
157 params : ndarray Spherical harmonic coefficients.
158 sph_mat : ndarray Spherical harmonic matrix.
159 txt_int : ndarray Textured S(Q) intensity.
160
161 Returns
162 -------
163 float : Norm of the residuals.
164 """
165 result = sph_mat @ params
166 residuals = result - txt_int
167
168 return np.linalg.norm(residuals)
169
170
171if not os.path.exists(out_dir):
172 os.makedirs(out_dir)
173
174# ================
175# Read in Q values
176# ================
177print("[Info] Read in Q values...")
178bank = 1
179while True:
180 tr_file = os.path.join(
181 run_dir,
182 stem_name + f"bank{bank}.dat"
183 )
184 if os.path.exists(tr_file):
185 data = np.loadtxt(tr_file, skiprows=header_num, dtype=float)
186 q_vals = data[:, 0]
187 break
188 else:
189 bank += 1
190
191# ===================
192# Read in S(Q) values
193# ===================
194print("[Info] Read in all S(Q)'s...")
195num_groups = int(180. / ttheta_binw) * int(360. / azm_binw)
196all_sofq = dict()
197for i in range(num_groups):
198 group_tmp = i + 1
199 tr_file = os.path.join(
200 run_dir,
201 stem_name + "bank%d.dat" % group_tmp
202 )
203 if os.path.exists(tr_file):
204 data = np.loadtxt(tr_file, skiprows=header_num, dtype=float)
205 sofq_tmp = data[:, 1]
206 all_sofq[group_tmp] = sofq_tmp
207
208# ===========================================================================
209# Read in the detector map, telling the center two theta and azimuthal angle,
210# and the ID for each group of detectors.
211# ===========================================================================
212det_pointer = json.load(open(det_map_file, "r"))
213
214# =============================================================================
215# Determine the valid azimuthal bins for each two theta bin. Here follows are
216# the criteria:
217# 1. The 2theta bin is in use
218# 2. The peak fitting succeeded
219# 3. The fitted peak intensity is close to the median intensity
220# =============================================================================
221with open(ttheta_group_file, "r") as f:
222 ttheta_groups = json.load(f)
223
224print(
225 "[Info] Pre-processing the data for all "
226 "the two theta and azimuthal bins..."
227)
228ttheta_bins = np.arange(0, 181, ttheta_binw)
229for i in range(len(ttheta_bins) - 1):
230 ttheta_cv = (ttheta_bins[i] + ttheta_bins[i + 1]) / 2.
231 ttheta_cvk = "{:d}".format(int(np.floor(ttheta_cv)))
232 if ttheta_cvk in det_pointer:
233 ttheta_valid = False
234 for tkey, item in ttheta_groups.items():
235 if ttheta_cvk in item["QRange"].keys():
236 ttheta_valid = True
237 ttheta_group = tkey
238 break
239
240 # A valid two theta value means it belongs to one of the defined
241 # groups in the `ttheta_group_file` file.
242 if ttheta_valid:
243 pa_lb = ttheta_groups[ttheta_group]["PAParams"]["LeftBound"]
244 pa_rb = ttheta_groups[ttheta_group]["PAParams"]["RightBound"]
245 mask = (q_vals >= pa_lb) & (q_vals <= pa_rb)
246 q_vals_fit = q_vals[mask]
247
248 b0 = (pa_lb + pa_rb) / 2.
249 c0 = 1.
250
251 amp_tmp = dict()
252 center_tmp = dict()
253
254 for key, item in det_pointer[ttheta_cvk]["AZMDict"].items():
255 # Check whether the data exist for each azimuthal bin. If not,
256 # we just discard the bin.
257 if int(item["GroupID"]) in all_sofq.keys():
258 sofq_fit = all_sofq[int(item["GroupID"])][mask]
259 a0 = np.max(sofq_fit)
260 p_init = [a0, b0, c0]
261 try:
262 popt, _ = curve_fit(
263 gaussian,
264 q_vals_fit,
265 sofq_fit,
266 p0=p_init
267 )
268 amp_tmp[key] = popt[0]
269 center_tmp[key] = popt[1]
270 except RuntimeError:
271 # We want to handle the peak fitting failure. For
272 # testing mode, we just set the amplitude to 1 and the
273 # center to 0. For real data, we just discard the data.
274 if testing:
275 amp_tmp[key] = 1.
276 center_tmp[key] = 0.
277 else:
278 det_pointer[ttheta_cvk][
279 "AZMDict"
280 ][key]["Use"] = False
281 det_pointer[ttheta_cvk][
282 "AZMDict"
283 ][key]["Offset"] = 0.
284 else:
285 det_pointer[ttheta_cvk][
286 "AZMDict"
287 ][key]["Use"] = False
288 det_pointer[ttheta_cvk][
289 "AZMDict"
290 ][key]["Offset"] = 0.
291
292 if (len(center_tmp) > 0):
293 # center_ref = next(iter(center_tmp.items()))[1] # first item
294 center_ref = 5.06
295
296 # Here we check the peak intensity. If the intensity is too
297 # far from the median intensity, we discard the data. If not,
298 # we calculate the offset for each azimuthal bin, relative to
299 # the center of the first azimuthal bin, within the same two
300 # theta bin.
301 median_amp = np.median(list(amp_tmp.values()))
302 used_keys = list()
303 for key, item in amp_tmp.items():
304 if abs((item - median_amp) / median_amp) < amp_thresh:
305 used_keys.append(key)
306 for key in det_pointer[ttheta_cvk]["AZMDict"].keys():
307 if key in used_keys:
308 det_pointer[ttheta_cvk]["AZMDict"][key]["Use"] = True
309 offset = center_ref - center_tmp[key]
310 det_pointer[ttheta_cvk][
311 "AZMDict"
312 ][key]["Offset"] = offset
313 else:
314 det_pointer[ttheta_cvk][
315 "AZMDict"
316 ][key]["Use"] = False
317 det_pointer[ttheta_cvk][
318 "AZMDict"
319 ][key]["Offset"] = 0.
320 else:
321 # If none of the detector groups in the two theta bin is
322 # valid (either file not existing, or fitting failed),
323 # we mask out the whole two theta bin.
324 for key in det_pointer[ttheta_cvk]["AZMDict"].keys():
325 det_pointer[ttheta_cvk]["AZMDict"][key]["Use"] = False
326 det_pointer[ttheta_cvk]["AZMDict"][key]["Offset"] = 0.
327 else:
328 # If the two theta bin is invalid, we just discard the bin.
329 # By 'invalid' two theta bin, we mean that the two theta value
330 # is not included in the `ttheta_group_file`.
331 for key in det_pointer[ttheta_cvk]["AZMDict"].keys():
332 det_pointer[ttheta_cvk]["AZMDict"][key]["Use"] = False
333 det_pointer[ttheta_cvk]["AZMDict"][key]["Offset"] = 0.
334
335# =============================================================================
336# Loop over the two theta bins and optimize the spherical harmonic coefficients
337# =============================================================================
338print("[Info] Working on the isotropic correction for each two theta bin...")
339ttheta_bins = np.arange(0, 181, ttheta_binw)
340total_items = len(ttheta_bins)
341sofq_corrected_first = list()
342ttheta_valid = list()
343print("[Info] Processing 2Theta array (0, 180)...")
344for i in range(len(ttheta_bins) - 1):
345 ttheta_cv = (ttheta_bins[i] + ttheta_bins[i + 1]) / 2.
346 ttheta_cvk = "{:d}".format(int(np.floor(ttheta_cv)))
347 print(f"[Info] Processing 2Theta: {ttheta_cvk}...")
348 if ttheta_cvk in det_pointer:
349 sofq_corrected = list()
350 s_coefficients = list()
351 valid_group_found = True
352 for j in range(len(q_vals)):
353 sofq_phi = list()
354 sph_matrix = list()
355 for _, item in det_pointer[ttheta_cvk]["AZMDict"].items():
356 # According to the `Use` flag created for each azimuthal bin
357 # in previous step, we are going to check whether there
358 # exist valid azimuthal bins in a two theta bin.
359 # NOTE: We may need to add in further check for whether the
360 # number of valid azimuthal bins is greater than the number
361 # of parameters to fit.
362 if int(item["GroupID"]) in all_sofq.keys() and item["Use"]:
363 azm_v = item["CentrAMZ"]
364 sph_row = list()
365 for l in range(sph_l_max + 1):
366 for m in range(-l, l + 1):
367 if l > 0 and m == 0:
368 continue
369 sph_row.append(
370 sph_harm_real(
371 m,
372 l,
373 ttheta_cv * np.pi / 180.,
374 azm_v * np.pi / 180.
375 )
376 )
377 sph_matrix.append(sph_row)
378 sofq_phi.append(all_sofq[int(item["GroupID"])][j])
379 else:
380 pass
381
382 sph_matrix = np.array(sph_matrix)
383 sofq_phi = np.array(sofq_phi)
384
385 if len(sph_matrix) == 0 or len(sofq_phi) == 0:
386 valid_group_found = False
387 break
388
389 num_terms = (sph_l_max + 1)**2 - sph_l_max
390 params = np.array(
391 [1 for _ in range(num_terms)]
392 )
393
394 result = minimize(
395 target_function,
396 params,
397 args=(sph_matrix, sofq_phi),
398 method='SLSQP'
399 )
400
401 optimal_params = result.x
402
403 # Calculate the S(Q) corrected, taking into account only the
404 # isotropic term.
405 y00 = sph_harm_real(0, 0, 0, 0)
406 s00 = optimal_params[0]
407 sofq_corrected.append(y00 * s00)
408
409 s_coefficients.append(optimal_params)
410
411 # Output the corrected S(Q) values and the coefficients for
412 # each two theta bin.
413 # For a certain two theta bin, if no valid azimuthal bin
414 # found for any Q value, the `valid_group_found` would be
415 # False and thus the corresponding two theta bin will be
416 # marked as invalid, i.e., not included in the valid two
417 # theta bin list `ttheta_valid`.
418 if valid_group_found:
419 # The valid two theta bin array and the isotropic
420 # corrected S(Q) array are synchronized, i.e., they
421 # share the same index, making it convenient for next
422 # step implementation.
423 sofq_corrected_first.append(sofq_corrected)
424 ttheta_valid.append(ttheta_cv)
425 with open(
426 os.path.join(
427 out_dir,
428 f"{stem_name}2theta_{ttheta_cvk}.dat"
429 ),
430 "w"
431 ) as f:
432 f.write(f"{len(q_vals)}\n")
433 f.write("\n")
434 for j in range(len(q_vals)):
435 f.write(
436 f"{q_vals[j]:.4f} {sofq_corrected[j]:.4f}\n"
437 )
438
439 with open(
440 os.path.join(
441 out_dir,
442 f"{stem_name}2theta_{ttheta_cvk}_coeffs.dat"
443 ),
444 "w"
445 ) as f:
446 for j in range(len(q_vals)):
447 f.write(f"{q_vals[j]:.4f} ")
448 for k in range(len(s_coefficients[j])):
449 f.write(
450 f"{s_coefficients[j][k]:.4f} "
451 )
452 f.write("\n")
453
454ttheta_valid = np.array(ttheta_valid)
455sofq_corrected_first = np.array(sofq_corrected_first)
456
457# =============================================================================
458# Pre-process the isotropic corrected S(Q) data corresponding to all the two
459# theta bins. Here is the thing to do:
460# 1. For each two theta bin, grab the data in a certain Q range according to
461# read-in parameters from `ttheta_group_file`.
462# 2. For each two theta bin, fill the data in the low Q range with the data
463# in the same low Q range that comes from the first two theta bin in the
464# same group.
465# =============================================================================
466print(
467 "[Info] Pre-processing (step-1) the isotropic corrected data for the "
468 "second stage correction..."
469)
470ttheta_k_valid = [
471 "{:d}".format(int(np.floor(ttheta))) for ttheta in ttheta_valid
472]
473for i, ttheta in enumerate(ttheta_valid):
474 ttheta_k = ttheta_k_valid[i]
475 for key, item in ttheta_groups.items():
476 if ttheta_k in item["QRange"].keys():
477 group = key
478 break
479
480 qrange_tmp = ttheta_groups[group]["QRange"]
481 left_b = qrange_tmp[ttheta_k]["LeftBound"]
482 leftmost_b = next(iter(qrange_tmp.items()))[1]["LeftBound"]
483 right_b_fill = left_b
484
485 # Fill the low Q range with the data from the first two theta bin
486 # in the same group.
487 ref_key_ig = next(iter(qrange_tmp))
488 ref_key_id = ttheta_k_valid.index(ref_key_ig)
489 mask_fill = (q_vals >= leftmost_b) & (q_vals < right_b_fill)
490 sofq_corrected_first[i][mask_fill] = sofq_corrected_first[
491 ref_key_id
492 ][mask_fill]
493
494# =============================================================================
495# Process each of the two theta groups separately. Here is the thing to do:
496# 1. For each group, align all the patterns according to the peak fitting.
497# 2. Within each group, we,
498# - for each two theta, figure out and remove the background, using an
499# exponential function
500# - integrate intensities in those pre-defined Q chunks
501# - for each Q chunk, fit against the spherical harmonics across two
502# theta angles, just like what we did with each single Q point when
503# correcting the phi part.
504# - extract the isotropic term for each Q chunk, calculate the ratio
505# of the original integrated intensity over the isotropic intensity
506# for each two theta value. Then rescale each Q chunk for each two
507# theta.
508# - add back the exponential background
509# =============================================================================
510print(
511 "[Info] Pre-processing (step-2) the isotropic corrected data for the "
512 "second stage correction..."
513)
514sofq_bkg_removed = dict()
515bkg_off = np.inf
516
517for tg_key, item in ttheta_groups.items():
518 print(f"[Info] Processing 2Theta group {tg_key}...")
519 pa_lb = item["PAParams"]["LeftBound"]
520 pa_rb = item["PAParams"]["RightBound"]
521 center_tmp = dict()
522 for tt_key in item["QRange"].keys():
523 if tt_key not in ttheta_k_valid:
524 continue
525 tt_index = ttheta_k_valid.index(tt_key)
526 sofq_tmp = sofq_corrected_first[tt_index]
527 mask = (q_vals >= pa_lb) & (q_vals <= pa_rb)
528 q_vals_fit = q_vals[mask]
529 sofq_fit = sofq_tmp[mask]
530
531 p_init = [np.max(sofq_fit), (pa_lb + pa_rb) / 2., 1.]
532
533 if testing:
534 center_tmp[tt_key] = 0.
535 else:
536 try:
537 popt, _ = curve_fit(
538 gaussian,
539 q_vals_fit,
540 sofq_fit,
541 p0=p_init
542 )
543 center_tmp[tt_key] = popt[1]
544 except RuntimeError:
545 center_tmp[tt_key] = 0.
546
547 # center_ref = next(iter(center_tmp.items()))[1]
548 center_ref = 5.06
549 for tt_key, c_item in center_tmp.items():
550 # According to the peak fitting failure handling just above, the
551 # corresponding peak center would be set to 0.
552 if c_item == 0.:
553 offset = 0.
554 else:
555 offset = center_ref - c_item
556
557 tt_index = ttheta_k_valid.index(tt_key)
558 q_vals_tmp = q_vals + offset
559 sofq_tmp = sofq_corrected_first[tt_index].copy()
560 q_bin_s = q_vals[1] - q_vals[0]
561 if offset < 0:
562 while q_vals_tmp[-1] < q_vals[-1]:
563 q_vals_tmp = np.append(
564 q_vals_tmp,
565 q_vals_tmp[-1] + q_bin_s
566 )
567 sofq_tmp = np.append(
568 sofq_tmp,
569 sofq_corrected_first[tt_index][-1]
570 )
571 else:
572 while q_vals_tmp[0] > q_vals[0]:
573 q_vals_tmp = np.insert(
574 q_vals_tmp,
575 0,
576 q_vals_tmp[0] - q_bin_s
577 )
578 sofq_tmp = np.insert(
579 sofq_tmp,
580 0,
581 sofq_corrected_first[tt_index][0]
582 )
583 _, sofq_rebin = rebin(
584 q_vals_tmp,
585 sofq_tmp,
586 q_vals[0],
587 q_bin_s,
588 q_vals[-1]
589 )
590 sofq_corrected_first[tt_index] = sofq_rebin
591
592 for tt_key in item["QRange"].keys():
593 if tt_key not in ttheta_k_valid:
594 continue
595 tt_index = ttheta_k_valid.index(tt_key)
596 ttheta = ttheta_valid[tt_index]
597 sofq_ttheta = sofq_corrected_first[tt_index]
598
599 bkg_lb = item["QRange"][tt_key]["LeftBound"]
600 bkg_rb = item["QRange"][tt_key]["RightBound"]
601
602 # Here we output the aligned patterns.
603 # N.B. The alignment only happens internally in each of the defined
604 # groups. Therefore, a final stage alignment may still be needed to
605 # align the patterns after all steps.
606 with open(
607 os.path.join(
608 out_dir,
609 f"{stem_name}ttheta_{tt_key}_corrected_aligned.dat"
610 ),
611 "w"
612 ) as f:
613 f.write(f"{len(q_vals)}\n")
614 f.write("\n")
615 for i, q_val in enumerate(q_vals):
616 f.write("{0:10.3F}{1:20.5F}\n".format(q_val, sofq_ttheta[i]))
617
618 # We try to use the `argrelextrema` method in `scipy` to figure out
619 # local valley points on the pattern and use those valley points for
620 # fitting an exponential background.
621 mask_tmp = q_vals >= bkg_lb
622 sofq_ttheta_m = sofq_ttheta[mask_tmp]
623 q_vals_m = q_vals[mask_tmp]
624
625 # The `order` parameter here controls how 'local' the valley will be.
626 # If set to a very small value, it will reproduce the overall pattern
627 # itself. But setting the value too high may induce too few points.
628 q_min = argrelextrema(sofq_ttheta_m, np.less, order=20)
629 sofq_min = np.array([sofq_ttheta_m[item_i] for item_i in q_min[0]])
630 q_min = np.array([q_vals_m[item_i] for item_i in q_min[0]])
631
632 # For the background fitting purpose, we set all S(Q) values above
633 # the specified threshold to the value just below (inclusive) the
634 # boundary. This will get rid of the impact of those strong noise
635 # in the high Q region.
636 indices = np.where(q_min <= bkg_rb)[0]
637 max_tmp = np.max(q_min[indices])
638 up_index = np.where(q_min == max_tmp)[0][0]
639 for qm_i, qm_val in enumerate(q_min):
640 if qm_val > bkg_rb:
641 sofq_min[qm_i] = sofq_min[up_index]
642
643 # Specifying the bounding parameter for `curve_fit` helps a lot with
644 # fitting convergence.
645 p_init = [0, 0.1, 0.1]
646 bounds_l = [-np.inf, 0., 0.]
647 bounds_u = [np.inf, 1., 1.]
648 popt, pconv = curve_fit(
649 bkg_expo,
650 q_min,
651 sofq_min,
652 p0=p_init,
653 bounds=(bounds_l, bounds_u)
654 )
655
656 is_infinite = False
657 for row in pconv:
658 for element in row:
659 if np.isinf(element):
660 is_infinite = True
661 break
662
663 if is_infinite:
664 print("[Warning] Infinite covariance in the curve fitting.")
665
666 with open(
667 os.path.join(
668 out_dir,
669 f"{stem_name}ttheta_{tt_key}_corrected_bkg_pts.dat"
670 ),
671 "w"
672 ) as f:
673 f.write(f"{len(q_min)}\n")
674 f.write("\n")
675 for i, q_val in enumerate(q_min):
676 f.write(
677 "{0:10.3F}{1:20.5F}\n".format(
678 q_val, sofq_min[i]
679 )
680 )
681
682 sofq_ttheta_bkg = bkg_expo(q_vals, popt[0], popt[1], popt[2])
683 sofq_ttheta_mb = sofq_ttheta - sofq_ttheta_bkg
684 sofq_bkg_removed[tt_key] = sofq_ttheta_mb
685
686 # We want to keep a record of the background that most closely resemble
687 # 1 at high Q.
688 bkg_hq_val = bkg_expo(q_vals[-1], popt[0], popt[1], popt[2])
689 bkg_off_tmp = abs(1. - bkg_hq_val)
690 if bkg_off_tmp < bkg_off:
691 bkg_off = bkg_off_tmp
692 bkg_popt = popt
693
694 with open(
695 os.path.join(
696 out_dir,
697 f"{stem_name}ttheta_{tt_key}_corrected_bkg_rm_fittedbkg.dat"
698 ),
699 "w"
700 ) as f:
701 f.write(f"{len(q_vals)}\n")
702 f.write("\n")
703 for i, q_val in enumerate(q_vals):
704 f.write(
705 "{0:10.3F}{1:20.5F}{2:20.5F}\n".format(
706 q_val, sofq_ttheta_mb[i], sofq_ttheta_bkg[i]
707 )
708 )
709
710 # Integrate intensities inside Q chunks.
711 ttheta_groups[tg_key]["QRange"][tt_key]["QChunkInt"] = list()
712 for qv_i, q_val in enumerate(q_vals):
713 v_t = sofq_ttheta_mb[qv_i]
714 for qc_i, qc_val in enumerate(item["QChunkDef"]):
715 if qc_i == 0:
716 if q_val <= qc_val:
717 if len(item["QRange"][tt_key]["QChunkInt"]) == 0:
718 ttheta_groups[
719 tg_key
720 ]["QRange"][tt_key]["QChunkInt"].append(v_t)
721 else:
722 ttheta_groups[
723 tg_key
724 ]["QRange"][tt_key]["QChunkInt"][0] += v_t
725 break
726 else:
727 if item["QChunkDef"][qc_i - 1] < q_val <= qc_val:
728 if len(item["QRange"][tt_key]["QChunkInt"]) == qc_i:
729 in_range1 = bkg_lb <= item["QChunkDef"][qc_i - 1]
730 in_range2 = bkg_rb >= qc_val
731 if in_range1 and in_range2:
732 ttheta_groups[
733 tg_key
734 ]["QRange"][tt_key]["QChunkInt"].append(v_t)
735 else:
736 ttheta_groups[
737 tg_key
738 ]["QRange"][tt_key]["QChunkInt"].append(np.inf)
739 else:
740 ttheta_groups[
741 tg_key
742 ]["QRange"][tt_key]["QChunkInt"][-1] += v_t
743 break
744
745# Treat each Q chunk as a single 'point' and perform the isotropic correction
746# just as before.
747sph00 = sph_harm_real(0, 0, 0, 0)
748for tg_key, item in ttheta_groups.items():
749 params_all = list()
750 qchunks_len = len(item["QChunkDef"])
751 for i in range(qchunks_len):
752 sph_matrix = list()
753 sofq_ttheta = list()
754 lin_limit = item["LinLimit"][i]
755
756 num_eqs = 0
757 for tt_key in item["QRange"].keys():
758 if tt_key in ttheta_k_valid:
759 num_eqs += 1
760
761 tt_sph_l_max = 0
762 while (tt_sph_l_max + 1 + 1)**2 < num_eqs:
763 tt_sph_l_max += 1
764
765 for tt_key in item["QRange"].keys():
766 if "Factor" not in item["QRange"][tt_key].keys():
767 ttheta_groups[tg_key]["QRange"][tt_key]["Factor"] = list()
768
769 if tt_key not in ttheta_k_valid:
770 continue
771
772 tt_index = ttheta_k_valid.index(tt_key)
773 ttheta_tmp = ttheta_valid[tt_index]
774 v_t = item["QRange"][tt_key]["QChunkInt"][i]
775 if v_t == np.inf:
776 continue
777 sofq_ttheta.append(v_t)
778 sph_row = list()
779 for l in range(tt_sph_l_max + 1):
780 for m in range(-l, l + 1):
781 sph_row.append(
782 sph_harm_real(
783 m,
784 l,
785 ttheta_tmp * np.pi / 180.,
786 0.
787 )
788 )
789
790 if "Infinity" not in lin_limit:
791 sph_row.append(ttheta_tmp)
792
793 sph_matrix.append(sph_row)
794
795 sph_matrix = np.array(sph_matrix)
796 sofq_ttheta = np.array(sofq_ttheta)
797
798 if len(sph_matrix) > 0 or len(sofq_ttheta) > 0:
799 if "Infinity" in lin_limit:
800 num_terms = (tt_sph_l_max + 1)**2
801 else:
802 num_terms = (tt_sph_l_max + 1)**2 + 1
803
804 params = np.array(
805 [1 for _ in range(num_terms)]
806 )
807
808 min_b = min(sofq_ttheta) / sph00
809 max_b = max(sofq_ttheta) / sph00
810 bounds = [(min_b, max_b)]
811 if "Infinity" not in lin_limit:
812 num_b_add = num_terms - 2
813 else:
814 num_b_add = num_terms - 1
815
816 for _ in range(num_b_add):
817 bounds.append(
818 (-float('inf'), float('inf'))
819 )
820
821 if "Infinity" not in lin_limit:
822 bounds.append((lin_limit[0], lin_limit[1]))
823
824 result = minimize(
825 target_function,
826 params,
827 args=(sph_matrix, sofq_ttheta),
828 method='SLSQP',
829 bounds=bounds
830 )
831
832 optimal_params = result.x
833
834 y00 = sph_harm_real(0, 0, 0, 0)
835 s00 = optimal_params[0]
836 if "Infinity" not in lin_limit:
837 ctt = optimal_params[-1]
838 else:
839 y00 = 0.
840 s00 = 0.
841
842 # Calculate the ratio of the original integrated intensity over
843 # the isotropic intensity.
844 for tt_key in item["QRange"].keys():
845 if tt_key not in ttheta_k_valid:
846 continue
847
848 if i == 0:
849 factor_v = 1.
850 else:
851 tt_index = ttheta_k_valid.index(tt_key)
852 ttheta_tmp = ttheta_valid[tt_index]
853 if "Infinity" in lin_limit:
854 top_val = y00 * s00
855 else:
856 top_val = y00 * s00 + ctt * ttheta_tmp
857
858 bottom_val = item["QRange"][tt_key]["QChunkInt"][i]
859 factor_v = top_val / bottom_val
860
861 # if tg_key == "Group-1":
862 # if i == 2:
863 # print("Debugging -> ")
864 # print(
865 # f"fit val: {top_val}, init val: {bottom_val}",
866 # f"y00: {y00}, s00: {s00}",
867 # f"tt: {ttheta_tmp}"
868 # )
869 # print(
870 # f"fit val: {top_val}, init val: {bottom_val}",
871 # f"y00: {y00}, s00: {s00}, ctt: {ctt}",
872 # f"tt: {ttheta_tmp}"
873 # )
874
875 ttheta_groups[tg_key]["QRange"][tt_key]["Factor"].append(
876 factor_v
877 )
878
879 # Rescale the data by Q chunks according to the ratio obtained above.
880 for tt_key in item["QRange"].keys():
881 if tt_key not in ttheta_k_valid:
882 continue
883
884 for i, q_val in enumerate(q_vals):
885 for ii, qc_val in enumerate(item["QChunkDef"]):
886 if ii > 0:
887 # if q_vals[ii - 1] < q_val <= qc_val and q_val > 4.:
888 if q_vals[ii - 1] < q_val <= qc_val:
889 sofq_bkg_removed[tt_key][i] *= item[
890 "QRange"
891 ][tt_key]["Factor"][ii]
892
893 break
894
895 # Add back in the exponential background.
896 bkg_add = bkg_expo(q_vals, bkg_popt[0], bkg_popt[1], bkg_popt[2])
897 sofq_final = sofq_bkg_removed[tt_key] + bkg_add
898
899 with open(
900 os.path.join(
901 out_dir,
902 f"{stem_name}ttheta_{tt_key}_corrected_wo_bkg.dat"
903 ),
904 "w"
905 ) as f:
906 f.write(f"{len(q_vals)}\n")
907 f.write("\n")
908 for i, q_val in enumerate(q_vals):
909 f.write(
910 "{0:10.3F}{1:20.5F}\n".format(
911 q_val, sofq_bkg_removed[tt_key][i]
912 )
913 )
914
915 with open(
916 os.path.join(
917 out_dir,
918 f"{stem_name}ttheta_{tt_key}_corrected_final.dat"
919 ),
920 "w"
921 ) as f:
922 f.write(f"{len(q_vals)}\n")
923 f.write("\n")
924 for i, q_val in enumerate(q_vals):
925 f.write(
926 "{0:10.3F}{1:20.5F}\n".format(
927 q_val, sofq_final[i]
928 )
929 )
930
931with open(os.path.join(out_dir, f"{stem_name}_ttheta_1na_2a.json"), "w") as f:
932 json.dump(ttheta_groups, f, indent=4)
933
934print("\n=========================================================")
935print("[Info] Done with the two-stage isotropic S(Q) correction.")
936print("[Info] Proceed to merge the corrected S(Q) data by bank.")
937print("=========================================================\n")
938
939with open("output_group.json", "r") as f:
940 group_dict = json.load(f)
941
942bank_dict = dict()
943for key in ttheta_k_valid:
944 for keykey in ttheta_groups.keys():
945 if key in ttheta_groups[keykey]["QRange"].keys():
946 key_group = keykey
947 break
948
949 file_t = os.path.join(
950 out_dir,
951 f"{stem_name}ttheta_{key}_corrected_final.dat"
952 )
953 if os.path.exists(file_t):
954 y_tmp = np.loadtxt(file_t, skiprows=2, dtype=float)[:, 1]
955 for gkey, item in group_dict.items():
956 if item[0] <= float(key) < item[1]:
957 bank = gkey
958 break
959
960 if bank not in bank_dict:
961 bank_dict[bank] = {
962 "y": y_tmp,
963 "num_bands": 1,
964 "q_counts": [0 for _ in range(len(q_vals))]
965 }
966 else:
967 bank_dict[bank]["y"] += y_tmp
968 bank_dict[bank]["num_bands"] += 1
969
970 q_chunk_tmp = ttheta_groups[key_group]["QChunkDef"]
971 for i, q_val in enumerate(q_vals):
972 for ii, qc_val in enumerate(q_chunk_tmp):
973 if ii == 0:
974 if q_val <= qc_val:
975 q_chunk_id = ii
976 break
977 else:
978 if q_chunk_tmp[ii - 1] < q_val <= qc_val:
979 q_chunk_id = ii
980 break
981
982 if q_chunk_id == 0:
983 factor = 1.
984 else:
985 factor = ttheta_groups[
986 key_group
987 ]["QRange"][key]["Factor"][q_chunk_id]
988
989 if factor > 0.:
990 bank_dict[bank]["q_counts"][i] += 1
991
992for key, item in bank_dict.items():
993 for i in range(len(q_vals)):
994 if item["q_counts"][i] > 0:
995 item["y"][i] /= item["q_counts"][i]
996 else:
997 item["y"][i] /= item["num_bands"]
998
999 with open(
1000 os.path.join(out_dir, f"{stem_name}{key}_merged_1na_2a_ave.dat"),
1001 "w"
1002 ) as f:
1003 f.write(f"{len(q_vals)}\n")
1004 f.write("\n")
1005 for i, q_val in enumerate(q_vals):
1006 f.write("{0:10.3F}{1:20.5F}\n".format(q_val, item["y"][i]))
1007
1008print("=========================================================")
1009print("[Info] Done with merging the corrected S(Q) by banks.")
1010print("=========================================================\n")