Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-10-26 08:47:16

0001 ### Visualises eigenmodes of the alignment
0002 # problem by drawing the original and distorted
0003 # detector geometries in 2D projections.
0004 # Intended for singular / weak mode diagnostics.
0005 # Disclaimer: AI generated.
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 # Configuration
0014 # ============================================================
0015 
0016 N_LAYERS = 9
0017 
0018 # arbitrary scales for the layer spacing
0019 # and distortions - used for visualisation
0020 # purposes, adjust to taste.
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 # Projection axes: (horizontal coordinate, vertical coordinate)
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 # 1. Convert sparse eigenvector to a 9 x 6 array
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 # 2. Calculate nominal and shifted sensor planes
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     # Local sensor corners: sensor lies in the x-z plane.
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     # Translation and rotation are independently exaggerated
0094     # for visualisation; they are not physical corrections.
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 # 3. Plot one eigenmode
0107 # ============================================================
0108 
0109 
0110 def plot_mode(mode, mode_index):
0111     alignment = unpack_eigenvector(mode)
0112 
0113     # Compute each plane only once; reuse for plotting and bounds.
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     # Include every nominal and shifted corner when determining bounds.
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             # Nominal geometry: dashed grey outlines.
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             # Shifted geometry: solid layer-coloured outlines.
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             # Mark centres in the two longitudinal projections.
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         # Projection-specific limits, derived from all corners.
0180         ax.set_xlim(lower[ix], upper[ix])
0181         ax.set_ylim(lower[iy], upper[iy])
0182         ax.set_autoscale_on(False)
0183 
0184     # Indicate beam direction in the longitudinal views.
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     # Legend.
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     # Largest eigenvector coefficients.
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 # 4. Plot all eigenmodes
0247 # ============================================================
0248 
0249 
0250 def plot_all_modes(eigenvectors):
0251     for i, mode in enumerate(eigenvectors):
0252         plot_mode(mode, i)