|
| 1 | +# %% [markdown] |
| 2 | +# # Ineherent correction error |
| 3 | +# |
| 4 | +# There is an inherent error in the correction due to the finite width |
| 5 | +# of the detectors pixels. The inside and outside edges of the pixel |
| 6 | +# will correct to slightly different 2θ and ɸ values. When this exceeds |
| 7 | +# the target resolution there is no longer a gain from integrating |
| 8 | +# to higher ɸ/z. |
| 9 | + |
| 10 | +# %% |
| 11 | +import matplotlib as mpl |
| 12 | +import matplotlib.pyplot as plt |
| 13 | +import numpy as np |
| 14 | +from matplotlib.transforms import blended_transform_factory |
| 15 | +from multihead.config import AnalyzerConfig |
| 16 | +from multihead.corrections import tth_from_z |
| 17 | + |
| 18 | +from hrd_tools.detector_stats import detectors |
| 19 | + |
| 20 | + |
| 21 | +# %% |
| 22 | +# Configuration and setup for plotting |
| 23 | +cfg = AnalyzerConfig( |
| 24 | + 910, |
| 25 | + 120, |
| 26 | + np.rad2deg(np.arcsin(0.8 / (2 * 3.1355))), |
| 27 | + 2 * np.rad2deg(np.arcsin(0.8 / (2 * 3.1355))), |
| 28 | + detector_roll=0, |
| 29 | +) |
| 30 | + |
| 31 | +# Use all available detectors |
| 32 | +selected_detectors = list(detectors.keys()) |
| 33 | + |
| 34 | +# Calculate detector properties (all in mm) |
| 35 | +det_props = {} |
| 36 | +for name in selected_detectors: |
| 37 | + det = detectors[name] |
| 38 | + width = det.sensor_shape[1] * det.pixel_pitch * 1e-3 # convert µm to mm |
| 39 | + pixel_size = det.pixel_pitch * 1e-3 # convert µm to mm |
| 40 | + det_props[name] = {"width": width, "pixel_size": pixel_size} |
| 41 | +print(det_props) |
| 42 | + |
| 43 | +# %% |
| 44 | +# Plot 1: Error vs z position for different 2theta values |
| 45 | +# Group detectors by pixel size |
| 46 | +pixel_size_groups = {} |
| 47 | +for det_name in selected_detectors: |
| 48 | + pixel_size = det_props[det_name]["pixel_size"] |
| 49 | + if pixel_size not in pixel_size_groups: |
| 50 | + pixel_size_groups[pixel_size] = [] |
| 51 | + pixel_size_groups[pixel_size].append(det_name) |
| 52 | + |
| 53 | +# Sort pixel sizes for consistent ordering |
| 54 | +sorted_pixel_sizes = sorted(pixel_size_groups.keys()) |
| 55 | + |
| 56 | +# Create subplots - one per pixel size |
| 57 | +n_pixel_sizes = len(sorted_pixel_sizes) |
| 58 | +fig1, axes1 = plt.subplots( |
| 59 | + 1, |
| 60 | + n_pixel_sizes, |
| 61 | + layout="constrained", |
| 62 | + figsize=(4 * n_pixel_sizes, 4), |
| 63 | + dpi=100, |
| 64 | + sharey=True, |
| 65 | + sharex=True, |
| 66 | +) |
| 67 | + |
| 68 | +# Handle case where there's only one pixel size |
| 69 | +if n_pixel_sizes == 1: |
| 70 | + axes1 = [axes1] |
| 71 | + |
| 72 | +tth_arm_values = [10, 30, 60, 90] # degrees |
| 73 | +tth_colors = mpl.colormaps["viridis"](np.linspace(0, 1, len(tth_arm_values))) |
| 74 | + |
| 75 | +# Find the largest detector width to set the plotting range (in mm) |
| 76 | +max_width = max(det_props[det]["width"] for det in selected_detectors) |
| 77 | + |
| 78 | +for subplot_idx, pixel_size in enumerate(sorted_pixel_sizes): |
| 79 | + ax = axes1[subplot_idx] |
| 80 | + |
| 81 | + # Create z positions at pixel edges from center to edge of detector (in mm) |
| 82 | + z_positions = np.arange(0, max_width / 2 + pixel_size, pixel_size) |
| 83 | + |
| 84 | + # Calculate correction for all tth values and positions at once |
| 85 | + (tth_all), (phi_all) = tth_from_z( |
| 86 | + z_positions.reshape(1, -1), |
| 87 | + np.array(tth_arm_values).reshape(-1, 1), |
| 88 | + cfg, |
| 89 | + ) |
| 90 | + |
| 91 | + # Calculate the difference between adjacent positions (pixel edge effect) |
| 92 | + delta_2theta = np.abs(tth_all[:, 1:] - tth_all[:, :-1]) |
| 93 | + |
| 94 | + # Z positions corresponding to pixel centers (in mm) |
| 95 | + z_pos = z_positions[:-1] + pixel_size / 2 |
| 96 | + |
| 97 | + # Plot lines for different scatter 2theta values |
| 98 | + for i, tth in enumerate(tth_arm_values): |
| 99 | + ax.plot( |
| 100 | + z_pos, |
| 101 | + delta_2theta[i], |
| 102 | + color=tth_colors[i], |
| 103 | + label=f"$2\\Theta$ = {tth}°", |
| 104 | + linewidth=2, |
| 105 | + ) |
| 106 | + |
| 107 | + # Create blended transform: data coords for x, axes coords for y |
| 108 | + trans = blended_transform_factory(ax.transData, ax.transAxes) |
| 109 | + |
| 110 | + # Add vertical lines for detectors with this pixel size |
| 111 | + for det_name in pixel_size_groups[pixel_size]: |
| 112 | + det_width = det_props[det_name]["width"] |
| 113 | + ax.axvline(x=det_width / 2, color="gray", linestyle="--", alpha=0.7) |
| 114 | + |
| 115 | + # Add detector name annotation at top of axes |
| 116 | + ax.annotate( |
| 117 | + det_name, |
| 118 | + xy=(det_width / 2, 1.0), |
| 119 | + xytext=(2, -2), |
| 120 | + textcoords="offset points", |
| 121 | + xycoords=trans, |
| 122 | + color="gray", |
| 123 | + fontsize=8, |
| 124 | + rotation=90, |
| 125 | + va="top", |
| 126 | + ha="left", |
| 127 | + ) |
| 128 | + |
| 129 | + ax.set_title(f"{pixel_size * 1e3:.0f} µm pixel") |
| 130 | + ax.grid(True, alpha=0.3) |
| 131 | + |
| 132 | + # Only add legend to first subplot |
| 133 | + if subplot_idx == 0: |
| 134 | + ax.legend() |
| 135 | + ax.set_ylabel("2θ correction error (deg)") |
| 136 | + |
| 137 | + ax.set_xlabel("Distance from detector center (mm)") |
| 138 | + |
| 139 | +fig1.suptitle("Inherent 2θ correction error vs 2Θ angle") |
| 140 | +plt.show() |
| 141 | + |
0 commit comments