Repository: scipy/scipy

Issue:
Add support for sos (second-order sections) format
After https://github.com/scipy/scipy/issues/2443 is done, it would be good to support sos format (same as tf format, but broken up into second-order sections).  

I started working on this and then found https://github.com/scipy/scipy/issues/1175 which does this, as well as several other things, but is based on Octave GPL code, so can't be included in BSD SciPy. https://gist.github.com/endolith/4525003

 Either those functions can be re-written from scratch as BSD, or we can possibly get permission to relicense it.  I contacted the original author a while ago and he is ok with relicensing Octave signal stuff for SciPy as long as the people who run Octave are ok with it, but I never got a reply from them.  So either follow up on that or rewrite them from scratch.



Discussion:
@endolith any progress or changes on this issue? It might be nice to have native SOS support in `scipy.signal` so we can have stable higher-order IIR filtering.

(If not, I might be able to help looking into solutions.)

No, I haven't worked on second-order sections, though I've got the zpk filter design stuff done, which is a prerequisite for it actually being useful.

some questions:
- numpy.poly uses exact float equality to match up poles, but [cplxpair](http://www.mathworks.com/help/matlab/ref/cplxpair.html) from matlab uses approximate equality with a tolerance, which is probably necessary for the general case.  it's conceivable but unlikely that the pairs could be matched up incorrectly if they're close together and get sorted wrong because of numerical error.  maybe use a better fitting algorithm or maybe it doesn't matter.
  - actually matlab's zp2sos uses functions breakconjrealzp, orderp, orderz,  formsos, how do they work?
- Should sosfilt be its own function or should lfilter be retrofitted to accept sos or ba formats?  what about backward compatibility?
- how do you do sos filtering with initial conditions?  pass the output state zf of one filter to the zi of the next?

+1 for using a tolerance. This seems safest.

I think it would be great if `lfilter` and `filtfilt` could be retrofitted, but I don't see a simple API to use that would maintain backward compatibility. Did you have an idea?

I'll look into the initial conditions cases (my state-space type analysis is a little rusty) and see if I can figure it out if nobody beats me to it.

In terms of your first bullet, the documentation for those functions describes most of it. Briefly, poles and zeros are each sorted such that complex pairs are appear in order by ascending real part; these conjugate pairs are then followed by any real poles (`cplxpair` does the heavy lifting here). A tolerance of `100*eps` is used to determine which poles are real, as well as whether the pairs formed by ordering are correct. This seems safe enough to me -- if users complain it doesn't work or we hit errors in testing, then we can do something more sophisticated. But my guess is that improving it would be YAGNI territory...

Following this procedure, poles and zeros are each split into two vectors, one containing the complex pairs, and the other containing the real-valued poles/zeros. This is separated from the first step because `cplxpair` is a standalone function, and returns a concatenation of the pairs and real values. We can combine these computations to make a clearer presentation.

Then the poles (both complex pairs and real ones) are each sorted "according to proximity to [the] unit circle". (`orderp`)

`orderz` is slightly more complex, because the zeros effectively get "paired off" to the poles based on proximity. The "priority list" is to 1) minimize the Euclidean distance to the remaining pole pair closest to the unit circle, 2) minimize Euclidean distance to the remaining real pole closest to the unit circle, then 3) keep original zero order. This is done in a loop, where each time a zero (complex pair, or real-valued) is matched with a pole (complex pair, or real-valued), the pole isn't considered during any subsequent matchings.

`formsos` then handles the various use cases necessary to form the second-order systems.

Having looked over the concepts in the code, I can take a stab at implementing this, unless you were planning on giving it a shot.

I'm still looking into the initial conditions case.

BTW I didn't notice it until I had typed all that out, but Mathworks actually provides a similar (probably better) summary if you click the "Algorithms" button here:

http://www.mathworks.com/help/signal/ref/zp2sos.html

I was planning to eventually, but if you want to, go for it. There are other things I want to work on too.  Hopefully my cplxpair function is useful at least.

I can give it a shot. I'll try to build a `sosfilt`, and we can eventually figure out whether/how to integrate it with `lfilter` and/or `filtfilt`. I assume you mean `cplxpair` from here:

https://gist.github.com/endolith/4525003

How much of that is license-safe? If parts are and parts aren't, I could start from scratch and validate using your code.

cplxreal cplxpair and sosfilt i wrote from scratch, so theyre bsd licensed. the rest is direct translation from octave, so is gpl and needs to be rewritten. i have some other changes i never posted that i will update when im on my computer

Ok, now https://gist.github.com/endolith/4525003 has all my latest changes and notes :)

Excellent. Let's wait until #3085 is merged. Then if you start a new `scipy` branch with those changes from your Gist pasted into seemingly appropriate places, I can pull it and take it from there. That way you'd get credit for what you've done so far. Sound good?

I just realized Matlab has 2 functions for normal IIR filtering, but only 1 for zero-phase filtering:

```
y = filter(b,a,X) or
y = sosfilt(sos,x)

y = filtfilt(b,a,x)
y = filtfilt(SOS,G,x)
```

Are sosfilt and filter implemented so differently that they need to be separate functions?  I guess we need to do an sosfilt() also because of the number of arguments, but as part of https://github.com/scipy/scipy/issues/3259 maybe it could be a lfilter() function that does both?

(Oh I missed the part about adding a branch with my changes so far.  I'll try to do that soon.)

@endolith I'm not sure why Mathworks chose to do separate for `filter` and combined for `filtfilt`. I'm also not sure what the ideal API would be for `scipy`... my vote would be leave that discussion for #3259. Perhaps here (and in a PR) we could work on the simplest / cleanest API we can design to get SOS properly working for `lfilter`-equivalent functionality at first. Then we can worry about how to interface it with the existing functionality, most likely by making our underlying SOS filtering functions private in concert with whatever is decided in #3259. WDYT?

I'm happy to let you work on SOS filtering, work in tandem with you on it, or work on it mostly myself (based on what you have done so far), depending on how much time/effort you feel up to devoting. No rush on making a branch with your changes thus far -- but don't forget about the BSD licensing (or at least specify what isn't compatible) as you create your branch if I'm taking over.

One stumbling block for me is how to deal with initial conditions. I don't know how it's done with SOS -- do you have any resources that specify it? I'm not sure of any textbooks that have it, but I might have someone I could email to see if they knew a place we could look.

+1 for a simple `sosfilt` here. Even if we get a new fancy API tomorrow, it may still be useful for users with existing code to have this one old-style.

> One stumbling block for me is how to deal with initial conditions. I don't know how it's done with SOS -- do you have any resources that specify it?

sosfilt would have a zi input and a zf output, same as lfilter, and 
I believe you'd just use the zi argument as the zi for the first 2nd-order lfilter stage, then feed its zf to the zi of the next 2nd-order stage, and so on until you get a zf from the last stage, which is what you'd return along with the filtered output


Actually I don't know what I'm talking about.  
1. For an 8th-order single-stage filter, for instance, you would need enough initial conditions to fill all 8 delays, not just the first 2.  So all the 2nd-order stages need to be filled, not just the first.
2. 
Even if the user doesn't specify initial conditions, the stages will still have to pass them internally?
  No that's wrong, too.  You don't pass initial conditions from one stage to the next after filtering.  If the initial conditions of a single-stage filter are zero then they are for the second-order stages too,

I went through the same thought process when I hit this issue :) I'll see if I can find the answer somewhere. If the initial conditions are zero, I _think_ you can just keep using zero initial conditions for each stage and just pass the previous output signal through the next section sequentially. Thinking about SOS as a bunch of transfer functions multiplied together makes sense that way at least.

I think we might end up needing to find the roots of the initial condition polynomial so that we can split the initial conditions up into second-order chunks as well. Thinking about the response of the system as the sum of a zero-state response (i.e., what would have happened if the system had been at rest) plus the zero-input response (i.e., what would have happened due to initial conditions if the input was zero) might allow this approach to work. See something like:

http://www.csupomona.edu/~zaliyazici/ece308/ece308-12.pdf

I'd have to work out the math to figure out how to make it work. I'll probably still try to see if I can ask someone who's actually implemented SOS w/initial conditions, though, since there might be a better way to do it.


Resolving patch:
diff --git a/scipy/signal/__init__.py b/scipy/signal/__init__.py
--- a/scipy/signal/__init__.py
+++ b/scipy/signal/__init__.py
@@ -56,6 +56,10 @@
 
    deconvolve    -- 1-d deconvolution using lfilter.
 
+   sosfilt       -- 1-dimensional IIR digital linear filtering using
+                 -- a second-order-sections filter representation.
+   sosfilt_zi    -- Compute an initial state zi for the sosfilt function that
+                 -- corresponds to the steady state of the step response.
    hilbert       -- Compute 1-D analytic signal, using the Hilbert transform.
    hilbert2      -- Compute 2-D analytic signal, using the Hilbert transform.
 
@@ -167,11 +171,15 @@
    :toctree: generated/
 
    tf2zpk        -- transfer function to zero-pole-gain.
-   zpk2tf        -- zero-pole-gain to transfer function.
+   tf2sos        -- transfer function to second-order sections.
    tf2ss         -- transfer function to state-space.
-   ss2tf         -- state-pace to transfer function.
+   zpk2tf        -- zero-pole-gain to transfer function.
+   zpk2sos       -- zero-pole-gain to second-order sections.
    zpk2ss        -- zero-pole-gain to state-space.
+   ss2tf         -- state-pace to transfer function.
    ss2zpk        -- state-space to pole-zero-gain.
+   sos2zpk       -- second-order-sections to zero-pole-gain.
+   sos2tf        -- second-order-sections to transfer function.
    cont2discrete -- continuous-time to discrete-time LTI conversion.
 
 Waveforms
diff --git a/scipy/signal/filter_design.py b/scipy/signal/filter_design.py
--- a/scipy/signal/filter_design.py
+++ b/scipy/signal/filter_design.py
@@ -8,8 +8,9 @@
 from numpy import (atleast_1d, poly, polyval, roots, real, asarray, allclose,
                    resize, pi, absolute, logspace, r_, sqrt, tan, log10,
                    arctan, arcsinh, sin, exp, cosh, arccosh, ceil, conjugate,
-                   zeros, sinh, append, concatenate, prod, ones)
+                   zeros, sinh, append, concatenate, prod, ones, array)
 from numpy import mintypecode
+import numpy as np
 from scipy import special, optimize
 from scipy.special import comb
 
@@ -18,7 +19,8 @@
            'iirfilter', 'butter', 'cheby1', 'cheby2', 'ellip', 'bessel',
            'band_stop_obj', 'buttord', 'cheb1ord', 'cheb2ord', 'ellipord',
            'buttap', 'cheb1ap', 'cheb2ap', 'ellipap', 'besselap',
-           'filter_dict', 'band_dict', 'BadCoefficients']
+           'filter_dict', 'band_dict', 'BadCoefficients',
+           'tf2sos', 'sos2tf', 'zpk2sos', 'sos2zpk']
 
 
 class BadCoefficients(UserWarning):
@@ -246,16 +248,188 @@ def freqz(b, a=1, worN=None, whole=0, plot=None):
     return w, h
 
 
+def _cplxreal(z, tol=None):
+    """
+    Split into complex and real parts, combining conjugate pairs.
+
+    The 1D input vector `z` is split up into its complex (`zc`) and real (`zr`)
+    elements.  Every complex element must be part of a complex-conjugate pair,
+    which are combined into a single number (with positive imaginary part) in
+    the output.  Two complex numbers are considered a conjugate pair if their
+    real and imaginary parts differ in magnitude by less than ``tol * abs(z)``.
+
+    Parameters
+    ----------
+    z : array_like
+        Vector of complex numbers to be sorted and split
+    tol : float, optional
+        Relative tolerance for testing realness and conjugate equality.
+        Default is ``100 * spacing(1)`` of `z`'s data type (i.e. 2e-14 for
+        float64)
+
+    Returns
+    -------
+    zc : ndarray
+        Complex elements of `z`, with each pair represented by a single value
+        having positive imaginary part, sorted first by real part, and then
+        by magnitude of imaginary part.  The pairs are averaged when combined
+        to reduce error.
+    zr : ndarray
+        Real elements of `z` (those having imaginary part less than
+        `tol` times their magnitude), sorted by value.
+
+    Raises
+    ------
+    ValueError
+        If there are any complex numbers in `z` for which a conjugate
+        cannot be found.
+
+    See Also
+    --------
+    _cplxpair
+
+    Examples
+    --------
+    >>> a = [4, 3, 1, 2-2j, 2+2j, 2-1j, 2+1j, 2-1j, 2+1j, 1+1j, 1-1j]
+    >>> zc, zr = _cplxreal(a)
+    >>> print zc
+    [ 1.+1.j  2.+1.j  2.+1.j  2.+2.j]
+    >>> print zr
+    [ 1.  3.  4.]
+    """
+
+    z = atleast_1d(z)
+    if z.size == 0:
+        return z, z
+    elif z.ndim != 1:
+        raise ValueError('_cplxreal only accepts 1D input')
+
+    if tol is None:
+        # Get tolerance from dtype of input
+        tol = 100 * np.finfo((1.0 * z).dtype).eps
+
+    # Sort by real part, magnitude of imaginary part (speed up further sorting)
+    z = z[np.lexsort((abs(z.imag), z.real))]
+
+    # Split reals from conjugate pairs
+    real_indices = abs(z.imag) <= tol * abs(z)
+    zr = z[real_indices].real
+
+    if len(zr) == len(z):
+        # Input is entirely real
+        return array([]), zr
+
+    # Split positive and negative halves of conjugates
+    z = z[~real_indices]
+    zp = z[z.imag > 0]
+    zn = z[z.imag < 0]
+
+    if len(zp) != len(zn):
+        raise ValueError('Array contains complex value with no matching '
+                         'conjugate.')
+
+    # Find runs of (approximately) the same real part
+    same_real = np.diff(zp.real) <= tol * abs(zp[:-1])
+    diffs = numpy.diff(concatenate(([0], same_real, [0])))
+    run_starts = numpy.where(diffs > 0)[0]
+    run_stops = numpy.where(diffs < 0)[0]
+
+    # Sort each run by their imaginary parts
+    for i in range(len(run_starts)):
+        start = run_starts[i]
+        stop = run_stops[i] + 1
+        for chunk in (zp[start:stop], zn[start:stop]):
+            chunk[...] = chunk[np.lexsort([abs(chunk.imag)])]
+
+    # Check that negatives match positives
+    if any(abs(zp - zn.conj()) > tol * abs(zn)):
+        raise ValueError('Array contains complex value with no matching '
+                         'conjugate.')
+
+    # Average out numerical inaccuracy in real vs imag parts of pairs
+    zc = (zp + zn.conj()) / 2
+
+    return zc, zr
+
+
+def _cplxpair(z, tol=None):
+    """
+    Sort into pairs of complex conjugates.
+
+    Complex conjugates in `z` are sorted by increasing real part.  In each
+    pair, the number with negative imaginary part appears first.
+
+    If pairs have identical real parts, they are sorted by increasing
+    imaginary magnitude.
+
+    Two complex numbers are considered a conjugate pair if their real and
+    imaginary parts differ in magnitude by less than ``tol * abs(z)``.  The
+    pairs are forced to be exact complex conjugates by averaging the positive
+    and negative values.
+
+    Purely real numbers are also sorted, but placed after the complex
+    conjugate pairs.  A number is considered real if its imaginary part is
+    smaller than `tol` times the magnitude of the number.
+
+    Parameters
+    ----------
+    z : array_like
+        1-dimensional input array to be sorted.
+    tol : float, optional
+        Relative tolerance for testing realness and conjugate equality.
+        Default is ``100 * spacing(1)`` of `z`'s data type (i.e. 2e-14 for
+        float64)
+
+    Returns
+    -------
+    y : ndarray
+        Complex conjugate pairs followed by real numbers.
+
+    Raises
+    ------
+    ValueError
+        If there are any complex numbers in `z` for which a conjugate
+        cannot be found.
+
+    See Also
+    --------
+    _cplxreal
+
+    Examples
+    --------
+    >>> a = [4, 3, 1, 2-2j, 2+2j, 2-1j, 2+1j, 2-1j, 2+1j, 1+1j, 1-1j]
+    >>> z = _cplxpair(a)
+    >>> print(z)
+    [ 1.-1.j  1.+1.j  2.-1.j  2.+1.j  2.-1.j  2.+1.j  2.-2.j  2.+2.j  1.+0.j
+      3.+0.j  4.+0.j]
+    """
+
+    z = atleast_1d(z)
+    if z.size == 0 or np.isrealobj(z):
+        return np.sort(z)
+
+    if z.ndim != 1:
+        raise ValueError('z must be 1-dimensional')
+
+    zc, zr = _cplxreal(z, tol)
+
+    # Interleave complex values and their conjugates, with negative imaginary
+    # parts first in each pair
+    zc = np.dstack((zc.conj(), zc)).flatten()
+    z = np.append(zc, zr)
+    return z
+
+
 def tf2zpk(b, a):
-    r"""Return zero, pole, gain (z,p,k) representation from a numerator,
+    r"""Return zero, pole, gain (z, p, k) representation from a numerator,
     denominator representation of a linear filter.
 
     Parameters
     ----------
-    b : ndarray
-        Numerator polynomial.
-    a : ndarray
-        Denominator polynomial.
+    b : array_like
+        Numerator polynomial coefficients.
+    a : array_like
+        Denominator polynomial coefficients.
 
     Returns
     -------
@@ -318,14 +492,14 @@ def tf2zpk(b, a):
 
 
 def zpk2tf(z, p, k):
-    """Return polynomial transfer function representation from zeros
-    and poles
+    """
+    Return polynomial transfer function representation from zeros and poles
 
     Parameters
     ----------
-    z : ndarray
+    z : array_like
         Zeros of the transfer function.
-    p : ndarray
+    p : array_like
         Poles of the transfer function.
     k : float
         System gain.
@@ -333,9 +507,9 @@ def zpk2tf(z, p, k):
     Returns
     -------
     b : ndarray
-        Numerator polynomial.
+        Numerator polynomial coefficients.
     a : ndarray
-        Denominator polynomial.
+        Denominator polynomial coefficients.
 
     """
     z = atleast_1d(z)
@@ -376,6 +550,324 @@ def zpk2tf(z, p, k):
     return b, a
 
 
+def tf2sos(b, a, pairing='nearest'):
+    """
+    Return second-order sections from transfer function representation
+
+    Parameters
+    ----------
+    b : array_like
+        Numerator polynomial coefficients.
+    a : array_like
+        Denominator polynomial coefficients.
+    pairing : {'nearest', 'keep_odd'}
+        The method to use to combine pairs of poles and zeros into sections.
+        See `zpk2sos`.
+
+    Returns
+    -------
+    sos : ndarray
+        Array of second-order filter coefficients, with shape
+        ``(n_sections, 6)``. See `sosfilt` for the SOS filter format
+        specification.
+
+    See Also
+    --------
+    zpk2sos, sosfilt
+
+    Notes
+    -----
+    It is generally discouraged to convert from TF to SOS format, since doing
+    so usually will not improve numerical precision errors. Instead, consider
+    designing filters in ZPK format and converting directly to SOS. TF is
+    converted to SOS by first converting to ZPK format, then converting
+    ZPK to SOS.
+
+    .. versionadded:: 0.16.0
+    """
+    return zpk2sos(*tf2zpk(b, a), pairing=pairing)
+
+
+def sos2tf(sos):
+    """
+    Return a single transfer function from a series of second-order sections
+
+    Parameters
+    ----------
+    sos : array_like
+        Array of second-order filter coefficients, must have shape
+        ``(n_sections, 6)``. See `sosfilt` for the SOS filter format
+        specification.
+
+    Returns
+    -------
+    b : ndarray
+        Numerator polynomial coefficients.
+    a : ndarray
+        Denominator polynomial coefficients.
+
+    Notes
+    -----
+    .. versionadded:: 0.16.0
+    """
+    sos = np.asarray(sos)
+    b = [1.]
+    a = [1.]
+    n_sections = sos.shape[0]
+    for section in range(n_sections):
+        b = np.polymul(b, sos[section, :3])
+        a = np.polymul(a, sos[section, 3:])
+    return b, a
+
+
+def sos2zpk(sos):
+    """
+    Return zeros, poles, and gain of a series of second-order sections
+
+    Parameters
+    ----------
+    sos : array_like
+        Array of second-order filter coefficients, must have shape
+        ``(n_sections, 6)``. See `sosfilt` for the SOS filter format
+        specification.
+
+    Returns
+    -------
+    z : ndarray
+        Zeros of the transfer function.
+    p : ndarray
+        Poles of the transfer function.
+    k : float
+        System gain.
+
+    Notes
+    -----
+    .. versionadded:: 0.16.0
+    """
+    sos = np.asarray(sos)
+    n_sections = sos.shape[0]
+    z = np.empty(n_sections*2, np.complex128)
+    p = np.empty(n_sections*2, np.complex128)
+    k = 1.
+    for section in range(n_sections):
+        zpk = tf2zpk(sos[section, :3], sos[section, 3:])
+        z[2*section:2*(section+1)] = zpk[0]
+        p[2*section:2*(section+1)] = zpk[1]
+        k *= zpk[2]
+    return z, p, k
+
+
+def _nearest_real_complex_idx(fro, to, which):
+    """Get the next closest real or complex element based on distance"""
+    assert which in ('real', 'complex')
+    order = np.argsort(np.abs(fro - to))
+    mask = np.isreal(fro[order])
+    if which == 'complex':
+        mask = ~mask
+    return order[np.where(mask)[0][0]]
+
+
+def zpk2sos(z, p, k, pairing='nearest'):
+    """
+    Return second-order sections from zeros, poles, and gain of a system
+
+    Parameters
+    ----------
+    z : array_like
+        Zeros of the transfer function.
+    p : array_like
+        Poles of the transfer function.
+    k : float
+        System gain.
+    pairing : {'nearest', 'keep_odd'}
+        The method to use to combine pairs of poles and zeros into sections.
+        See Notes below.
+
+    Returns
+    -------
+    sos : ndarray
+        Array of second-order filter coefficients, with shape
+        ``(n_sections, 6)``. See `sosfilt` for the SOS filter format
+        specification.
+
+    See Also
+    --------
+    sosfilt
+
+    Notes
+    -----
+    The algorithm used to convert ZPK to SOS format is designed to
+    minimize errors due to numerical precision issues. The pairing
+    algorithm attempts to minimize the peak gain of each biquadratic
+    section. This is done by pairing poles with the nearest zeros, starting
+    with the poles closest to the unit circle.
+
+    *Algorithms*
+
+    The current algorithms are designed specifically for use with digital
+    filters. Although they can operate on analog filters, the results may
+    be sub-optimal.
+
+    The steps in the ``pairing='nearest'`` and ``pairing='keep_odd'``
+    algorithms are mostly shared. The ``nearest`` algorithm attempts to
+    minimize the peak gain, while ``'keep_odd'`` minimizes peak gain under
+    the constraint that odd-order systems should retain one section
+    as first order. The algorithm steps and are as follows:
+
+    As a pre-processing step, add poles or zeros to the origin as
+    necessary to obtain the same number of poles and zeros for pairing.
+    If ``pairing == 'nearest'`` and there are an odd number of poles,
+    add an additional pole and a zero at the origin.
+
+    The following steps are then iterated over until no more poles or
+    zeros remain:
+
+    1. Take the (next remaining) pole (complex or real) closest to the
+       unit circle to begin a new filter section.
+
+    2. If the pole is real and there are no other remaining real poles [#]_,
+       add the closest real zero to the section and leave it as a first
+       order section. Note that after this step we are guaranteed to be
+       left with an even number of real poles, complex poles, real zeros,
+       and complex zeros for subsequent pairing iterations.
+
+    3. Else:
+
+        1. If the pole is complex and the zero is the only remaining real
+           zero*, then pair the pole with the *next* closest zero
+           (guaranteed to be complex). This is necessary to ensure that
+           there will be a real zero remaining to eventually create a
+           first-order section (thus keeping the odd order).
+
+        2. Else pair the pole with the closest remaining zero (complex or
+           real).
+
+        3. Proceed to complete the second-order section by adding another
+           pole and zero to the current pole and zero in the section:
+
+            1. If the current pole and zero are both complex, add their
+               conjugates.
+
+            2. Else if the pole is complex and the zero is real, add the
+               conjugate pole and the next closest real zero.
+
+            3. Else if the pole is real and the zero is complex, add the
+               conjugate zero and the real pole closest to those zeros.
+
+            4. Else (we must have a real pole and real zero) add the next
+               real pole closest to the unit circle, and then add the real
+               zero closest to that pole.
+
+    .. [#] This conditional can only be met for specific odd-order inputs
+           with the ``pairing == 'keep_odd'`` method.
+
+    .. versionadded:: 0.16.0
+    """
+    # TODO in the near future:
+    # 1. Add SOS capability to `filtfilt`, `freqz`, etc. somehow (#3259).
+    # 2. Make `decimate` use `sosfilt` instead of `lfilter`.
+    # 3. Make sosfilt automatically simplify sections to first order
+    #    when possible. Note this might make `sosfiltfilt` a bit harder (ICs).
+    # 4. Further optimizations of the section ordering / pole-zero pairing.
+    # See the wiki for other potential issues.
+
+    valid_pairings = ['nearest', 'keep_odd']
+    if pairing not in valid_pairings:
+        raise ValueError('pairing must be one of %s, not %s'
+                         % (valid_pairings, pairing))
+    if len(z) == len(p) == 0:
+        return array([[k, 0., 0., 1., 0., 0.]])
+
+    # ensure we have the same number of poles and zeros, and make copies
+    p = np.concatenate((p, np.zeros(max(len(z) - len(p), 0))))
+    z = np.concatenate((z, np.zeros(max(len(p) - len(z), 0))))
+    n_sections = (max(len(p), len(z)) + 1) // 2
+    sos = zeros((n_sections, 6))
+
+    if len(p) % 2 == 1 and pairing == 'nearest':
+        p = np.concatenate((p, [0.]))
+        z = np.concatenate((z, [0.]))
+    assert len(p) == len(z)
+
+    # Ensure we have complex conjugate pairs
+    # (note that _cplxreal only gives us one element of each complex pair):
+    z = np.concatenate(_cplxreal(z))
+    p = np.concatenate(_cplxreal(p))
+
+    p_sos = np.zeros((n_sections, 2), np.complex128)
+    z_sos = np.zeros_like(p_sos)
+    for si in range(n_sections):
+        # Select the next "worst" pole
+        p1_idx = np.argmin(np.abs(1 - np.abs(p)))
+        p1 = p[p1_idx]
+        p = np.delete(p, p1_idx)
+
+        # Pair that pole with a zero
+
+        if np.isreal(p1) and np.isreal(p).sum() == 0:
+            # Special case to set a first-order section
+            z1_idx = _nearest_real_complex_idx(z, p1, 'real')
+            z1 = z[z1_idx]
+            z = np.delete(z, z1_idx)
+            p2 = z2 = 0
+        else:
+            if not np.isreal(p1) and np.isreal(z).sum() == 1:
+                # Special case to ensure we choose a complex zero to pair
+                # with so later (setting up a first-order section)
+                z1_idx = _nearest_real_complex_idx(z, p1, 'complex')
+                assert not np.isreal(z[z1_idx])
+            else:
+                # Pair the pole with the closest zero (real or complex)
+                z1_idx = np.argmin(np.abs(p1 - z))
+            z1 = z[z1_idx]
+            z = np.delete(z, z1_idx)
+
+            # Now that we have p1 and z1, figure out what p2 and z2 need to be
+            if not np.isreal(p1):
+                if not np.isreal(z1):  # complex pole, complex zero
+                    p2 = p1.conj()
+                    z2 = z1.conj()
+                else:  # complex pole, real zero
+                    p2 = p1.conj()
+                    z2_idx = _nearest_real_complex_idx(z, p1, 'real')
+                    z2 = z[z2_idx]
+                    assert np.isreal(z2)
+                    z = np.delete(z, z2_idx)
+            else:
+                if not np.isreal(z1):  # real pole, complex zero
+                    z2 = z1.conj()
+                    p2_idx = _nearest_real_complex_idx(p, z1, 'real')
+                    p2 = p[p2_idx]
+                    assert np.isreal(p2)
+                else:  # real pole, real zero
+                    # pick the next "worst" pole to use
+                    idx = np.where(np.isreal(p))[0]
+                    assert len(idx) > 0
+                    p2_idx = idx[np.argmin(np.abs(np.abs(p[idx]) - 1))]
+                    p2 = p[p2_idx]
+                    # find a real zero to match the added pole
+                    assert np.isreal(p2)
+                    z2_idx = _nearest_real_complex_idx(z, p2, 'real')
+                    z2 = z[z2_idx]
+                    assert np.isreal(z2)
+                    z = np.delete(z, z2_idx)
+                p = np.delete(p, p2_idx)
+        p_sos[si] = [p1, p2]
+        z_sos[si] = [z1, z2]
+    assert len(p) == len(z) == 0  # we've consumed all poles and zeros
+    del p, z
+
+    # Construct the system, reversing order so the "worst" are last
+    p_sos = np.reshape(p_sos[::-1], (n_sections, 2))
+    z_sos = np.reshape(z_sos[::-1], (n_sections, 2))
+    gains = np.ones(n_sections)
+    gains[0] = k
+    for si in range(n_sections):
+        x = zpk2tf(z_sos[si], p_sos[si], gains[si])
+        sos[si] = np.concatenate(x)
+    return sos
+
+
 def normalize(b, a):
     """Normalize polynomial representation of a transfer function.
 
@@ -582,7 +1074,8 @@ def iirdesign(wp, ws, gpass, gstop, analog=False, ftype='ellip', output='ba'):
 
     Given passband and stopband frequencies and gains, construct an analog or
     digital IIR filter of minimum order for a given basic type.  Return the
-    output in numerator, denominator ('ba') or pole-zero ('zpk') form.
+    output in numerator, denominator ('ba'), pole-zero ('zpk') or second order
+    sections ('sos') form.
 
     Parameters
     ----------
@@ -615,9 +1108,9 @@ def iirdesign(wp, ws, gpass, gstop, analog=False, ftype='ellip', output='ba'):
             - Cauer/elliptic: 'ellip'
             - Bessel/Thomson: 'bessel'
 
-    output : {'ba', 'zpk'}, optional
-        Type of output:  numerator/denominator ('ba') or pole-zero ('zpk').
-        Default is 'ba'.
+    output : {'ba', 'zpk', 'sos'}, optional
+        Type of output:  numerator/denominator ('ba'), pole-zero ('zpk'), or
+        second-order sections ('sos'). Default is 'ba'.
 
     Returns
     -------
@@ -627,6 +1120,9 @@ def iirdesign(wp, ws, gpass, gstop, analog=False, ftype='ellip', output='ba'):
     z, p, k : ndarray, ndarray, float
         Zeros, poles, and system gain of the IIR filter transfer
         function.  Only returned if ``output='zpk'``.
+    sos : ndarray
+        Second-order sections representation of the IIR filter.
+        Only returned if ``output=='sos'``.
 
     See Also
     --------
@@ -636,6 +1132,9 @@ def iirdesign(wp, ws, gpass, gstop, analog=False, ftype='ellip', output='ba'):
     cheb1ord, cheb2ord, ellipord
     iirfilter : General filter design using order and critical frequencies
 
+    Notes
+    -----
+    The ``'sos'`` output parameter was added in 0.16.0.
     """
     try:
         ordfunc = filter_dict[ftype][1]
@@ -666,7 +1165,7 @@ def iirfilter(N, Wn, rp=None, rs=None, btype='band', analog=False,
     IIR digital and analog filter design given order and critical points.
 
     Design an Nth order digital or analog filter and return the filter
-    coefficients in (B,A) (numerator, denominator) or (Z,P,K) form.
+    coefficients.
 
     Parameters
     ----------
@@ -698,9 +1197,21 @@ def iirfilter(N, Wn, rp=None, rs=None, btype='band', analog=False,
             - Cauer/elliptic: 'ellip'
             - Bessel/Thomson: 'bessel'
 
-    output : {'ba', 'zpk'}, optional
-        Type of output:  numerator/denominator ('ba') or pole-zero ('zpk').
-        Default is 'ba'.
+    output : {'ba', 'zpk', 'sos'}, optional
+        Type of output:  numerator/denominator ('ba'), pole-zero ('zpk'), or
+        second-order sections ('sos'). Default is 'ba'.
+
+    Returns
+    -------
+    b, a : ndarray, ndarray
+        Numerator (`b`) and denominator (`a`) polynomials of the IIR filter.
+        Only returned if ``output='ba'``.
+    z, p, k : ndarray, ndarray, float
+        Zeros, poles, and system gain of the IIR filter transfer
+        function.  Only returned if ``output='zpk'``.
+    sos : ndarray
+        Second-order sections representation of the IIR filter.
+        Only returned if ``output=='sos'``.
 
     See Also
     --------
@@ -710,6 +1221,10 @@ def iirfilter(N, Wn, rp=None, rs=None, btype='band', analog=False,
     cheb1ord, cheb2ord, ellipord
     iirdesign : General filter design using passband and stopband spec
 
+    Notes
+    -----
+    The ``'sos'`` output parameter was added in 0.16.0.
+
     Examples
     --------
     Generate a 17th-order Chebyshev II bandpass filter and plot the frequency
@@ -744,7 +1259,7 @@ def iirfilter(N, Wn, rp=None, rs=None, btype='band', analog=False,
     except KeyError:
         raise ValueError("'%s' is not a valid basic IIR filter." % ftype)
 
-    if output not in ['ba', 'zpk']:
+    if output not in ['ba', 'zpk', 'sos']:
         raise ValueError("'%s' is not a valid output form." % output)
 
     if rp is not None and rp < 0:
@@ -816,6 +1331,8 @@ def iirfilter(N, Wn, rp=None, rs=None, btype='band', analog=False,
         return z, p, k
     elif output == 'ba':
         return zpk2tf(z, p, k)
+    elif output == 'sos':
+        return zpk2sos(z, p, k)
 
 
 def _relative_degree(z, p):
@@ -1144,7 +1661,7 @@ def butter(N, Wn, btype='low', analog=False, output='ba'):
     Butterworth digital and analog filter design.
 
     Design an Nth order digital or analog Butterworth filter and return
-    the filter coefficients in (B,A) or (Z,P,K) form.
+    the filter coefficients.
 
     Parameters
     ----------
@@ -1163,9 +1680,9 @@ def butter(N, Wn, btype='low', analog=False, output='ba'):
     analog : bool, optional
         When True, return an analog filter, otherwise a digital filter is
         returned.
-    output : {'ba', 'zpk'}, optional
-        Type of output:  numerator/denominator ('ba') or pole-zero ('zpk').
-        Default is 'ba'.
+    output : {'ba', 'zpk', 'sos'}, optional
+        Type of output:  numerator/denominator ('ba'), pole-zero ('zpk'), or
+        second-order sections ('sos'). Default is 'ba'.
 
     Returns
     -------
@@ -1175,8 +1692,11 @@ def butter(N, Wn, btype='low', analog=False, output='ba'):
     z, p, k : ndarray, ndarray, float
         Zeros, poles, and system gain of the IIR filter transfer
         function.  Only returned if ``output='zpk'``.
+    sos : ndarray
+        Second-order sections representation of the IIR filter.
+        Only returned if ``output=='sos'``.
 
-    See also
+    See Also
     --------
     buttord
 
@@ -1185,6 +1705,8 @@ def butter(N, Wn, btype='low', analog=False, output='ba'):
     The Butterworth filter has maximally flat frequency response in the
     passband.
 
+    The ``'sos'`` output parameter was added in 0.16.0.
+
     Examples
     --------
     Plot the filter's frequency response, showing the critical points:
@@ -1213,7 +1735,7 @@ def cheby1(N, rp, Wn, btype='low', analog=False, output='ba'):
     Chebyshev type I digital and analog filter design.
 
     Design an Nth order digital or analog Chebyshev type I filter and
-    return the filter coefficients in (B,A) or (Z,P,K) form.
+    return the filter coefficients.
 
     Parameters
     ----------
@@ -1235,9 +1757,9 @@ def cheby1(N, rp, Wn, btype='low', analog=False, output='ba'):
     analog : bool, optional
         When True, return an analog filter, otherwise a digital filter is
         returned.
-    output : {'ba', 'zpk'}, optional
-        Type of output:  numerator/denominator ('ba') or pole-zero ('zpk').
-        Default is 'ba'.
+    output : {'ba', 'zpk', 'sos'}, optional
+        Type of output:  numerator/denominator ('ba'), pole-zero ('zpk'), or
+        second-order sections ('sos'). Default is 'ba'.
 
     Returns
     -------
@@ -1247,8 +1769,11 @@ def cheby1(N, rp, Wn, btype='low', analog=False, output='ba'):
     z, p, k : ndarray, ndarray, float
         Zeros, poles, and system gain of the IIR filter transfer
         function.  Only returned if ``output='zpk'``.
+    sos : ndarray
+        Second-order sections representation of the IIR filter.
+        Only returned if ``output=='sos'``.
 
-    See also
+    See Also
     --------
     cheb1ord
 
@@ -1265,6 +1790,8 @@ def cheby1(N, rp, Wn, btype='low', analog=False, output='ba'):
     5th-order filter has 3 maxima and 2 minima).  Consequently, the DC gain is
     unity for odd-order filters, or -rp dB for even-order filters.
 
+    The ``'sos'`` output parameter was added in 0.16.0.
+
     Examples
     --------
     Plot the filter's frequency response, showing the critical points:
@@ -1294,7 +1821,7 @@ def cheby2(N, rs, Wn, btype='low', analog=False, output='ba'):
     Chebyshev type II digital and analog filter design.
 
     Design an Nth order digital or analog Chebyshev type II filter and
-    return the filter coefficients in (B,A) or (Z,P,K) form.
+    return the filter coefficients.
 
     Parameters
     ----------
@@ -1316,9 +1843,9 @@ def cheby2(N, rs, Wn, btype='low', analog=False, output='ba'):
     analog : bool, optional
         When True, return an analog filter, otherwise a digital filter is
         returned.
-    output : {'ba', 'zpk'}, optional
-        Type of output:  numerator/denominator ('ba') or pole-zero ('zpk').
-        Default is 'ba'.
+    output : {'ba', 'zpk', 'sos'}, optional
+        Type of output:  numerator/denominator ('ba'), pole-zero ('zpk'), or
+        second-order sections ('sos'). Default is 'ba'.
 
     Returns
     -------
@@ -1328,8 +1855,11 @@ def cheby2(N, rs, Wn, btype='low', analog=False, output='ba'):
     z, p, k : ndarray, ndarray, float
         Zeros, poles, and system gain of the IIR filter transfer
         function.  Only returned if ``output='zpk'``.
+    sos : ndarray
+        Second-order sections representation of the IIR filter.
+        Only returned if ``output=='sos'``.
 
-    See also
+    See Also
     --------
     cheb2ord
 
@@ -1341,6 +1871,8 @@ def cheby2(N, rs, Wn, btype='low', analog=False, output='ba'):
 
     Type II filters do not roll off as fast as Type I (`cheby1`).
 
+    The ``'sos'`` output parameter was added in 0.16.0.
+
     Examples
     --------
     Plot the filter's frequency response, showing the critical points:
@@ -1370,7 +1902,7 @@ def ellip(N, rp, rs, Wn, btype='low', analog=False, output='ba'):
     Elliptic (Cauer) digital and analog filter design.
 
     Design an Nth order digital or analog elliptic filter and return
-    the filter coefficients in (B,A) or (Z,P,K) form.
+    the filter coefficients.
 
     Parameters
     ----------
@@ -1395,9 +1927,9 @@ def ellip(N, rp, rs, Wn, btype='low', analog=False, output='ba'):
     analog : bool, optional
         When True, return an analog filter, otherwise a digital filter is
         returned.
-    output : {'ba', 'zpk'}, optional
-        Type of output:  numerator/denominator ('ba') or pole-zero ('zpk').
-        Default is 'ba'.
+    output : {'ba', 'zpk', 'sos'}, optional
+        Type of output:  numerator/denominator ('ba'), pole-zero ('zpk'), or
+        second-order sections ('sos'). Default is 'ba'.
 
     Returns
     -------
@@ -1407,8 +1939,11 @@ def ellip(N, rp, rs, Wn, btype='low', analog=False, output='ba'):
     z, p, k : ndarray, ndarray, float
         Zeros, poles, and system gain of the IIR filter transfer
         function.  Only returned if ``output='zpk'``.
+    sos : ndarray
+        Second-order sections representation of the IIR filter.
+        Only returned if ``output=='sos'``.
 
-    See also
+    See Also
     --------
     ellipord
 
@@ -1428,6 +1963,8 @@ def ellip(N, rp, rs, Wn, btype='low', analog=False, output='ba'):
     5th-order filter has 3 maxima and 2 minima).  Consequently, the DC gain is
     unity for odd-order filters, or -rp dB for even-order filters.
 
+    The ``'sos'`` output parameter was added in 0.16.0.
+
     Examples
     --------
     Plot the filter's frequency response, showing the critical points:
@@ -1457,7 +1994,7 @@ def bessel(N, Wn, btype='low', analog=False, output='ba'):
     """Bessel/Thomson digital and analog filter design.
 
     Design an Nth order digital or analog Bessel filter and return the
-    filter coefficients in (B,A) or (Z,P,K) form.
+    filter coefficients.
 
     Parameters
     ----------
@@ -1477,9 +2014,9 @@ def bessel(N, Wn, btype='low', analog=False, output='ba'):
     analog : bool, optional
         When True, return an analog filter, otherwise a digital filter is
         returned.
-    output : {'ba', 'zpk'}, optional
-        Type of output:  numerator/denominator ('ba') or pole-zero ('zpk').
-        Default is 'ba'.
+    output : {'ba', 'zpk', 'sos'}, optional
+        Type of output:  numerator/denominator ('ba'), pole-zero ('zpk'), or
+        second-order sections ('sos'). Default is 'ba'.
 
     Returns
     -------
@@ -1489,6 +2026,9 @@ def bessel(N, Wn, btype='low', analog=False, output='ba'):
     z, p, k : ndarray, ndarray, float
         Zeros, poles, and system gain of the IIR filter transfer
         function.  Only returned if ``output='zpk'``.
+    sos : ndarray
+        Second-order sections representation of the IIR filter.
+        Only returned if ``output=='sos'``.
 
     Notes
     -----
@@ -1508,6 +2048,8 @@ def bessel(N, Wn, btype='low', analog=False, output='ba'):
     For a given `Wn`, the lowpass and highpass filter have the same phase vs
     frequency curves; they are "phase-matched".
 
+    The ``'sos'`` output parameter was added in 0.16.0.
+
     Examples
     --------
     Plot the filter's frequency response, showing the flat group delay and
diff --git a/scipy/signal/signaltools.py b/scipy/signal/signaltools.py
--- a/scipy/signal/signaltools.py
+++ b/scipy/signal/signaltools.py
@@ -18,7 +18,8 @@
                    iscomplexobj, isscalar, mean, ndarray, newaxis, ones, pi,
                    poly, polyadd, polyder, polydiv, polymul, polysub, polyval,
                    prod, product, r_, ravel, real_if_close, reshape,
-                   roots, sort, sum, take, transpose, unique, where, zeros)
+                   roots, sort, sum, take, transpose, unique, where, zeros,
+                   zeros_like)
 import numpy as np
 from scipy.special import factorial
 from .windows import get_window
@@ -27,10 +28,10 @@
 
 __all__ = ['correlate', 'fftconvolve', 'convolve', 'convolve2d', 'correlate2d',
            'order_filter', 'medfilt', 'medfilt2d', 'wiener', 'lfilter',
-           'lfiltic', 'deconvolve', 'hilbert', 'hilbert2', 'cmplx_sort',
-           'unique_roots', 'invres', 'invresz', 'residue', 'residuez',
-           'resample', 'detrend', 'lfilter_zi', 'filtfilt', 'decimate',
-           'vectorstrength']
+           'lfiltic', 'sosfilt', 'deconvolve', 'hilbert', 'hilbert2',
+           'cmplx_sort', 'unique_roots', 'invres', 'invresz', 'residue',
+           'residuez', 'resample', 'detrend', 'lfilter_zi', 'sosfilt_zi',
+           'filtfilt', 'decimate', 'vectorstrength']
 
 
 _modedict = {'valid': 0, 'same': 1, 'full': 2}
@@ -1885,6 +1886,56 @@ def lfilter_zi(b, a):
     return zi
 
 
+def sosfilt_zi(sos):
+    """
+    Compute an initial state `zi` for the sosfilt function that corresponds
+    to the steady state of the step response.
+
+    A typical use of this function is to set the initial state so that the
+    output of the filter starts at the same value as the first element of
+    the signal to be filtered.
+
+    Parameters
+    ----------
+    sos : array_like
+        Array of second-order filter coefficients, must have shape
+        ``(n_sections, 6)``. See `sosfilt` for the SOS filter format
+        specification.
+
+    Returns
+    -------
+    zi : ndarray
+        Initial conditions suitable for use with ``sosfilt``, shape
+        ``(n_sections, 2)``.
+
+    See Also
+    --------
+    sosfilt, zpk2sos
+
+    Notes
+    -----
+    .. versionadded:: 0.16.0
+
+    """
+    sos = np.asarray(sos)
+    if sos.ndim != 2 or sos.shape[1] != 6:
+        raise ValueError('sos must be shape (n_sections, 6)')
+
+    n_sections = sos.shape[0]
+    zi = np.empty((n_sections, 2))
+    scale = 1.0
+    for section in range(n_sections):
+        b = sos[section, :3]
+        a = sos[section, 3:]
+        zi[section] = scale * lfilter_zi(b, a)
+        # If H(z) = B(z)/A(z) is this section's transfer function, then
+        # b.sum()/a.sum() is H(1), the gain at omega=0.  That's the steady
+        # state value of this section's step response.
+        scale *= b.sum() / a.sum()
+
+    return zi
+
+
 def _filtfilt_gust(b, a, x, axis=-1, irlen=None):
     """Forward-backward IIR filter that uses Gustafsson's method.
 
@@ -2292,6 +2343,110 @@ def filtfilt(b, a, x, axis=-1, padtype='odd', padlen=None, method='pad',
     return y
 
 
+def sosfilt(sos, x, axis=-1, zi=None):
+    """
+    Filter data along one dimension using cascaded second-order sections
+
+    Filter a data sequence, `x`, using a digital IIR filter defined by
+    `sos`. This is implemented by performing `lfilter` for each
+    second-order section.  See `lfilter` for details.
+
+    Parameters
+    ----------
+    sos : array_like
+        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.
+    x : array_like
+        An N-dimensional input array.
+    axis : int
+        The axis of the input data array along which to apply the
+        linear filter. The filter is applied to each subarray along
+        this axis.  Default is -1.
+    zi : array_like, optional
+        Initial conditions for the cascaded filter delays.  It is a (at least
+        2D) vector of shape ``(n_sections, ..., 2)``, with middle dimensions
+        equal to those of the input shape (without the filtered axis).
+        If `zi` is None or is not given then initial rest is assumed. Note
+        that these initial conditions are *not* the same as the initial
+        conditions given by `lfiltic` or `lfilter_zi`.
+
+    Returns
+    -------
+    y : ndarray
+        The output of the digital filter.
+    zf : ndarray, optional
+        If `zi` is None, this is not returned, otherwise, `zf` holds the
+        final filter delay values.
+
+    See Also
+    --------
+    zpk2sos, sos2zpk, sosfilt_zi
+
+    Notes
+    -----
+    The filter function is implemented as a series of second-order filters
+    with direct-form II transposed structure. It is designed to minimize
+    numerical precision errors for high-order filters.
+
+    .. versionadded:: 0.16.0
+
+    Examples
+    --------
+    Plot a 13th-order filter's impulse response using both `lfilter` and
+    `sosfilt`, showing the instability that results from trying to do a
+    13th-order filter in a single stage (the numerical error pushes some poles
+    outside of the unit circle):
+
+    >>> import matplotlib.pyplot as plt
+    >>> from scipy import signal
+    >>> b, a = signal.ellip(13, 0.009, 80, 0.05, output='ba')
+    >>> sos = signal.ellip(13, 0.009, 80, 0.05, output='sos')
+    >>> x = np.zeros(700)
+    >>> x[0] = 1.
+    >>> y_tf = signal.lfilter(b, a, x)
+    >>> y_sos = signal.sosfilt(sos, x)
+    >>> plt.plot(y_tf, 'r', label='TF')
+    >>> plt.plot(y_sos, 'k', label='SOS')
+    >>> plt.legend(loc='best')
+    >>> plt.show()
+
+    """
+
+    sos = atleast_2d(sos)
+    if sos.ndim != 2:
+        raise ValueError('sos array must be 2D')
+
+    n_sections, m = sos.shape
+    if m != 6:
+        raise ValueError('sos array must be shape (n_sections, 6)')
+
+    if zi is not None:
+        use_zi = True
+        zi = np.array(zi)
+        x_zi_shape = np.delete(np.array(x.shape), axis)
+        proper_shape = (zi.ndim >= 2 and
+                        zi.shape[0] == n_sections and zi.shape[-1] == 2 and
+                        np.array_equal(zi.shape[1:-1], x_zi_shape))
+        if not proper_shape:
+            raise ValueError('sos initial states must be shape '
+                             '(n_sections, ..., 2)')
+        zf = zeros_like(zi)
+    else:
+        use_zi = False
+
+    for section in range(n_sections):
+        if use_zi:
+            x, zf[section] = lfilter(sos[section, :3], sos[section, 3:],
+                                     x, axis, zi=zi[section])
+        else:
+            x = lfilter(sos[section, :3], sos[section, 3:], x, axis)
+    out = (x, zf) if use_zi else x
+    return out
+
+
 from scipy.signal.filter_design import cheby1
 from scipy.signal.fir_filter_design import firwin
 


Test patch:
diff --git a/scipy/signal/tests/test_filter_design.py b/scipy/signal/tests/test_filter_design.py
--- a/scipy/signal/tests/test_filter_design.py
+++ b/scipy/signal/tests/test_filter_design.py
@@ -7,12 +7,137 @@
                            assert_array_equal, assert_array_less,
                            assert_raises, assert_equal, assert_,
                            run_module_suite, assert_allclose)
+from numpy import array, spacing, sin, pi, sort
 
-from scipy.signal import (tf2zpk, zpk2tf, BadCoefficients, freqz, normalize,
+from scipy.signal import (tf2zpk, zpk2tf, tf2sos, sos2tf, sos2zpk, zpk2sos,
+                          BadCoefficients, freqz, normalize,
                           buttord, cheby1, cheby2, ellip, cheb1ord, cheb2ord,
                           ellipord, butter, bessel, buttap, besselap,
                           cheb1ap, cheb2ap, ellipap, iirfilter, freqs,
                           lp2lp, lp2hp, lp2bp, lp2bs, bilinear)
+from scipy.signal.filter_design import _cplxreal, _cplxpair
+
+
+class TestCplxPair(TestCase):
+
+    def test_trivial_input(self):
+        assert_equal(_cplxpair([]).size, 0)
+        assert_equal(_cplxpair(1), 1)
+
+    def test_output_order(self):
+        assert_allclose(_cplxpair([1+1j, 1-1j]), [1-1j, 1+1j])
+
+        a = [1+1j, 1+1j, 1, 1-1j, 1-1j, 2]
+        b = [1-1j, 1+1j, 1-1j, 1+1j, 1, 2]
+        assert_allclose(_cplxpair(a), b)
+
+        # points spaced around the unit circle
+        z = np.exp(2j*pi*array([4, 3, 5, 2, 6, 1, 0])/7)
+        z1 = np.copy(z)
+        np.random.shuffle(z)
+        assert_allclose(_cplxpair(z), z1)
+        np.random.shuffle(z)
+        assert_allclose(_cplxpair(z), z1)
+        np.random.shuffle(z)
+        assert_allclose(_cplxpair(z), z1)
+
+        # Should be able to pair up all the conjugates
+        x = np.random.rand(10000) + 1j * np.random.rand(10000)
+        y = x.conj()
+        z = np.random.rand(10000)
+        x = np.concatenate((x, y, z))
+        np.random.shuffle(x)
+        c = _cplxpair(x)
+
+        # Every other element of head should be conjugates:
+        assert_allclose(c[0:20000:2], np.conj(c[1:20000:2]))
+        # Real parts of head should be in sorted order:
+        assert_allclose(c[0:20000:2].real, np.sort(c[0:20000:2].real))
+        # Tail should be sorted real numbers:
+        assert_allclose(c[20000:], np.sort(c[20000:]))
+
+    def test_real_integer_input(self):
+        assert_array_equal(_cplxpair([2, 0, 1]), [0, 1, 2])
+
+    def test_tolerances(self):
+        eps = spacing(1)
+        assert_allclose(_cplxpair([1j, -1j, 1+1j*eps], tol=2*eps),
+                        [-1j, 1j, 1+1j*eps])
+
+        # sorting close to 0
+        assert_allclose(_cplxpair([-eps+1j, +eps-1j]), [-1j, +1j])
+        assert_allclose(_cplxpair([+eps+1j, -eps-1j]), [-1j, +1j])
+        assert_allclose(_cplxpair([+1j, -1j]), [-1j, +1j])
+
+    def test_unmatched_conjugates(self):
+        # 1+2j is unmatched
+        assert_raises(ValueError, _cplxpair, [1+3j, 1-3j, 1+2j])
+
+        # 1+2j and 1-3j are unmatched
+        assert_raises(ValueError, _cplxpair, [1+3j, 1-3j, 1+2j, 1-3j])
+
+        # 1+3j is unmatched
+        assert_raises(ValueError, _cplxpair, [1+3j, 1-3j, 1+3j])
+
+        # Not conjugates
+        assert_raises(ValueError, _cplxpair, [4+5j, 4+5j])
+        assert_raises(ValueError, _cplxpair, [1-7j, 1-7j])
+
+        # No pairs
+        assert_raises(ValueError, _cplxpair, [1+3j])
+        assert_raises(ValueError, _cplxpair, [1-3j])
+
+
+class TestCplxReal(TestCase):
+
+    def test_trivial_input(self):
+        assert_equal(_cplxreal([]), ([], []))
+        assert_equal(_cplxreal(1), ([], [1]))
+
+    def test_output_order(self):
+        zc, zr = _cplxreal(np.roots(array([1, 0, 0, 1])))
+        assert_allclose(np.append(zc, zr), [1/2 + 1j*sin(pi/3), -1])
+
+        eps = spacing(1)
+
+        a = [0+1j, 0-1j, eps + 1j, eps - 1j, -eps + 1j, -eps - 1j,
+             1, 4, 2, 3, 0, 0,
+             2+3j, 2-3j,
+             1-eps + 1j, 1+2j, 1-2j, 1+eps - 1j,  # sorts out of order
+             3+1j, 3+1j, 3+1j, 3-1j, 3-1j, 3-1j,
+             2-3j, 2+3j]
+        zc, zr = _cplxreal(a)
+        assert_allclose(zc, [1j, 1j, 1j, 1+1j, 1+2j, 2+3j, 2+3j, 3+1j, 3+1j,
+                             3+1j])
+        assert_allclose(zr, [0, 0, 1, 2, 3, 4])
+
+        z = array([1-eps + 1j, 1+2j, 1-2j, 1+eps - 1j, 1+eps+3j, 1-2*eps-3j,
+                   0+1j, 0-1j, 2+4j, 2-4j, 2+3j, 2-3j, 3+7j, 3-7j, 4-eps+1j,
+                   4+eps-2j, 4-1j, 4-eps+2j])
+
+        zc, zr = _cplxreal(z)
+        assert_allclose(zc, [1j, 1+1j, 1+2j, 1+3j, 2+3j, 2+4j, 3+7j, 4+1j,
+                             4+2j])
+        assert_equal(zr, [])
+
+    def test_unmatched_conjugates(self):
+        # 1+2j is unmatched
+        assert_raises(ValueError, _cplxreal, [1+3j, 1-3j, 1+2j])
+
+        # 1+2j and 1-3j are unmatched
+        assert_raises(ValueError, _cplxreal, [1+3j, 1-3j, 1+2j, 1-3j])
+
+        # 1+3j is unmatched
+        assert_raises(ValueError, _cplxreal, [1+3j, 1-3j, 1+3j])
+
+        # No pairs
+        assert_raises(ValueError, _cplxreal, [1+3j])
+        assert_raises(ValueError, _cplxreal, [1-3j])
+
+    def test_real_integer_input(self):
+        zc, zr = _cplxreal([2, 0, 1, 4])
+        assert_array_equal(zc, [])
+        assert_array_equal(zr, [0, 1, 2, 4])
 
 
 class TestTf2zpk(TestCase):
@@ -62,6 +187,209 @@ def test_identity(self):
         assert_(isinstance(a, np.ndarray))
 
 
+class TestSos2Zpk(TestCase):
+
+    def test_basic(self):
+        sos = [[1, 0, 1, 1, 0, -0.81],
+               [1, 0, 0, 1, 0, +0.49]]
+        z, p, k = sos2zpk(sos)
+        z2 = [1j, -1j, 0, 0]
+        p2 = [0.9, -0.9, 0.7j, -0.7j]
+        k2 = 1
+        assert_array_almost_equal(sort(z), sort(z2), decimal=4)
+        assert_array_almost_equal(sort(p), sort(p2), decimal=4)
+        assert_array_almost_equal(k, k2)
+
+        sos = [[1.00000, +0.61803, 1.0000, 1.00000, +0.60515, 0.95873],
+               [1.00000, -1.61803, 1.0000, 1.00000, -1.58430, 0.95873],
+               [1.00000, +1.00000, 0.0000, 1.00000, +0.97915, 0.00000]]
+        z, p, k = sos2zpk(sos)
+        z2 = [-0.3090 + 0.9511j, -0.3090 - 0.9511j, 0.8090 + 0.5878j,
+              0.8090 - 0.5878j, -1.0000 + 0.0000j, 0]
+        p2 = [-0.3026 + 0.9312j, -0.3026 - 0.9312j, 0.7922 + 0.5755j,
+              0.7922 - 0.5755j, -0.9791 + 0.0000j, 0]
+        k2 = 1
+        assert_array_almost_equal(sort(z), sort(z2), decimal=4)
+        assert_array_almost_equal(sort(p), sort(p2), decimal=4)
+
+        sos = array([[1, 2, 3, 1, 0.2, 0.3],
+                     [4, 5, 6, 1, 0.4, 0.5]])
+        z = array([-1 - 1.41421356237310j, -1 + 1.41421356237310j,
+                  -0.625 - 1.05326872164704j, -0.625 + 1.05326872164704j])
+        p = array([-0.2 - 0.678232998312527j, -0.2 + 0.678232998312527j,
+                  -0.1 - 0.538516480713450j, -0.1 + 0.538516480713450j])
+        k = 4
+        z2, p2, k2 = sos2zpk(sos)
+        assert_allclose(_cplxpair(z2), z)
+        assert_allclose(_cplxpair(p2), p)
+        assert_allclose(k2, k)
+
+
+class TestSos2Tf(TestCase):
+
+    def test_basic(self):
+        sos = [[1, 1, 1, 1, 0, -1],
+               [-2, 3, 1, 1, 10, 1]]
+        b, a = sos2tf(sos)
+        assert_array_almost_equal(b, [-2, 1, 2, 4, 1])
+        assert_array_almost_equal(a, [1, 10, 0, -10, -1])
+
+
+class TestTf2Sos(TestCase):
+
+    def test_basic(self):
+        num = [2, 16, 44, 56, 32]
+        den = [3, 3, -15, 18, -12]
+        sos = tf2sos(num, den)
+        sos2 = [[0.6667, 4.0000, 5.3333, 1.0000, +2.0000, -4.0000],
+                [1.0000, 2.0000, 2.0000, 1.0000, -1.0000, +1.0000]]
+        assert_array_almost_equal(sos, sos2, decimal=4)
+
+        b = [1, -3, 11, -27, 18]
+        a = [16, 12, 2, -4, -1]
+        sos = tf2sos(b, a)
+        sos2 = [[0.0625, -0.1875, 0.1250, 1.0000, -0.2500, -0.1250],
+                [1.0000, +0.0000, 9.0000, 1.0000, +1.0000, +0.5000]]
+        # assert_array_almost_equal(sos, sos2, decimal=4)
+
+
+class TestZpk2Sos(TestCase):
+
+    def test_basic(self):
+        for pairing in ('nearest', 'keep_odd'):
+            #
+            # Cases that match octave
+            #
+
+            z = [-1, -1]
+            p = [0.57149 + 0.29360j, 0.57149 - 0.29360j]
+            k = 1
+            sos = zpk2sos(z, p, k, pairing=pairing)
+            sos2 = [[1, 2, 1, 1, -1.14298, 0.41280]]  # octave & MATLAB
+            assert_array_almost_equal(sos, sos2, decimal=4)
+
+            z = [1j, -1j]
+            p = [0.9, -0.9, 0.7j, -0.7j]
+            k = 1
+            sos = zpk2sos(z, p, k, pairing=pairing)
+            sos2 = [[1, 0, 1, 1, 0, +0.49],
+                    [1, 0, 0, 1, 0, -0.81]]  # octave
+            # sos2 = [[0, 0, 1, 1, -0.9, 0],
+            #         [1, 0, 1, 1, 0.9, 0]]  # MATLAB
+            assert_array_almost_equal(sos, sos2, decimal=4)
+
+            z = []
+            p = [0.8, -0.5+0.25j, -0.5-0.25j]
+            k = 1.
+            sos = zpk2sos(z, p, k, pairing=pairing)
+            sos2 = [[1., 0., 0., 1., 1., 0.3125],
+                    [1., 0., 0., 1., -0.8, 0.]]  # octave, MATLAB fails
+            assert_array_almost_equal(sos, sos2, decimal=4)
+
+            z = [1., 1., 0.9j, -0.9j]
+            p = [0.99+0.01j, 0.99-0.01j, 0.1+0.9j, 0.1-0.9j]
+            k = 1
+            sos = zpk2sos(z, p, k, pairing=pairing)
+            sos2 = [[1, 0, 0.81, 1, -0.2, 0.82],
+                    [1, -2, 1, 1, -1.98, 0.9802]]  # octave
+            # sos2 = [[1, -2, 1, 1,  -0.2, 0.82],
+            #         [1, 0, 0.81, 1, -1.98, 0.9802]]  # MATLAB
+            assert_array_almost_equal(sos, sos2, decimal=4)
+
+            z = [0.9+0.1j, 0.9-0.1j, -0.9]
+            p = [0.75+0.25j, 0.75-0.25j, 0.9]
+            k = 1
+            sos = zpk2sos(z, p, k, pairing=pairing)
+            if pairing == 'keep_odd':
+                sos2 = [[1, -1.8, 0.82, 1, -1.5, 0.625],
+                        [1, 0.9, 0, 1, -0.9, 0]]  # octave; MATLAB fails
+                assert_array_almost_equal(sos, sos2, decimal=4)
+            else:  # pairing == 'nearest'
+                sos2 = [[1, 0.9, 0, 1, -1.5, 0.625],
+                        [1, -1.8, 0.82, 1, -0.9, 0]]  # our algorithm
+                assert_array_almost_equal(sos, sos2, decimal=4)
+
+            #
+            # Cases that differ from octave:
+            #
+
+            z = [-0.3090 + 0.9511j, -0.3090 - 0.9511j, 0.8090 + 0.5878j,
+                 +0.8090 - 0.5878j, -1.0000 + 0.0000j]
+            p = [-0.3026 + 0.9312j, -0.3026 - 0.9312j, 0.7922 + 0.5755j,
+                 +0.7922 - 0.5755j, -0.9791 + 0.0000j]
+            k = 1
+            sos = zpk2sos(z, p, k, pairing=pairing)
+            # sos2 = [[1, 0.618, 1, 1, 0.6052, 0.95870],
+            #         [1, -1.618, 1, 1, -1.5844, 0.95878],
+            #         [1, 1, 0, 1, 0.9791, 0]]  # octave, MATLAB fails
+            sos2 = [[1, 1, 0, 1, +0.97915, 0],
+                    [1, 0.61803, 1, 1, +0.60515, 0.95873],
+                    [1, -1.61803, 1, 1, -1.58430, 0.95873]]
+            assert_array_almost_equal(sos, sos2, decimal=4)
+
+            z = [-1 - 1.4142j, -1 + 1.4142j,
+                 -0.625 - 1.0533j, -0.625 + 1.0533j]
+            p = [-0.2 - 0.6782j, -0.2 + 0.6782j,
+                 -0.1 - 0.5385j, -0.1 + 0.5385j]
+            k = 4
+            sos = zpk2sos(z, p, k, pairing=pairing)
+            sos2 = [[4, 8, 12, 1, 0.2, 0.3],
+                    [1, 1.25, 1.5, 1, 0.4, 0.5]]  # MATLAB
+            # sos2 = [[4, 8, 12, 1, 0.4, 0.5],
+            #         [1, 1.25, 1.5, 1, 0.2, 0.3]]  # octave
+            assert_allclose(sos, sos2, rtol=1e-4, atol=1e-4)
+
+            z = []
+            p = [0.2, -0.5+0.25j, -0.5-0.25j]
+            k = 1.
+            sos = zpk2sos(z, p, k, pairing=pairing)
+            sos2 = [[1., 0., 0., 1., -0.2, 0.],
+                    [1., 0., 0., 1., 1., 0.3125]]
+            # sos2 = [[1., 0., 0., 1., 1., 0.3125],
+            #         [1., 0., 0., 1., -0.2, 0]]  # octave, MATLAB fails
+            assert_array_almost_equal(sos, sos2, decimal=4)
+
+            # The next two examples are adapted from Leland B. Jackson,
+            # "Digital Filters and Signal Processing (1995) p.400:
+            # http://books.google.com/books?id=VZ8uabI1pNMC&lpg=PA400&ots=gRD9pi8Jua&dq=Pole%2Fzero%20pairing%20for%20minimum%20roundoff%20noise%20in%20BSF.&pg=PA400#v=onepage&q=Pole%2Fzero%20pairing%20for%20minimum%20roundoff%20noise%20in%20BSF.&f=false
+
+            deg2rad = np.pi / 180.
+            k = 1.
+
+            # first example
+            thetas = [22.5, 45, 77.5]
+            mags = [0.8, 0.6, 0.9]
+            z = np.array([np.exp(theta * deg2rad * 1j) for theta in thetas])
+            z = np.concatenate((z, np.conj(z)))
+            p = np.array([mag * np.exp(theta * deg2rad * 1j)
+                          for theta, mag in zip(thetas, mags)])
+            p = np.concatenate((p, np.conj(p)))
+            sos = zpk2sos(z, p, k)
+            # sos2 = [[1, -0.43288, 1, 1, -0.38959, 0.81],  # octave,
+            #         [1, -1.41421, 1, 1, -0.84853, 0.36],  # MATLAB fails
+            #         [1, -1.84776, 1, 1, -1.47821, 0.64]]
+            # Note that pole-zero pairing matches, but ordering is different
+            sos2 = [[1, -1.41421, 1, 1, -0.84853, 0.36],
+                    [1, -1.84776, 1, 1, -1.47821, 0.64],
+                    [1, -0.43288, 1, 1, -0.38959, 0.81]]
+            assert_array_almost_equal(sos, sos2, decimal=4)
+
+            # second example
+            z = np.array([np.exp(theta * deg2rad * 1j)
+                          for theta in (85., 10.)])
+            z = np.concatenate((z, np.conj(z), [1, -1]))
+            sos = zpk2sos(z, p, k)
+
+            # sos2 = [[1, -0.17431, 1, 1, -0.38959, 0.81],  # octave "wrong",
+            #         [1, -1.96962, 1, 1, -0.84853, 0.36],  # MATLAB fails
+            #         [1, 0, -1, 1, -1.47821, 0.64000]]
+            # Our pole-zero pairing matches the text, Octave does not
+            sos2 = [[1, 0, -1, 1, -0.84853, 0.36],
+                    [1, -1.96962, 1, 1, -1.47821, 0.64],
+                    [1, -0.17431, 1, 1, -0.38959, 0.81]]
+            assert_array_almost_equal(sos, sos2, decimal=4)
+
+
 class TestFreqz(TestCase):
 
     def test_ticket1441(self):
@@ -1693,6 +2021,30 @@ def test_ba_output(self):
         assert_allclose(a, a2, rtol=1e-4)
 
 
+def test_sos_consistency():
+    # Consistency checks of output='sos' for the specialized IIR filter
+    # design functions.
+    design_funcs = [(bessel, (0.1,)),
+                    (butter, (0.1,)),
+                    (cheby1, (45.0, 0.1)),
+                    (cheby2, (0.087, 0.1)),
+                    (ellip, (0.087, 45, 0.1))]
+    for func, args in design_funcs:
+        name = func.__name__
+
+        b, a = func(2, *args, output='ba')
+        sos = func(2, *args, output='sos')
+        assert_allclose(sos, [np.hstack((b, a))], err_msg="%s(2,...)" % name)
+
+        zpk = func(3, *args, output='zpk')
+        sos = func(3, *args, output='sos')
+        assert_allclose(sos, zpk2sos(*zpk), err_msg="%s(3,...)" % name)
+
+        zpk = func(4, *args, output='zpk')
+        sos = func(4, *args, output='sos')
+        assert_allclose(sos, zpk2sos(*zpk), err_msg="%s(4,...)" % name)
+
+
 class TestIIRFilter(TestCase):
 
     def test_symmetry(self):
@@ -1709,7 +2061,7 @@ def test_symmetry(self):
                 assert_equal(k, np.real(k))
 
                 b, a = iirfilter(N, 1.1, 1, 20, 'low', analog=True,
-                                    ftype=ftype, output='ba')
+                                 ftype=ftype, output='ba')
                 assert_(issubclass(b.dtype.type, np.floating))
                 assert_(issubclass(a.dtype.type, np.floating))
 
diff --git a/scipy/signal/tests/test_signaltools.py b/scipy/signal/tests/test_signaltools.py
--- a/scipy/signal/tests/test_signaltools.py
+++ b/scipy/signal/tests/test_signaltools.py
@@ -15,7 +15,7 @@
 from scipy.signal import (
     correlate, convolve, convolve2d, fftconvolve,
     hilbert, hilbert2, lfilter, lfilter_zi, filtfilt, butter, tf2zpk,
-    invres, vectorstrength, signaltools, lfiltic)
+    invres, vectorstrength, signaltools, lfiltic, tf2sos, sosfilt, sosfilt_zi)
 from scipy.signal.signaltools import _filtfilt_gust
 
 
@@ -1337,5 +1337,119 @@ def test_negative_period_ValueError(self):
         assert_raises(ValueError, vectorstrength, events, period)
 
 
+class TestSOSFilt(TestCase):
+
+    # For sosfilt we only test a single datatype. Since sosfilt wraps
+    # to lfilter under the hood, it's hopefully good enough to ensure
+    # lfilter is extensively tested.
+    dt = np.float64
+
+    # The test_rank* tests are pulled from _TestLinearFilter
+    def test_rank1(self):
+        x = np.linspace(0, 5, 6).astype(self.dt)
+        b = np.array([1, -1]).astype(self.dt)
+        a = np.array([0.5, -0.5]).astype(self.dt)
+
+        # Test simple IIR
+        y_r = np.array([0, 2, 4, 6, 8, 10.]).astype(self.dt)
+        assert_array_almost_equal(sosfilt(tf2sos(b, a), x), y_r)
+
+        # Test simple FIR
+        b = np.array([1, 1]).astype(self.dt)
+        # NOTE: This was changed (rel. to TestLinear...) to add a pole @zero:
+        a = np.array([1, 0]).astype(self.dt)
+        y_r = np.array([0, 1, 3, 5, 7, 9.]).astype(self.dt)
+        assert_array_almost_equal(sosfilt(tf2sos(b, a), x), y_r)
+
+        b = [1, 1, 0]
+        a = [1, 0, 0]
+        x = np.ones(8)
+        sos = np.concatenate((b, a))
+        sos.shape = (1, 6)
+        y = sosfilt(sos, x)
+        assert_allclose(y, [1, 2, 2, 2, 2, 2, 2, 2])
+
+    def test_rank2(self):
+        shape = (4, 3)
+        x = np.linspace(0, np.prod(shape) - 1, np.prod(shape)).reshape(shape)
+        x = x.astype(self.dt)
+
+        b = np.array([1, -1]).astype(self.dt)
+        a = np.array([0.5, 0.5]).astype(self.dt)
+
+        y_r2_a0 = np.array([[0, 2, 4], [6, 4, 2], [0, 2, 4], [6, 4, 2]],
+                           dtype=self.dt)
+
+        y_r2_a1 = np.array([[0, 2, 0], [6, -4, 6], [12, -10, 12],
+                            [18, -16, 18]], dtype=self.dt)
+
+        y = sosfilt(tf2sos(b, a), x, axis=0)
+        assert_array_almost_equal(y_r2_a0, y)
+
+        y = sosfilt(tf2sos(b, a), x, axis=1)
+        assert_array_almost_equal(y_r2_a1, y)
+
+    def test_rank3(self):
+        shape = (4, 3, 2)
+        x = np.linspace(0, np.prod(shape) - 1, np.prod(shape)).reshape(shape)
+
+        b = np.array([1, -1]).astype(self.dt)
+        a = np.array([0.5, 0.5]).astype(self.dt)
+
+        # Test last axis
+        y = sosfilt(tf2sos(b, a), x)
+        for i in range(x.shape[0]):
+            for j in range(x.shape[1]):
+                assert_array_almost_equal(y[i, j], lfilter(b, a, x[i, j]))
+
+    def test_initial_conditions(self):
+        b1, a1 = signal.butter(2, 0.25, 'low')
+        b2, a2 = signal.butter(2, 0.75, 'low')
+        b3, a3 = signal.butter(2, 0.75, 'low')
+        b = np.convolve(np.convolve(b1, b2), b3)
+        a = np.convolve(np.convolve(a1, a2), a3)
+        sos = np.array((np.r_[b1, a1], np.r_[b2, a2], np.r_[b3, a3]))
+
+        x = np.random.rand(50)
+
+        # Stopping filtering and continuing
+        y_true, zi = lfilter(b, a, x[:20], zi=np.zeros(6))
+        y_true = np.r_[y_true, lfilter(b, a, x[20:], zi=zi)[0]]
+        assert_allclose(y_true, lfilter(b, a, x))
+
+        y_sos, zi = sosfilt(sos, x[:20], zi=np.zeros((3, 2)))
+        y_sos = np.r_[y_sos, sosfilt(sos, x[20:], zi=zi)[0]]
+        assert_allclose(y_true, y_sos)
+
+        # Use a step function
+        zi = sosfilt_zi(sos)
+        x = np.ones(8)
+        y, zf = sosfilt(sos, x, zi=zi)
+
+        assert_allclose(y, np.ones(8))
+        assert_allclose(zf, zi)
+
+        # Initial condition shape matching
+        x.shape = (1, 1) + x.shape  # 3D
+        assert_raises(ValueError, sosfilt, sos, x, zi=zi)
+        zi_nd = zi.copy()
+        zi_nd.shape = (zi.shape[0], 1, 1, zi.shape[-1])
+        assert_raises(ValueError, sosfilt, sos, x,
+                      zi=zi_nd[:, :, :, [0, 1, 1]])
+        y, zf = sosfilt(sos, x, zi=zi_nd)
+        assert_allclose(y[0, 0], np.ones(8))
+        assert_allclose(zf[:, 0, 0, :], zi)
+
+    def test_sosfilt_zi(self):
+        sos = signal.butter(6, 0.2, output='sos')
+        zi = sosfilt_zi(sos)
+
+        y, zf = sosfilt(sos, np.ones(40), zi=zi)
+        assert_allclose(zf, zi, rtol=1e-13)
+
+        # Expected steady state value of the step response of this filter:
+        ss = np.prod(sos[:, :3].sum(axis=-1) / sos[:, 3:].sum(axis=-1))
+        assert_allclose(y, ss, rtol=1e-13)
+
 if __name__ == "__main__":
     run_module_suite()