Repository: sympy/sympy
Pull request: banded matrix construction
<!-- Your title above should be a short description of what
was changed. Do not include the issue number in the title. -->

#### References to other Issues or PRs
<!-- If this pull request fixes an issue, write "Fixes #NNNN" in that exact
format, e.g. "Fixes #1234". See
https://github.com/blog/1506-closing-issues-via-pull-requests . Please also
write a comment on that issue linking back to this pull request once it is
open. -->

closes #16180 as alternate
fixes #16178

#### Brief description of what is fixed or changed

A syntax is proposed whereby banded matrices can be created.

The input to `banded` is a dictionary containing keys for the diagonal to fill, and values containing information about what to put on the diagonal:

```
{0: 1} - 1s on the main diagonal
{0: x, 1:1} - an upper Jordan-block matrix
{1: (1, 2, 3), -1: (4, 5, 6)} - 1, 2, 3 on the 1st diag above main and 4, 5, 6 on the diagonal below the main
```
The values above can also be matrices/lists or single argument functions which calculate the value on the diagonal based on the displacement along the diagonal.

#### Other comments
This is not a class of matrix, it is just a helper function for creating the matrix. The rationale for this is that one should not need to instantiate a full matrix if only needing to define values down a few diagonals. It automatically comes back as a SparseMatrix which can be fed to any other matrix constructor if another type is needed (e.g. dense).

#### Release Notes

<!-- Write the release notes for this release below. See
https://github.com/sympy/sympy/wiki/Writing-Release-Notes for more information
on how to write release notes. The bot will check your release notes
automatically to see if they are formatted correctly. -->

<!-- BEGIN RELEASE NOTES -->
- matrices
    - `banded` added to sparsetools.py as a helper to create a banded matrices
<!-- END RELEASE NOTES -->


Problem statement:
easier creation of banded matrices
currently we have the `Matrix.diag` method which allows for easy creation of a single diagonal. To create a banded matrix is a little more difficult. This is the current docstring extract:
```
        A given band off the diagonal can be made by padding with a
        vertical or horizontal "kerning" vector:

        >>> hpad = Matrix(0, 2, [])
        >>> vpad = Matrix(2, 0, [])
        >>> Matrix.diag(vpad, 1, 2, 3, hpad) + Matrix.diag(hpad, 4, 5, 6, vpad)
        Matrix([
        [0, 0, 4, 0, 0],
        [0, 0, 0, 5, 0],
        [1, 0, 0, 0, 6],
        [0, 2, 0, 0, 0],
        [0, 0, 3, 0, 0]])
```
And here's a suggestion of how it could be enhanced:
```
        This can also be done by passing a dictionary giving the diagonal number
        and value:

        >>> Matrix.diag({0: 1, 1: 2}, rows=3, cols=4)
        Matrix([
        [1, 2, 0, 0],
        [0, 1, 2, 0],
        [0, 0, 1, 2]])

        If the values for the diagonal are not all the same they can be given as
        a) a list (and a check will be made that there are the right number present)
        or b) a 2-argument function that will compute the value from the
        row and column position or c) a single-argument function that will compute
        the value from the distance along the diagonal:
        
        >>> Matrix.diag({
        ... -1: [-1, -2],
        ... 0: lambda i, j: i + 1,
        ... 1: lambda d: 2*d + 1},
        ... rows=3, cols=4)
        ... 
        Matrix([
        [ 1,  1, 0, 0],
        [-1,  2, 3, 0],
        [ 0, -2, 3, 5]])

        Note: it is also possible to create a diagonal matrix with
        a Piecewise function without using the diag method:

        >>> Matrix(4,6,lambda i,j: Piecewise((x, Eq(i, j)),(1, Eq(i - 1, j)), (0, True)))
        Matrix([
        [x, 0, 0, 0, 0, 0],
        [1, x, 0, 0, 0, 0],
        [0, 1, x, 0, 0, 0],
        [0, 0, 1, x, 0, 0]])

        >>> Matrix(5,5,lambda i,j: Piecewise((1, Eq(i + 2, j)), (0, True)))
        Matrix([
        [0, 0, 1, 0, 0],
        [0, 0, 0, 1, 0],
        [0, 0, 0, 0, 1],
        [0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0]])
        >>> Matrix(5,5,lambda i,j: Piecewise((1, Eq(i - 2, j)), (0, True)))
        Matrix([
        [0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0],
        [1, 0, 0, 0, 0],
        [0, 1, 0, 0, 0],
        [0, 0, 1, 0, 0]])
        >>> Matrix(5,5,lambda i,j: Piecewise((1, Eq(5-1,i+j+2)),(0,True)))
        Matrix([
        [0, 0, 1, 0, 0],
        [0, 1, 0, 0, 0],
        [1, 0, 0, 0, 0],
        [0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0]])
        >>> Matrix(5,5,lambda i,j: Piecewise((1, Eq(5-1,i+j-2)),(0,True)))
        Matrix([
        [0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0],
        [0, 0, 0, 0, 1],
        [0, 0, 0, 1, 0],
        [0, 0, 1, 0, 0]])
```

Hints from issue discussion:
I'm working on this.

Commit message: added `banded` for creation of banded matrices

Patch to review:
diff --git a/sympy/geometry/point.py b/sympy/geometry/point.py
index 7630b517cef6..6a061ec78c5b 100644
--- a/sympy/geometry/point.py
+++ b/sympy/geometry/point.py
@@ -152,7 +152,7 @@ def __new__(cls, *args, **kwargs):
                 raise ValueError(filldedent('''
                         on_morph value should be 'error',
                         'warn' or 'ignore'.'''))
-        if any(i for i in coords[dim:]):
+        if any(coords[dim:]):
             raise ValueError('Nonzero coordinates cannot be removed.')
         if any(a.is_number and im(a) for a in coords):
             raise ValueError('Imaginary coordinates are not permitted.')
diff --git a/sympy/matrices/common.py b/sympy/matrices/common.py
index 832115f4f54d..ad857b03c46a 100644
--- a/sympy/matrices/common.py
+++ b/sympy/matrices/common.py
@@ -779,6 +779,7 @@ def diag(kls, *args, **kwargs):
         """
         from sympy.matrices.matrices import MatrixBase
         from sympy.matrices.dense import Matrix
+        from sympy.matrices.sparse import SparseMatrix
         klass = kwargs.get('cls', kls)
         strict = kwargs.get('strict', False) # lists -> Matrices
         unpack = kwargs.get('unpack', True)  # unpack single sequence
@@ -790,35 +791,33 @@ def diag(kls, *args, **kwargs):
         diag_entries = defaultdict(int)
         R = C = 0  # keep track of the biggest index seen
         for m in args:
-            if hasattr(m, 'rows') or isinstance(m, list):
-                # in this case, we're a matrix or list
-                if hasattr(m, 'rows'):
-                    # convert to list of lists
+            if isinstance(m, list):
+                if not strict:
+                    m = SparseMatrix(m)
+                    for (i, j), mij in m._smat.items():
+                        diag_entries[(i + R, j + C)] = mij
                     r, c = m.shape
-                    m = m.tolist()
+                    m = []  # to skip process below
                 else:
-                    # make sure all list elements are lists
-                    r = len(m)
-                    if strict:
-                        # let Matrix raise the error
-                        m = Matrix(m)
-                        c = m.cols
-                        m = m.tolist()
-                    else:
-                        m = [mi if isinstance(mi, list) else [mi]
-                            for mi in m]
-                        c = max(map(len, m))
-                # process list of lists
-                for i in range(len(m)):
-                    for j, mij in enumerate(m[i]):
-                        diag_entries[(i + R, j + C)] = mij
-                R += r
-                C += c
-            else:
-                # in this case, we're a single value
+                    # if malformed, Matrix will raise an error
+                    _ = Matrix(m)
+                    r, c = _.shape
+                    m = _.tolist()
+            elif hasattr(m, 'shape'):  # a Matrix
+                # convert to list of lists
+                r, c = m.shape
+                m = m.tolist()
+            else:  # in this case, we're a single value
                 diag_entries[(R, C)] = m
                 R += 1
                 C += 1
+                continue
+            # process list of lists
+            for i in range(len(m)):
+                for j, mij in enumerate(m[i]):
+                    diag_entries[(i + R, j + C)] = mij
+            R += r
+            C += c
         rows = kwargs.get('rows', None)
         cols = kwargs.get('cols', None)
         if rows is None:
@@ -2500,3 +2499,219 @@ def classof(A, B):
             return A.__class__
 
     raise TypeError("Incompatible classes %s, %s" % (A.__class__, B.__class__))
+
+def banded(*args, **kwargs):
+    """Returns a SparseMatrix from the given dictionary describing
+    the diagonals of the matrix. The keys are positive for upper
+    diagonals and negative for those below the main diagonal. The
+    values may be:
+        * expressions or single-argument functions,
+        * tuples of values,
+        * lists/matrices
+    Unless dimensions are given, the size of the returned matrix will
+    be large enough to contain the largest non-zero value provided.
+
+    Examples
+    ========
+
+    >>> from sympy.matrices.common import banded
+    >>> from sympy import ones, Matrix
+    >>> from sympy.abc import x
+
+    If explicit values are given in tuples,
+    the matrix will autosize to contain all values, otherwise
+    a single value is filled onto the entire diagonal:
+
+    >>> banded({1: (1, 2, 3), -1: (4, 5, 6), 0: x})
+    Matrix([
+    [x, 1, 0, 0],
+    [4, x, 2, 0],
+    [0, 5, x, 3],
+    [0, 0, 6, x]])
+
+    A function accepting a single argument can be used to fill the
+    diagonal as a function of diagonal index (which starts at 0).
+    The size (or shape) of the matrix must be given to obtain more
+    than a 1x1 matrix:
+
+    >>> s = lambda d: (1 + d)**2
+    >>> banded(5, {0: s, 2: s, -2: 2})
+    Matrix([
+    [1, 0, 1,  0,  0],
+    [0, 4, 0,  4,  0],
+    [2, 0, 9,  0,  9],
+    [0, 2, 0, 16,  0],
+    [0, 0, 2,  0, 25]])
+
+    Matrices (or lists which are interpreted as the same) fill
+    with their diagonal on the indicated diagonal:
+
+    >>> vert = [1, 2, 3]
+    >>> banded({0: vert}, cols=3)
+    Matrix([
+    [1, 0, 0],
+    [2, 1, 0],
+    [3, 2, 1],
+    [0, 3, 2],
+    [0, 0, 3]])
+
+    >>> banded(4, {0: ones(2)})
+    Matrix([
+    [1, 1, 0, 0],
+    [1, 1, 0, 0],
+    [0, 0, 1, 1],
+    [0, 0, 1, 1]])
+
+    Errors are raised if the designated size will not hold
+    all values an integral number of times. Here, the rows
+    are designated as odd (but an even number is required to
+    hold the off-diagonal 2x2 ones):
+
+    >>> banded({0: 2, 1: ones(2)}, rows=5)
+    Traceback (most recent call last):
+    ...
+    ValueError:
+    sequence does not fit an integral number of times in the matrix
+
+    And here, an even number of rows is given...but the square
+    matrix has an even number of columns, too. As we saw
+    in the previous example, an odd number is required:
+
+    >>> banded(4, {0: 2, 1: ones(2)})  # trying to make 4x4 and cols must be odd
+    Traceback (most recent call last):
+    ...
+    ValueError:
+    sequence does not fit an integral number of times in the matrix
+
+    A way around having to count rows is to enclosing matrix elements
+    in a tuple and indicate the desired number of them to the right:
+
+    >>> banded({0: 2, 2: (ones(2),)*3})
+    Matrix([
+    [2, 0, 1, 1, 0, 0, 0, 0],
+    [0, 2, 1, 1, 0, 0, 0, 0],
+    [0, 0, 2, 0, 1, 1, 0, 0],
+    [0, 0, 0, 2, 1, 1, 0, 0],
+    [0, 0, 0, 0, 2, 0, 1, 1],
+    [0, 0, 0, 0, 0, 2, 1, 1]])
+
+    An error will be raised if more than one value
+    is written to a given entry. Here, the ones overlap
+    with the main diagonal if they are placed on the
+    first diagonal:
+
+    >>> banded({0: (2,)*5, 1: (ones(2),)*3})
+    Traceback (most recent call last):
+    ...
+    ValueError: collision at (1, 1)
+
+    By placing a 0 at the bottom left of the 2x2 matrix of
+    ones, the collision is avoided:
+
+    >>> u2 = Matrix([
+    ... [1, 1],
+    ... [0, 1]])
+    >>> banded({0: (2,)*5, 1: (u2,)*3})
+    Matrix([
+    [2, 1, 1, 0, 0, 0, 0],
+    [0, 2, 1, 0, 0, 0, 0],
+    [0, 0, 2, 1, 1, 0, 0],
+    [0, 0, 0, 2, 1, 0, 0],
+    [0, 0, 0, 0, 2, 1, 1],
+    [0, 0, 0, 0, 0, 0, 1]])
+    """
+    from sympy import Dict, Dummy, SparseMatrix
+    try:
+        if len(args) not in (1, 2, 3):
+            raise TypeError
+        if not isinstance(args[-1], (dict, Dict)):
+            raise TypeError
+        if len(args) == 1:
+            rows = kwargs.get('rows', None)
+            cols = kwargs.get('cols', None)
+        elif len(args) == 2:
+            rows = cols = as_int(args[0])
+        else:
+            rows, cols = map(as_int, args[:2])
+        # fails with ValueError if any keys are not ints
+        _ = all(as_int(k) for k in args[-1])
+    except (ValueError, TypeError):
+        raise TypeError(filldedent(
+            '''unrecognized input to banded:
+            expecting [[row,] col,] {int: value}'''))
+    def rc(d):
+        # return row,col coord of diagonal start
+        r = -d if d < 0 else 0
+        c = 0 if r else d
+        return r, c
+    smat = {}
+    undone = []
+    tba = Dummy()
+    # first handle objects with size
+    for d, v in args[-1].items():
+        r, c = rc(d)
+        # note: only tuple is being recognized since this
+        # will allow other Basic objects like Tuple
+        # into the matrix if so desired
+        if isinstance(v, tuple):
+            xtra = 0
+            for i, vi in enumerate(v):
+                i += xtra
+                if is_sequence(vi):
+                    vi = SparseMatrix(vi)
+                    smat[r + i, c + i] = vi
+                    xtra += min(vi.shape) - 1
+                else:
+                    smat[r + i, c + i] = vi
+        elif is_sequence(v):
+            v = SparseMatrix(v)
+            rv, cv = v.shape
+            if rows and cols:
+                nr, xr = divmod(rows - r, rv)
+                nc, xc = divmod(cols - c, cv)
+                x = xr or xc
+                do = min(nr, nc)
+            elif rows:
+                do, x = divmod(rows - r, rv)
+            elif cols:
+                do, x = divmod(cols - c, cv)
+            else:
+                do = 1
+                x = 0
+            if x:
+                raise ValueError(filldedent('''
+                    sequence does not fit an integral number of times
+                    in the matrix'''))
+            j = min(v.shape)
+            for i in range(do):
+                smat[r, c] = v
+                r += j
+                c += j
+        elif v:
+            smat[r, c] = tba
+            undone.append((d, v))
+    s = SparseMatrix(None, smat)  # to expand matrices
+    smat = s._smat
+    # check for dim errors here
+    if rows is not None and rows < s.rows:
+        raise ValueError('Designated rows %s < needed %s' % (rows, s.rows))
+    if cols is not None and cols < s.cols:
+        raise ValueError('Designated cols %s < needed %s' % (cols, s.cols))
+    rows = rows or s.rows
+    cols = cols or s.cols
+    def update(i, j, v):
+        # update smat and make sure there are
+        # no collisions
+        if v:
+            if (i, j) in smat and smat[i, j] not in (tba, v):
+                raise ValueError('collision at %s' % ((i, j),))
+            smat[i, j] = v
+    if undone:
+        for d, vi in undone:
+            r, c = rc(d)
+            v = vi if callable(vi) else lambda _: vi
+            i = 0
+            while r + i < rows and c + i < cols:
+                update(r + i, c + i, v(i))
+                i += 1
+    return SparseMatrix(rows, cols, smat)
diff --git a/sympy/matrices/sparse.py b/sympy/matrices/sparse.py
index 176f7e963066..12c554906ef0 100644
--- a/sympy/matrices/sparse.py
+++ b/sympy/matrices/sparse.py
@@ -133,7 +133,7 @@ def __new__(cls, *args, **kwargs):
                         value = self._sympify(
                             op(self._sympify(i), self._sympify(j)))
                         if value:
-                            self._smat[(i, j)] = value
+                            self._smat[i, j] = value
             elif isinstance(args[2], (dict, Dict)):
                 def update(i, j, v):
                     # update self._smat and make sure there are
@@ -193,7 +193,7 @@ def update(i, j, v):
                         row = [row]
                     for j, vij in enumerate(row):
                         if vij:
-                            self._smat[(i, j)] = self._sympify(vij)
+                            self._smat[i, j] = self._sympify(vij)
                     c = max(c, len(row))
                 self.rows = len(v) if c else 0
                 self.cols = c
@@ -206,7 +206,7 @@ def update(i, j, v):
                     for j in range(self.cols):
                         value = _list[self.cols*i + j]
                         if value:
-                            self._smat[(i, j)] = value
+                            self._smat[i, j] = value
         return self
 
     def __eq__(self, other):
@@ -397,11 +397,11 @@ def _eval_col_insert(self, icol, other):
             row, col = key
             if col >= icol:
                 col += other.cols
-            new_smat[(row, col)] = val
+            new_smat[row, col] = val
         # add other's keys
         for key, val in other._smat.items():
             row, col = key
-            new_smat[(row, col + icol)] = val
+            new_smat[row, col + icol] = val
         return self._new(self.rows, self.cols + other.cols, new_smat)
 
     def _eval_conjugate(self):
@@ -422,7 +422,7 @@ def _eval_extract(self, rowsList, colsList):
             # keeping only the ones that are desired
             for rk, ck in self._smat:
                 if rk in urow and ck in ucol:
-                    smat[(urow.index(rk), ucol.index(ck))] = self._smat[(rk, ck)]
+                    smat[urow.index(rk), ucol.index(ck)] = self._smat[rk, ck]
 
         rv = self._new(len(urow), len(ucol), smat)
         # rv is nominally correct but there might be rows/cols
@@ -484,7 +484,7 @@ def _eval_matrix_mul(self, other):
                 indices = set(col_lookup[col].keys()) & set(row_lookup[row].keys())
                 if indices:
                     val = sum(row_lookup[row][k]*col_lookup[col][k] for k in indices)
-                    smat[(row, col)] = val
+                    smat[row, col] = val
         return self._new(self.rows, other.cols, smat)
 
     def _eval_row_insert(self, irow, other):
@@ -496,11 +496,11 @@ def _eval_row_insert(self, irow, other):
             row, col = key
             if row >= irow:
                 row += other.rows
-            new_smat[(row, col)] = val
+            new_smat[row, col] = val
         # add other's keys
         for key, val in other._smat.items():
             row, col = key
-            new_smat[(row + irow, col)] = val
+            new_smat[row + irow, col] = val
         return self._new(self.rows + other.rows, self.cols, new_smat)
 
     def _eval_scalar_mul(self, other):
@@ -1022,9 +1022,9 @@ def __setitem__(self, key, value):
         if rv is not None:
             i, j, value = rv
             if value:
-                self._smat[(i, j)] = value
+                self._smat[i, j] = value
             elif (i, j) in self._smat:
-                del self._smat[(i, j)]
+                del self._smat[i, j]
 
     def as_mutable(self):
         return self.copy()
@@ -1120,7 +1120,7 @@ def col_join(self, other):
                 for j in range(B.cols):
                     v = b[k]
                     if v:
-                        A._smat[(i + A.rows, j)] = v
+                        A._smat[i + A.rows, j] = v
                     k += 1
         else:
             for (i, j), v in B._smat.items():
@@ -1148,7 +1148,7 @@ def col_op(self, j, f):
             v = self._smat.get((i, j), S.Zero)
             fv = f(v, i)
             if fv:
-                self._smat[(i, j)] = fv
+                self._smat[i, j] = fv
             elif v:
                 self._smat.pop((i, j))
 
@@ -1328,11 +1328,11 @@ def row_join(self, other):
                 for j in range(B.cols):
                     v = b[k]
                     if v:
-                        A._smat[(i, j + A.cols)] = v
+                        A._smat[i, j + A.cols] = v
                     k += 1
         else:
             for (i, j), v in B._smat.items():
-                A._smat[(i, j + A.cols)] = v
+                A._smat[i, j + A.cols] = v
         A.cols += B.cols
         return A
 
@@ -1363,7 +1363,7 @@ def row_op(self, i, f):
             v = self._smat.get((i, j), S.Zero)
             fv = f(v, j)
             if fv:
-                self._smat[(i, j)] = fv
+                self._smat[i, j] = fv
             elif v:
                 self._smat.pop((i, j))
 
diff --git a/sympy/matrices/tests/test_commonmatrix.py b/sympy/matrices/tests/test_commonmatrix.py
index f71501f1216e..c53104983c8f 100644
--- a/sympy/matrices/tests/test_commonmatrix.py
+++ b/sympy/matrices/tests/test_commonmatrix.py
@@ -17,7 +17,7 @@
 from sympy.logic.boolalg import (And, Or)
 from sympy.matrices.common import (ShapeError, MatrixError, NonSquareMatrixError,
     _MinimalMatrix, MatrixShaping, MatrixProperties, MatrixOperations, MatrixArithmetic,
-    MatrixSpecial)
+    MatrixSpecial, banded)
 from sympy.matrices.matrices import (MatrixDeterminant,
     MatrixReductions, MatrixSubspaces, MatrixEigen, MatrixCalculus)
 from sympy.matrices import (Matrix, diag, eye,
@@ -1560,3 +1560,88 @@ def test___eq__():
         [[0, 1, 1],
         [1, 0, 0],
         [1, 1, 1]]) == {}) is False
+
+
+def test_banded():
+    raises(TypeError, lambda: banded())
+    raises(TypeError, lambda: banded(1))
+    raises(TypeError, lambda: banded(1, 2))
+    raises(TypeError, lambda: banded(1, 2, 3))
+    raises(TypeError, lambda: banded(1, 2, 3, 4))
+    raises(ValueError, lambda: banded({0: (1,2)}, rows=1))
+    raises(ValueError, lambda: banded({0: (1,2)}, cols=1))
+    raises(ValueError, lambda: banded(1, {0: (1,2)}))
+    raises(ValueError, lambda: banded(2, 1, {0: (1,2)}))
+    raises(ValueError, lambda: banded(1, 2, {0: (1,2)}))
+
+    assert isinstance(banded(2, 4, {}), SparseMatrix)
+    assert banded(2, 4, {}) == zeros(2, 4)
+    assert banded({0: 0, 1: 0}) == zeros(0)
+    assert banded({0: [1, 2]}) == Matrix([1, 2])
+    assert banded({1: (1, 2, 3, 0), -1: (4, 5, 6)}) == \
+        banded({1: (1, 2, 3), -1: (4, 5, 6)}) == \
+        Matrix([
+        [0, 1, 0, 0],
+        [4, 0, 2, 0],
+        [0, 5, 0, 3],
+        [0, 0, 6, 0]])
+    assert banded(3, 4, {-1: 1, 0: 2, 1: 3}) == \
+        Matrix([
+        [2, 3, 0, 0],
+        [1, 2, 3, 0],
+        [0, 1, 2, 3]])
+    s = lambda d: (1 + d)**2
+    assert banded(5, {0: s, 2: s}) == \
+        Matrix([
+        [1, 0, 1,  0,  0],
+        [0, 4, 0,  4,  0],
+        [0, 0, 9,  0,  9],
+        [0, 0, 0, 16,  0],
+        [0, 0, 0,  0, 25]])
+    assert banded(2, {0: 1}) == \
+        Matrix([
+        [1, 0],
+        [0, 1]])
+    assert banded(2, 3, {0: 1}) == \
+        Matrix([
+        [1, 0, 0],
+        [0, 1, 0]])
+    vert = [1, 2, 3]
+    assert banded({0: vert}, cols=3) == \
+        Matrix([
+        [1, 0, 0],
+        [2, 1, 0],
+        [3, 2, 1],
+        [0, 3, 2],
+        [0, 0, 3]])
+    assert banded(4, {0: ones(2)}) == \
+        Matrix([
+        [1, 1, 0, 0],
+        [1, 1, 0, 0],
+        [0, 0, 1, 1],
+        [0, 0, 1, 1]])
+    raises(ValueError, lambda: banded({0: 2, 1: ones(2)}, rows=5))
+    assert banded({0: 2, 2: (ones(2),)*3}) == \
+        Matrix([
+        [2, 0, 1, 1, 0, 0, 0, 0],
+        [0, 2, 1, 1, 0, 0, 0, 0],
+        [0, 0, 2, 0, 1, 1, 0, 0],
+        [0, 0, 0, 2, 1, 1, 0, 0],
+        [0, 0, 0, 0, 2, 0, 1, 1],
+        [0, 0, 0, 0, 0, 2, 1, 1]])
+    raises(ValueError, lambda: banded({0: (2,)*5, 1: (ones(2),)*3}))
+    u2 = Matrix([[1, 1], [0, 1]])
+    assert banded({0: (2,)*5, 1: (u2,)*3}) == \
+        Matrix([
+        [2, 1, 1, 0, 0, 0, 0],
+        [0, 2, 1, 0, 0, 0, 0],
+        [0, 0, 2, 1, 1, 0, 0],
+        [0, 0, 0, 2, 1, 0, 0],
+        [0, 0, 0, 0, 2, 1, 1],
+        [0, 0, 0, 0, 0, 0, 1]])
+    assert banded({0:(0,ones(2)),2:2}) == \
+        Matrix([
+        [0, 0, 2],
+        [0, 1, 1],
+        [0, 1, 1]])
+    raises(ValueError, lambda: banded({0: (0, ones(2)), 1: 2}))
diff --git a/sympy/polys/polytools.py b/sympy/polys/polytools.py
index f4ef829b03eb..418ac6e5aad0 100644
--- a/sympy/polys/polytools.py
+++ b/sympy/polys/polytools.py
@@ -635,7 +635,7 @@ def ltrim(f, gen):
 
         for monom, coeff in rep.items():
 
-            if any(i for i in monom[:j]):
+            if any(monom[:j]):
                 # some generator is used in the portion to be trimmed
                 raise PolynomialError("can't left trim %s" % f)
 
diff --git a/sympy/solvers/ode.py b/sympy/solvers/ode.py
index 21ddc9e40e5a..afc138caa71b 100644
--- a/sympy/solvers/ode.py
+++ b/sympy/solvers/ode.py
@@ -6284,7 +6284,7 @@ def lie_heuristic_bivariate(match, comp=False):
                 soldict = solve(polyy.values(), *symset)
                 if isinstance(soldict, list):
                     soldict = soldict[0]
-                if any(x for x in soldict.values()):
+                if any(soldict.values()):
                     xired = xieq.subs(soldict)
                     etared = etaeq.subs(soldict)
                     # Scaling is done by substituting one for the parameters
@@ -6351,7 +6351,7 @@ def lie_heuristic_chi(match, comp=False):
                     soldict = solve(cpoly.values(), *solsyms)
                     if isinstance(soldict, list):
                         soldict = soldict[0]
-                    if any(x for x in soldict.values()):
+                    if any(soldict.values()):
                         chieq = chieq.subs(soldict)
                         dict_ = dict((sym, 1) for sym in solsyms)
                         chieq = chieq.subs(dict_)
diff --git a/sympy/solvers/polysys.py b/sympy/solvers/polysys.py
index 004fdb2fa875..aa22a1df93f9 100644
--- a/sympy/solvers/polysys.py
+++ b/sympy/solvers/polysys.py
@@ -162,7 +162,7 @@ def solve_generic(polys, opt):
     def _is_univariate(f):
         """Returns True if 'f' is univariate in its last variable. """
         for monom in f.monoms():
-            if any(m for m in monom[:-1]):
+            if any(monom[:-1]):
                 return False
 
         return True


Code review comments:
diff_hunk: @@ -790,35 +791,33 @@ def diag(kls, *args, **kwargs):
         diag_entries = defaultdict(int)
         R = C = 0  # keep track of the biggest index seen
         for m in args:
-            if hasattr(m, 'rows') or isinstance(m, list):
-                # in this case, we're a matrix or list
-                if hasattr(m, 'rows'):
-                    # convert to list of lists
+            if isinstance(m, list):
+                if not strict:
+                    m = SparseMatrix(m)
+                    for (i, j), mij in m._smat.items():
+                        diag_entries[(i + R, j + C)] = mij
                     r, c = m.shape
-                    m = m.tolist()
+                    m = []  # to skip process below
                 else:
-                    # make sure all list elements are lists
-                    r = len(m)
-                    if strict:
-                        # let Matrix raise the error
-                        m = Matrix(m)
-                        c = m.cols
-                        m = m.tolist()
-                    else:
-                        m = [mi if isinstance(mi, list) else [mi]
-                            for mi in m]
-                        c = max(map(len, m))
-                # process list of lists
-                for i in range(len(m)):
-                    for j, mij in enumerate(m[i]):
-                        diag_entries[(i + R, j + C)] = mij
-                R += r
-                C += c
-            else:
-                # in this case, we're a single value
+                    # if malformed, Matrix will raise an error
+                    _ = Matrix(m)
+                    r, c = _.shape
+                    m = _.tolist()
+            elif hasattr(m, 'shape'):  # a Matrix
+                # convert to list of lists
+                r, c = m.shape
+                m = m.tolist()
+            else:  # in this case, we're a single value
                 diag_entries[(R, C)] = m
                 R += 1
                 C += 1
+                continue
+            # process list of lists
+            for i in range(len(m)):
+                for j, mij in enumerate(m[i]):
path: sympy/matrices/common.py
text: @user1:
`mij` -> `m_ij`

diff_hunk: @@ -2500,3 +2499,219 @@ def classof(A, B):
             return A.__class__
 
     raise TypeError("Incompatible classes %s, %s" % (A.__class__, B.__class__))
+
+def banded(*args, **kwargs):
+    """Returns a SparseMatrix from the given dictionary describing
+    the diagonals of the matrix. The keys are positive for upper
+    diagonals and negative for those below the main diagonal. The
+    values may be:
+        * expressions or single-argument functions,
+        * tuples of values,
+        * lists/matrices
+    Unless dimensions are given, the size of the returned matrix will
+    be large enough to contain the largest non-zero value provided.
+
+    Examples
+    ========
+
+    >>> from sympy.matrices.common import banded
+    >>> from sympy import ones, Matrix
+    >>> from sympy.abc import x
+
+    If explicit values are given in tuples,
+    the matrix will autosize to contain all values, otherwise
+    a single value is filled onto the entire diagonal:
+
+    >>> banded({1: (1, 2, 3), -1: (4, 5, 6), 0: x})
+    Matrix([
+    [x, 1, 0, 0],
+    [4, x, 2, 0],
+    [0, 5, x, 3],
+    [0, 0, 6, x]])
+
+    A function accepting a single argument can be used to fill the
+    diagonal as a function of diagonal index (which starts at 0).
+    The size (or shape) of the matrix must be given to obtain more
+    than a 1x1 matrix:
+
+    >>> s = lambda d: (1 + d)**2
+    >>> banded(5, {0: s, 2: s, -2: 2})
+    Matrix([
+    [1, 0, 1,  0,  0],
+    [0, 4, 0,  4,  0],
+    [2, 0, 9,  0,  9],
+    [0, 2, 0, 16,  0],
+    [0, 0, 2,  0, 25]])
+
+    Matrices (or lists which are interpreted as the same) fill
+    with their diagonal on the indicated diagonal:
+
+    >>> vert = [1, 2, 3]
+    >>> banded({0: vert}, cols=3)
+    Matrix([
+    [1, 0, 0],
+    [2, 1, 0],
+    [3, 2, 1],
+    [0, 3, 2],
+    [0, 0, 3]])
+
+    >>> banded(4, {0: ones(2)})
+    Matrix([
+    [1, 1, 0, 0],
+    [1, 1, 0, 0],
+    [0, 0, 1, 1],
+    [0, 0, 1, 1]])
+
+    Errors are raised if the designated size will not hold
+    all values an integral number of times. Here, the rows
+    are designated as odd (but an even number is required to
+    hold the off-diagonal 2x2 ones):
+
+    >>> banded({0: 2, 1: ones(2)}, rows=5)
+    Traceback (most recent call last):
+    ...
+    ValueError:
+    sequence does not fit an integral number of times in the matrix
+
+    And here, an even number of rows is given...but the square
+    matrix has an even number of columns, too. As we saw
+    in the previous example, an odd number is required:
+
+    >>> banded(4, {0: 2, 1: ones(2)})  # trying to make 4x4 and cols must be odd
+    Traceback (most recent call last):
+    ...
+    ValueError:
+    sequence does not fit an integral number of times in the matrix
+
+    A way around having to count rows is to enclosing matrix elements
+    in a tuple and indicate the desired number of them to the right:
+
+    >>> banded({0: 2, 2: (ones(2),)*3})
+    Matrix([
+    [2, 0, 1, 1, 0, 0, 0, 0],
+    [0, 2, 1, 1, 0, 0, 0, 0],
+    [0, 0, 2, 0, 1, 1, 0, 0],
+    [0, 0, 0, 2, 1, 1, 0, 0],
+    [0, 0, 0, 0, 2, 0, 1, 1],
+    [0, 0, 0, 0, 0, 2, 1, 1]])
+
+    An error will be raised if more than one value
+    is written to a given entry. Here, the ones overlap
+    with the main diagonal if they are placed on the
+    first diagonal:
+
+    >>> banded({0: (2,)*5, 1: (ones(2),)*3})
+    Traceback (most recent call last):
+    ...
+    ValueError: collision at (1, 1)
+
+    By placing a 0 at the bottom left of the 2x2 matrix of
+    ones, the collision is avoided:
+
+    >>> u2 = Matrix([
+    ... [1, 1],
+    ... [0, 1]])
+    >>> banded({0: (2,)*5, 1: (u2,)*3})
+    Matrix([
+    [2, 1, 1, 0, 0, 0, 0],
+    [0, 2, 1, 0, 0, 0, 0],
+    [0, 0, 2, 1, 1, 0, 0],
+    [0, 0, 0, 2, 1, 0, 0],
+    [0, 0, 0, 0, 2, 1, 1],
+    [0, 0, 0, 0, 0, 0, 1]])
+    """
+    from sympy import Dict, Dummy, SparseMatrix
+    try:
+        if len(args) not in (1, 2, 3):
+            raise TypeError
+        if not isinstance(args[-1], (dict, Dict)):
+            raise TypeError
+        if len(args) == 1:
+            rows = kwargs.get('rows', None)
+            cols = kwargs.get('cols', None)
+        elif len(args) == 2:
+            rows = cols = as_int(args[0])
+        else:
+            rows, cols = map(as_int, args[:2])
+        # fails with ValueError if any keys are not ints
+        _ = all(as_int(k) for k in args[-1])
+    except (ValueError, TypeError):
+        raise TypeError(filldedent(
+            '''unrecognized input to banded:
+            expecting [[row,] col,] {int: value}'''))
path: sympy/matrices/common.py
text: @user1:
Is there a way we could also allow the kwargs `rows`, `cols`, and `shape`?

@author:
rows and cols are already supported: `banded({0: 1}, cols=3) == eye(3)`. I now note this in the docstring and improved handling for cases when either is None.

@author:
`shape` is not yet supported in matrices as a keyword so I didn't try to add it here.

diff_hunk: @@ -2500,3 +2499,219 @@ def classof(A, B):
             return A.__class__
 
     raise TypeError("Incompatible classes %s, %s" % (A.__class__, B.__class__))
+
+def banded(*args, **kwargs):
+    """Returns a SparseMatrix from the given dictionary describing
+    the diagonals of the matrix. The keys are positive for upper
+    diagonals and negative for those below the main diagonal. The
+    values may be:
+        * expressions or single-argument functions,
+        * tuples of values,
+        * lists/matrices
+    Unless dimensions are given, the size of the returned matrix will
+    be large enough to contain the largest non-zero value provided.
+
+    Examples
+    ========
+
+    >>> from sympy.matrices.common import banded
+    >>> from sympy import ones, Matrix
+    >>> from sympy.abc import x
+
+    If explicit values are given in tuples,
+    the matrix will autosize to contain all values, otherwise
+    a single value is filled onto the entire diagonal:
+
+    >>> banded({1: (1, 2, 3), -1: (4, 5, 6), 0: x})
+    Matrix([
+    [x, 1, 0, 0],
+    [4, x, 2, 0],
+    [0, 5, x, 3],
+    [0, 0, 6, x]])
+
+    A function accepting a single argument can be used to fill the
+    diagonal as a function of diagonal index (which starts at 0).
+    The size (or shape) of the matrix must be given to obtain more
+    than a 1x1 matrix:
+
+    >>> s = lambda d: (1 + d)**2
+    >>> banded(5, {0: s, 2: s, -2: 2})
+    Matrix([
+    [1, 0, 1,  0,  0],
+    [0, 4, 0,  4,  0],
+    [2, 0, 9,  0,  9],
+    [0, 2, 0, 16,  0],
+    [0, 0, 2,  0, 25]])
+
+    Matrices (or lists which are interpreted as the same) fill
+    with their diagonal on the indicated diagonal:
+
+    >>> vert = [1, 2, 3]
+    >>> banded({0: vert}, cols=3)
+    Matrix([
+    [1, 0, 0],
+    [2, 1, 0],
+    [3, 2, 1],
+    [0, 3, 2],
+    [0, 0, 3]])
+
+    >>> banded(4, {0: ones(2)})
+    Matrix([
+    [1, 1, 0, 0],
+    [1, 1, 0, 0],
+    [0, 0, 1, 1],
+    [0, 0, 1, 1]])
+
+    Errors are raised if the designated size will not hold
+    all values an integral number of times. Here, the rows
+    are designated as odd (but an even number is required to
+    hold the off-diagonal 2x2 ones):
+
+    >>> banded({0: 2, 1: ones(2)}, rows=5)
+    Traceback (most recent call last):
+    ...
+    ValueError:
+    sequence does not fit an integral number of times in the matrix
+
+    And here, an even number of rows is given...but the square
+    matrix has an even number of columns, too. As we saw
+    in the previous example, an odd number is required:
+
+    >>> banded(4, {0: 2, 1: ones(2)})  # trying to make 4x4 and cols must be odd
+    Traceback (most recent call last):
+    ...
+    ValueError:
+    sequence does not fit an integral number of times in the matrix
+
+    A way around having to count rows is to enclosing matrix elements
+    in a tuple and indicate the desired number of them to the right:
+
+    >>> banded({0: 2, 2: (ones(2),)*3})
+    Matrix([
+    [2, 0, 1, 1, 0, 0, 0, 0],
+    [0, 2, 1, 1, 0, 0, 0, 0],
+    [0, 0, 2, 0, 1, 1, 0, 0],
+    [0, 0, 0, 2, 1, 1, 0, 0],
+    [0, 0, 0, 0, 2, 0, 1, 1],
+    [0, 0, 0, 0, 0, 2, 1, 1]])
+
+    An error will be raised if more than one value
+    is written to a given entry. Here, the ones overlap
+    with the main diagonal if they are placed on the
+    first diagonal:
+
+    >>> banded({0: (2,)*5, 1: (ones(2),)*3})
+    Traceback (most recent call last):
+    ...
+    ValueError: collision at (1, 1)
+
+    By placing a 0 at the bottom left of the 2x2 matrix of
+    ones, the collision is avoided:
path: sympy/matrices/common.py
text: @user1:
I feel like this behaviour will be counter-intuitive. It could also be accomplished with `banded(7, {1: (u2,)*3}) + banded(7, {0:(2,)*5})`.

Would it be more readable to use `[2]*5` instead of `(2,)*5` ?

@user1:
Ah, I see, passing a list has a completely different meaning compared with passing a tuple. This concerns me. Would it be too onerous to have the user pass a `Matrix` if they want a matrix on the diag and to interpret all other iterables the same way?

@author:
> Would it be too onerous to have the user pass a `Matrix` if they want a matrix on the diag

I agree that that will make it a little more transparent. And I don't like having to replicate a tuple as `(foo,)*n` instead of `[foo]*n`. I have made the change.

@author:
> this behaviour will be counter-intuitive. It could also be accomplished with

I saved this until the very end of the docstring. It answers the question about what will happen if entries clash. The example is for illustration purposes only and one might accomplish the end result in a variety of ways.

diff_hunk: @@ -790,35 +791,33 @@ def diag(kls, *args, **kwargs):
         diag_entries = defaultdict(int)
         R = C = 0  # keep track of the biggest index seen
path: sympy/matrices/common.py
text: @user1:
I'm not a fan of the capital-letter variables...

diff_hunk: @@ -790,35 +791,33 @@ def diag(kls, *args, **kwargs):
         diag_entries = defaultdict(int)
         R = C = 0  # keep track of the biggest index seen
         for m in args:
-            if hasattr(m, 'rows') or isinstance(m, list):
-                # in this case, we're a matrix or list
-                if hasattr(m, 'rows'):
-                    # convert to list of lists
+            if isinstance(m, list):
+                if not strict:
path: sympy/matrices/common.py
text: @user1:
I think it would be easier to read if it were `if strict: ... else:` instead of `if not strict: ...`

@author:
ok