From 58aa313d06058734c8e79ffce7867242fbff3af8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 21:51:05 +0000 Subject: [PATCH 1/2] Initial plan From 5eb290da9c2e4d44409282daddbb23d87c8cced8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 21:54:54 +0000 Subject: [PATCH 2/2] Implement sos2ss, sos2tf, and sos2zp functions in transfer.py Co-authored-by: cokelaer <778821+cokelaer@users.noreply.github.com> --- src/spectrum/transfer.py | 88 ++++++++++++++++++++++++++++++++++++++-- test/test_transfer.py | 40 ++++++++++++++++++ 2 files changed, 125 insertions(+), 3 deletions(-) diff --git a/src/spectrum/transfer.py b/src/spectrum/transfer.py index 11f6632..bd36bce 100644 --- a/src/spectrum/transfer.py +++ b/src/spectrum/transfer.py @@ -2,9 +2,9 @@ import numpy as np -__all__ = ["tf2zp", 'eqtflength', 'latc2tf', 'latcfilt', - 'ss2zpk', 'tf2sos', 'tf2ss', 'tf2zpk', 'zpk2ss', 'zpk2tf'] - +__all__ = ['eqtflength', 'latc2tf', 'latcfilt', + 'sos2ss', 'sos2tf', 'sos2zp', + 'ss2zpk', 'tf2zp', 'tf2sos', 'tf2ss', 'tf2zpk', 'zpk2ss', 'zpk2tf'] """to be done @@ -249,3 +249,85 @@ def zpk2ss(z, p, k): return scipy.signal.zpk2ss(z,p,k) +def sos2tf(sos): + """Return a single transfer function from a series of second-order sections. + + :param array_like sos: Array of second-order filter coefficients, must have + shape ``(n_sections, 6)``. Each row corresponds to a second-order + section, with the first three columns providing the numerator + coefficients and the last three providing the denominator coefficients. + + :return: + * b : ndarray Numerator polynomial coefficients. + * a : ndarray Denominator polynomial coefficients. + + .. doctest:: + + >>> import scipy.signal + >>> from spectrum.transfer import sos2tf + >>> sos = scipy.signal.butter(4, 0.2, output='sos') + >>> b, a = sos2tf(sos) + + .. note:: wrapper of scipy function sos2tf + .. seealso:: :func:`sos2ss`, :func:`sos2zp` + """ + import scipy.signal + return scipy.signal.sos2tf(sos) + + +def sos2zp(sos): + """Return zeros, poles, and gain of a series of second-order sections. + + :param array_like sos: Array of second-order filter coefficients, must have + shape ``(n_sections, 6)``. Each row corresponds to a second-order + section, with the first three columns providing the numerator + coefficients and the last three providing the denominator coefficients. + + :return: + * z : ndarray Zeros of the transfer function. + * p : ndarray Poles of the transfer function. + * k : float System gain. + + .. doctest:: + + >>> import scipy.signal + >>> from spectrum.transfer import sos2zp + >>> sos = scipy.signal.butter(4, 0.2, output='sos') + >>> z, p, k = sos2zp(sos) + + .. note:: wrapper of scipy function sos2zpk + .. seealso:: :func:`sos2ss`, :func:`sos2tf` + """ + import scipy.signal + return scipy.signal.sos2zpk(sos) + + +def sos2ss(sos): + """Convert digital filter second-order section parameters to state-space form. + + :param array_like sos: Array of second-order filter coefficients, must have + shape ``(n_sections, 6)``. Each row corresponds to a second-order + section, with the first three columns providing the numerator + coefficients and the last three providing the denominator coefficients. + + :return: + * A : ndarray State-space representation of the IIR filter. + * B : ndarray State-space representation of the IIR filter. + * C : ndarray State-space representation of the IIR filter. + * D : ndarray State-space representation of the IIR filter. + + .. doctest:: + + >>> import scipy.signal + >>> from spectrum.transfer import sos2ss + >>> sos = scipy.signal.butter(4, 0.2, output='sos') + >>> A, B, C, D = sos2ss(sos) + + .. note:: Implemented by chaining :func:`sos2tf` and ``scipy.signal.tf2ss``. + .. seealso:: :func:`sos2tf`, :func:`sos2zp` + """ + import scipy.signal + b, a = sos2tf(sos) + return scipy.signal.tf2ss(b, a) + + diff --git a/test/test_transfer.py b/test/test_transfer.py index 56b9971..3db10b1 100644 --- a/test/test_transfer.py +++ b/test/test_transfer.py @@ -89,6 +89,46 @@ def test_ss2zpk(): assert k == kp +def test_sos2tf(): + from scipy import signal + sos = signal.butter(4, 0.2, output='sos') + b, a = transfer.sos2tf(sos) + # Verify round-trip: sos2tf then tf2zpk should match sos2zpk + z_tf, p_tf, k_tf = signal.tf2zpk(b, a) + z_sos, p_sos, k_sos = signal.sos2zpk(sos) + assert_almost_equal(abs(k_tf), abs(k_sos), decimal=5) + assert len(z_tf) == len(z_sos) + assert len(p_tf) == len(p_sos) + + +def test_sos2zp(): + from scipy import signal + sos = signal.butter(4, 0.2, output='sos') + z, p, k = transfer.sos2zp(sos) + z_ref, p_ref, k_ref = signal.sos2zpk(sos) + assert_almost_equal(np.sort(np.abs(z)), np.sort(np.abs(z_ref)), decimal=5) + assert_almost_equal(np.sort(np.abs(p)), np.sort(np.abs(p_ref)), decimal=5) + assert_almost_equal(k, k_ref, decimal=5) + + +def test_sos2ss(): + from scipy import signal + sos = signal.butter(4, 0.2, output='sos') + A, B, C, D = transfer.sos2ss(sos) + # Verify state-space matrices are returned with correct shapes + n_sections = sos.shape[0] + order = 2 * n_sections + assert A.shape == (order, order) + assert B.shape == (order, 1) + assert C.shape == (1, order) + assert D.shape == (1, 1) + # Verify round-trip: ss2zpk should recover poles/zeros matching sos2zpk + z_ss, p_ss, k_ss = signal.ss2zpk(A, B, C, D) + z_sos, p_sos, k_sos = signal.sos2zpk(sos) + assert_almost_equal(np.sort(np.abs(p_ss)), np.sort(np.abs(p_sos)), decimal=5) + assert_almost_equal(abs(k_ss), abs(k_sos), decimal=5) + +