Source code for Modules.UncertaintyLobes.uncertainty_lobes_mesh

"""
Mesh generation for uncertainty lobes.

This module builds wedge-shaped mesh geometry for uncertainty lobe visualization:
    - Create wedge vertices for each lobe
    - Triangulate wedge faces
    - Support dual-lobe rendering (inner + outer)
"""

import numpy as np


def _create_wedge_vertices(center, r, theta_start, theta_end, median_angle, num_points=20):
    """
    Create vertices for a wedge (circular sector).
    
    The arc is always drawn in the direction that includes the median angle,
    ensuring the wedge represents the correct angular spread.
    
    Parameters:
    -----------
    center : numpy.ndarray
        Shape (2,) - center position [x, y]
    r : float
        Radius of the wedge
    theta_start : float
        Starting angle in radians (minimum angle)
    theta_end : float
        Ending angle in radians (maximum angle)
    median_angle : float
        Median direction angle in radians (used to determine arc direction)
    num_points : int
        Number of points along the arc (default: 20)
    
    Returns:
    --------
    vertices : numpy.ndarray
        Shape (num_points + 1, 2) - vertices [center, arc_point_1, ..., arc_point_n]
    """
    # Normalize angles to [0, 2π)
    theta_start_norm = theta_start % (2 * np.pi)
    theta_end_norm = theta_end % (2 * np.pi)
    median_angle_norm = median_angle % (2 * np.pi)
    
    # Calculate the angular span in both directions
    # Direction 1: counterclockwise from start to end
    if theta_end_norm >= theta_start_norm:
        span_ccw = theta_end_norm - theta_start_norm
    else:
        span_ccw = (2 * np.pi - theta_start_norm) + theta_end_norm
    
    # Direction 2: clockwise from start to end (or ccw from end to start)
    span_cw = 2 * np.pi - span_ccw
    
    # Check which direction contains the median angle
    # Test if median is in the counterclockwise arc from start to end
    if theta_end_norm >= theta_start_norm:
        median_in_ccw = theta_start_norm <= median_angle_norm <= theta_end_norm
    else:
        # Arc wraps around 0
        median_in_ccw = median_angle_norm >= theta_start_norm or median_angle_norm <= theta_end_norm
    
    # Generate arc points in the correct direction
    if median_in_ccw:
        # Go counterclockwise from start to end
        if theta_end_norm >= theta_start_norm:
            theta_range = np.linspace(theta_start_norm, theta_end_norm, num_points)
        else:
            # Wrap around: go from start to 2π, then from 0 to end
            theta_range = np.linspace(theta_start_norm, theta_start_norm + span_ccw, num_points)
            theta_range = theta_range % (2 * np.pi)
    else:
        # Go clockwise from start to end (same as ccw from end to start)
        if theta_start_norm >= theta_end_norm:
            theta_range = np.linspace(theta_start_norm, theta_start_norm + span_cw, num_points)
            theta_range = theta_range % (2 * np.pi)
        else:
            # Wrap around the other way
            theta_range = np.linspace(theta_start_norm, theta_start_norm - span_cw, num_points)
            theta_range = theta_range % (2 * np.pi)
    
    arc_points = np.column_stack([
        center[0] + r * np.cos(theta_range),
        center[1] + r * np.sin(theta_range)
    ])
    
    # Prepend center point
    vertices = np.vstack([center, arc_points])
    
    return vertices


def _triangulate_wedge(num_points):
    """
    Create triangle indices for a wedge.
    
    Parameters:
    -----------
    num_points : int
        Number of points along the arc
    
    Returns:
    --------
    triangles : numpy.ndarray
        Shape (num_points - 1, 3) - triangle indices
    """
    triangles = []
    for i in range(num_points - 1):
        # Triangle: [center, arc_point_i, arc_point_i+1]
        triangles.append([0, i + 1, i + 2])
    
    return np.array(triangles, dtype=int)


[docs] def uncertainty_lobes_mesh(positions, stats, scale=0.2, arc_resolution=20): """ Build uncertainty lobe mesh from statistics. Creates wedge-shaped glyphs with: 1. Outer lobe (percentile1 - larger angular and magnitude spread) 2. Inner lobe (percentile2 - smaller spread, optional) 3. Median arrow direction Parameters: ----------- positions : numpy.ndarray Shape (n, 2) - lobe center positions stats : dict From uncertainty_lobes_summary_statistics() scale : float Glyph scale factor (default: 0.2) arc_resolution : int Number of points per wedge arc (default: 20) Returns: -------- mesh : dict { 'wedges': list of dicts - each containing 'vertices' and 'triangles' for outer lobe, 'inner_wedges': list of dicts - each containing 'vertices' and 'triangles' for inner lobe (if percentile2 != None), 'arrows': dict - {'positions': (n, 2), 'directions': (n, 2), 'lengths': (n,)} } """ num_positions = positions.shape[0] outer_lobe_angles = stats['outer_lobe_angles'] # (n, 2) - [min_angle, max_angle] for outer lobe inner_lobe_angles = stats['inner_lobe_angles'] # (n, 2) or None - for inner lobe median_angles = stats['median_angles'] # (n,) - median angles outer_lobe_radii = stats['outer_lobe_radii'] # (n,) - minimum magnitude (outer lobe) inner_lobe_radii = stats['inner_lobe_radii'] # (n,) - maximum magnitude (inner lobe) median_magnitudes = stats['median_magnitudes'] # (n,) - median magnitudes # Apply scale outer_lobe_radii_scaled = outer_lobe_radii * scale inner_lobe_radii_scaled = inner_lobe_radii * scale median_magnitudes_scaled = median_magnitudes * scale # Build wedges wedges = [] # Outer lobes inner_wedges = [] # Inner lobes for i in range(num_positions): # Outer lobe (percentile1) theta_start = outer_lobe_angles[i, 0] # min_angle theta_end = outer_lobe_angles[i, 1] # max_angle median = median_angles[i] # median angle vertices = _create_wedge_vertices( positions[i], outer_lobe_radii_scaled[i], theta_start, theta_end, median, arc_resolution ) triangles = _triangulate_wedge(arc_resolution) wedges.append({ 'vertices': vertices, 'triangles': triangles, 'position_idx': i }) # Inner lobe (percentile2, if provided) if inner_lobe_angles is not None and inner_lobe_radii_scaled[i] > 0.0: theta_start2 = inner_lobe_angles[i, 0] theta_end2 = inner_lobe_angles[i, 1] vertices2 = _create_wedge_vertices( positions[i], inner_lobe_radii_scaled[i], theta_start2, theta_end2, median, arc_resolution ) triangles2 = _triangulate_wedge(arc_resolution) inner_wedges.append({ 'vertices': vertices2, 'triangles': triangles2, 'position_idx': i }) # Build arrow data arrow_directions = np.column_stack([ np.cos(median_angles), np.sin(median_angles) ]) arrows = { 'positions': positions, 'directions': arrow_directions, 'lengths': median_magnitudes_scaled } return { 'wedges': wedges, 'inner_wedges': inner_wedges if len(inner_wedges) > 0 else None, 'arrows': arrows }