Source code for upxo.pxtalops.twin3d

"""
twin3d.py
=========
3D voxel-space twin lamella introduction for representative grain
structure generation.

This module is the 3D counterpart of the polygon-space twin introduction
functions in :mod:`upxo.pxtalops.gssmooth2d`.  It uses the
:class:`~upxo.geoEntities.plane.Plane` class for perpendicular-distance
voxel selection and crystallographically-derived habit plane normals
(computed by
:func:`~upxo.xtalphy.crystal_orientation.compute_s3_habit_plane_3d`) to
ensure lamella geometry is physically consistent with the host grain's
crystal orientation.
"""

import numpy as np
from upxo.geoEntities.plane import Plane


[docs] def introduce_twin_lamella_3d( lgi, host_gid, host_centroid, normal, half_width, next_gid, abrupt=False, rng=None, host_coords=None, ): """ Carve a single Sigma3 twin lamella out of a host grain in a 3D lgi array. Uses :class:`~upxo.geoEntities.plane.Plane` to compute perpendicular distances from each host-grain voxel to the habit plane, then selects voxels within *half_width* as the twin lamella region. This mirrors the production morphological pipeline (``identify_twins_gid``) but drives the plane normal from the host's crystallographic {111} habit plane instead of three randomly chosen voxel points, ensuring lamella geometry and crystal orientation remain consistent. Parameters ---------- lgi : ndarray (nx, ny, nz), int 3D labelled grain image. Modified in place. host_gid : int Grain ID of the host grain to twin. host_centroid : array-like (3,) Centroid of the host grain's voxel coordinates; used as the plane origin. normal : array-like (3,) Unit normal of the habit plane in the sample frame (typically from :func:`~upxo.xtalphy.crystal_orientation.compute_s3_habit_plane_3d`). half_width : float Lamella half-thickness in voxels. next_gid : int Grain ID to assign to the new twin region. abrupt : bool If True, truncate the lamella to one half of the host grain along an in-plane axis, mimicking EBSD-measured abrupt twins. rng : numpy.random.Generator or None Required when *abrupt* is True. host_coords : ndarray (M, 3) or None Pre-built coordinate array of host-grain voxels. When provided the O(N_total_voxels) ``np.argwhere`` scan is skipped entirely, giving a large speedup for large domains. If None the scan is performed as a fallback. Returns ------- twin_voxels : ndarray (N, 3) or None Global voxel coordinates of the twin region, or None if no voxels were selected. """ coords = host_coords if host_coords is not None else np.argwhere(lgi == host_gid) if coords.shape[0] == 0: return None plane = Plane(point=host_centroid, normal=normal) dist = plane.calc_perp_distances(coords.astype(float), signed=True) sel = np.abs(dist) <= half_width if abrupt and np.any(sel) and rng is not None: arbitrary = np.array([1.0, 0.0, 0.0]) if abs(np.dot(arbitrary, normal)) > 0.9: arbitrary = np.array([0.0, 1.0, 0.0]) in_plane = np.cross(normal, arbitrary) in_plane = in_plane / np.linalg.norm(in_plane) extent = (coords[sel].astype(float) - host_centroid) @ in_plane cutoff = np.median(extent) side = rng.choice([-1.0, 1.0]) keep_local = side * (extent - cutoff) >= 0 sel_idx = np.where(sel)[0] sel[sel_idx[~keep_local]] = False if not np.any(sel): return None twin_voxels = coords[sel] lgi[twin_voxels[:, 0], twin_voxels[:, 1], twin_voxels[:, 2]] = next_gid return twin_voxels