.. DO NOT EDIT. .. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. .. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: .. "auto_examples/Interactive/plot_ipf_explorer.py" .. LINE NUMBERS ARE GIVEN BELOW. .. only:: html .. note:: :class: sphx-glr-download-link-note :ref:`Go to the end ` to download the full example code. .. rst-class:: sphx-glr-example-title .. _sphx_glr_auto_examples_Interactive_plot_ipf_explorer.py: Inverse Pole Figure (IPF) Explorer ================================== An EBSD-style orientation explorer for a synthetic polycrystal: * **Left panel** — IPF-Z orientation map, colored with the standard cubic IPF key (red = ⟨001⟩, green = ⟨011⟩, blue = ⟨111⟩). Rendered as a true-color RGB image. * **Right panel** — the *reduced 3-D inverse pole figure*: every grain's sample-Z direction, expressed in crystal coordinates and folded into the cubic fundamental sector, plotted as an IPF-colored point cloud on a shaded, wireframed unit sphere. Drag the crosshair on the map: the grain's orientation is marked with a highlighted dot on the sphere, and the sphere **rotates so that direction faces you**. Drag on the sphere to orbit freely; the next crosshair move re-aims the camera. .. GENERATED FROM PYTHON SOURCE LINES 20-126 .. raw:: html
.. raw:: html .. code-block:: Python import numpy as np import anyplotlib as apl rng = np.random.default_rng(42) # ── 1. Synthetic polycrystal: nearest-seed grain map ──────────────────────── H = W = 192 N_GRAINS = 60 seeds = rng.uniform(0, [H, W], size=(N_GRAINS, 2)) yy, xx = np.mgrid[0:H, 0:W] d2 = (yy[..., None] - seeds[:, 0]) ** 2 + (xx[..., None] - seeds[:, 1]) ** 2 grain_id = np.argmin(d2, axis=-1) # (H, W) labels # ── 2. Random orientation per grain (uniform rotations via quaternions) ───── def random_rotations(n): """Uniform random rotation matrices, shape (n, 3, 3) (Shoemake method).""" u1, u2, u3 = rng.random((3, n)) q = np.stack([ np.sqrt(1 - u1) * np.sin(2 * np.pi * u2), np.sqrt(1 - u1) * np.cos(2 * np.pi * u2), np.sqrt(u1) * np.sin(2 * np.pi * u3), np.sqrt(u1) * np.cos(2 * np.pi * u3), ], axis=1) # (n, 4) unit quats x, y, z, w = q.T return np.stack([ np.stack([1 - 2 * (y * y + z * z), 2 * (x * y - z * w), 2 * (x * z + y * w)], -1), np.stack([2 * (x * y + z * w), 1 - 2 * (x * x + z * z), 2 * (y * z - x * w)], -1), np.stack([2 * (x * z - y * w), 2 * (y * z + x * w), 1 - 2 * (x * x + y * y)], -1), ], axis=1) rotations = random_rotations(N_GRAINS) # Sample-Z expressed in each grain's crystal frame: d = Rᵀ · ẑ dirs = rotations[:, 2, :] # row 2 of R == Rᵀ·ẑ # ── 3. Reduce to the cubic fundamental sector and IPF-color ──────────────── # For cubic symmetry, sorting |components| ascending lands every direction # in the standard 001–011–111 stereographic triangle. reduced = np.sort(np.abs(dirs), axis=1) # (a ≤ b ≤ c) a, b, c = reduced.T # Classic IPF key: distance to each triangle corner → R, G, B rgb = np.stack([c - b, b - a, a], axis=1) rgb /= rgb.max(axis=1, keepdims=True) + 1e-12 # vivid normalisation grain_rgb_u8 = (rgb * 255).astype(np.uint8) # (N_GRAINS, 3) ipf_map = grain_rgb_u8[grain_id] # (H, W, 3) true-color # ── 4. Figure: RGB map + reduced 3-D IPF point cloud ─────────────────────── fig, (ax_map, ax_ipf) = apl.subplots( 1, 2, figsize=(880, 420), help="Drag the crosshair: the sphere rotates to face that grain's\n" "crystal direction. Drag the sphere to orbit freely.") vmap = ax_map.imshow(ipf_map) # (H, W, 3) → RGB vmap.set_title("IPF-Z orientation map") cross = vmap.add_widget("crosshair", cx=W // 2, cy=H // 2, color="#ffffff") # reduced directions live on the unit sphere → fix bounds to keep the # origin centred and the geometry origin-true vipf = ax_ipf.scatter3d( reduced[:, 0], reduced[:, 1], reduced[:, 2], colors=grain_rgb_u8, point_size=6, x_label="[100]", y_label="[010]", z_label="[001]", bounds=((-1, 1),) * 3, zoom=1.4, ) vipf.set_title("Reduced 3D IPF (cubic fundamental sector)") # Shaded unit sphere with lat/long wireframe behind the direction vectors vipf.set_sphere(1.0) # ── 5. Crosshair → highlight + rotate-to-face ─────────────────────────────── def face_camera(v): """(azimuth°, elevation°) that aim the camera straight down *v*. With the turntable camera, the view faces unit vector ``v`` when ``el = asin(vz)`` and ``az = atan2(vx, -vy)``. """ vx, vy, vz = v el = np.degrees(np.arcsin(np.clip(vz, -1.0, 1.0))) az = np.degrees(np.arctan2(vx, -vy)) return az, el def show_orientation(gid: int) -> None: v = reduced[gid] vipf.set_highlight(*v, color="#ffffff", size=8) az, el = face_camera(v) vipf.set_view(azimuth=az, elevation=el) @cross.add_event_handler("pointer_move") def on_move(event): ix = int(np.clip(round(cross.cx), 0, W - 1)) iy = int(np.clip(round(cross.cy), 0, H - 1)) show_orientation(int(grain_id[iy, ix])) show_orientation(int(grain_id[H // 2, W // 2])) fig # Interactive .. rst-class:: sphx-glr-timing **Total running time of the script:** (0 minutes 0.699 seconds) .. _sphx_glr_download_auto_examples_Interactive_plot_ipf_explorer.py: .. only:: html .. container:: sphx-glr-footer sphx-glr-footer-example .. container:: sphx-glr-download sphx-glr-download-jupyter :download:`Download Jupyter notebook: plot_ipf_explorer.ipynb ` .. container:: sphx-glr-download sphx-glr-download-python :download:`Download Python source code: plot_ipf_explorer.py ` .. container:: sphx-glr-download sphx-glr-download-zip :download:`Download zipped: plot_ipf_explorer.zip ` .. only:: html .. rst-class:: sphx-glr-signature `Gallery generated by Sphinx-Gallery `_