Source code for haskoning_atr_tools.signal_processing_tool.time_domain.collection

### ===================================================================================================================
###   Time Domain Collection Class
### ===================================================================================================================
# Copyright ©2025 Haskoning Nederland B.V.

### ===================================================================================================================
###   1. Import modules
### ===================================================================================================================

# General imports
import warnings
import numpy as np
from pathlib import Path
from typing import List, Optional
from dataclasses import dataclass, field

# References for functions and classes in the haskoning_atr_tools package
from haskoning_atr_tools.signal_processing_tool.time_domain.data import TimeDomainData
from haskoning_atr_tools.signal_processing_tool.utils.plot import plot_function, bar_plot_function


### ===================================================================================================================
###   2. TimeDomainCollection class
### ===================================================================================================================

[docs] @dataclass class TimeDomainCollection: """ The TimeDomainCollection class represents a collection of TimeDomainData objects. Input: - name (str): The name of the time domain collection. - parent (obj): The parent project or entity that this collection belongs to. - time_domain_data (list): A list of TimeDomainData objects that are part of the collection. """ parent: 'ATRProject' name: str time_domain_data: dict = field(default_factory=dict) def __post_init__(self): """ Automatically add this TimeDomainCollection to a parent project's collection list.""" if self.parent: self.parent.add(self) def __repr__(self): """ Returns a string representation of the TimeDomainCollection.""" return f"TimeDomainCollection(name={self.name})"
[docs] def add_time_domain_data_to_collection(self, time_domain_data: TimeDomainData) -> None: """ Adds a TimeDomainData object to the time_domain_data. Input: - time_domain_data (TimeDomainData): The TimeDomainData object to be added to the collection. Output: - TimeDomainCollection is added to the collection. """ if not isinstance(time_domain_data, TimeDomainData): raise TypeError("ERROR: The provided time_domain_data must be an instance of TimeDomainData.") self.time_domain_data[time_domain_data.name] = time_domain_data
[docs] def check_time_stamps_alignment(self) -> bool: """ Check if time stamps align across all TimeDomainData objects in the collection.""" if not self.time_domain_data: raise ValueError("ERROR: The time domain list is empty.") reference = self.time_domain_data[0].time_stamps return all(np.array_equal(reference, data.time_stamps) for data in self.time_domain_data[1:])
[docs] def rotate_orthogonal_pair( self, first_time_domain_data_name: str, second_time_domain_data_name: str, angle: float, new_names: List[str] = None) -> tuple[TimeDomainData, TimeDomainData]: """ Method rotates two orthogonal time domain data objects by a specified angle. This method rotates the signal represented by two time domain that are orthogonal to each other by a specified angle. The rotation is performed on the plane defined by the two time domain. The two time domain should be orthogonal, have the same time stamps (time interval/length) and be synchronised. Input: - first_time_domain_data_name (str): The name of the first time domain data. - second_time_domain_data_name (str): The name of the second time domain data. - angle (float): The angle of rotation in degrees. - new_names (list of str): The new names for the TimeDomainData objects. Default value None, which will Output: - Returns a tuple containing the rotated time domain data. """ first_time_domain_data = None second_time_domain_data = None for time_domain_data in self.time_domain_data.values(): if time_domain_data.name == first_time_domain_data_name: first_time_domain_data = time_domain_data if time_domain_data.name == second_time_domain_data_name: second_time_domain_data = time_domain_data if first_time_domain_data is None or second_time_domain_data is None: raise ValueError("One or both of the specified time domain data names were not found in the collection.") return first_time_domain_data.rotate_with_orthogonal_pair(second_time_domain_data, angle, new_names)
[docs] def convert_collection_to_frequency_domain( self, window_type: Optional[str] = None, highpass_limit: float = None, lowpass_limit: float = None, gpass: float = 1, gstop: float = 5, calibrate: bool = True, normalise_length: bool = True, name: Optional[str] = None) -> 'FrequencyDomainCollection': """ Create frequency domain data from all the time domain data in the collection and group them in one collection. Uses convert_to_frequency_domain which applies a filter to the time domain data, normalises the signal length, performs a Fast Fourier Transform (FFT) on the time domain data, and creates a new FrequencyDomainData object with the transformed data. Input: - window_type (str): Select the type of window to apply to the signal before FFT. Default value is None, in which case no window is applied (same as applying 'Rectangular' window). - highpass_limit (float): The high-pass limit for the filter. If None, by default, no high pass filter is applied. - lowpass_limit (float): The low-pass limit for the filter. If None, by default, no low pass filter is applied. - gpass (float): The maximum loss in the passband, in [dB]. Default is 1 dB. - gstop (float): The minimum attenuation in the stopband, in [dB]. Default is 5 dB. - calibrate (str): Select to apply the windowing compensating factor. Default value is True. - normalise_length (bool): Select to normalise the amplitude spectrum by the signal length. Default value is True. - name (str): The name for the FrequencyDomainData. Default value None, in which case it is created from the name of the time domain data instance self. Output: - Returns the collection of frequency domain data created from the time domain collection. """ if not name: name = f"Frequency domain Collection from {self.name}" for time_domain_data in self.time_domain_data.values(): time_domain_data.convert_to_frequency_domain( window_type=window_type, highpass_limit=highpass_limit, lowpass_limit=lowpass_limit, gpass=gpass, gstop=gstop, calibrate=calibrate, normalise_length=normalise_length, collection_name=name) return self.parent.frequency_domain_collections[name]
[docs] def plot( self, log_scale_x: bool = False, log_scale_y: bool = False, x_lim: Optional[List[float]] = None, y_lim: Optional[List[float]] = None, file: Optional[Path] = None, show: bool = True, plot_max_values: bool = False) -> Optional[Path]: """ Method creates plot of all the transient data in the collection. Input: - log_scale_x (bool): Select to set the x-axis to a logarithmic scale. Default value is False. - log_scale_y (bool): Select to set the y-axis to a logarithmic scale. Default value is False. - x_lim (list of float): Optional list to set limits [bottom limit, top limit] for the x-axis. Default value is None. - y_lim (list of float): Optional list to set limits [bottom limit, top limit] for the y-axis. Default value is None. - file (Path): Provide the full path and filename for the image of the graph to be created. The image is a png-file, if the suffix is omitted it will be added. An error will occur if the picture format is set to a non-supported format. It is also possible to provide the folder only, the title is used as filename. - show (bool): Select to plot the graph in the environment when running the script. Default value True. - plot_max_values (bool): Select to calculate the maximum y value for each line and add it to the plot. Default value is False. Output: - Returns the path of the file if the plot was selected to be saved to a file, otherwise returns None. """ amplitude_type = [tdd.amplitude_type for tdd in self.time_domain_data.values()][0] time_stamps_units = [tdd.time_stamps_units for tdd in self.time_domain_data.values()][0] amplitude_units = [tdd.amplitude_units for tdd in self.time_domain_data.values()][0] plot_function( x_values=[tdd.time_stamps for tdd in self.time_domain_data.values()], y_values=[tdd.amplitudes for tdd in self.time_domain_data.values()], labels=[tdd.name for tdd in self.time_domain_data.values()], title=f'{amplitude_type.capitalize()} Plot for {self.name}', x_label=f'Time [{time_stamps_units}]', y_label=f'{amplitude_type.capitalize()} [{amplitude_units}]', log_scale_x=log_scale_x, log_scale_y=log_scale_y, x_lim=x_lim, y_lim=y_lim, file=file, show=show, line_width=0.9, plot_max_values=plot_max_values)
[docs] def plot_collection_octaves_tdd( self, third_octave: bool = False, level_type: Optional[str] = 'RMS', window_type: str = 'Hanning', calibrate: bool = True, normalise_length: bool = True, averaging: Optional[str] = None, overlap: float = 0.5, segment_length: Optional[float] = None, segment_duration: Optional[float] = None, segment_amount: Optional[int] = None, remove_baseline: bool = False, log_scale_y: bool = False, x_lim: Optional[List[float]] = None, y_lim: Optional[List[float]] = None, show: bool = True, file: Optional[Path] = None, show_bar_values: bool = True, decimals_displayed: int = 2, bar_width: float = 1) -> None: """ Method to create plot the octave or third-octave bands for all TimeDomainData objects in the collection. Input: - third_octave (bool): Input determines the frequency resolution of the output bands. When third-octave is selected the function calculates third-octave bands, which divide each octave into three narrower bands. Default value is False, the function calculates standard octave bands, which group frequencies more broadly. - level_type (str): Specifies the method used to compute the level of each frequency band. Options include 'peak', 'energy', 'power', and 'RMS'. Default value is 'RMS'. - window_type (str): Select the type of window to apply to the signal before FFT. Default value is 'Hanning'. - calibrate (str): Select to apply the windowing compensating factor. Default value is True. - normalise_length (bool): Select to normalise the amplitude spectrum by the signal length. Default value is True. - averaging (str): Controls whether and how the signal is segmented and averaged before computing frequency bands. Options include: '1s', '0.125s', or 'custom'. In case of 'custom' the segment-length, segment-duration or segment-amount should be specified. Default value is None, in which case no segmentation or averaging is applied. The entire signal is processed as a single block. - overlap (float): The percentage of overlap between segments in decimal format. 50% (0.5) is recommended and used by default. - segment_length (int): The length of the segment in amount of data points. Default value is None. Input only used for custom averaging. - segment_duration (float): The duration of the segment in time unit. Default value is None. Input only used for custom averaging. - segment_amount (int): The amount of segments to divide the signal into. Default value is None. Input only used for custom averaging. - remove_baseline (bool): Select to remove the baseline from each segment. Default value is True. - log_scale_y (bool): Select to set the y-axis to a logarithmic scale. Default value is False. - x_lim (list of float): Optional list to set limits [bottom limit, top limit] for the x-axis. Default value is None. - y_lim (list of float): Optional list to set limits [bottom limit, top limit] for the y-axis. Default value is None. - file (Path): Provide the full path and filename for the image of the graph to be created. The image is a png-file, if the suffix is omitted it will be added. An error will occur if the picture format is set to a non-supported format. It is also possible to provide the folder only, the title is used as filename. - show (bool): Select to plot the graph in the environment when running the script. Default value True. - show_bar_values (bool): Select to display the labels on the bars. Default value is True. - decimals_displayed (int): The number of decimal places for formatting values. Default value is 2. - bar_width (float): The width of the bars. Default value is 1. Output: - Returns the path of the file if the plot was selected to be saved to a file, otherwise returns None. """ if not self.time_domain_data: raise ValueError("ERROR: The time domain list is empty.") level_type = level_type.lower() if level_type else None first_tdd = next(iter(self.time_domain_data.values())) amplitude_type = first_tdd.amplitude_type time_stamps_units = first_tdd.time_stamps_units amplitude_units = first_tdd.amplitude_units x_values = None y_values = [] group_labels = [] for tdd in self.time_domain_data.values(): if (tdd.amplitude_type != amplitude_type or tdd.time_stamps_units != time_stamps_units or tdd.amplitude_units != amplitude_units): warnings.warn( "WARNING: Inconsistent amplitude type, time stamps units, or amplitude units in the time domain " "list.") # Calculate octave or third-octave bands bands = tdd.octaves_from_tdd( third_octave=third_octave, level_type=level_type, window_type=window_type, calibrate=calibrate, normalise_length=normalise_length, averaging=averaging, overlap=overlap, segment_length=segment_length, segment_duration=segment_duration, segment_amount=segment_amount, remove_baseline=remove_baseline) if x_values is None: x_values = list(bands.keys()) y_values.append(list(bands.values())) group_labels.append(tdd.name) # Set units based on the type units = 'unknown' if level_type == 'peak': units = amplitude_units elif level_type == 'rms': units = amplitude_units elif level_type == 'power': units = f'{amplitude_units}^2' elif level_type == 'energy': units = f'{amplitude_units}^2*{time_stamps_units}' # Create the title for the plot title = f"{level_type.capitalize()} {'Third ' if third_octave else ''}Octave Band Plot of {self.name}" if averaging: title += f" (With {averaging} averaging)" # Plot using the grouped_bar_plot_function bar_plot_function( x_values=x_values, y_values=y_values, labels=group_labels, title=title, y_label=f'{amplitude_type.capitalize()} {level_type.capitalize()} [{units}]', x_label=f'Frequency [1/{time_stamps_units}]', log_scale_y=log_scale_y, x_lim=x_lim, y_lim=y_lim, file=file, show=show, bar_width=bar_width, show_bar_values=show_bar_values, decimals_displayed=decimals_displayed)
### =================================================================================================================== ### 3. End of script ### ===================================================================================================================