Rare-event identification ========================= Overview -------- Rare-event identification (REI) locates spatially coherent regions of a CPFE field that are extreme in some quantity (e.g. high Nye-tensor norm, high von Mises stress). It is driven by :class:`~graintrace.IdentifyRareClusters`, which runs a two-stage clustering: an over-segmenting graph pass (:class:`~graintrace.GraphSpatialCluster`) followed by a hierarchical merge in feature space (:class:`~graintrace.ClusterAnalysisIndicator`), then selects the rare merged clusters and exports them to VTK. Why two stages -------------- A single clustering cannot answer the question REI asks. Partitioning the field purely in *feature* space finds the extreme values but scatters them across the domain, so the result is a histogram tail rather than a region. Partitioning purely in *space* finds compact blobs that need not be extreme in anything. REI therefore runs one pass of each, in that order: an over-segmenting graph pass constrained by the spatial graph, which guarantees every stage-1 cluster is a connected neighbourhood, then a feature-space merge over the *reduced* clusters, which is what makes the second pass affordable -- :math:`10^6` field points become :math:`10^3`--:math:`10^4` cluster means before any :math:`O(n^2)` work happens. The cost asymmetry is the design constraint. Stage 2 evaluates a pairwise distance matrix with a Python-callable metric, so its work grows as the square of the number of stage-1 clusters; :math:`\gamma` in stage 1 is therefore as much a budget knob as a physical one. Method ------ Points are compared through a **similarity metric** on their field features (:class:`~graintrace.SimilarityMetricLibrary`). Every metric is a *distance*: smaller means more alike, and the weight kernels of :doc:`segmentation` invert it. Built-in metrics are .. list-table:: :header-rows: 1 :widths: 26 46 28 * - Metric - :math:`d(\mathbf{u}, \mathbf{v})` - Feature columns * - ``nye_tensor_norm`` - :math:`\lVert \mathbf{A}_i - \mathbf{A}_j \rVert_F` - the nine ``nye_tensor_ij`` * - ``von_mises_stress`` - relative difference, :eq:`rei-vm` - six stress components * - ``misorientation`` - disorientation angle, :eq:`seg-disorientation` - three orientation components * - ``abs_scalar_diff`` - :math:`\lvert u_1 - v_1 \rvert` - exactly one column The tensor metric is the Frobenius norm of the *difference*, not the difference of the norms, so it separates two points whose Nye tensors have equal magnitude but different structure. The von Mises metric is **relative**, not absolute: .. math:: :label: rei-vm d = \frac{\lvert \sigma^{vM}_i - \sigma^{vM}_j\rvert} {\lvert \sigma^{vM}_i\rvert + \lvert \sigma^{vM}_j\rvert + \varepsilon}, \qquad \sigma^{vM} = \sqrt{\tfrac{1}{2}\!\left[(\sigma_{xx}-\sigma_{yy})^2 + (\sigma_{yy}-\sigma_{zz})^2 + (\sigma_{zz}-\sigma_{xx})^2\right] + 3(\sigma_{xy}^2+\sigma_{yz}^2+\sigma_{xz}^2)}, with :math:`\varepsilon = 10^{-8}` fixed in the implementation. Normalising by the sum of the two magnitudes makes :math:`d` dimensionless and bounded on :math:`[0, 1]`, so a stage-2 ``threshold`` carries over between load steps and between materials. The cost is that it is *scale-free*: a 1 MPa step beside a 2 MPa neighbour is as distant as 500 beside 1000. If the absolute level is what matters, drive the run from ``abs_scalar_diff`` on a precomputed von Mises column instead. **Stage 1 (over-segment).** A spatial graph over the field points (grid connectivity at a Manhattan radius, or mutual kNN) carries the metric distance on each edge, mapped to a weight by one of the kernels in :doc:`segmentation`, and is partitioned by Leiden into many small, spatially compact clusters. Each cluster :math:`C` is reduced to .. math:: :label: rei-reduce n_C = |C|, \qquad \bar{x}_C = \frac{1}{n_C}\sum_{i \in C} x_i, \qquad \overline{f}_C = \frac{1}{n_C}\sum_{i \in C} f_i, one ``