Repository: pyfar/pyfar
Pull request #664 (2024-08-13 11:28:41)

allow pf.signals.impulse(3, 0, np.ones((1,1))
- pf.signals.impulse(3, 0, np.ones((1,1)) was not working
- bug is fixed and tests are added



Patch:
diff --git a/pyfar/signals/deterministic.py b/pyfar/signals/deterministic.py
index 2dd4c73b..4a660750 100644
--- a/pyfar/signals/deterministic.py
+++ b/pyfar/signals/deterministic.py
@@ -32,12 +32,11 @@ def sine(frequency, n_samples, amplitude=1, phase=0, sampling_rate=44100,
 
     Notes
     -----
-    The parameters `frequency`, `amplitude`, and `phase` are
-    Numpy broadcasting :doc:`broadcasted<numpy:user/basics.broadcasting>`
-    to the parameter that contains the most elements. For example `frequency`
-    could be of shape ``(2, 4)``, `amplitude` of shape ``(2, 1)``, and `phase`
-    could be a scalar. In this case all parameters would be broadcasted to a
-    shape of ``(2, 4)``.
+    The parameters `frequency`, `amplitude`, and `phase` are broadcasted using
+    the :doc:`numpy rules<numpy:user/basics.broadcasting>`. For example
+    `frequency` could be of shape ``(2, 4)``, `amplitude` of shape ``(2, 1)``,
+    and `phase` could be a scalar. In this case all parameters would be
+    broadcasted to a shape of ``(2, 4)``.
     """
 
     # check and match the cshape
@@ -98,7 +97,7 @@ def impulse(n_samples, delay=0, amplitude=1, sampling_rate=44100):
         Length of the impulse in samples
     delay : double, array like, optional
         Delay in samples. The default is ``0``.
-    amplitude : double, optional
+    amplitude : double, array like, optional
         The peak amplitude of the impulse. The default is ``1``.
     sampling_rate : int, optional
         The sampling rate in Hz. The default is ``44100``.
@@ -113,9 +112,8 @@ def impulse(n_samples, delay=0, amplitude=1, sampling_rate=44100):
 
     Notes
     -----
-    The parameters `delay` and `amplitude` are
-    :doc:`broadcasted<numpy:user/basics.broadcasting>`
-    to the parameter that contains the most elements. For example `delay`
+    The parameters `delay` and `amplitude` are broadcasted using the
+    :doc:`numpy rules<numpy:user/basics.broadcasting>`. For example `delay`
     could be of shape ``(2, 4)``, `amplitude` of shape ``(2, 1)`` or a scalar.
     In this case all parameters would be broadcasted to a shape of ``(2, 4)``.
     """
@@ -338,7 +336,7 @@ def _exponential_sweep(n_samples, frequency_range, amplitude, sweep_rate,
 def _match_shape(*args):
     """
     Match the shape of *args to the shape of the arg with the largest size
-    using np.broadcast_to()
+    using np.broadcast_shapes and np.broadcast_to()
 
     Parameters
     ----------
@@ -354,20 +352,14 @@ def _match_shape(*args):
         (*arg_1, *arg_2, ..., *arg_N)
     """
 
-    # find the shape of the largest array
-    size = 1
-    shape = (1, )
-    for arg in args:
-        arg = np.asarray(arg)
-        if arg.size > size:
-            size = arg.size
-            shape = arg.shape
+    # broadcast shapes
+    shape = np.broadcast_shapes(*[np.atleast_1d(arg).shape for arg in args])
 
-    # try to match the shape
+    # match the shape
     result = []
     for arg in args:
         arg = np.broadcast_to(arg, shape)
         arg.setflags(write=1)
-        result.append(np.atleast_1d(arg))
+        result.append(arg)
 
     return shape, result


Test patch:
diff --git a/tests/test_signals.py b/tests/test_signals.py
index a141f881..ead3ccf0 100644
--- a/tests/test_signals.py
+++ b/tests/test_signals.py
@@ -97,6 +97,41 @@ def test_impulse_multi_channel():
     npt.assert_allclose(signal.time, ref)
 
 
+@pytest.mark.parametrize("delay_shape", [
+    (1,), (1, 1), (2, 2), (2, 1), (1, 2), None])
+@pytest.mark.parametrize("amp_shape", [
+    (1,), (1, 1), (2, 2), (2, 1), (1, 2), None])
+def test_impulse_different_cshapes(delay_shape, amp_shape,):
+    """test impulse different cshapes."""
+    signal = pfs.impulse(
+        3,
+        0 if delay_shape is None else np.zeros(delay_shape, dtype=int),
+        1 if amp_shape is None else np.ones(amp_shape),
+        )
+    desired = np.ones(np.broadcast_shapes(delay_shape, amp_shape, (1,)))
+    npt.assert_allclose(signal.time[..., 0], desired)
+    npt.assert_allclose(np.sum(signal.time, axis=-1), desired)
+
+
+@pytest.mark.parametrize("frequency_shape", [
+    (1,), (1, 1), (2, 2), (2, 1), (1, 2), None])
+@pytest.mark.parametrize("amp_shape", [
+    (1,), (1, 1), (2, 2), (2, 1), (1, 2), None])
+@pytest.mark.parametrize("phase_shape", [
+    (1,), (1, 1), (2, 2), (2, 1), (1, 2), None])
+def test_sine_different_cshapes(frequency_shape, amp_shape, phase_shape,):
+    """test impulse different cshapes."""
+    signal = pfs.sine(
+        1 if frequency_shape is None else np.ones(frequency_shape),
+        5,
+        1 if amp_shape is None else np.ones(amp_shape),
+        0 if phase_shape is None else np.zeros(phase_shape),
+        )
+    desired_cshape = np.broadcast_shapes(
+        frequency_shape, amp_shape, phase_shape, (1,))
+    npt.assert_allclose(signal.cshape, desired_cshape)
+
+
 def test_impulse_float():
     """Test impulse signal with float number of samples."""
     signal = pfs.impulse(441.8)