Source code for spectoprep.pipeline.utils

"""
Utility functions for pipeline optimization.
"""

import itertools

import numpy as np

from spectoprep.logging import get_logger

logger = get_logger("PipelineUtils")


[docs] def choose_nearest(value: float, allowed: list[float]) -> float: """ Choose the nearest allowed value to the given value. Args: value: The input value allowed: List of allowed values Returns: The nearest allowed value """ return min(allowed, key=lambda x: abs(x - value))
[docs] def is_valid_pipeline(pipeline: tuple[str, ...], incompatible_sets: list[list[str]]) -> bool: """ Check if a pipeline configuration is valid based on incompatibility rules. Args: pipeline: Tuple of preprocessing step names incompatible_sets: List of sets, each containing mutually incompatible steps Returns: bool: True if the pipeline is valid, False otherwise """ # Convert pipeline to set for easier intersection checks pipeline_set = set(pipeline) # Check against each incompatibility set for incompatible_set in incompatible_sets: # If the intersection of pipeline and incompatible set has more than 1 element, # it means we have incompatible steps in the pipeline if len(pipeline_set.intersection(incompatible_set)) > 1: return False return True
[docs] def generate_pipeline_configurations( preprocessing_steps: list[str], incompatible_sets: list[list[str]], max_length: int = 5, allowed_lengths: int | list[int] | tuple[int, ...] | None = None, ) -> list[tuple[str, ...]]: """ Generate all valid pipeline configurations based on available steps and incompatibility rules. Args: preprocessing_steps: List of available preprocessing step names incompatible_sets: List of sets, each containing mutually incompatible steps max_length: Maximum number of steps in a pipeline allowed_lengths: Specific pipeline lengths to allow (e.g., [1, 2]) Returns: List of tuples, each representing a valid pipeline configuration """ # Normalize allowed lengths if allowed_lengths is None: allowed_lengths = list(range(1, max_length + 1)) elif isinstance(allowed_lengths, int): allowed_lengths = [allowed_lengths] # Ensure allowed lengths are within bounds allowed_lengths = [length for length in allowed_lengths if 1 <= length <= max_length] # Generate all possible combinations for the allowed lengths all_valid_pipelines = [] logger.info( "generating_pipeline_configurations", lengths=allowed_lengths, n_steps=len(preprocessing_steps), ) for length in allowed_lengths: # Generate all combinations of the given length combinations = list(itertools.combinations(preprocessing_steps, length)) # Filter out invalid combinations based on incompatibility rules valid_combinations = [ comb for comb in combinations if is_valid_pipeline(comb, incompatible_sets) ] all_valid_pipelines.extend(valid_combinations) logger.info("pipeline_configurations_generated", n_configs=len(all_valid_pipelines)) return all_valid_pipelines
[docs] def calculate_rmse(y_true: np.ndarray, y_pred: np.ndarray) -> float: """ Compute Root Mean Square Error (RMSE). Args: y_true: True target values y_pred: Predicted target values Returns: float: RMSE value """ return np.sqrt(np.mean((y_true - y_pred) ** 2))
[docs] def calculate_r2(y_true: np.ndarray, y_pred: np.ndarray) -> float: """ Compute the R² score. Args: y_true: True target values y_pred: Predicted target values Returns: float: R² value """ ss_res = np.sum((y_true - y_pred) ** 2) ss_tot = np.sum((y_true - np.mean(y_true)) ** 2) return 1 - ss_res / ss_tot if ss_tot != 0 else 0.0