Source code for epyr.baseline.selection

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Region selection utilities for baseline correction.

This module provides functions for creating and managing baseline regions
for both 1D and 2D EPR data.
"""

from typing import List, Optional, Tuple

import numpy as np


[docs] def create_region_mask_1d( x: np.ndarray, regions: List[Tuple[float, float]], mode: str = "exclude" ) -> np.ndarray: """Boolean mask for 1D data, marking which points to keep for a fit. Parameters ---------- x : np.ndarray Field or time axis. regions : list of (float, float) ``[(x_low, x_high), ...]``. Bounds are inclusive; order within a pair does not matter. mode : {'exclude', 'include'}, optional ``'exclude'`` masks out the listed regions (default); ``'include'`` keeps only those regions. Returns ------- np.ndarray of bool ``True`` where the point should be used for fitting. Examples -------- >>> import numpy as np >>> from epyr.baseline import create_region_mask_1d >>> x = np.arange(10) >>> mask = create_region_mask_1d(x, [(3, 5)], mode="exclude") >>> mask.astype(int) array([1, 1, 1, 0, 0, 0, 1, 1, 1, 1]) """ if mode == "exclude": mask = np.ones(len(x), dtype=bool) for x1, x2 in regions: mask &= ~((x >= min(x1, x2)) & (x <= max(x1, x2))) elif mode == "include": mask = np.zeros(len(x), dtype=bool) for x1, x2 in regions: mask |= (x >= min(x1, x2)) & (x <= max(x1, x2)) else: raise ValueError("mode must be 'exclude' or 'include'") return mask
[docs] def create_region_mask_2d( X: np.ndarray, Y: np.ndarray, regions: List[Tuple[Tuple[float, float], Tuple[float, float]]], mode: str = "exclude", ) -> np.ndarray: """Boolean mask for 2D data, marking which points to keep for a fit. Parameters ---------- X, Y : np.ndarray Coordinate meshgrids, identical shape. regions : list of ((float, float), (float, float)) ``[((x_lo, x_hi), (y_lo, y_hi)), ...]``. Inclusive bounds. mode : {'exclude', 'include'}, optional ``'exclude'`` masks out the listed rectangles (default); ``'include'`` keeps only those rectangles. Returns ------- np.ndarray of bool Same shape as ``X``. ``True`` where the point should be used. Examples -------- >>> import numpy as np >>> from epyr.baseline import create_region_mask_2d >>> X, Y = np.meshgrid(np.arange(5), np.arange(5)) >>> mask = create_region_mask_2d(X, Y, [((1, 3), (1, 3))], mode="exclude") >>> int(mask.sum()) # 25 - 3*3 inner block 16 """ if mode == "exclude": mask = np.ones_like(X, dtype=bool) for (x1, x2), (y1, y2) in regions: x1, x2 = min(x1, x2), max(x1, x2) y1, y2 = min(y1, y2), max(y1, y2) mask &= ~((X >= x1) & (X <= x2) & (Y >= y1) & (Y <= y2)) elif mode == "include": mask = np.zeros_like(X, dtype=bool) for (x1, x2), (y1, y2) in regions: x1, x2 = min(x1, x2), max(x1, x2) y1, y2 = min(y1, y2), max(y1, y2) mask |= (X >= x1) & (X <= x2) & (Y >= y1) & (Y <= y2) else: raise ValueError("mode must be 'exclude' or 'include'") return mask
[docs] def create_center_exclusion_mask_1d( x: np.ndarray, center_fraction: float = 0.3 ) -> np.ndarray: """ Create a mask that excludes the center portion of 1D data. This is useful for CW EPR spectra where the signal is in the center and we want to fit the baseline using the wings. Args: x: X-coordinate array center_fraction: Fraction of the data range to exclude from center Returns: Boolean mask array (True = use for fitting, False = exclude) """ x_min, x_max = x.min(), x.max() x_range = x_max - x_min center = (x_min + x_max) / 2 exclude_half_width = (center_fraction * x_range) / 2 exclude_min = center - exclude_half_width exclude_max = center + exclude_half_width mask = (x < exclude_min) | (x > exclude_max) return mask
[docs] def create_center_exclusion_mask_2d( X: np.ndarray, Y: np.ndarray, center_fraction: float = 0.3 ) -> np.ndarray: """ Create a mask that excludes the center portion of 2D data. Args: X: X-coordinate meshgrid Y: Y-coordinate meshgrid center_fraction: Fraction of the data range to exclude from center Returns: Boolean mask array (True = use for fitting, False = exclude) """ # X-direction exclusion x_min, x_max = X.min(), X.max() x_range = x_max - x_min x_center = (x_min + x_max) / 2 x_exclude_half_width = (center_fraction * x_range) / 2 # Y-direction exclusion y_min, y_max = Y.min(), Y.max() y_range = y_max - y_min y_center = (y_min + y_max) / 2 y_exclude_half_width = (center_fraction * y_range) / 2 # Create exclusion mask (exclude center rectangle) x_mask = (X < (x_center - x_exclude_half_width)) | ( X > (x_center + x_exclude_half_width) ) y_mask = (Y < (y_center - y_exclude_half_width)) | ( Y > (y_center + y_exclude_half_width) ) # Include point if it's outside the center region in either x OR y mask = x_mask | y_mask return mask
[docs] def create_edge_exclusion_mask_1d( x: np.ndarray, exclude_initial: int = 0, exclude_final: int = 0 ) -> np.ndarray: """ Create a mask that excludes points at the beginning and end of 1D data. This is useful for time-series data where initial/final points may have artifacts or noise. Args: x: X-coordinate array exclude_initial: Number of initial points to exclude exclude_final: Number of final points to exclude Returns: Boolean mask array (True = use for fitting) """ mask = np.ones(len(x), dtype=bool) if exclude_initial > 0: mask[:exclude_initial] = False if exclude_final > 0: mask[-exclude_final:] = False return mask
[docs] def combine_masks(*masks: np.ndarray) -> np.ndarray: """ Combine multiple boolean masks using logical AND. Args: masks: Variable number of boolean mask arrays Returns: Combined boolean mask (True where ALL masks are True) """ if not masks: raise ValueError("At least one mask must be provided") combined = masks[0].copy() for mask in masks[1:]: combined &= mask return combined
[docs] def get_baseline_regions_1d( x: np.ndarray, y: np.ndarray, exclude_center: bool = True, center_fraction: float = 0.3, exclude_initial: int = 0, exclude_final: int = 0, manual_regions: Optional[List[Tuple[float, float]]] = None, region_mode: str = "exclude", ) -> np.ndarray: """ Build a 1D baseline mask combining all exclusion criteria. Args: x: X-coordinate array y: Y-data array exclude_center: Whether to exclude center region center_fraction: Fraction of center to exclude exclude_initial: Number of initial points to exclude exclude_final: Number of final points to exclude manual_regions: List of manually specified regions region_mode: How to handle manual_regions ('exclude' or 'include') Returns: Boolean mask array for baseline fitting """ masks = [] # Edge exclusion mask if exclude_initial > 0 or exclude_final > 0: edge_mask = create_edge_exclusion_mask_1d(x, exclude_initial, exclude_final) masks.append(edge_mask) # Center exclusion mask if exclude_center: center_mask = create_center_exclusion_mask_1d(x, center_fraction) masks.append(center_mask) # Manual regions mask if manual_regions: manual_mask = create_region_mask_1d(x, manual_regions, region_mode) masks.append(manual_mask) # Combine all masks if masks: return combine_masks(*masks) else: # No exclusions, use all points return np.ones(len(x), dtype=bool)
[docs] def get_baseline_regions_2d( X: np.ndarray, Y: np.ndarray, Z: np.ndarray, exclude_center: bool = True, center_fraction: float = 0.3, manual_regions: Optional[ List[Tuple[Tuple[float, float], Tuple[float, float]]] ] = None, region_mode: str = "exclude", ) -> np.ndarray: """ Build a 2D baseline mask combining all exclusion criteria. Args: X: X-coordinate meshgrid Y: Y-coordinate meshgrid Z: Z-data array exclude_center: Whether to exclude center region center_fraction: Fraction of center to exclude manual_regions: List of manually specified regions region_mode: How to handle manual_regions ('exclude' or 'include') Returns: Boolean mask array for baseline fitting """ masks = [] # Center exclusion mask if exclude_center: center_mask = create_center_exclusion_mask_2d(X, Y, center_fraction) masks.append(center_mask) # Manual regions mask if manual_regions: manual_mask = create_region_mask_2d(X, Y, manual_regions, region_mode) masks.append(manual_mask) # Combine all masks if masks: return combine_masks(*masks) else: # No exclusions, use all points return np.ones_like(X, dtype=bool)
[docs] def validate_regions_1d( regions: List[Tuple[float, float]], x_min: float, x_max: float ) -> bool: """ Validate that 1D regions are within data bounds and properly formatted. Args: regions: List of region tuples x_min: Minimum x value in data x_max: Maximum x value in data Returns: True if all regions are valid Raises: ValueError: If regions are invalid """ for i, (x1, x2) in enumerate(regions): if not (x_min <= x1 <= x_max and x_min <= x2 <= x_max): raise ValueError( f"Region {i} ({x1}, {x2}) is outside data bounds ({x_min}, {x_max})" ) if x1 == x2: raise ValueError(f"Region {i} has zero width: ({x1}, {x2})") return True
[docs] def validate_regions_2d( regions: List[Tuple[Tuple[float, float], Tuple[float, float]]], x_min: float, x_max: float, y_min: float, y_max: float, ) -> bool: """ Validate that 2D regions are within data bounds and properly formatted. Args: regions: List of region tuples x_min, x_max: X data bounds y_min, y_max: Y data bounds Returns: True if all regions are valid Raises: ValueError: If regions are invalid """ for i, ((x1, x2), (y1, y2)) in enumerate(regions): if not (x_min <= x1 <= x_max and x_min <= x2 <= x_max): raise ValueError( f"Region {i} X bounds ({x1}, {x2}) " f"outside data bounds ({x_min}, {x_max})" ) if not (y_min <= y1 <= y_max and y_min <= y2 <= y_max): raise ValueError( f"Region {i} Y bounds ({y1}, {y2}) " f"outside data bounds ({y_min}, {y_max})" ) if x1 == x2 or y1 == y2: raise ValueError(f"Region {i} has zero area: (({x1}, {x2}), ({y1}, {y2}))") return True