### ===================================================================================================================
### 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
### ===================================================================================================================