diff --git a/pyrato/parametric.py b/pyrato/parametric.py index d0809ac8..d9cbdd4b 100644 --- a/pyrato/parametric.py +++ b/pyrato/parametric.py @@ -5,6 +5,73 @@ """ import numpy as np +def schroeder_frequency(volume, reverberation_time): + r""" + Calculate the Schroeder cut-off frequency of a room. + + 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, diff --git a/tests/test_schroeder_frequency.py b/tests/test_schroeder_frequency.py new file mode 100644 index 00000000..ccdedada --- /dev/null +++ b/tests/test_schroeder_frequency.py @@ -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)