diff --git a/doc/changelog.d/180.added.md b/doc/changelog.d/180.added.md new file mode 100644 index 00000000..110f3048 --- /dev/null +++ b/doc/changelog.d/180.added.md @@ -0,0 +1 @@ +feat: source broadband noise 2 parameters for sound composer \ No newline at end of file diff --git a/doc/source/api/sound_composer.rst b/doc/source/api/sound_composer.rst index d67f3694..39cd6788 100644 --- a/doc/source/api/sound_composer.rst +++ b/doc/source/api/sound_composer.rst @@ -8,6 +8,7 @@ Sound composer SourceSpectrum SourceBroadbandNoise + SourceBroadbandNoiseTwoParameters SourceAudio SourceControlSpectrum SourceControlTime diff --git a/src/ansys/sound/core/__init__.py b/src/ansys/sound/core/__init__.py index 61909bd0..7e676031 100644 --- a/src/ansys/sound/core/__init__.py +++ b/src/ansys/sound/core/__init__.py @@ -30,6 +30,26 @@ __version__ = importlib_metadata.version(__name__.replace(".", "-")) """PyAnsys Sound version.""" -from . import examples_helpers, server_helpers, signal_utilities +from . import ( + examples_helpers, + psychoacoustics, + server_helpers, + signal_utilities, + sound_composer, + sound_power, + spectral_processing, + spectrogram_processing, + xtract, +) -__all__ = ("examples_helpers", "server_helpers", "signal_utilities") +__all__ = ( + "examples_helpers", + "psychoacoustics", + "server_helpers", + "signal_utilities", + "sound_composer", + "sound_power", + "spectral_processing", + "spectrogram_processing", + "xtract", +) diff --git a/src/ansys/sound/core/sound_composer/__init__.py b/src/ansys/sound/core/sound_composer/__init__.py index cae58e49..8d7e3381 100644 --- a/src/ansys/sound/core/sound_composer/__init__.py +++ b/src/ansys/sound/core/sound_composer/__init__.py @@ -30,6 +30,7 @@ from ._source_parent import SourceParent from .source_audio import SourceAudio from .source_broadband_noise import SourceBroadbandNoise +from .source_broadband_noise_two_parameters import SourceBroadbandNoiseTwoParameters from .source_control_spectrum import SourceControlSpectrum from .source_control_time import SourceControlTime from .source_spectrum import SourceSpectrum @@ -42,6 +43,7 @@ "SourceSpectrum", "SourceControlSpectrum", "SourceBroadbandNoise", + "SourceBroadbandNoiseTwoParameters", "SourceControlTime", "SourceAudio", ) diff --git a/src/ansys/sound/core/sound_composer/source_broadband_noise_two_parameters.py b/src/ansys/sound/core/sound_composer/source_broadband_noise_two_parameters.py new file mode 100644 index 00000000..b245b2bd --- /dev/null +++ b/src/ansys/sound/core/sound_composer/source_broadband_noise_two_parameters.py @@ -0,0 +1,414 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Sound Composer's broadband noise source with two parameters.""" +import warnings + +from ansys.dpf.core import Field, FieldsContainer, Operator +from matplotlib import pyplot as plt +import numpy as np + +from .._pyansys_sound import PyAnsysSoundException, PyAnsysSoundWarning +from ._source_parent import SourceParent +from .source_control_time import SourceControlTime + +ID_COMPUTE_LOAD_SOURCE_BBN_2PARAMS = "sound_composer_load_source_bbn_two_parameters" +ID_COMPUTE_GENERATE_SOUND_BBN_2PARAMS = "sound_composer_generate_sound_bbn_two_parameters" + + +class SourceBroadbandNoiseTwoParameters(SourceParent): + """Sound Composer's broadband noise source with two parameters class. + + This class creates a broadband noise source with two parameters for the Sound Composer. A + broadband noise source with two parameters is used to generate a sound signal from a given + broadband noise and its two source controls. The broadband noise consists of a series of noise + spectra, each corresponding to a pair of control parameter values. The source controls contain + each a control parameter's values over time. + """ + + def __init__( + self, + file: str = "", + control1: SourceControlTime = None, + control2: SourceControlTime = None, + ): + """Class instantiation takes the following parameters. + + Parameters + ---------- + file : str, default: "" + Path to the broadband noise with two parameters file. Supported files are text files + with the header `AnsysSound_BBN_MultipleParameters`. + control1 : SourceControlTime, default: None + First Source control, consisting of the control parameter values over time, to use when + generating the sound from this source. + control2 : SourceControlTime, default: None + Second source control, consisting of the control parameter values over time, to use + when generating the sound from this source. + """ + super().__init__() + self.source_control1 = control1 + self.source_control2 = control2 + + # Define DPF Sound operators. + self.__operator_load = Operator(ID_COMPUTE_LOAD_SOURCE_BBN_2PARAMS) + self.__operator_generate = Operator(ID_COMPUTE_GENERATE_SOUND_BBN_2PARAMS) + + if len(file) > 0: + self.load_source_bbn_two_parameters(file) + else: + self.source_bbn_two_parameters = None + + def __str__(self) -> str: + """Return the string representation of the object.""" + # Source info. + if self.source_bbn_two_parameters is not None: + ( + spectrum_type, + delta_f, + control_name1, + control_unit1, + control_min_max1, + control_name2, + control_unit2, + control_min_max2, + ) = self.__extract_bbn_two_parameters_info() + + # Source name. + str_name = self.source_bbn_two_parameters.name + if str_name is None: + str_name = "" + + # # Spectrum type. TODO: reactivate if/else when quantity_type is available in python. + # if spectrum_type == "NARROWBAND": + # str_type = f"{spectrum_type} (DeltaF: {delta_f:.1f} Hz)" + # else: + # str_type = spectrum_type + str_type = spectrum_type + + str_source = ( + f"'{str_name}'\n" + f"\tSpectrum type: {str_type}\n" + f"\tSpectrum count: {len(self.source_bbn_two_parameters)}\n" + f"\tControl parameter 1: {control_name1}, " + f"{control_min_max1[0]}-{control_min_max1[1]} {control_unit1}\n" + f"\tControl parameter 2: {control_name2}, " + f"{control_min_max2[0]}-{control_min_max2[1]} {control_unit2}" + ) + else: + str_source = "Not set" + + # Source control info. + if self.is_source_control_valid(): + str_source_control1 = ( + f"{self.source_control1.control.name}\n" + f"\t\tMin: {self.source_control1.control.data.min()}\n" + f"\t\tMax: {self.source_control1.control.data.max()}\n" + f"\t\tDuration: " + f"{self.source_control1.control.time_freq_support.time_frequencies.data[-1]} s" + ) + str_source_control2 = ( + f"{self.source_control2.control.name}\n" + f"\t\tMin: {self.source_control2.control.data.min()}\n" + f"\t\tMax: {self.source_control2.control.data.max()}\n" + f"\t\tDuration: " + f"{self.source_control2.control.time_freq_support.time_frequencies.data[-1]} s" + ) + else: + str_source_control1 = "Not set" + str_source_control2 = "Not set" + + return ( + f"Broadband noise source with two parameters: {str_source}\n" + f"Source control:\n" + f"\tControl 1: {str_source_control1}\n" + f"\tControl 2: {str_source_control2}" + ) + + @property + def source_control1(self) -> SourceControlTime: + """First source control for broadband noise source with two parameters. + + Contains the first control parameter values over time. + """ + return self.__source_control1 + + @source_control1.setter + def source_control1(self, source_control: SourceControlTime): + """Set the source control.""" + if not (isinstance(source_control, SourceControlTime) or source_control is None): + raise PyAnsysSoundException( + "Specified first source control object must be of type ``SourceControlTime``." + ) + self.__source_control1 = source_control + + @property + def source_control2(self) -> SourceControlTime: + """Second source control for broadband noise source with two parameters. + + Contains the second control parameter values over time. + """ + return self.__source_control2 + + @source_control2.setter + def source_control2(self, source_control: SourceControlTime): + """Set the source control.""" + if not (isinstance(source_control, SourceControlTime) or source_control is None): + raise PyAnsysSoundException( + "Specified second source control object must be of type ``SourceControlTime``." + ) + self.__source_control2 = source_control + + @property + def source_bbn_two_parameters(self) -> FieldsContainer: + """Source data for broadband noise source with two parameters. + + The broadband noise source with two parameters data consists of a series of spectra, each + corresponding to a pair of control parameter values. Spectra can be narrowband (in dB or + dB/Hz), octave-band levels, or 1/3-octave-band levels. + """ + return self.__source_bbn_two_parameters + + @source_bbn_two_parameters.setter + def source_bbn_two_parameters(self, source: FieldsContainer): + """Set the broadband noise source with two parameters data, from a DPF fields container.""" + if source is not None: + if not isinstance(source, FieldsContainer): + raise PyAnsysSoundException( + "Specified broadband noise source with two parameters must be provided as a " + "DPF fields container." + ) + + if len(source) < 1: + raise PyAnsysSoundException( + "Specified broadband noise source with two parameters must contain at least " + "one spectrum." + ) + + for spectrum in source: + if len(spectrum.data) < 1: + raise PyAnsysSoundException( + "Each spectrum in the specified broadband noise source with two " + "parameters must contain at least one element." + ) + + support_data = source.get_support("control_parameter_1") + support_properties = support_data.available_field_supported_properties() + support1_values = support_data.field_support_by_property(support_properties[0]) + support_data = source.get_support("control_parameter_2") + support_properties = support_data.available_field_supported_properties() + support2_values = support_data.field_support_by_property(support_properties[0]) + if len(support1_values) != len(source) or len(support2_values) != len(source): + raise PyAnsysSoundException( + "Broadband noise source with two parameters must contain as many spectra as " + "the number of values in both associated control parameters (in the provided " + "DPF fields container, the number of fields should be the same as the number " + "of values in both fields container supports)." + ) + + self.__source_bbn_two_parameters = source + + def is_source_control_valid(self) -> bool: + """Source control verification function. + + Checks if both source controls are set. + + Returns + ------- + bool + True if both source controls are set. + """ + return ( + self.source_control1 is not None + and self.source_control1.control is not None + and self.source_control2 is not None + and self.source_control2.control is not None + ) + + def load_source_bbn_two_parameters(self, file: str): + """Load the broadband noise source with two parameters data from a file. + + Parameters + ---------- + file : str + Path to the broadband noise source with two parameters file. Supported files have the + same text format (with the `AnsysSound_BBN_MultipleParameters` header) as that which is + supported by Ansys Sound SAS. + """ + # Set operator inputs. + self.__operator_load.connect(0, file) + + # Run the operator. + self.__operator_load.run() + + # Get the loaded sound power level parameters. + self.source_bbn_two_parameters = self.__operator_load.get_output(0, "fields_container") + + def process(self, sampling_frequency: float = 44100.0): + """Generate the sound of the broadband noise source with two parameters. + + This method generates the sound of the broadband noise source with two parameters, using + the current broadband noise data and source controls. + + Parameters + ---------- + sampling_frequency : float, default: 44100.0 + Sampling frequency of the generated sound in Hz. + """ + if sampling_frequency <= 0.0: + raise PyAnsysSoundException("Sampling frequency must be strictly positive.") + + if not self.is_source_control_valid(): + raise PyAnsysSoundException( + "At least one source control for broadband noise with two parameters is not set. " + f"Use ``{__class__.__name__}.source_control1`` and/or " + f"``{__class__.__name__}.source_control2``." + ) + + if self.source_bbn_two_parameters is None: + raise PyAnsysSoundException( + "Broadband noise source with two parameters data is not set. Use " + f"``{__class__.__name__}.source_bbn_two_parameters`` or method " + f"``{__class__.__name__}.load_source_bbn_two_parameters()``." + ) + + # Set operator inputs. + self.__operator_generate.connect(0, self.source_bbn_two_parameters) + self.__operator_generate.connect(1, self.source_control1.control) + self.__operator_generate.connect(2, self.source_control2.control) + self.__operator_generate.connect(3, sampling_frequency) + + # Run the operator. + self.__operator_generate.run() + + # Get the loaded sound power level parameters. + self._output = self.__operator_generate.get_output(0, "field") + + def get_output(self) -> Field: + """Get the generated sound as a DPF field. + + Returns + ------- + Field + Generated sound as a DPF field. + """ + if self._output == None: + warnings.warn( + PyAnsysSoundWarning( + "Output is not processed yet. " + f"Use the ``{__class__.__name__}.process()`` method." + ) + ) + return self._output + + def get_output_as_nparray(self) -> np.ndarray: + """Get the generated sound as a NumPy array. + + Returns + ------- + numpy.ndarray + Generated sound (signal samples in Pa) as a NumPy array. + """ + output = self.get_output() + + return np.array(output.data if output is not None else []) + + def plot(self): + """Plot the resulting signal in a figure.""" + if self._output == None: + raise PyAnsysSoundException( + f"Output is not processed yet. Use the '{__class__.__name__}.process()' method." + ) + output = self.get_output() + + time_data = output.time_freq_support.time_frequencies.data + + plt.plot(time_data, output.data) + plt.title( + output.name + if len(output.name) > 0 + else "Signal from broadband noise source with two parameters" + ) + plt.xlabel("Time (s)") + plt.ylabel("Amplitude (Pa)") + plt.grid(True) + plt.show() + + def __extract_bbn_two_parameters_info( + self, + ) -> tuple[str, float, str, str, tuple[float], str, str, tuple[float]]: + """Extract the broadband noise source with two parameters information. + + Returns + ------- + tuple[str, float, str, str, tuple[float], str, str, tuple[float]] + Broadband noise source with two parameters information, consisting of the following + elements: + First element is the spectrum type ('NARROWBAND', 'OCTAVE1:1', or 'OCTAVE1:3'). + + Second element is the spectrum frequency resolution in Hz (only if spectrum type is + 'NARROWBAND', 0.0 otherwise). + + Third element is the first control parameter name. + + Sixth element is the first control parameter unit. + + Seventh element is the first control parameter min and max values in a tuple. + + Eighth element is the second control parameter name. + + Ninth element is the second control parameter unit. + + Tenth element is the second control parameter min and max values in a tuple. + + """ + if self.source_bbn_two_parameters is None: + return ("", 0.0, "", "", (), "", "", ()) + + # Spectrum info. + # TODO: for now quantity_type can't be accessed in python. When it is, the line below + # should be uncommented, and replace the one after. + # type = self.source_bbn[0].field_definition.quantity_type + type = "Not available" + frequencies = self.source_bbn_two_parameters[0].time_freq_support.time_frequencies.data + if len(frequencies) > 1: + delta_f = frequencies[1] - frequencies[0] + else: + delta_f = 0.0 + + # Control parameter 1 info. + control_data = self.source_bbn_two_parameters.get_support("control_parameter_1") + parameter_ids = control_data.available_field_supported_properties() + unit1 = parameter_ids[0] + name1 = control_data.field_support_by_property(unit1).name + values = control_data.field_support_by_property(unit1).data + min_max1 = (float(values.min()), float(values.max())) + + # Control parameter 2 info. + control_data = self.source_bbn_two_parameters.get_support("control_parameter_2") + parameter_ids = control_data.available_field_supported_properties() + unit2 = parameter_ids[0] + name2 = control_data.field_support_by_property(unit2).name + values = control_data.field_support_by_property(unit2).data + min_max2 = (float(values.min()), float(values.max())) + + return type, delta_f, name1, unit1, min_max1, name2, unit2, min_max2 diff --git a/tests/conftest.py b/tests/conftest.py index 5ae3d7d6..7d591ecf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -68,6 +68,9 @@ def pytest_configure(): pytest.data_path_sound_composer_bbn_source_40_values_in_container = ( "C:\\data\\AnsysSound_BBN dBSPLperHz NARROWBAND v2_40values_2024R2_20241128.txt" ) + pytest.data_path_sound_composer_bbn_source_2p_in_container = ( + "C:\\data\\AnsysSound_BBN_MultipleParameters Pa2PerHz Narrowband v2_2024R2_20240418.txt" + ) @pytest.fixture(scope="session") diff --git a/tests/data/AnsysSound_BBN_MultipleParameters Pa2PerHz Narrowband v2_2024R2_20240418.txt b/tests/data/AnsysSound_BBN_MultipleParameters Pa2PerHz Narrowband v2_2024R2_20240418.txt new file mode 100644 index 00000000..1a865430 --- /dev/null +++ b/tests/data/AnsysSound_BBN_MultipleParameters Pa2PerHz Narrowband v2_2024R2_20240418.txt @@ -0,0 +1,25 @@ +AnsysSound_BBN_MultipleParameters 2 +Pa2/Hz NARROWBAND +temperature +celsius +charge +% +#### +c:/data/data_charge0.dat +0 +temperature 10 20 30 40 +1000 0 1 2 3 +2000 1 2 3 4 +3000 2 3 4 5 +4000 3 4 5 6 +5000 4 5 6 7 +#### +c:/data/data_charge10.dat +10 +temperature 2.5 3.5 3.9 4.1 +1000 00 10 20 30 +2000 10 20 30 40 +3000 20 30 40 50 +4000 30 40 50 60 +5000 40 50 60 70 +#### \ No newline at end of file diff --git a/tests/tests_sound_composer/test_sound_composer_source_broadband_noise_two_parameters.py b/tests/tests_sound_composer/test_sound_composer_source_broadband_noise_two_parameters.py new file mode 100644 index 00000000..f6c7eb49 --- /dev/null +++ b/tests/tests_sound_composer/test_sound_composer_source_broadband_noise_two_parameters.py @@ -0,0 +1,607 @@ +# Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from unittest.mock import patch + +from ansys.dpf.core import ( + Field, + FieldsContainer, + TimeFreqSupport, + fields_container_factory, + fields_factory, + locations, +) +import numpy as np +import pytest + +from ansys.sound.core._pyansys_sound import PyAnsysSoundException, PyAnsysSoundWarning +from ansys.sound.core.sound_composer import SourceBroadbandNoiseTwoParameters, SourceControlTime +from ansys.sound.core.spectral_processing import PowerSpectralDensity + +REF_ACOUSTIC_POWER = 4e-10 + +EXP_LEVEL_OCTAVE_BAND_250 = 123.0 +EXP_LEVEL_OCTAVE_BAND_1000 = 132.0 +EXP_LEVEL_OCTAVE_BAND_4000 = 140.0 +EXP_SPECTRUM_DATA03 = 3.000000238418579 +EXP_STR_NOT_SET = ( + "Broadband noise source with two parameters: Not set\n" + "Source control:\n" + "\tControl 1: Not set\n" + "\tControl 2: Not set" +) +EXP_STR_ALL_SET = ( + "Broadband noise source with two parameters: ''\n" + "\tSpectrum type: Not available\n" + "\tSpectrum count: 8\n" + "\tControl parameter 1: temperature, 2.5-40.0 celsius\n" + "\tControl parameter 2: charge, 0.0-10.0 %\n" + "Source control:\n" + "\tControl 1: \n" + "\t\tMin: 3.0\n" + "\t\tMax: 38.0\n" + "\t\tDuration: 3.0 s\n" + "\tControl 2: \n" + "\t\tMin: 3.0\n" + "\t\tMax: 38.0\n" + "\t\tDuration: 3.0 s" +) + + +def test_source_broadband_noise_two_parameters_instantiation_no_arg(dpf_sound_test_server): + """Test SourceBroadbandNoiseTwoParameters instantiation without arguments.""" + source_bbn_two_parameters_obj = SourceBroadbandNoiseTwoParameters() + assert isinstance(source_bbn_two_parameters_obj, SourceBroadbandNoiseTwoParameters) + assert source_bbn_two_parameters_obj.source_bbn_two_parameters is None + + +def test_source_broadband_noise_two_parameters_instantiation_file_arg(dpf_sound_test_server): + """Test SourceBroadbandNoiseTwoParameters instantiation with file argument.""" + source_bbn_two_parameters_obj = SourceBroadbandNoiseTwoParameters( + file=pytest.data_path_sound_composer_bbn_source_2p_in_container + ) + assert isinstance(source_bbn_two_parameters_obj, SourceBroadbandNoiseTwoParameters) + assert source_bbn_two_parameters_obj.source_bbn_two_parameters is not None + + +def test_source_broadband_noise_two_parameters___str___not_set(dpf_sound_test_server): + """Test SourceBroadbandNoiseTwoParameters __str__ method when nothing is set.""" + source_bbn_two_parameters_obj = SourceBroadbandNoiseTwoParameters() + assert str(source_bbn_two_parameters_obj) == EXP_STR_NOT_SET + + +def test_source_broadband_noise_two_parameters___str___all_set(dpf_sound_test_server): + """Test SourceBroadbandNoiseTwoParameters __str__ method when all data are set.""" + # Create a field to use in a SourceControlTime object. + f_source_control = fields_factory.create_scalar_field( + num_entities=1, location=locations.time_freq + ) + f_source_control.append([3, 4, 35, 38], 1) + support = TimeFreqSupport() + f_time = fields_factory.create_scalar_field(num_entities=1, location=locations.time_freq) + f_time.append([0, 1, 2, 3], 1) + support.time_frequencies = f_time + f_source_control.time_freq_support = support + + # Create a first SourceControlTime object. + source_control1 = SourceControlTime() + source_control1.control = f_source_control + + # Create a second SourceControlTime object. + source_control2 = SourceControlTime() + source_control2.control = f_source_control + + # Create a SourceBroadbandNoiseTwoParameters object test source file with less and created + # source controls. + source_bbn_two_parameters_obj = SourceBroadbandNoiseTwoParameters( + file=pytest.data_path_sound_composer_bbn_source_2p_in_container, + control1=source_control1, + control2=source_control2, + ) + + assert str(source_bbn_two_parameters_obj) == EXP_STR_ALL_SET + + +def test_source_broadband_noise_two_parameters_properties(dpf_sound_test_server): + """Test SourceBroadbandNoiseTwoParameters properties.""" + source_bbn_two_parameters_obj = SourceBroadbandNoiseTwoParameters() + + # Test source_control1 property. + source_bbn_two_parameters_obj.source_control1 = SourceControlTime() + assert isinstance(source_bbn_two_parameters_obj.source_control1, SourceControlTime) + + # Test source_control2 property. + source_bbn_two_parameters_obj.source_control2 = SourceControlTime() + assert isinstance(source_bbn_two_parameters_obj.source_control2, SourceControlTime) + + # Test source_bbn_two_parameters property. + # Create a second object and then reuse its source_bbn_two_parameters property. + source_bbn_two_parameters_obj_tmp = SourceBroadbandNoiseTwoParameters() + source_bbn_two_parameters_obj_tmp.load_source_bbn_two_parameters( + pytest.data_path_sound_composer_bbn_source_2p_in_container + ) + fc_source = source_bbn_two_parameters_obj_tmp.source_bbn_two_parameters + source_bbn_two_parameters_obj.source_bbn_two_parameters = fc_source + assert isinstance(source_bbn_two_parameters_obj.source_bbn_two_parameters, FieldsContainer) + + +def test_source_broadband_noise_two_parameters_properties_exceptions(dpf_sound_test_server): + """Test SourceBroadbandNoiseTwoParameters properties' exceptions.""" + source_bbn_two_parameters_obj = SourceBroadbandNoiseTwoParameters() + + # Test source_control1 setter exception (str instead of SourceControlTime). + with pytest.raises( + PyAnsysSoundException, + match="Specified first source control object must be of type ``SourceControlTime``.", + ): + source_bbn_two_parameters_obj.source_control1 = "InvalidType" + + # Test source_control1 setter exception (str instead of SourceControlTime). + with pytest.raises( + PyAnsysSoundException, + match="Specified second source control object must be of type ``SourceControlTime``.", + ): + source_bbn_two_parameters_obj.source_control2 = "InvalidType" + + # Test source_bbn_two_parameters setter exception 1 (str instead a Field). + with pytest.raises( + PyAnsysSoundException, + match=( + "Specified broadband noise source with two parameters must be provided as a DPF " + "fields container." + ), + ): + source_bbn_two_parameters_obj.source_bbn_two_parameters = "InvalidType" + + # Test source_bbn_two_parameters setter exception 2 (less than 1 spectrum). + fc_source_bbn = FieldsContainer() + with pytest.raises( + PyAnsysSoundException, + match=( + "Specified broadband noise source with two parameters must contain at least one " + "spectrum." + ), + ): + source_bbn_two_parameters_obj.source_bbn_two_parameters = fc_source_bbn + + # Test source_bbn_two_parameters setter exception 3 (empty spectrum). + fc_source_bbn = fields_container_factory.over_time_freq_fields_container([Field()]) + with pytest.raises( + PyAnsysSoundException, + match=( + "Each spectrum in the specified broadband noise source with two parameters must " + "contain at least one element." + ), + ): + source_bbn_two_parameters_obj.source_bbn_two_parameters = fc_source_bbn + + # Test source_bbn_two_parameters setter exception 4 (empty bbn source's first control data). + # For this, we use a valid dataset, and then remove the control data. + source_bbn_two_parameters_obj = SourceBroadbandNoiseTwoParameters() + source_bbn_two_parameters_obj.load_source_bbn_two_parameters( + pytest.data_path_sound_composer_bbn_source_2p_in_container + ) + support_data = source_bbn_two_parameters_obj.source_bbn_two_parameters.get_support( + "control_parameter_1" + ) + support_properties = support_data.available_field_supported_properties() + support_values = support_data.field_support_by_property(support_properties[0]) + support_values.data = [] + fc_source_bbn = source_bbn_two_parameters_obj.source_bbn_two_parameters + with pytest.raises( + PyAnsysSoundException, + match=( + "Broadband noise source with two parameters must contain as many spectra as the " + "number of values in both associated control parameters \\(in the provided DPF fields " + "container, the number of fields should be the same as the number of values in both " + "fields container supports\\)." + ), + ): + source_bbn_two_parameters_obj.source_bbn_two_parameters = fc_source_bbn + + +def test_source_broadband_noise_two_parameters_is_source_control_valid(dpf_sound_test_server): + """Test SourceBroadbandNoiseTwoParameters is_source_control_valid method.""" + source_bbn_two_parameters_obj = SourceBroadbandNoiseTwoParameters() + + # Test is_source_control_valid method (attribute not set). + assert source_bbn_two_parameters_obj.is_source_control_valid() is False + + # Test is_source_control_valid method (first attribute set, but not the second). + source_control_obj = SourceControlTime() + source_bbn_two_parameters_obj.source_control1 = source_control_obj + assert source_bbn_two_parameters_obj.is_source_control_valid() is False + + # Test is_source_control_valid method (both attributes set, but attributes' fields not set). + source_control_obj = SourceControlTime() + source_bbn_two_parameters_obj.source_control2 = source_control_obj + assert source_bbn_two_parameters_obj.is_source_control_valid() is False + + # Test is_source_control_valid method (only one attribute's field set). + f_source_control = fields_factory.create_scalar_field( + num_entities=1, location=locations.time_freq + ) + f_source_control.append([1.0, 2.0, 3.0, 4.0, 5.0], 1) + source_bbn_two_parameters_obj.source_control1.control = f_source_control + assert source_bbn_two_parameters_obj.is_source_control_valid() is False + + # Test is_source_control_valid method (all set). + source_bbn_two_parameters_obj.source_control2.control = f_source_control + assert source_bbn_two_parameters_obj.is_source_control_valid() is True + + +def test_source_specrum_load_source_bbn_two_parameters(dpf_sound_test_server): + """Test SourceBroadbandNoiseTwoParameters load_source_bbn method.""" + source_bbn_two_parameters_obj = SourceBroadbandNoiseTwoParameters() + source_bbn_two_parameters_obj.load_source_bbn_two_parameters( + pytest.data_path_sound_composer_bbn_source_2p_in_container + ) + assert isinstance(source_bbn_two_parameters_obj.source_bbn_two_parameters, FieldsContainer) + assert source_bbn_two_parameters_obj.source_bbn_two_parameters[0].data[3] == pytest.approx( + EXP_SPECTRUM_DATA03 + ) + + +def test_source_broadband_noise_two_parameters_process(dpf_sound_test_server): + """Test SourceBroadbandNoiseTwoParameters process method.""" + # Create a field to use in a SourceControlTime object. + f_source_control = fields_factory.create_scalar_field( + num_entities=1, location=locations.time_freq + ) + f_source_control.append([3, 4, 35, 38], 1) + support = TimeFreqSupport() + f_time = fields_factory.create_scalar_field(num_entities=1, location=locations.time_freq) + f_time.append([0, 1, 2, 3], 1) + support.time_frequencies = f_time + f_source_control.time_freq_support = support + + # Create a first SourceControlTime object. + source_control1 = SourceControlTime() + source_control1.control = f_source_control + + # Create another field to use in a SourceControlTime object. + f_source_control = fields_factory.create_scalar_field( + num_entities=1, location=locations.time_freq + ) + f_source_control.append([9.5, 9, 1, 0.5], 1) + support = TimeFreqSupport() + f_time = fields_factory.create_scalar_field(num_entities=1, location=locations.time_freq) + f_time.append([0, 1, 2, 3], 1) + support.time_frequencies = f_time + f_source_control.time_freq_support = support + + # Create a second SourceControlTime object. + source_control2 = SourceControlTime() + source_control2.control = f_source_control + + # Create a SourceBroadbandNoiseTwoParameters object test source file with less and created + # source controls. + source_bbn_two_parameters_obj = SourceBroadbandNoiseTwoParameters( + file=pytest.data_path_sound_composer_bbn_source_2p_in_container, + control1=source_control1, + control2=source_control2, + ) + + source_bbn_two_parameters_obj.process() + assert source_bbn_two_parameters_obj._output is not None + + +def test_source_broadband_noise_two_parameters_process_exceptions(dpf_sound_test_server): + """Test SourceBroadbandNoiseTwoParameters process method exceptions.""" + # Test process method exception1 (missing controls). + source_bbn_two_parameters_obj = SourceBroadbandNoiseTwoParameters( + pytest.data_path_sound_composer_bbn_source_2p_in_container + ) + with pytest.raises( + PyAnsysSoundException, + match=( + "At least one source control for broadband noise with two parameters is not set. " + "Use ``SourceBroadbandNoiseTwoParameters.source_control1`` and/or " + "``SourceBroadbandNoiseTwoParameters.source_control2``." + ), + ): + source_bbn_two_parameters_obj.process() + + # Test process method exception2 (missing bbn source data). + source_bbn_two_parameters_obj.source_bbn_two_parameters = None + f_source_control = fields_factory.create_scalar_field( + num_entities=1, location=locations.time_freq + ) + f_source_control.append([1.0, 2.0, 3.0, 4.0, 5.0], 1) + source_control_obj = SourceControlTime() + source_control_obj.control = f_source_control + source_bbn_two_parameters_obj.source_control1 = source_control_obj + source_bbn_two_parameters_obj.source_control2 = source_control_obj + with pytest.raises( + PyAnsysSoundException, + match=( + "Broadband noise source with two parameters data is not set. Use " + "``SourceBroadbandNoiseTwoParameters.source_bbn_two_parameters`` or method " + "``SourceBroadbandNoiseTwoParameters.load_source_bbn_two_parameters\\(\\)``." + ), + ): + source_bbn_two_parameters_obj.process() + + # Test process method exception3 (invalid sampling frequency value). + source_bbn_two_parameters_obj.load_source_bbn_two_parameters( + pytest.data_path_sound_composer_bbn_source_2p_in_container + ) + with pytest.raises( + PyAnsysSoundException, match="Sampling frequency must be strictly positive." + ): + source_bbn_two_parameters_obj.process(sampling_frequency=0.0) + + +def test_source_broadband_noise_two_parameters_get_output(dpf_sound_test_server): + """Test SourceBroadbandNoiseTwoParameters get_output method.""" + # Create a field to use in a SourceControlTime object. + f_source_control = fields_factory.create_scalar_field( + num_entities=1, location=locations.time_freq + ) + f_source_control.append([3, 4, 35, 38], 1) + support = TimeFreqSupport() + f_time = fields_factory.create_scalar_field(num_entities=1, location=locations.time_freq) + f_time.append([0, 1, 2, 3], 1) + support.time_frequencies = f_time + f_source_control.time_freq_support = support + + # Create a first SourceControlTime object. + source_control1 = SourceControlTime() + source_control1.control = f_source_control + + # Create another field to use in a SourceControlTime object. + f_source_control = fields_factory.create_scalar_field( + num_entities=1, location=locations.time_freq + ) + f_source_control.append([9.5, 9, 1, 0.5], 1) + support = TimeFreqSupport() + f_time = fields_factory.create_scalar_field(num_entities=1, location=locations.time_freq) + f_time.append([0, 1, 2, 3], 1) + support.time_frequencies = f_time + f_source_control.time_freq_support = support + + # Create a second SourceControlTime object. + source_control2 = SourceControlTime() + source_control2.control = f_source_control + + # Create a SourceBroadbandNoiseTwoParameters object test source file and created source + # controls. + source_bbn_two_parameters_obj = SourceBroadbandNoiseTwoParameters( + file=pytest.data_path_sound_composer_bbn_source_2p_in_container, + control1=source_control1, + control2=source_control2, + ) + + source_bbn_two_parameters_obj.process(sampling_frequency=44100.0) + f_output = source_bbn_two_parameters_obj.get_output() + assert isinstance(f_output, Field) + assert len(f_output.data) / 44100.0 == pytest.approx(3.0) + + # Compute the power spectral density over the output signal. + psd = PowerSpectralDensity( + input_signal=f_output, + fft_size=8192, + window_type="HANN", + window_length=8192, + overlap=0.75, + ) + psd.process() + psd_squared, psd_freq = psd.get_PSD_squared_linear_as_nparray() + delat_f = psd_freq[1] - psd_freq[0] + + # Check the sound power level in the octave bands centered at 250, 1000 and 4000 Hz. + # Due to the non-deterministic nature of the produced signal, tolerance is set to 3 dB. + psd_squared_band = psd_squared[ + (psd_freq >= 250 * 2 ** (-1 / 2)) & (psd_freq < 250 * 2 ** (1 / 2)) + ] + level = 10 * np.log10(psd_squared_band.sum() * delat_f / REF_ACOUSTIC_POWER) + assert level == pytest.approx(EXP_LEVEL_OCTAVE_BAND_250, abs=3.0) + + psd_squared_band = psd_squared[ + (psd_freq >= 1000 * 2 ** (-1 / 2)) & (psd_freq < 1000 * 2 ** (1 / 2)) + ] + level = 10 * np.log10(psd_squared_band.sum() * delat_f / REF_ACOUSTIC_POWER) + assert level == pytest.approx(EXP_LEVEL_OCTAVE_BAND_1000, abs=3.0) + + psd_squared_band = psd_squared[ + (psd_freq >= 4000 * 2 ** (-1 / 2)) & (psd_freq < 4000 * 2 ** (1 / 2)) + ] + level = 10 * np.log10(psd_squared_band.sum() * delat_f / REF_ACOUSTIC_POWER) + assert level == pytest.approx(EXP_LEVEL_OCTAVE_BAND_4000, abs=3.0) + + +def test_source_broadband_noise_two_parameters_get_output_unprocessed(dpf_sound_test_server): + """Test SourceBroadbandNoiseTwoParameters get_output method's exception.""" + source_bbn_two_parameters_obj = SourceBroadbandNoiseTwoParameters() + with pytest.warns( + PyAnsysSoundWarning, + match=( + "Output is not processed yet. Use the " + "``SourceBroadbandNoiseTwoParameters.process\\(\\)`` method." + ), + ): + f_output = source_bbn_two_parameters_obj.get_output() + assert f_output is None + + +def test_source_broadband_noise_two_parameters_get_output_as_nparray(dpf_sound_test_server): + """Test SourceBroadbandNoiseTwoParameters get_output_as_nparray method.""" + # Create a field to use in a SourceControlTime object. + f_source_control = fields_factory.create_scalar_field( + num_entities=1, location=locations.time_freq + ) + f_source_control.append([3, 4, 35, 38], 1) + support = TimeFreqSupport() + f_time = fields_factory.create_scalar_field(num_entities=1, location=locations.time_freq) + f_time.append([0, 1, 2, 3], 1) + support.time_frequencies = f_time + f_source_control.time_freq_support = support + + # Create a first SourceControlTime object. + source_control1 = SourceControlTime() + source_control1.control = f_source_control + + # Create another field to use in a SourceControlTime object. + f_source_control = fields_factory.create_scalar_field( + num_entities=1, location=locations.time_freq + ) + f_source_control.append([9.5, 9, 1, 0.5], 1) + support = TimeFreqSupport() + f_time = fields_factory.create_scalar_field(num_entities=1, location=locations.time_freq) + f_time.append([0, 1, 2, 3], 1) + support.time_frequencies = f_time + f_source_control.time_freq_support = support + + # Create a second SourceControlTime object. + source_control2 = SourceControlTime() + source_control2.control = f_source_control + + # Create a SourceBroadbandNoiseTwoParameters object test source file with less and created + # source controls. + source_bbn_two_parameters_obj = SourceBroadbandNoiseTwoParameters( + file=pytest.data_path_sound_composer_bbn_source_2p_in_container, + control1=source_control1, + control2=source_control2, + ) + + source_bbn_two_parameters_obj.process(sampling_frequency=44100.0) + output_nparray = source_bbn_two_parameters_obj.get_output_as_nparray() + assert isinstance(output_nparray, np.ndarray) + assert len(output_nparray) / 44100.0 == pytest.approx(3.0) + + +def test_source_broadband_noise_two_parameters_get_output_as_nparray_unprocessed( + dpf_sound_test_server, +): + """Test SourceBroadbandNoiseTwoParameters get_output_as_nparray method's exception.""" + source_bbn_two_parameters_obj = SourceBroadbandNoiseTwoParameters() + with pytest.warns( + PyAnsysSoundWarning, + match=( + "Output is not processed yet. Use the " + "``SourceBroadbandNoiseTwoParameters.process\\(\\)`` method." + ), + ): + output_nparray = source_bbn_two_parameters_obj.get_output_as_nparray() + assert len(output_nparray) == 0 + + +@patch("matplotlib.pyplot.show") +def test_source_broadband_noise_two_parameters_plot(mock_show, dpf_sound_test_server): + """Test SourceBroadbandNoiseTwoParameters plot method.""" + # Create a field to use in a SourceControlTime object. + f_source_control = fields_factory.create_scalar_field( + num_entities=1, location=locations.time_freq + ) + f_source_control.append([3, 4, 35, 38], 1) + support = TimeFreqSupport() + f_time = fields_factory.create_scalar_field(num_entities=1, location=locations.time_freq) + f_time.append([0, 1, 2, 3], 1) + support.time_frequencies = f_time + f_source_control.time_freq_support = support + + # Create a first SourceControlTime object. + source_control1 = SourceControlTime() + source_control1.control = f_source_control + + # Create another field to use in a SourceControlTime object. + f_source_control = fields_factory.create_scalar_field( + num_entities=1, location=locations.time_freq + ) + f_source_control.append([9.5, 9, 1, 0.5], 1) + support = TimeFreqSupport() + f_time = fields_factory.create_scalar_field(num_entities=1, location=locations.time_freq) + f_time.append([0, 1, 2, 3], 1) + support.time_frequencies = f_time + f_source_control.time_freq_support = support + + # Create a second SourceControlTime object. + source_control2 = SourceControlTime() + source_control2.control = f_source_control + + # Create a SourceBroadbandNoiseTwoParameters object test source file with less and created + # source controls. + source_bbn_two_parameters_obj = SourceBroadbandNoiseTwoParameters( + file=pytest.data_path_sound_composer_bbn_source_2p_in_container, + control1=source_control1, + control2=source_control2, + ) + + source_bbn_two_parameters_obj.process() + source_bbn_two_parameters_obj.plot() + + +def test_source_broadband_noise_two_parameters_plot_exceptions(dpf_sound_test_server): + """Test SourceBroadbandNoiseTwoParameters plot method's exception.""" + source_bbn_two_parameters_obj = SourceBroadbandNoiseTwoParameters() + with pytest.raises( + PyAnsysSoundException, + match=( + "Output is not processed yet. Use the " + "'SourceBroadbandNoiseTwoParameters.process\\(\\)' method." + ), + ): + source_bbn_two_parameters_obj.plot() + + +def test_source_broadband_noise_two_parameters___extract_bbn_two_parameters_info( + dpf_sound_test_server, +): + """Test SourceBroadbandNoiseTwoParameters __extract_bbn_two_parameters_info method.""" + source = SourceBroadbandNoiseTwoParameters() + assert source._SourceBroadbandNoiseTwoParameters__extract_bbn_two_parameters_info() == ( + "", + 0.0, + "", + "", + (), + "", + "", + (), + ) + + source.load_source_bbn_two_parameters( + pytest.data_path_sound_composer_bbn_source_2p_in_container + ) + assert source._SourceBroadbandNoiseTwoParameters__extract_bbn_two_parameters_info() == ( + "Not available", + 1000.0, + "temperature", + "celsius", + (2.5, 40.0), + "charge", + "%", + (0, 10.0), + ) + + # Test with empty control support (delta_f not applicable). + source.source_bbn_two_parameters[0].time_freq_support.time_frequencies.data = [] + assert source._SourceBroadbandNoiseTwoParameters__extract_bbn_two_parameters_info() == ( + "Not available", + 0.0, + "temperature", + "celsius", + (2.5, 40.0), + "charge", + "%", + (0, 10.0), + )