### ===================================================================================================================
### Time Domain Data Class
### ===================================================================================================================
# Copyright ©2025 Haskoning Nederland B.V.
### ===================================================================================================================
### 1. Import modules
### ===================================================================================================================
# General imports
import warnings
import numpy as np
from pathlib import Path
from dataclasses import dataclass, field
from typing import Tuple, List, Union, Optional, Dict, Self
# References for functions and classes in the haskoning_atr_tools package
from haskoning_atr_tools.signal_processing_tool.utils import get_sbr_vibration_limit, plot_function, \
plot_signal_composition, bar_plot_function
from haskoning_atr_tools.signal_processing_tool.time_domain.tools import apply_filter_to_time_domain_data, \
amplitude_spectrum_from_time_domain_data, check_td_ft_suitability, stitch_time_domain_data, crop_time_domain_data, \
combine_time_domain_data, shift_time_stamp_start, check_td_average, check_td_noise, \
check_time_domain_compatibility, rotate_signals, zero_pad_time_domain_data, calculate_sbr_b_rms
### ===================================================================================================================
### 2. TimeDomainData class
### ===================================================================================================================
[docs]
@dataclass
class TimeDomainData:
"""
The TimeDomainData class represents individual time domain data sets.
Input:
- parent (TimeDomainCollection): The parent collection that this data belongs to.
- name (str): The name of the time domain data.
- amplitude_type (str): The type of the time domain data. Example: 'velocity, acceleration, pressure'.
- amplitudes (list of float): List of amplitude values for the time domain data. Initialised as empty list.
- time_stamps (list of float): List of time stamps corresponding to the amplitude values. Initialised as empty
list.
- amplitude_units (str): The units of the amplitude values. Defaults to 'Unknown units'.
- time_stamps_units (str): The units of the time stamps. Defaults to 'Unknown units'.
"""
warnings.warn(
"WARNING:The Signal Processing Tool is currently under active development. Functionality may change in future "
"updates, and formal validation is still pending. Please verify your results carefully before using them in "
"your applications.")
parent: 'TimeDomainCollection'
name: str
amplitude_type: str
amplitudes: List[float] = field(default_factory=list)
amplitude_units: str = "Unknown units"
time_stamps: List[float] = field(default_factory=list)
time_stamps_units: str = "Unknown units"
sbr_b_rms: List[float] = field(default_factory=list)
def __post_init__(self):
""" Automatically add this TimeDomainData to a parent project's collection list."""
if self.parent:
self.parent.add_time_domain_data_to_collection(self)
def __repr__(self):
""" Returns a string representation of the TimeDomainData."""
return f"TimeDomainData(name={self.name})"
[docs]
def check_time_domain(
self, check_average: bool = True, check_ft_suitability: bool = True, check_noise: bool = True) -> bool:
"""
Perform various checks on the signal to ensure it is suitable for further processing.
Input:
- check_average (bool): Select to check if the signal has an average of more than 1/10 the biggest
amplitude. Default value True.
- check_ft_suitability (bool): Select to check if the signal is suitable for Fourier Transform. By checking
if time_stamps are constant, if the signal is periodic and the signal is stationary. Default value True.
- check_noise (bool): Select to check if the signal has noise (variance is significantly different from
zero). Default value True.
Output:
- Warnings are raised if the checked criteria are not met.
"""
overall_check = True
if check_average:
if not check_td_average(amplitudes=self.amplitudes, relative_limit=0.1):
overall_check = False
if check_ft_suitability:
if not check_td_ft_suitability(amplitudes=self.amplitudes, time_stamps=self.time_stamps):
overall_check = False
if check_noise:
if not check_td_noise(amplitudes=self.amplitudes, relative_limit=1e-2):
overall_check = False
return overall_check
@property
def sampling_interval(self) -> Optional[float]:
""" Returns the time step delta of the time domain data, only if the time steps are evenly spaced."""
if len(self.time_stamps) < 2:
return None
intervals = np.diff(self.time_stamps)
if not np.allclose(intervals, intervals[0]):
return None
return float(intervals[0])
[docs]
def scale_amplitude(self, factor: float, override: bool = True) -> Self:
"""
Method to scale the amplitudes of the TimeDomainData by a given factor.
Input:
- factor (float): The scaling factor to apply to the amplitudes.
- override (bool): If True, by default, modify the current object. If False, return a new scaled object.
Output:
- Returns the scaled time domain data object.
"""
scaled_amplitudes = [amp * factor for amp in self.amplitudes]
if override:
self.amplitudes = scaled_amplitudes
return self
return TimeDomainData(
parent=self.parent, name=f'Scaled {self.name}', amplitude_type=self.amplitude_type,
amplitudes=scaled_amplitudes, amplitude_units=self.amplitude_units, time_stamps=self.time_stamps,
time_stamps_units=self.time_stamps_units)
[docs]
def crop_time_domain(
self, from_front: bool = True, number_of_items: Optional[int] = None, time_amount: Optional[float] = None,
percentage_to_crop: Optional[float] = None, crop_at: Optional[float] = None, override: bool = True) -> Self:
"""
Method to crop the signal at the front or back given different cues.
Input:
- from_front (bool): If True, by default, crop from the front. If False, crop from the back.
- number_of_items (int): Optional input for the number of items to remove.
- time_amount (float): Optional input for the amount of time to remove.
- percentage_to_crop (float): Optional input for the percentage of the signal to remove. (10 = 10%)
- crop_at (float): Optional input for the value of the time_stamp signal from where to remove.
- override (bool): If True, by default, modify the current object. If False, return a new cropped object.
Output:
- Returns the cropped time domain data object.
"""
cropped_time_stamps, cropped_amplitudes = crop_time_domain_data(
time_stamps=self.time_stamps, amplitudes=self.amplitudes, from_front=from_front,
number_of_items=number_of_items, time_amount=time_amount, percentage_to_crop=percentage_to_crop,
crop_at=crop_at)
if override:
self.time_stamps = cropped_time_stamps
self.amplitudes = cropped_amplitudes
return self
return TimeDomainData(
parent=self.parent, name=f"Cropped {self.name}", amplitude_type=self.amplitude_type,
amplitudes=cropped_amplitudes, amplitude_units=self.amplitude_units, time_stamps=cropped_time_stamps,
time_stamps_units=self.time_stamps_units)
[docs]
def zero_pad_time_domain(
self, pad_from: str = "both", number_of_items: int = None, time_amount: float = None,
percentage_to_pad: float = None, override: bool = True) -> Self:
"""
Method to zero pad the signal at the front, back, or both given different cues.
Input:
- pad_from (str): Where to pad the signal. Options are 'front', 'back', 'both'. Defaults to 'both'.
- number_of_items (int): Optional input for the number of items to pad.
- time_amount (float): Optional input for the amount of time to pad.
- percentage_to_pad (float): Optional input for the percentage of the signal to pad (10 = 10%).
- override (bool): If True, by default, modify the current object. If False, return a new cropped object.
Output:
- Returns the padded time domain data object.
"""
padded_time_stamps, padded_amplitudes = zero_pad_time_domain_data(
time_stamps=self.time_stamps, amplitudes=self.amplitudes, pad_from=pad_from,
number_of_items=number_of_items, time_amount=time_amount, percentage_to_pad=percentage_to_pad)
if override:
self.time_stamps = padded_time_stamps
self.amplitudes = padded_amplitudes
return self
return TimeDomainData(
parent=self.parent, name=f"Padded {self.name}", amplitude_type=self.amplitude_type,
amplitudes=padded_amplitudes, amplitude_units=self.amplitude_units, time_stamps=padded_time_stamps,
time_stamps_units=self.time_stamps_units)
[docs]
def combine_time_domains(self, other_time_domain_list: List[Self], override: bool = True) -> Self:
"""
Method to combine the amplitudes of this TimeDomainData with a list of other TimeDomainData.
Input:
- other_time_domain_list (list of obj): List of other time domain data to combine with.
- override (bool): If True, by default, modify the current object. If False, return a new cropped object.
Output:
- Returns the combined time domain data object.
"""
all_time_domain = \
[{'time_stamps': self.time_stamps, 'amplitudes': self.amplitudes}] + \
[{'time_stamps': other.time_stamps, 'amplitudes': other.amplitudes} for other in other_time_domain_list]
combined_time_stamps, combined_amplitudes = combine_time_domain_data(time_domain_list=all_time_domain)
if override:
self.amplitudes = combined_amplitudes
self.time_stamps = combined_time_stamps
return self
return TimeDomainData(
parent=self.parent, name=f"Combined {self.name} and {[other.name for other in other_time_domain_list]}",
amplitude_type=self.amplitude_type, amplitudes=combined_amplitudes, amplitude_units=self.amplitude_units,
time_stamps=combined_time_stamps, time_stamps_units=self.time_stamps_units)
[docs]
def stitch_time_domain(self, other_time_domain_list: List[Self], override: bool = True) -> Self:
"""
Stitch together multiple TimeDomainData objects.
Input:
- other_time_domain_list (list of obj): List of other time domain data to stitch with.
- override (bool): If True, by default, modify the current object. If False, return a new cropped object.
Output:
- Returns the stitched time domain data object.
"""
all_time_domain = [
{'time_stamps': ts.time_stamps, 'amplitudes': ts.amplitudes, 'amplitude_type': ts.amplitude_type,
'amplitude_units': ts.amplitude_units, 'time_stamps_units': ts.time_stamps_units}
for ts in [self] + other_time_domain_list]
# Check compatibility of time domain
check_time_domain_compatibility(time_domain_list=all_time_domain, identifier=self.name + ' and other signals')
stitched_time_stamps, stitched_amplitudes = stitch_time_domain_data(time_domain_list=all_time_domain)
if override:
self.amplitudes = stitched_amplitudes
self.time_stamps = stitched_time_stamps
return self
return TimeDomainData(
parent=self.parent, name=f"Stitched {self.name}{['-' + other.name for other in other_time_domain_list]}",
amplitude_type=self.amplitude_type, amplitudes=stitched_amplitudes, amplitude_units=self.amplitude_units,
time_stamps=stitched_time_stamps, time_stamps_units=self.time_stamps_units)
[docs]
def shift_start_time(self, start_time: float, override: bool = True) -> Self:
"""
Shift the start time of the time domain data.
Input:
- start_time (float): The new start time for the time domain.
- override (bool): If True, by default, modify the current object. If False, return a new cropped object.
Output:
- Returns the shifted time domain data object.
"""
shifted_time_stamps = shift_time_stamp_start(time_stamps=self.time_stamps, start_time=start_time)
if override:
self.time_stamps = shifted_time_stamps
return self
return TimeDomainData(
parent=self.parent, name=f"Shifted {self.name}", amplitude_type=self.amplitude_type,
amplitudes=self.amplitudes, amplitude_units=self.amplitude_units, time_stamps=shifted_time_stamps,
time_stamps_units=self.time_stamps_units)
[docs]
def rotate_with_orthogonal_pair(
self, partner_signal: Self, angle: float, new_names: Optional[List[str]] = None) -> Tuple[Self, Self]:
"""
This method rotates the signal represented by this time domain and another orthogonal time domain by a specified
angle. The rotation is performed on the plane defined by the two time domain signals.
The two time domain signals should be orthogonal, have the same time stamps (time interval/length) and be
synchronised.
Input:
- partner_signal (Obj): The other time domain data object defining the orthogonal component of the signal.
It should have the same time stamps as this time domain.
- angle (float): The angle of rotation in degrees.
- new_names (list of str): Optional input for a list containing new names for the rotated time domain data.
Output:
- Returns a tuple with the rotated time domain data objects.
"""
# Ensure the other time domain has the same time stamps
assert self.time_stamps == partner_signal.time_stamps, "Time stamps must match for rotation."
# Rotate the signals
self.amplitudes, partner_signal.amplitudes = rotate_signals(self.amplitudes, partner_signal.amplitudes, angle)
if new_names:
self.name = new_names[0]
partner_signal.name = new_names[1]
return self, partner_signal
[docs]
def remove_baseline(self, override: bool = True) -> Self:
"""
Remove the baseline component from the time domain data. For electrical signals known as DC offset. But in
mechanics can be a constant value of the signals such as a constant acceleration due to gravity for an
acceleration measurement, or a constant velocity for a velocity measurement or an average value position in
terms of displacement signal.
Input:
- override (bool): If True, by default, modify the current object. If False, return a new cropped object.
Output:
- Returns the time domain data object with removed baseline.
"""
mean_amplitude = np.mean(self.amplitudes)
baseline_removed_amplitudes = [amp - mean_amplitude for amp in self.amplitudes]
if override:
self.amplitudes = baseline_removed_amplitudes
return self
return TimeDomainData(
parent=self.parent, name=f"Baseline-removed {self.name}", amplitude_type=self.amplitude_type,
amplitudes=baseline_removed_amplitudes, amplitude_units=self.amplitude_units, time_stamps=self.time_stamps,
time_stamps_units=self.time_stamps_units)
[docs]
def apply_filter_to_signal(
self, highpass_limit: Optional[float] = None, lowpass_limit: Optional[float] = None, gpass: float = 1,
gstop: float = 5, override: bool = True, name: Optional[str] = None, collection_name: Optional[str] = None)\
-> Self:
"""
This method applies a highpass and/or lowpass filter to the time domain data based on the provided parameters.
Input:
- highpass_limit (float): The highpass limit for the filter. If None, by default, no highpass filter is
applied.
- lowpass_limit (float): The lowpass limit for the filter. If None, by default, no lowpass filter is
applied.
- gpass (float): The maximum loss in the passband (in dB). Default is 1.
- gstop (float): The minimum attenuation in the stopband (in dB). Default is 5.
- override (bool): If True, by default, modify the current object. If False, return a new cropped object.
- name (str): The name for the new TimeDomainData object if override is False. If None, use the original
name.
- collection_name (str, optional): The collection name for the new TimeDomainData object if override is
False. If None, use the original collection name.
Output:
- Returns the filtered time domain data object.
"""
# Determine the sampling rate from the time stamps
tot_time = self.time_stamps[-1] - self.time_stamps[0]
delta_t = tot_time / (len(self.time_stamps) - 1)
sampling_rate = 1 / delta_t
filtered_amplitudes = apply_filter_to_time_domain_data(
amplitudes=self.amplitudes, sampling_rate=sampling_rate, highpass_limit=highpass_limit,
lowpass_limit=lowpass_limit, gpass=gpass, gstop=gstop)
if override:
self.amplitudes = filtered_amplitudes
return self
if not name:
name = 'Filtered ' + self.name
if not collection_name:
collection_name = self.parent.name
return self.parent.parent.create_time_domain_data(
name=name, amplitude_type=self.amplitude_type, amplitudes=filtered_amplitudes, time_stamps=self.time_stamps,
amplitude_units=self.amplitude_units, time_stamps_units=self.time_stamps_units,
collection_name=collection_name)
[docs]
def segment_time_domain(
self, segment_length: Optional[int] = None, segment_duration: Optional[float] = None,
segment_amount: Optional[int] = None, overlap: float = 0.5, remove_baseline: bool = False,
collection_name: Optional[str] = None) -> 'TimeDomainCollection':
"""
This method divides the time domain data into smaller segments. Segmenting, overlapping and averaging is done as
integral part of Welch method to reduce Random Error. Nevertheless, if the segments are left with low amount of
time steps, the bias error will increase and the frequency resolution will decrease.
Input:
- segment_length (int): The length of the segment in amount of data points. Default is None.
- segment_duration (float): The duration of the segment in time unit. Default is None.
- segment_amount (int): The amount of segments to divide the signal into. Default is None.
- overlap (float): The percentage of overlap between segments in decimal format. 50% (0.5) is recommended
and used by default.
- remove_baseline (bool): If True, remove the baseline from each segment. Default is True.
- collection_name (str): The name of the collection to which the segmented data belongs. If None, a default
name is used.
Output:
- Returns the collection of segmented time domain data.
"""
if sum([segment_length is not None, segment_duration is not None, segment_amount is not None]) != 1:
raise ValueError(
"ERROR: Exactly one of segment_length, segment_duration, or segment_amount must be provided for the "
"segmentation of the time domain data.")
if not (0.0 <= overlap <= 1.0):
raise ValueError(
"ERROR: The overlap for the segmentation of the time domain data must be between 0 and 100%.")
if not collection_name:
collection_name = f"Segments of {self.name}"
total_length = len(self.amplitudes)
sampling_rate = 1 / (self.time_stamps[1] - self.time_stamps[0])
if segment_length:
segment_length = int(segment_length)
elif segment_duration:
segment_length = int(segment_duration * sampling_rate)
elif segment_amount:
segment_length = total_length // segment_amount
step_size = int(segment_length * (1 - overlap))
for start in range(0, total_length - segment_length + 1, step_size):
end = start + segment_length
segment_amplitudes = self.amplitudes[start:end]
segment_time_stamps = self.time_stamps[start:end]
segment_name = f"{self.name}_segment_{start // step_size + 1}"
tdd = self.parent.parent.create_time_domain_data(
name=segment_name, amplitude_type=self.amplitude_type, amplitudes=segment_amplitudes,
time_stamps=segment_time_stamps, amplitude_units=self.amplitude_units,
time_stamps_units=self.time_stamps_units, collection_name=collection_name)
if remove_baseline:
tdd.remove_baseline()
return self.parent.parent.time_domain_collections.get(collection_name)
[docs]
def convert_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: str = None,
collection_name: str = None) -> 'FrequencyDomainData':
"""
Create frequency domain data from the time domain data. This method applies a filter to the time domain data,
normalises to 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 (bool): 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.
- collection_name (str): The name of the collection to which the frequency domain data belongs. Default
value None, in which case it is created from the name of the time domain collection instance self.
Output:
- Returns the newly created instance of FrequencyDomainData class, the frequency domain data created from
the time domain data.
"""
self.check_time_domain()
# determine the sampling rate from the time stamps
tot_time = self.time_stamps[-1] - self.time_stamps[0]
delta_t = tot_time / (len(self.time_stamps) - 1)
sampling_rate = 1 / delta_t
temp_amplitudes = self.amplitudes
# apply signal filters
if highpass_limit or lowpass_limit:
temp_amplitudes = apply_filter_to_time_domain_data(
amplitudes=temp_amplitudes, sampling_rate=sampling_rate, highpass_limit=highpass_limit,
lowpass_limit=lowpass_limit, gpass=gpass, gstop=gstop)
# Create the frequency domain quantities
amplitude_spectrum, phase_angles, frequencies = amplitude_spectrum_from_time_domain_data(
amplitudes=temp_amplitudes, sampling_rate=sampling_rate, window_type=window_type, calibrate=calibrate,
normalise_length=normalise_length)
if not name:
name = f"FreqDom from {self.name}"
if not collection_name:
collection_name = self.parent.name
return self.parent.parent.create_frequency_domain_data(
name=name, values=amplitude_spectrum, spectrum_type='amplitude', frequencies=frequencies,
amplitude_type=self.amplitude_type, phase_angles=phase_angles, phase_angle_units='degrees',
value_units=self.amplitude_units, frequency_units='1/' + self.time_stamps_units,
signal_duration=tot_time, window_used=window_type, collection_name=collection_name)
[docs]
def octaves_from_tdd(
self, third_octave: bool = False, level_type: str = 'RMS', window_type: Optional[str] = None,
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) -> Dict[float, float]:
"""
This function creates octaves from the TimeDomainData by converting it to frequency domain data first.
.. note: Only provide input for segment-length, segment-duration or segment-amount when custom averaging is
requested.
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 None, in
which case no window is applied (same as applying 'Rectangular' window).
- 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.
Output:
- Returns dictionary with the calculated octave bands.
"""
if averaging:
if averaging == '1s':
segment_duration = 1
elif averaging == '0.125s':
segment_duration = 0.125
elif averaging == 'custom':
if sum([segment_length is not None, segment_duration is not None, segment_amount is not None]) != 1:
raise ValueError(
"ERROR: For custom averaging in creating octaves from time domain data (only) one of "
"segment_length, segment_duration or segment_amount must be provided.")
tdc = self.segment_time_domain(
segment_length=segment_length, segment_duration=segment_duration, segment_amount=segment_amount,
overlap=overlap, remove_baseline=remove_baseline,
collection_name=f"{averaging.capitalize()} segments from {self.name}")
fdc = tdc.convert_collection_to_frequency_domain(
window_type=window_type, calibrate=calibrate, normalise_length=normalise_length)
octave_bands = fdc.averaged_octaves(third_octave=third_octave, level_type=level_type)
else:
fdd = self.convert_to_frequency_domain(
window_type=window_type, calibrate=calibrate, normalise_length=normalise_length)
octave_bands = fdd.octaves(third_octave=third_octave, level_type=level_type)
return octave_bands
[docs]
def perform_sbr_b_rms(
self, method: str = 'fast', tau: float = 0.125, apply_frequency_weighting: bool = True,
highpass_frequency: float = 5.6, lowpass_frequency: float = 80.0) -> List[float]:
"""
Method to calculate and add SBR-B RMS values to the TimeDomainData object.
Input:
- method (str): Method to use for calculation ('fast' or 'slow'). Default value is 'fast'.
- tau (float): Time constant, in [s]. Default value 0.125s according SBR-B.
- apply_frequency_weighting (bool): Select to apply frequency weighting. Default value is True.
- highpass_frequency (float): High-pass filter frequency, in [Hz]. Default value is 5.6 Hz according SBR-B.
- lowpass_frequency (float): Low-pass filter frequency, in [Hz]. Default value is 80 Hz according SBR-B.
Output:
- Returns the calculated RMS values.
"""
rms_values = calculate_sbr_b_rms(
amplitudes=self.amplitudes, time_stamps=self.time_stamps, method=method, tau=tau,
highpass_freq=highpass_frequency, lowpass_freq=lowpass_frequency,
apply_frequency_weighting=apply_frequency_weighting)
# You could store the result as an attribute or in a dictionary
self.sbr_b_rms = rms_values
return rms_values
[docs]
def plot(
self, log_scale_x: bool = False, log_scale_y: bool = False, x_lim: list = None, y_lim: list = None,
show: bool = True, file: Optional[Union[Path, str]] = None, plot_max_values: bool = False,
horizontal_line: Optional[float] = None) -> None:
"""
Function to plot the transient data of the TimeDomainData object.
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.
- horizontal_line (float): Optional input to draw a horizontal line at this y-value. Default value is None.
Output:
- Returns the path of the file if the plot was selected to be saved to a file, otherwise returns None.
"""
return plot_function(
x_values=[self.time_stamps], y_values=[self.amplitudes], labels=[self.name],
title=f'{self.amplitude_type.capitalize()} plot for {self.name}',
x_label=f'Time [{self.time_stamps_units}]',
y_label=f'{self.amplitude_type.capitalize()} [{self.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.8,
plot_max_values=plot_max_values, horizontal_line=horizontal_line)
[docs]
def plot_signal_composition_3d(
self, amplitude_threshold: Optional[float] = None, log_scale_frequency: bool = False,
file: Optional[Union[Path, str]] = None, show: bool = True) -> Optional[Path]:
"""
Create a 3D plot of the signal composition for the TimeDomainData. The frequency domain data is either taken
from an existing FrequencyDomainData with the same name as the TimeDomainData, or it is created with default
settings and window-type 'Hanning'.
Input:
- amplitude_threshold (float): Optional input for the threshold for significant amplitudes. Default value
None, in which case 10% of max amplitude is used.
- log_scale_frequency (bool): Select to set the frequency axis to a logarithmic scale. Default value is
False.
- 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.
Output:
- Returns the path of the file if the plot was selected to be saved to a file, otherwise returns None.
"""
# Check if there is a FrequencyDomainData with the same name as the TimeDomainData
project = self.parent.parent
frequency_domain_data = None
for collection in project.frequency_domain_collections.values():
if self.name in collection.frequency_domain_data:
frequency_domain_data = collection.frequency_domain_data[self.name]
break
# Convert to frequency domain if not already available
if frequency_domain_data is None:
frequency_domain_data = self.convert_to_frequency_domain(window_type='Hanning')
# Create the 3D plot
return plot_signal_composition(
time_domain=(self.time_stamps, self.amplitudes),
frequency_domain=(frequency_domain_data.frequencies, frequency_domain_data.amplitudes),
significant_amplitudes=frequency_domain_data.find_significant_amplitudes(
amplitude_threshold=amplitude_threshold),
log_scale_frequency=log_scale_frequency, file=file, show=show)
[docs]
def plot_octaves_tdd(
self, third_octave: bool = False, level_type: 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[Union[Path, str]] = None, show_bar_values: bool = True, decimals_displayed: int = 2,
bar_width: float = 1) -> Optional[Path]:
"""
Method to plot the octave or third-octave bands of the TimeDomainData object.
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.
"""
# Calculate octave or third-octave bands
bands = self.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)
# Set units based on the type
units = 'unknown'
if level_type == 'peak':
units = f'{self.amplitude_units}'
elif level_type == 'rms':
units = f'{self.amplitude_units}'
elif level_type == 'power':
units = f'{self.amplitude_units}^2'
elif level_type == 'energy':
units = f'{self.amplitude_units}^2*{self.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=list(bands.keys()), y_values=[list(bands.values())], labels=[self.name], title=title,
y_label=f'{self.amplitude_type.capitalize()} {level_type.capitalize()} [{units}]',
x_label=f'Frequency [1/{self.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)
[docs]
def plot_sbr_b_rms(
self, log_scale_x: bool = False, log_scale_y: bool = False, x_lim: Optional[List[float]] = None,
y_lim: Optional[List[float]] = None, show: bool = True, file: Optional[Union[Path, str]] = None,
limit_parameters: Optional[list] = None, plot_max_values: bool = True) -> Optional[Path]:
"""
Method to plot the SBR-B RMS of the TimeDomainData object.
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.
- limit_parameters (list): List of parameters for the SBR-B vibration limit. Example: [building function,
time of day, and limit type] = ['office', 'day', 'A1']. Default value is None, in which case no limit is
plotted.
- 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.
"""
if not self.sbr_b_rms:
self.perform_sbr_b_rms()
y_lim = None
if limit_parameters:
limit = get_sbr_vibration_limit(*limit_parameters) if limit_parameters else None
if y_lim is None and max(self.sbr_b_rms) < limit:
# Extend the plot to include the limit if the max value is below the limit
y_lim = [0, limit*1.05]
plot_function(
x_values=[self.time_stamps], y_values=[self.sbr_b_rms], labels=[self.name],
title=f'SBR-B RMS plot for {self.name}', x_label=f'Time [{self.time_stamps_units}]',
y_label='SBR-B RMS [mm/s]', 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.8, plot_max_values=plot_max_values,
horizontal_line=get_sbr_vibration_limit(*limit_parameters) if limit_parameters else None)
### ===================================================================================================================
### 3. End of script
### ===================================================================================================================