Skip to content
67 changes: 67 additions & 0 deletions pyrato/parametric.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,73 @@
"""
import numpy as np

def schroeder_frequency(volume, reverberation_time):
r"""
Calculate the Schroeder cut-off frequency of a room.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add the source - the citation is broken

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice hint, but we don´t know how ... seems as broken as for energy_decay_curve_analytic function below.

Calculation according to [#]_:

.. math::

f_s = 2000 \sqrt{\left(\frac{T}{V}\right)}

Parameters
----------
volume : float, np.ndarray
room volume in m^3
reverberation_time : float, np.ndarray
reverberation time in s

Returns
-------
schroeder_frequency : float, np.ndarray
schroeder frequency in Hz

Raises
------
TypeError
If inputs are not numeric or NumPy arrays.
ValueError
If inputs are non-positive or have incompatible shapes

References
----------
.. [#] H. Kuttruff, Room acoustics, 4th Ed. Taylor & Francis, 2009.

Note
----
this function still needs some tests ...

"""
if volume is None or reverberation_time is None:
raise TypeError("volume and reverberation_time cannot be None.")

if isinstance(volume, str) or isinstance(reverberation_time, str):
raise TypeError("volume and reverberation_time cannot be strings.")
if isinstance(volume, np.ndarray) and volume.dtype.kind in {"U", "S", "O"}:
raise TypeError("volume must contain only numeric values.")
if (
isinstance(reverberation_time, np.ndarray) and
reverberation_time.dtype.kind in {"U", "S", "O"}
):
raise TypeError("reverberation_time only numeric values.")

volume = np.asarray(volume, dtype=float)
reverberation_time = np.asarray(reverberation_time, dtype=float)
if not np.issubdtype(volume.dtype, np.floating):
raise TypeError("volume must be a float or a numeric array.")
if not np.issubdtype(reverberation_time.dtype, np.floating):
raise TypeError("reverberation_time must be float or numeric array.")
if np.any(volume <= 0) | np.any(reverberation_time <= 0):
raise ValueError("volume and reverberation_time " \
"must be positiv")
if volume.size != reverberation_time.size:
raise ValueError("volume and reverberation_time must have" \
" compatible shapes, either same shape or one is scalar.")
schroeder_frequency = 2000*np.sqrt(reverberation_time / volume)

return schroeder_frequency

def energy_decay_curve_analytic(
surfaces, alphas, volume, times, source=None,
receiver=None, method='eyring', c=343.4, frequency=None,
Expand Down
58 changes: 58 additions & 0 deletions tests/test_schroeder_frequency.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import numpy as np
import pytest
from pyrato.parametric import schroeder_frequency

def test_schroeder_frequency_scalar():
"""Test with scalar float inputs."""
f_s = schroeder_frequency(100.0, 1.0)
expected = 2000 * np.sqrt(1.0 / 100.0)
assert np.isclose(f_s, expected)


def test_schroeder_frequency_array():
"""Test with matching array inputs."""
volumes = np.array([100.0, 200.0, 400.0])
reverb_times = np.array([1.0, 2.0, 4.0])
f_s = schroeder_frequency(volumes, reverb_times)
expected = 2000 * np.sqrt(reverb_times / volumes)
np.testing.assert_allclose(f_s, expected,rtol=1e-5)


def test_schroeder_frequency_broadcasting():
"""Test with scalar and array combination (broadcasting)."""
volume = 100.0
reverb_times = np.array([0.5, 1.0, 2.0])
with pytest.raises(ValueError, match="volume and reverberation_time " \
"must have compatible shapes, either same shape or one is scalar."):
schroeder_frequency(volume, reverb_times)


@pytest.mark.parametrize(("volume", "reverb_time"), [(0, 1.0),
(-10, 1.0),
(100, 0),
(100, -2)])
def test_invalid_values(volume, reverb_time):
"""Test that non-positive values raise ValueError."""
with pytest.raises(ValueError, match="volume and reverberation_time " \
"must be positiv"):
schroeder_frequency(volume, reverb_time)


def test_shape_mismatch():
"""Test that mismatched shapes raise ValueError."""
volumes = np.array([100, 200])
reverb_times = np.array([1.0, 2.0, 3.0])
with pytest.raises(ValueError, match="volume and reverberation_time " \
"must have compatible shapes, either same shape or one is scalar."):
schroeder_frequency(volumes, reverb_times)


@pytest.mark.parametrize(("volume", "reverb_time"), [
("abc", 1.0),
(100, "1.0"),
(None, 1.0),
])
def test_invalid_types(volume, reverb_time):
"""Test that non-numeric inputs raise TypeError."""
with pytest.raises(TypeError):
schroeder_frequency(volume, reverb_time)