Repository: pyfar/pyfar
Pull request #756 (2025-01-17 16:21:45)

Add missing default in `pyfar.FilterIIR.init_state`
### Changes proposed in this pull request:

Technically a bug, but I assume this function is rarely used. Make it a bug or release with the next features?

- Add missing default in `pyfar.FilterIIR.init_state`
- Test default value
- Implement and test raising an error for invalud values



Patch:
diff --git a/pyfar/classes/filter.py b/pyfar/classes/filter.py
index be82fc20..5bf09657 100644
--- a/pyfar/classes/filter.py
+++ b/pyfar/classes/filter.py
@@ -159,11 +159,25 @@ class Filter(object):
         self._sampling_rate = sampling_rate
         self.comment = comment
 
-    def init_state(self, state='zeros'):
-        """Initialize the buffer elements to pre-defined initial conditions."""
+    def init_state(self, state):
+        """
+        Initialize the buffer elements to pre-defined initial conditions.
+
+        This method is overwritten in the child classes and called after
+        setting the state there.
+        """
         self._state = state
         self._initialized = True
 
+    @staticmethod
+    def _check_state_keyword(state):
+        """
+        Check the value of the state keyword for 'ini_state' class methods.
+        """
+        if state not in ['zeros', 'step']:
+            raise ValueError(
+                f"state is '{state}' but must be 'zeros' or 'step'")
+
     @property
     def coefficients(self):
         """
@@ -377,6 +391,8 @@ class FilterFIR(Filter):
             an empty filter, or ``'step'`` which constructs the initial
             conditions for step response steady-state. The default is 'zeros'.
         """
+        self._check_state_keyword(state)
+
         new_state = np.zeros((self.n_channels, *cshape, self.order))
         if state == 'step':
             for idx, coeff in enumerate(self._coefficients):
@@ -436,7 +452,7 @@ class FilterIIR(Filter):
         """The order of the filter."""
         return np.max(self._coefficients.shape[-2:]) - 1
 
-    def init_state(self, cshape, state):
+    def init_state(self, cshape, state='zeros'):
         """Initialize the buffer elements to pre-defined initial conditions.
 
         Parameters
@@ -449,6 +465,8 @@ class FilterIIR(Filter):
             an empty filter, or ``'step'`` which constructs the initial
             conditions for step response steady-state. The default is 'zeros'.
         """
+        self._check_state_keyword(state)
+
         new_state = np.zeros((self.n_channels, *cshape, self.order))
         if state == 'step':
             for idx, coeff in enumerate(self._coefficients):
@@ -543,6 +561,8 @@ class FilterSOS(Filter):
             an empty filter, or ``'step'`` which constructs the initial
             conditions for step response steady-state. The default is 'zeros'.
         """
+        self._check_state_keyword(state)
+
         new_state = np.zeros((self.n_channels, *cshape, self.n_sections, 2))
         if state == 'step':
             for idx, coeff in enumerate(self._coefficients):


Test patch:
diff --git a/tests/test_filter_classes.py b/tests/test_filter_classes.py
index 79c3f6e8..c646af5f 100644
--- a/tests/test_filter_classes.py
+++ b/tests/test_filter_classes.py
@@ -43,6 +43,32 @@ def test_filter_comment():
         pf.Signal([1, 2, 3], 44100, comment=[1, 2, 3])
 
 
+@pytest.mark.parametrize('filter_object', [
+    (fo.FilterFIR([[1, -1]], 48000)),
+    (fo.FilterIIR([[1, -1], [1, -1]], 48000)),
+    (fo.FilterSOS([[[1, -1, 1, 1, -1, 1]]], 48000)),
+])
+def test_filter_init_state_default_and_error(filter_object):
+    """
+    Test the default for the state keyword and if an error is raised if passing
+    an invalid value.
+    """
+
+    # set state explicitly
+    filter_object.init_state((1, ), 'zeros')
+    state_explicit = filter_object.state.copy()
+    # set state implicitly using the default
+    filter_object.init_state((1, ))
+    state_implicit = filter_object.state.copy()
+    # assert states
+    npt.assert_equal(state_implicit, state_explicit)
+
+    # test raising the error for an invalid value
+    with pytest.raises(ValueError, match="state is 'random' but must be"):
+        filter_object.init_state((1, ), 'random')
+
+
+
 def test_filter_iir_init():
     coeff = np.array([[1, 1/2, 0], [1, 0, 0]])
     filt = fo.FilterIIR(coeff, sampling_rate=2*np.pi)