File indexing completed on 2026-10-26 08:47:16
0001
0002
0003
0004
0005
0006
0007 import numpy as np
0008 import matplotlib.pyplot as plt
0009 from matplotlib.patches import Polygon
0010 from scipy.spatial.transform import Rotation
0011
0012
0013
0014
0015
0016 N_LAYERS = 9
0017
0018
0019
0020
0021
0022 LAYER_SPACING = 5.0
0023
0024 SENSOR_HALF_X = 2.0
0025 SENSOR_HALF_Z = 2.0
0026
0027 TRANSLATION_SCALE = 2.0
0028 ROTATION_SCALE = 30.0
0029
0030 DOF_NAMES = ["tx", "ty", "tz", "rx", "ry", "rz"]
0031
0032
0033 PROJECTIONS = [
0034 ("x-y", (0, 1), "x [a.u.]", "y [a.u.]"),
0035 ("z-y", (2, 1), "z [a.u.]", "y [a.u.]"),
0036 ("x-z", (0, 2), "x [a.u.]", "z [a.u.]"),
0037 ]
0038
0039
0040
0041
0042
0043
0044
0045 def unpack_eigenvector(mode):
0046 """
0047 mode.components: eigenvector coefficients (floats)
0048 mode.labels: 1-based global parameter labels (1..54)
0049
0050 Parameter order per layer: tx, ty, tz, rx, ry, rz.
0051 """
0052 labels = np.asarray(mode.labels, dtype=int) - 1
0053 values = np.asarray(mode.components, dtype=float)
0054
0055 if len(labels) != len(values):
0056 raise ValueError("labels and components must have equal lengths")
0057 if np.any((labels < 0) | (labels >= 54)):
0058 raise ValueError("Parameter labels must be between 1 and 54")
0059 if len(np.unique(labels)) != len(labels):
0060 raise ValueError("Duplicate parameter labels")
0061
0062 alignment = np.zeros((N_LAYERS, 6))
0063 alignment.ravel()[labels] = values
0064 return alignment
0065
0066
0067
0068
0069
0070
0071
0072 def transform_plane(alignment, layer):
0073 """Return nominal and shifted 3D corners for one layer."""
0074
0075 hx, hz = SENSOR_HALF_X, SENSOR_HALF_Z
0076
0077
0078 corners = np.array(
0079 [
0080 [-hx, 0, -hz],
0081 [hx, 0, -hz],
0082 [hx, 0, hz],
0083 [-hx, 0, hz],
0084 ],
0085 dtype=float,
0086 )
0087
0088 y0 = layer * LAYER_SPACING
0089 centre = np.array([0.0, y0, 0.0])
0090
0091 nominal = corners + centre
0092
0093
0094
0095 translation = alignment[layer, :3] * TRANSLATION_SCALE
0096 angles = alignment[layer, 3:] * ROTATION_SCALE
0097
0098 rotation = Rotation.from_euler("xyz", angles, degrees=True)
0099
0100 shifted = rotation.apply(corners) + centre + translation
0101
0102 return nominal, shifted
0103
0104
0105
0106
0107
0108
0109
0110 def plot_mode(mode, mode_index):
0111 alignment = unpack_eigenvector(mode)
0112
0113
0114 planes = [transform_plane(alignment, layer) for layer in range(N_LAYERS)]
0115
0116 nominal_planes = [p[0] for p in planes]
0117 shifted_planes = [p[1] for p in planes]
0118
0119
0120 all_points = np.concatenate(nominal_planes + shifted_planes)
0121 margin = 1.0
0122
0123 lower = all_points.min(axis=0) - margin
0124 upper = all_points.max(axis=0) + margin
0125
0126 fig, axes = plt.subplots(1, 3, figsize=(17, 8))
0127 colors = plt.get_cmap("tab10")(np.arange(N_LAYERS))
0128
0129 for ax, (name, (ix, iy), xlabel, ylabel) in zip(axes, PROJECTIONS):
0130 for layer, (nominal, shifted) in enumerate(planes):
0131
0132 ax.add_patch(
0133 Polygon(
0134 nominal[:, [ix, iy]],
0135 closed=True,
0136 fill=False,
0137 edgecolor="0.55",
0138 linestyle="--",
0139 alpha=0.7,
0140 )
0141 )
0142
0143
0144 ax.add_patch(
0145 Polygon(
0146 shifted[:, [ix, iy]],
0147 closed=True,
0148 fill=False,
0149 edgecolor=colors[layer],
0150 linewidth=2,
0151 )
0152 )
0153
0154
0155 if name != "x-z":
0156 ax.plot(
0157 nominal[:, [ix, iy]].mean(axis=0)[0],
0158 nominal[:, [ix, iy]].mean(axis=0)[1],
0159 "o",
0160 color="0.55",
0161 markersize=3,
0162 )
0163 ax.plot(
0164 shifted[:, [ix, iy]].mean(axis=0)[0],
0165 shifted[:, [ix, iy]].mean(axis=0)[1],
0166 "o",
0167 color=colors[layer],
0168 markersize=4,
0169 )
0170
0171 ax.set(
0172 xlabel=xlabel,
0173 ylabel=ylabel,
0174 title=f"Projection: {name}",
0175 )
0176 ax.grid(True, alpha=0.2)
0177 ax.set_aspect("auto")
0178
0179
0180 ax.set_xlim(lower[ix], upper[ix])
0181 ax.set_ylim(lower[iy], upper[iy])
0182 ax.set_autoscale_on(False)
0183
0184
0185 for ax in axes[:2]:
0186 ax.annotate(
0187 "Beam (+y)",
0188 xy=(0.95, 0.97),
0189 xycoords="axes fraction",
0190 ha="right",
0191 va="top",
0192 )
0193
0194
0195 from matplotlib.lines import Line2D
0196
0197 fig.legend(
0198 handles=[
0199 Line2D(
0200 [0],
0201 [0],
0202 color="0.55",
0203 linestyle="--",
0204 label="Nominal geometry",
0205 ),
0206 Line2D(
0207 [0],
0208 [0],
0209 color="black",
0210 linestyle="-",
0211 linewidth=2,
0212 label="Shifted geometry",
0213 ),
0214 ],
0215 loc="lower center",
0216 ncol=2,
0217 frameon=False,
0218 )
0219
0220
0221 flat = alignment.ravel()
0222 dominant_indices = np.argsort(np.abs(flat))[::-1][:4]
0223
0224 dominant = ", ".join(
0225 f"L{i // 6 + 1}:{DOF_NAMES[i % 6]}={flat[i]:+.3f}" for i in dominant_indices
0226 )
0227
0228 eigenvalue = getattr(mode, "EV", float("nan"))
0229
0230 fig.suptitle(
0231 f"Alignment eigenmode {mode_index + 1} | EV = {eigenvalue:.4g}\n"
0232 f"Dominant coefficients: {dominant}",
0233 fontsize=11,
0234 )
0235
0236 fig.tight_layout(rect=[0, 0.08, 1, 0.89])
0237
0238 fig.savefig(
0239 f"alignment_mode_{mode_index + 1:03d}.png",
0240 dpi=180,
0241 bbox_inches="tight",
0242 )
0243
0244
0245
0246
0247
0248
0249
0250 def plot_all_modes(eigenvectors):
0251 for i, mode in enumerate(eigenvectors):
0252 plot_mode(mode, i)