diff --git a/docs/api_reference.rst b/docs/api_reference.rst index 8f0383c..cc25d99 100644 --- a/docs/api_reference.rst +++ b/docs/api_reference.rst @@ -16,7 +16,7 @@ Modules .. toctree:: :maxdepth: 1 - modules/imkar + modules/imkar.scattering.diffuse .. _examples gallery: https://pyfar-gallery.readthedocs.io/en/latest/examples_gallery.html diff --git a/docs/modules/imkar.rst b/docs/modules/imkar.rst deleted file mode 100644 index 0ea5287..0000000 --- a/docs/modules/imkar.rst +++ /dev/null @@ -1,7 +0,0 @@ -imkar -===== - -.. automodule:: imkar - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/modules/imkar.scattering.diffuse.rst b/docs/modules/imkar.scattering.diffuse.rst new file mode 100644 index 0000000..61c187d --- /dev/null +++ b/docs/modules/imkar.scattering.diffuse.rst @@ -0,0 +1,7 @@ +imkar.scattering.diffuse +======================== + +.. automodule:: imkar.scattering.diffuse + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/imkar/__init__.py b/imkar/__init__.py index 0dcfbd5..c5cd5d6 100644 --- a/imkar/__init__.py +++ b/imkar/__init__.py @@ -5,3 +5,9 @@ __author__ = """The pyfar developers""" __email__ = '' __version__ = '0.1.0' + +from . import scattering + +__all__ = [ + "scattering", +] diff --git a/imkar/scattering/__init__.py b/imkar/scattering/__init__.py new file mode 100644 index 0000000..ec54f2d --- /dev/null +++ b/imkar/scattering/__init__.py @@ -0,0 +1,8 @@ + +"""Imkar scattering module.""" + +from . import diffuse + +__all__ = [ + "diffuse", +] diff --git a/imkar/scattering/diffuse.py b/imkar/scattering/diffuse.py new file mode 100644 index 0000000..6b793b1 --- /dev/null +++ b/imkar/scattering/diffuse.py @@ -0,0 +1,92 @@ +""" +This module contains functions for diffuse scattering calculations based on +ISO 17497-1:2004. +""" +import numpy as np +import pyfar as pf + + +def maximum_sample_absorption_coefficient(frequencies) -> pf.FrequencyData: + """Maximum absorption coefficient of the test sample. + + Based on section 6.3.4 in ISO 17497-1:2004 [#]_ the absorption coefficient + of the test sample should not exceed a value of :math:`alpha_s=0.5`. + However, if sound absorption is part of the sound-scattering structure, + this absorption shall also be present in the test sample. + + Parameters + ---------- + frequencies : numpy.ndarray + The frequencies at which the absorption coefficient is calculated. + + Returns + ------- + alpha_s_max : pyfar.FrequencyData + The maximum sample absorption coefficient. + + References + ---------- + .. [#] ISO 17497-1:2004, Sound-scattering properties of surfaces. Part 1: + Measurement of the random-incidence scattering coefficient in a + reverberation room. Geneva, Switzerland: International Organization + for Standards, 2004. + + """ + # input checks + try: + frequencies = np.asarray(frequencies, dtype=float) + except ValueError as exc: + raise TypeError( + "frequencies must be convertible to a float array.") from exc + if frequencies.ndim != 1: + raise ValueError("frequencies must be a 1D array.") + + # Calculate the maximum absorption coefficient + return pf.FrequencyData( + data=np.ones_like(frequencies) * 0.5, + frequencies=frequencies, + comment="Maximum absorption coefficient of the test sample", + ) + + +def maximum_baseplate_scattering_coefficient(N: int = 1) -> pf.FrequencyData: + """Maximum scattering coefficient for the base plate alone. + + This is based on Table 1 in ISO 17497-1:2004 [#]_. + + Parameters + ---------- + N : int + ratio of any linear dimension in a physical scale model to the + same linear dimension in full scale (1:N). The default is N=1. + + Returns + ------- + s_base_max : pyfar.FrequencyData + The maximum baseplate scattering coefficient. + + References + ---------- + .. [#] ISO 17497-1:2004, Sound-scattering properties of surfaces. Part 1: + Measurement of the random-incidence scattering coefficient in a + reverberation room. Geneva, Switzerland: International Organization + for Standards, 2004. + + """ + if not isinstance(N, int): + raise TypeError("N must be a positive integer.") + if N <= 0: + raise TypeError("N must be a positive integer.") + frequencies = [ + 100, 125, 160, 200, 250, 315, 400, 500, 630, + 800, 1000, 1250, 1600, 2000, 2500, 3150, 4000, 5000, + ] + s_base_max = [ + 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.10, + 0.10, 0.10, 0.15, 0.15, 0.15, 0.20, 0.20, 0.20, 0.25, + ] + return pf.FrequencyData( + data=s_base_max, + frequencies=np.array(frequencies)/N, + comment="Maximum scattering coefficient of the baseplate", + ) diff --git a/tests/test_scattering_diffuse.py b/tests/test_scattering_diffuse.py new file mode 100644 index 0000000..a91aa42 --- /dev/null +++ b/tests/test_scattering_diffuse.py @@ -0,0 +1,76 @@ +import numpy as np +import pytest +import imkar.scattering.diffuse as isd +import pyfar as pf + + +def test_maximum_sample_absorption_coefficient_basic(): + freqs = np.array([100, 200, 400, 800]) + result = isd.maximum_sample_absorption_coefficient(freqs) + assert isinstance(result, pf.FrequencyData) + np.testing.assert_allclose(result.frequencies, freqs) + np.testing.assert_allclose(result.freq, 0.5) + assert "Maximum absorption coefficient" in result.comment + + +def test_maximum_sample_absorption_coefficient_list_input(): + freqs = [100, 200, 400] + result = isd.maximum_sample_absorption_coefficient(freqs) + np.testing.assert_allclose(result.frequencies, np.array(freqs)) + np.testing.assert_allclose(result.freq, 0.5) + + +def test_maximum_sample_absorption_coefficient_non_1d_input(): + freqs = np.array([[100, 200], [300, 400]]) + with pytest.raises(ValueError, match="frequencies must be a 1D array"): + isd.maximum_sample_absorption_coefficient(freqs) + + +def test_maximum_sample_absorption_coefficient_invalid_type(): + freqs = "not_a_number" + with pytest.raises( + TypeError, + match="frequencies must be convertible to a float array"): + isd.maximum_sample_absorption_coefficient(freqs) + + +def test_maximum_baseplate_scattering_coefficient_default(): + result = isd.maximum_baseplate_scattering_coefficient() + assert isinstance(result, pf.FrequencyData) + expected_freqs = np.array([ + 100, 125, 160, 200, 250, 315, 400, 500, 630, + 800, 1000, 1250, 1600, 2000, 2500, 3150, 4000, 5000, + ]) + expected_data = np.array([[ + 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.10, + 0.10, 0.10, 0.15, 0.15, 0.15, 0.20, 0.20, 0.20, 0.25, + ]]) + np.testing.assert_allclose(result.frequencies, expected_freqs) + np.testing.assert_allclose(result.freq, expected_data) + assert "baseplate" in result.comment + + +def test_maximum_baseplate_scattering_coefficient_with_scale(): + N = 2 + result = isd.maximum_baseplate_scattering_coefficient(N) + expected_freqs = np.array([ + 100, 125, 160, 200, 250, 315, 400, 500, 630, + 800, 1000, 1250, 1600, 2000, 2500, 3150, 4000, 5000, + ]) / N + np.testing.assert_allclose(result.frequencies, expected_freqs) + +def test_maximum_baseplate_scattering_coefficient_invalid_type(): + with pytest.raises(TypeError, match="N must be a positive integer."): + isd.maximum_baseplate_scattering_coefficient(N=1.5) + with pytest.raises(TypeError, match="N must be a positive integer."): + isd.maximum_baseplate_scattering_coefficient(N=-1) + +def test_maximum_baseplate_scattering_coefficient_N(): + # Test with a positive integer N to verify expected behavior + N = 5 + result = isd.maximum_baseplate_scattering_coefficient(N) + expected_freqs = np.array([ + 100, 125, 160, 200, 250, 315, 400, 500, 630, + 800, 1000, 1250, 1600, 2000, 2500, 3150, 4000, 5000, + ]) / N + np.testing.assert_allclose(result.frequencies, expected_freqs)