Source code for torchsig.signals.signal_types

"""Signal and Signal Metadata classes.
This module defines the `Signal` class and its associated functionality,
which is used to represent and manipulate signal data and metadata.

Examples:
    Signal:
        >>> from torchsig.signals import Signal
        >>> import numpy as np
        >>> data = np.array([1.0, 2.0])
        >>> new_sig = Signal(data=data)
"""

from __future__ import annotations

from typing import Any

import numpy as np

from torchsig.utils.abstractions import HierarchicalMetadataObject
from torchsig.utils.dsp import (
    bandwidth_from_lower_upper_freq,
    center_freq_from_lower_upper_freq,
    lower_freq_from_center_freq_bandwidth,
    upper_freq_from_center_freq_bandwidth,
)


[docs] class SignalMetadataObject(HierarchicalMetadataObject): """Represents metadata associated with a signal. This class extends HierarchicalMetadataObject to provide signal-specific metadata properties and calculations. """
[docs] def __init__(self, **kwargs: Any) -> None: """Initializes the SignalMetadata object. Args: **kwargs: Metadata key-value pairs to initialize the object. """ super().__init__(**kwargs)
@property def start(self) -> float: """Signal start normalized to duration of signal. Returns: float: Signal start as a percentage of total time (0-1). """ return self.start_in_samples / self.num_iq_samples_dataset @start.setter def start(self, new_start: float) -> None: """Sets signal start. Args: new_start: Signal start as a percentage of total time (0-1). """ self["start_in_samples"] = int(new_start * self.num_iq_samples_dataset) @property def stop(self) -> float: """Signal stop normalized to duration of signal. Returns: float: Signal stop as a percentage of total time (0-1). """ return self.stop_in_samples / self.num_iq_samples_dataset @stop.setter def stop(self, new_stop: float) -> None: """Sets signal stop. Args: new_stop: Signal stop as a percentage of total time (0-1). """ self["duration_in_samples"] = ( new_stop * self.num_iq_samples_dataset ) - self.start_in_samples @property def duration(self) -> float: """Signal duration normalized to 0-1.0. Returns: float: Signal duration as a percentage of total time (0-1). """ return self.duration_in_samples / self.num_iq_samples_dataset @duration.setter def duration(self, new_duration: float) -> None: """Sets the duration of the signal. Args: new_duration: Duration as a percentage of total time (0-1). """ self["duration_in_samples"] = new_duration * self.num_iq_samples_dataset @property def stop_in_samples(self) -> int: """Signal stop in samples. Returns: int: Signal stop time in samples. """ return self.start_in_samples + self.duration_in_samples @stop_in_samples.setter def stop_in_samples(self, new_stop_in_samples: int) -> None: """Sets the stop time of the signal in samples. Args: new_stop_in_samples: Stop time in samples. """ self["duration_in_samples"] = new_stop_in_samples - self.start_in_samples @property def upper_freq(self) -> float: """Calculates the upper frequency of a signal. Returns: float: Upper frequency in Hz. Raises: ValueError: If center_freq or bandwidth are not available. """ try: self["_upper_frequency"] = upper_freq_from_center_freq_bandwidth( self.center_freq, self.bandwidth ) except (AttributeError, KeyError) as e: raise ValueError( "Cannot calculate upper frequency: missing center_freq or bandwidth" ) from e else: return self._upper_frequency @upper_freq.setter def upper_freq(self, new_upper_freq: float) -> None: """Sets the upper frequency of the signal. Args: new_upper_freq: Upper frequency in Hz. """ self["_upper_frequency"] = new_upper_freq if hasattr(self, "_lower_frequency") and self._lower_frequency is not None: self["bandwidth"] = bandwidth_from_lower_upper_freq( new_upper_freq, self.lower_freq ) self["center_freq"] = center_freq_from_lower_upper_freq( new_upper_freq, self.lower_freq ) @property def lower_freq(self) -> float: """Calculates the lower frequency of a signal. Returns: float: Lower frequency in Hz. Raises: ValueError: If center_freq or bandwidth are not available. """ try: self["_lower_frequency"] = lower_freq_from_center_freq_bandwidth( self.center_freq, self.bandwidth ) except (AttributeError, KeyError) as e: raise ValueError( "Cannot calculate lower frequency: missing center_freq or bandwidth" ) from e else: return self._lower_frequency @lower_freq.setter def lower_freq(self, new_lower_freq: float) -> None: """Sets the lower frequency of the signal. Args: new_lower_freq: Lower frequency in Hz. """ self["_lower_frequency"] = new_lower_freq if hasattr(self, "_upper_frequency") and self._upper_frequency is not None: self["bandwidth"] = bandwidth_from_lower_upper_freq( self.upper_freq, new_lower_freq ) self["center_freq"] = center_freq_from_lower_upper_freq( self.upper_freq, new_lower_freq ) @property def oversampling_rate(self) -> float: """Calculates the oversampling rate for a signal. Returns: float: Oversampling rate (sample_rate / bandwidth). """ return self.sample_rate / self.bandwidth
[docs] def to_dict(self) -> dict[str, Any]: """Returns SignalMetadataExternal as a full dictionary. Returns: Dict[str, Any]: Dictionary containing all metadata attributes. """ attributes_original = self.__dict__.copy() # Start with the instance variables attributes = attributes_original.copy() # exclude certain variables for var in attributes_original: if var in [ "applied_transforms", "dataset_metadata", "_dataset_metadata", "_center_freq_set", ]: del attributes[var] return attributes
[docs] class Signal(SignalMetadataObject): """Represents a signal with data and metadata. This class extends SignalMetadataObject to include actual signal data and component signals. Args: data: Signal IQ data. Defaults to empty numpy array. component_signals: List of component signals. Defaults to empty list. **kwargs: Additional metadata key-value pairs. """
[docs] def __init__( self, data: np.ndarray | None = None, component_signals: list[Signal] = [], **kwargs: Any, ) -> None: """Initializes the Signal with data and metadata. Args: data: Signal IQ data. Defaults to np.array([]). component_signals: List of component signals. Defaults to []. **kwargs: Additional metadata key-value pairs. """ super().__init__(**kwargs) self.data = np.array([]) if data is None else np.asarray(data) self["duration_in_samples"] = len(self.data) self.component_signals = component_signals
[docs] def __repr__(self) -> str: """Returns a string representation of the Signal. Returns: str: String representation showing class name, metadata, and component signals. """ return f"{self.__class__.__name__}(data={type(self.data)}. metadata={self.metadata}, component_signals={self.component_signals})"
[docs] def copy(self) -> Signal: """Returns a copy of the Signal. Note: Parent relationships are not guaranteed to be preserved across copies. Returns: Signal: A new Signal instance with copied data and metadata. """ return Signal( metadata=self.get_full_metadata(), data=self.data.copy(), component_signals=[sig.copy() for sig in self.component_signals], )