mathmat.mathmat

MathMat is a mathematically aware matrix library for Python.

See the Matrix class for more details.

  1"""MathMat is a mathematically aware matrix library for Python.
  2
  3See the Matrix class for more details.
  4"""
  5
  6import lazy_import
  7from functools import wraps
  8from sys import float_info
  9from scipy.sparse.linalg import eigs as sparse_eigs, eigsh as sparse_eigsh, \
 10    norm as sparse_norm
 11from scipy.sparse import identity, issparse, dia_array, csc_array, \
 12    tril as sparse_tril, triu as sparse_triu
 13from scipy.linalg import issymmetric, ishermitian, eig, eigh, eigvals, \
 14    eigvalsh, inv
 15import numpy as np
 16
 17
 18factor = lazy_import.lazy_module("mathmat.factor")
 19solve = lazy_import.lazy_module("mathmat.solve")
 20
 21EPS = float_info.epsilon
 22
 23
 24def uniquetol(arr, tolerance):
 25    """Find and return the unique elements of `arr` within a `tolerance`."""
 26    unique = np.unique(arr)
 27    if len(unique) <= 1:
 28        return unique
 29    u_diff = np.abs(np.diff(unique))
 30    where = np.argwhere(u_diff > tolerance)
 31    return np.insert(unique[where + 1], 0, unique[0])
 32
 33
 34class MatrixSizeException(Exception):
 35    """A `MatrixSizeException` is raised when two matrices have
 36    incompatible sizes for an operation, usually multiplication."""
 37
 38    def __init__(self, A, B, operation):
 39        super().__init__(
 40            "Incompatible matrix sizes {}x{} and {}x{} for {}.".format(
 41                A.nr, A.nc, B.nr, B.nc, operation))
 42
 43
 44class MatrixTypeException(Exception):
 45    """A `MatrixTypeException` is raised when a matrix does not satisfy
 46    the requirements of an attempted operation."""
 47
 48    def __init__(self, operation, req_type):
 49        super().__init__(
 50            "The matrix must be {} to perform: {}.".format(
 51                req_type, operation))
 52
 53
 54def _computable_property(name):
 55    """Decorate a function as defining a Computable Property of a Matrix."""
 56    def decorator(func):
 57        @wraps(func)
 58        def property_wrapper(*args, **kwargs):
 59            """Check if the Property already has a value."""
 60            if name not in args[0]._computed:
 61                # No value exists, perform the computation.
 62                args[0].set(name, func(*args, **kwargs))
 63            value = args[0]._computed.get(name, None)
 64            if callable(value) and not name == "lin_solver":
 65                # A value can be generated using another function.
 66                value = value()
 67                args[0].set(name, value)
 68            return value
 69        return property_wrapper
 70    return decorator
 71
 72
 73class Matrix:
 74    """A Matrix object is an immutable representation of a matrix.
 75
 76    In addition to the entries, the Matrix keeps track of
 77    computations which have been performed on it, and reuses
 78    existing computations whenever possible. Thus, a Matrix
 79    object is "aware" of linear algebra theorems.
 80
 81    Each Computable Property can be either:
 82     1. unset (default), in which case the corresponding
 83        function will be called to compute it.
 84     2. set to a callable, in which case that callable
 85        will be called to compute it.
 86     3. set to a value, which will be returned immediately.
 87
 88    List of Computable Properties:
 89     - complex        : `True` if $M$ has complex entries
 90     - conjugate      : The complex conjugate of $M$
 91     - cond_2         : Condition number in 2-norm
 92     - determinant    : `det(M)`
 93     - diagonal       : `True` if $M$ is zero except for main diag.
 94     - diagonalizable : `True` if $M = P D P^{-1}$
 95     - eigenvalues    : The eigenvalues of $M$
 96     - eigenvectors   : The right eigenvectors of M
 97     - hermitian      : `True` if conjugate transpose of $M$ equals $M$
 98     - inverse        : if it exists, M^{-1}
 99     - invertible     : `True` if M^{-1} exists
100     - lin_solver     : A function to solve Ax = b for x.
101     - norm_2         : The 2-norm of the matrix
102     - normal         : `True` if $M M^H = M^H M$
103     - nullity        : `dim(Kernel(M))`
104     - orthogonal     : `True` if $M^T = M^{-1}$
105     - posdef         : `True` if $M$ is positive definite
106     - qr             : The `(Q, R)` factorization of $M$
107     - rank           : `dim(Col(M))`
108     - sigmas         : Singular values of $M$
109     - sparsity       : The fraction of zero entries in $M$
110     - square         : `True` if $M$ is square
111     - svd            : The SVD `(U, S, V)` of $M$
112     - symmetric      : `True` if $M = M^T$
113     - trace          : The trace of $M$
114     - transpose      : $M^T$
115     - triangular_L   : `True` if $M$ is lower triangular.
116     - triangular_U   : `True` if $M$ is upper triangular.
117     - unitary        : `True` if $M^H = M^{-1}$
118
119    Non-Mathematical:
120     - sparse         : `True` if the matrix is stored as a
121                        SciPy Sparse object.
122     - to_dense       : A dense representation of $M$
123     - to_sparse      : A sparse representation of $M$
124    """
125
126    def set(self, prop, value):
127        self._computed[prop] = value
128
129    def __init__(self, arr):
130        """Initialize a Matrix object.
131
132        The argument `arr` can be a Numpy array, Scipy sparse matrix, list of
133        Vector objects, or an object convertible to a Numpy array.
134
135        If the resulting array is of dimension 1 in either axis, the
136        Matrix automatically becomes a Vector.
137        """
138        if isinstance(arr, Matrix):
139            arr = arr.entries
140        if issparse(arr):
141            # This is a SciPy sparse object. Preserve it.
142            self.entries = arr
143        elif (type(arr) is list or type(arr) is tuple) and len(arr) > 0 and \
144                isinstance(arr[0], Vector):
145            # This is a list of Vectors. Join them into a Matrix.
146            any_complex = sum(v.is_complex() for v in arr) > 0
147            if arr[0].nr == 1:
148                # Treat all as row vectors
149                self.entries = np.empty(
150                    shape=(len(arr), len(arr[0])),
151                    dtype=np.complex_ if any_complex else np.double)
152                for j in range(len(arr)):
153                    if len(arr[j]) != len(arr[0]):
154                        raise MatrixSizeException(
155                            arr[j], arr[0], "initialize Matrix from rows")
156                    self.entries[j, :] = arr[j].entries.ravel()
157            else:
158                # Treat all as column vectors
159                self.entries = np.empty(
160                    shape=(len(arr[0]), len(arr)),
161                    dtype=np.complex_ if any_complex else np.double)
162                for j in range(len(arr)):
163                    if len(arr[j]) != len(arr[0]):
164                        raise MatrixSizeException(
165                            arr[j], arr[0], "initialize Matrix from rows")
166                    self.entries[:, j] = arr[j].entries.ravel()
167        else:
168            # This is another kind of object, hopefully numpy can handle it.
169            self.entries = np.array(arr)
170        if len(self.entries.shape) == 1:
171            # Reshape ambiguous arrays into column vectors.
172            self.entries = self.entries.reshape((len(self.entries), 1))
173        if min(self.entries.shape) == 1 and not isinstance(self, Vector):
174            # Automatically become a Vector if has a dimension of 1
175            self.__dict__ = Vector(arr).__dict__
176            self.__class__ = Vector
177
178        # Initialize a blank dictionary of Computed Properties
179        self._computed = {}
180
181    """Directly accessible properties of the matrix"""
182
183    @property
184    def size(self) -> tuple:
185        """The shape of the underlying matrix representation."""
186        return self.entries.shape
187
188    @property
189    def nr(self) -> int:
190        """The number of rows in the Matrix."""
191        return self.size[0]
192
193    @property
194    def nc(self) -> int:
195        """The number of columns in the Matrix."""
196        return self.size[1]
197
198    def __str__(self):
199        return str(self.entries)
200
201    def __repr__(self):
202        return "{}x{} Matrix ({} properties)".format(
203            self.nr, self.nc, len(self._computed))
204
205    """Computable Properties corresponding to "checks".
206    All correspond to matrix "types" and thus are queried
207    using a method called `is_<type>()`."""
208
209    @_computable_property("sparse")
210    def is_sparse(self):
211        """`True` if the underlying matrix representation is SciPy Sparse."""
212        return issparse(self.entries)
213
214    @_computable_property("complex")
215    def is_complex(self):
216        """`True` if the matrix has any non-zero complex entry."""
217        return np.iscomplex(self.entries).any()
218
219    @_computable_property("diagonal")
220    def is_diagonal(self, tolerance=1e-10):
221        """`True` if the non-diagonal elements of the matrix are within
222        `tolerance` of zero."""
223        check = False
224        if self._computed.get("triangular_L", False) and \
225                self._computed.get("triangular_U", False):
226            return True
227        if self.is_square():
228            if self.is_sparse():
229                check = (self.entries -
230                         dia_array(
231                             (self.entries.diagonal().T, 0),
232                             shape=(self.nr, self.nc))).count_nonzero() == 0
233            else:
234                # Fast check for square matrices, @Daniel F on Stackoverflow
235                test = self.entries.reshape(-1)[:-1].reshape(
236                    self.nr-1, self.nc+1)
237                check = ~np.any(test[:, 1:])
238        else:
239            check = np.allclose(
240                self.entries - np.diag(self.entries, 0), 0, atol=tolerance)
241        if check:
242            # Automatically convert to a DiagonalMatrix
243            self.entries = dia_array((self.diagonal(0).entries.T, 0),
244                                     shape=(self.nr, self.nc))
245            self.__class__ = DiagonalMatrix
246        return check
247
248    @_computable_property("diagonalizable")
249    def is_diagonalizable(self, tolerance=1e-10):
250        """`True` if the matrix can be diagonalized, $M = P D P^{-1}$.
251        Does not necessarily compute the diagonalization. Densifies."""
252        if not self.is_square():
253            return False
254        if self.is_sparse():
255            return self.to_dense().is_diagonalizable()
256        if "eigenvalues" in self._computed:
257            if len(uniquetol(self.eigenvalues(), tolerance)) == self.nc:
258                # N distinct eigenvalues is sufficient for diagonalizability.
259                return True
260        if self.is_normal():
261            # Normal matrices are diagonalizable.
262            return True
263        # Check the linear independence of the eigenvectors through the SVD
264        return Matrix(self.eigenvectors()).rank(tolerance) == self.nc
265
266    @_computable_property("hermitian")
267    def is_hermitian(self, tolerance=1e-10):
268        """`True` if the matrix is Hermitian, $M^H = M$."""
269        if not self.is_square():
270            return False
271        if self.is_complex():
272            # Call the SciPy ishermitian function
273            return ishermitian(self.entries, atol=tolerance)
274        # For real matrices, equivalent to symmetric
275        return self.is_symmetric()
276
277    @_computable_property("invertible")
278    def is_invertible(self, tolerance=1e-10):
279        """`True` if the matrix has an inverse $M^{-1} M = Id$.
280        Does not compute the inverse."""
281        if "inverse" in self._computed:
282            return True
283        if "sigmas" in self._computed:
284            # No zero singular values => invertible
285            return np.isclose(self.sigmas(), 0, atol=tolerance).sum() == 0
286        if (self.is_triangular_L() or self.is_triangular_U()) or \
287                (not self.is_sparse() and "eigenvalues" in self._computed):
288            # No zero eigenvalues => invertible
289            return np.isclose(self.eigenvalues(), 0, atol=tolerance).sum() == 0
290        # Square matrices with a finite condition number are invertible
291        return self.is_square() and self.cond_2() < 1 / EPS
292
293    @_computable_property("normal")
294    def is_normal(self, tolerance=1e-10):
295        """`True` if the matrix satisfies the normal equation $M^H M = M M^H$.
296        Computes the conjugate transpose."""
297        if not self.is_square():
298            return False
299        CT = self.conjugate().transpose()
300        return (self @ CT).equals((CT @ self), tolerance)
301
302    @_computable_property("orthogonal")
303    def is_orthogonal(self, tolerance=1e-10):
304        """`True` if the matrix is orthogonal, $M^T = M^{-1}$.
305        Computes the transpose and identifies the inverse if `True`."""
306        if not self.is_square():
307            raise MatrixTypeException("orthogonality check", "square")
308        prod = self @ self.transpose()
309        check = prod.equals(Identity(prod.nc), tolerance)
310
311        if check:
312            # The inverse is now known.
313            self.set("inverse", self.transpose())
314            if not self.is_complex():
315                self.set("unitary", True)
316            self.transpose().set("orthogonal", True)
317            self.transpose().set("inverse", self)
318        return check
319
320    @_computable_property("posdef")
321    def is_posdef(self):
322        """`True` if the matrix is positive-definite.
323        Computes the eigenvalues. Densifies."""
324
325        if self.is_sparse():
326            return self.to_dense().is_posdef()
327        return self.is_symmetric() and np.all(self.eigenvalues() > 0)
328
329    def is_square(self):
330        """`True` if the matrix is square."""
331        return self.nr == self.nc
332
333    @_computable_property("symmetric")
334    def is_symmetric(self, tolerance=1e-10):
335        """`True` if the matrix is symmetric, $M^T = M$.
336        Computes the transpose only if the matrix is sparse."""
337        if not self.is_square():
338            return False
339        else:
340            if self.is_sparse():
341                return (abs(self.entries
342                            - self.transpose().entries) > tolerance).nnz == 0
343            # Call SciPy issymmetric
344            return issymmetric(self.entries, atol=tolerance)
345
346    @_computable_property("triangular_L")
347    def is_triangular_L(self, tolerance=1e-10):
348        """`True` if the matrix is lower (left) triangular."""
349        if self.is_sparse():
350            return (abs(self.entries
351                        - sparse_tril(self.entries)) > tolerance).sum() == 0
352        return np.allclose(self.entries, np.tril(self.entries), atol=tolerance)
353
354    @_computable_property("triangular_U")
355    def is_triangular_U(self, tolerance=1e-10):
356        """`True` if the matrix is upper (right) triangular."""
357        if self.is_sparse():
358            return (abs(self.entries
359                        - sparse_triu(self.entries)) > tolerance).sum() == 0
360        return np.allclose(self.entries, np.triu(self.entries), atol=tolerance)
361
362    @_computable_property("unitary")
363    def is_unitary(self, tolerance=1e-10):
364        """`True` if the matrix is unitary, $M^H = M^{-1}$.
365        Computes the conjugate transpose and identifies the inverse if True."""
366        if not self.is_square():
367            raise MatrixTypeException("unitary check", "square")
368        if not self.is_complex():
369            return self.is_orthogonal()
370        CT = self.conjugate().transpose()
371        prod = self @ CT
372        check = prod.equals(Identity(prod.nc), tolerance)
373
374        if check:
375            # The inverse is now known.
376            self.set("inverse", CT)
377            CT.set("unitary", True)
378            CT.set("inverse", self)
379        return check
380
381    """Computable Properties of the matrix that are not checks."""
382
383    @_computable_property("cond_2")
384    def cond_2(self):
385        """Compute the condition number $\\kappa_2 for inversion of the matrix.
386        Computes the singular values."""
387        sigmas = self.sigmas()
388        return sigmas[0] / sigmas[-1]
389
390    @_computable_property("determinant")
391    def determinant(self):
392        """Compute the determinant of the matrix. Densifies.
393        Computes the eigenvalues of triangular matrices."""
394        if not self.is_square():
395            raise MatrixTypeException("determinant", "square")
396        if self.is_sparse():
397            # For now, we have to go to a dense representation
398            return self.to_dense().determinant()
399        else:
400            if "eigenvalues" in self._computed or \
401                    (self._computed.get("triangular_L", False) or
402                     self._computed.get("triangular_U", False)):
403                # Use the eigenvalues if available
404                return np.prod(self.eigenvalues())
405            return np.linalg.det(self.entries)
406
407    @_computable_property("eigenvalues")
408    def eigenvalues(self, tolerance=1e-10):
409        """Compute the eigenvalues of the matrix.
410        Does not compute the eigenvectors, unless the matrix is sparse.
411        Checks whether $M$ is Hermitian, unless the matrix is triangular."""
412        if not self.is_square():
413            raise MatrixTypeException("eigenvalues", "square")
414        if self.is_triangular_L(tolerance) or self.is_triangular_U(tolerance):
415            # Triangular matrix eigenvalues are on the diagonal.
416            return np.sort(self.diagonal(0))
417        if self.is_sparse():
418            # Use sparse algorithms
419            self.eigenvectors(tolerance)
420            return self.eigenvalues()
421        if self.is_hermitian():
422            # Use the specialized algorithm for Hermitian matrices.
423            return eigvalsh(self.entries, check_finite=False)
424        vals = eigvals(self.entries, check_finite=False)
425        if np.iscomplex(vals).any():
426            return np.sort_complex(vals)
427        return np.sort(vals)
428
429    @_computable_property("eigenvectors")
430    def eigenvectors(self, tolerance=1e-10, sparse_k="max"):
431        """Compute the eigenvectors of the matrix.
432        Computes the eigenvalues. Checks whether $M$ is Hermitian.
433
434        Note that if the matrix has a sparse representation, then
435        only `sparse_k` many eigenvalues/vectors can be computed!
436        The `max` is 90% of the dimension of the matrix.
437        """
438        if not self.is_square():
439            raise MatrixTypeException("eigenvectors", "square")
440        if self.is_sparse():
441            sparse_k = min(self.nr - 1, int(0.9 * self.nr))
442            # Use the sparse algorithms
443            if self.is_hermitian():
444                # Use the specialized algorithm for Hermitian matrices
445                vals, vecs = sparse_eigsh(self.entries, k=sparse_k)
446            else:
447                vals, vecs = sparse_eigs(self.entries, k=sparse_k)
448            if not np.iscomplex(vals).any():
449                order = np.argsort(vals)
450                # Report the eigenvalues and vectors in increasing order
451                vals = vals[order]
452                vecs = vecs[:, order]
453        else:
454            if self.is_hermitian():
455                # Use the specialized algorithm for Hermitian matrices.
456                vals, vecs = eigh(self.entries, check_finite=False)
457            else:
458                vals, vecs = eig(self.entries, check_finite=False)
459                if not np.iscomplex(vals).any():
460                    order = np.argsort(vals)
461                    # Report the eigenvalues and vectors in increasing order
462                    vals = vals[order]
463                    vecs = vecs[:, order]
464        self.set("eigenvalues", vals)
465        return [Vector(vecs[:, j]) for j in range(len(vals))]
466
467    @_computable_property("norm_2")
468    def norm_2(self):
469        """Compute the 2-norm of the matrix $\\lVert M \\rVert_2."""
470        if self._computed.get("orthogonal", False) or \
471                self._computed.get("unitary", False):
472            # Unitary and Orthogonal matrices have norm 1.
473            return 1.0
474        if "sigmas" in self._computed:
475            # The norm is the largest singular value.
476            return self.sigmas()[0]
477        if self.is_sparse():
478            return sparse_norm(self.entries, ord=2)
479        return np.linalg.norm(self.entries, ord=2)
480
481    @_computable_property("nullity")
482    def nullity(self, tolerance=1e-10):
483        """Compute the nullity as the number of columns minus the rank."""
484        return self.nc - self.rank()
485
486    @_computable_property("rank")
487    def rank(self, tolerance=1e-10):
488        """Compute the rank of the matrix.
489        Computes the SVD if no inverse or eigenvalues are known."""
490        if "invertible" in self._computed or "inverse" in self._computed:
491            # Invertible => full rank
492            return self.nc
493        if not self.is_sparse() and "eigenvalues" in self._computed:
494            # Number of nonzero eigenvalues
495            return np.logical_not(
496                np.isclose(self.eigenvalues(), 0, atol=tolerance)).sum()
497        # Number of nonzero signular values
498        _, S, _ = factor.svd(self)
499        return np.logical_not(
500            np.isclose(S.diagonal(0).entries, 0, atol=tolerance)).sum()
501
502    @_computable_property("sigmas")
503    def sigmas(self):
504        """Compute the singular values of the matrix.
505        Computes the SVD if not positive-definite."""
506        if "posdef" in self._computed and self.is_posdef():
507            return np.flip(np.sqrt(self.eigenvalues()))
508        factor.svd(self)
509        return self.sigmas()
510
511    @_computable_property("sparsity")
512    def sparsity(self, tolerance=1e-10):
513        """Compute the sparsity of the matrix as the fraction of zero
514        entries out of all entries in the matrix."""
515        if self.is_sparse():
516            # Use the sparse methods if available
517            return len(self.entries.nnz) / (self.nr * self.nc)
518        return np.isclose(
519            self.entries, 0, atol=tolerance).sum() / (self.nr * self.nc)
520
521    @_computable_property("trace")
522    def trace(self):
523        """Compute the trace of the matrix."""
524        if self.is_sparse():
525            return self.entries.trace()
526        if "eigenvalues" in self._computed:
527            return np.sum(self.eigenvalues())
528        return np.trace(self.entries)
529
530    """Getter functions for elements of the matrix."""
531
532    def row(self, number):
533        """Return the 1-indexed row."""
534        if number < 0:
535            return Vector(self.entries[number, :])
536        return Vector(self.entries[number-1, :])
537
538    def column(self, number):
539        """Return the 1-indexed column."""
540        if number < 0:
541            return Vector(self.entries[:, number])
542        return Vector(self.entries[:, number-1])
543
544    def diagonal(self, number):
545        """Return the k-th diagonal."""
546        if self.is_sparse():
547            return Vector(self.entries.diagonal(number))
548        return Vector(np.diagonal(self.entries, number))
549
550    def entry(self, row, col):
551        """Return the 1-indexed entry $M_{ij}$."""
552        return self.entries[row if row < 0 else row - 1,
553                            col if col < 0 else col-1]
554
555    def equals(self, M, tolerance=1e-10):
556        """`True` if the matrix equals another elementwise,
557        within a tolerance."""
558        if type(M) is int or type(M) is float:
559            return np.allclose(self.entries, M, atol=tolerance)
560        if self.is_sparse() or M.is_sparse():
561            # Use subtraction if one or both are sparse
562            return np.allclose(self.entries - M.entries, 0, atol=tolerance)
563        return np.allclose(self.entries, M.entries, atol=tolerance)
564
565    def __eq__(self, M):
566        return self.equals(M)
567
568    """Transformations of the matrix."""
569
570    @_computable_property("dense_mat")
571    def to_dense(self):
572        """Switch from a sparse to a dense repsresentation."""
573        if self.is_sparse():
574            DM = Matrix(self.entries.todense())
575            DM._computed = self._computed
576            # Remove properties that are best recomputed
577            for k in ("cond_2", "dense", "eigenvalues", "eigenvectors", "rank",
578                      "invertible", "inverse", "sigmas", "sparse", "svd",
579                      "transpose"):
580                DM._computed.pop(k, None)
581            DM.set("sparse_mat", self)
582            return DM
583        return self
584
585    @_computable_property("sparse_mat")
586    def to_sparse(self, tolerance=1e-10):
587        """Switch from a dense to a sparse representation."""
588        if self.is_sparse():
589            return self
590        if self.is_diagonal():
591            return DiagonalMatrix(self.diagonal(0))
592        return Matrix(csc_array(self.entries))
593
594    @_computable_property("conjugate")
595    def conjugate(self):
596        """Compute the complex conjugate of the matrix."""
597        if self.is_sparse():
598            Conj = Matrix(self.entries.conjugate())
599        else:
600            Conj = Matrix(np.conjugate(self.entries))
601        Conj.set("conjugate", self)
602        if "triangular_L" in self._computed:
603            Conj.set("triangular_L", self.is_triangular_L())
604        if "triangular_U" in self._computed:
605            Conj.set("triangular_U", self.is_triangular_U())
606        if "diagonal" in self._computed:
607            Conj.set("diagonal", self.is_diagonal())
608        return Conj
609
610    @_computable_property("inverse")
611    def inverse(self):
612        """Compute the inverse of the matrix. Checks invertibility."""
613        if not self.is_invertible():
614            raise MatrixTypeException("inverse", "invertible")
615        if "qr" in self._computed:
616            # Use the QR if it exists
617            Q, R = self.qr()
618            Inv = R.inverse() @ Q.transpose()
619        elif "svd" in self._computed:
620            # Use the SVD if it exists
621            U, S, V = self.svd()
622            Inv = V.conjugate().transpose() @ S.inverse() \
623                @ U.conjugate().transpose()
624        else:
625            # Nothing known, compute inverse
626            Inv = Matrix(inv(self.entries))
627        Inv.set("inverse", self)
628        # Set known properties
629        if "determinant" in self._computed:
630            Inv.set("determinant", 1.0 / self.determinant())
631        if "eigenvalues" in self._computed:
632            Inv.set("eigenvalues", np.flip(1.0 / self.eigenvalues()))
633        # Set the lin solver to the found inverse
634        self.set("lin_solver", lambda v: solve.invert(self, v))
635        return Inv
636
637    @_computable_property("lin_solver")
638    def lin_solver(self):
639        """Return a method which solves $Ax=b$ for $x$.
640        See `solve.automatic`."""
641        return solve.automatic(self, None, get_method=True)
642
643    @_computable_property("qr")
644    def qr(self):
645        """Compute the QR factorization of the matrix. See `factor.qr`."""
646        return factor.qr(self)
647
648    @_computable_property("svd")
649    def svd(self):
650        """Compute the SVD factorization of the matrix. See `factor.svd`."""
651        return factor.svd(self)
652
653    @_computable_property("transpose")
654    def transpose(self):
655        """Compute the transpose of the matrix."""
656        if self.is_sparse():
657            T = Matrix(self.entries.transpose())
658        else:
659            T = Matrix(self.entries.T)
660        # Copy properties that are identical for the transpose
661        T._computed = {prop: self._computed[prop] for prop in [
662            "complex", "cond_2", "determinant", "diagonal"
663            "eigenvalues", "eigenvectors", "invertible", "rank",
664            "sigmas", "symmetric"
665        ] if prop in self._computed}
666        T.set("transpose", self)
667        if "triangular_L" in self._computed:
668            T.set("triangular_U", self.is_triangular_L())
669        if "triangular_U" in self._computed:
670            T.set("triangular_L", self.is_triangular_U())
671        if "svd" in self._computed:
672            U, S, V = self.svd()
673            T.set("svd", (V.transpose(), S, U.transpose()))
674        return T
675
676    """Operation implementations."""
677
678    def __mul__(self, M):
679        """Multiplication by a constant."""
680        if type(M) is int or type(M) is float:
681            return Matrix(M * self.entries)
682        raise TypeError("Use of * to multiply by a {}.".format(type(M)))
683
684    def __rmul__(self, M):
685        return self.__mul__(M)
686
687    def __neg__(self):
688        return self.__mul__(-1)
689
690    def __matmul__(self, M):
691        """Matrix multiplication."""
692        if not isinstance(M, Matrix):
693            # Try to promote to Matrix object
694            M = Matrix(M)
695        if self.nc == M.nr:
696            return Matrix(self.entries @ M.entries)
697        raise MatrixSizeException(self, M, "multiply")
698
699    def __add__(self, M):
700        """Addition"""
701        if self.nc != M.nc or self.nr != M.nr:
702            raise MatrixSizeException(self, M, "addition")
703        if not isinstance(M, Matrix):
704            # Try to promote to Matrix object
705            M = Matrix(M)
706        return Matrix(self.entries + M.entries)
707
708    def __radd__(self, M):
709        return self.__add__(M)
710
711    def __sub__(self, M):
712        """Subtraction"""
713        if self.nc != M.nc or self.nr != M.nr:
714            raise MatrixSizeException(self, M, "subtraction")
715        if not isinstance(M, Matrix):
716            # Try to promote to Matrix object
717            M = Matrix(M)
718        return Matrix(self.entries - M.entries)
719
720    def __rsub__(self, M):
721        return (-self).__add__(M)
722
723
724class DiagonalMatrix(Matrix):
725    """A DiagonalMatrix is a Matrix whose only nonzero entries
726    are on the primary diagonal."""
727
728    def __init__(self, diagonal):
729        """Initialize a DiagonalMatrix from an array of the diagonal."""
730        if isinstance(diagonal, Vector):
731            diagonal = diagonal.entries
732        # Use the SciPy sparse diagonal implementation.
733        super().__init__(dia_array((diagonal, 0),
734                                   shape=(len(diagonal), len(diagonal))))
735        self.set("diagonal", True)
736
737    def __getitem__(self, number):
738        """Return the 1-indexed entry along the diagonal."""
739        if number < 0:
740            return self.entries.diagonal()[number]
741        return self.entries.diagonal()[number-1]
742
743    @_computable_property("invertible")
744    def is_invertible(self, tolerance=1e-10):
745        """`True` if there are no zeros on the diagonal."""
746        if "inverse" in self._computed:
747            return True
748        return np.isclose(
749            self.entries.diagonal(), 0, atol=tolerance).sum() == 0
750
751    @_computable_property("determinant")
752    def determinant(self):
753        """Compute the determinant as the product of the diagonal."""
754        return np.prod(self.entries.diagonal())
755
756    @_computable_property("eigenvalues")
757    def eigenvalues(self, tolerance=1e-10):
758        """Compute the eigenvalues, equivalent to sorting the diagonal."""
759        return np.sort(self.entries.diagonal())
760
761    @_computable_property("inverse")
762    def inverse(self):
763        """Compute the inverse as the reciprocal of the diagonal."""
764        if self.is_invertible():
765            return DiagonalMatrix(1.0 / self.entries.diagonal())
766        raise MatrixTypeException("inverse", "invertible")
767
768    @_computable_property("trace")
769    def trace(self):
770        """Compute the trace of the matrix."""
771        return np.sum(self.diagonal().entries)
772
773
774class Identity(DiagonalMatrix):
775    """The Identity matrix is a DiagonalMatrix of ones."""
776
777    def __init__(self, n):
778        """Initialize the Identity of size n."""
779        Matrix.__init__(self, identity(n))
780
781
782class Vector(Matrix):
783    """A Vector is a Matrix of size 1 in one dimension."""
784
785    def __init__(self, arr, row=False):
786        """Initialize a Vector object from a Numpy array.
787        If the array has only one dimension, defaults to being a column vector.
788        A row vector can be forced by setting row=True."""
789        if isinstance(arr, Matrix):
790            arr = arr.entries
791        arr = np.array(arr)
792        if len(arr.shape) == 1:
793            # One-dimensional array.
794            if row:
795                super().__init__(arr.reshape(1, len(arr)))
796            else:
797                super().__init__(arr.reshape(len(arr), 1))
798        else:
799            # 2-dimensional array, initialize the Matrix.
800            super().__init__(arr)
801
802        if min(self.nr, self.nc) != 1:
803            raise ValueError(
804                "A Vector cannot have size {}x{}".format(self.nr, self.nc))
805
806    def __len__(self):
807        """Return the number of entries in the Vector."""
808        return max(self.nr, self.nc)
809
810    def __getitem__(self, number):
811        """Returns the 1-indexed entry in the Vector."""
812        if number < 0:
813            return self.entries[number]
814        return self.entries[number-1]
815
816    def __repr__(self):
817        return "{}x{} Vector ({} properties)".format(
818            self.nr, self.nc, len(self._computed))
819
820    @_computable_property("unit")
821    def unit(self):
822        """Compute the unit vector in the direction of this Vector.
823        Computes the 2-norm."""
824        return Vector(self.entries / self.norm_2())
EPS = 2.220446049250313e-16
def uniquetol(arr, tolerance):
25def uniquetol(arr, tolerance):
26    """Find and return the unique elements of `arr` within a `tolerance`."""
27    unique = np.unique(arr)
28    if len(unique) <= 1:
29        return unique
30    u_diff = np.abs(np.diff(unique))
31    where = np.argwhere(u_diff > tolerance)
32    return np.insert(unique[where + 1], 0, unique[0])

Find and return the unique elements of arr within a tolerance.

class MatrixSizeException(builtins.Exception):
35class MatrixSizeException(Exception):
36    """A `MatrixSizeException` is raised when two matrices have
37    incompatible sizes for an operation, usually multiplication."""
38
39    def __init__(self, A, B, operation):
40        super().__init__(
41            "Incompatible matrix sizes {}x{} and {}x{} for {}.".format(
42                A.nr, A.nc, B.nr, B.nc, operation))

A MatrixSizeException is raised when two matrices have incompatible sizes for an operation, usually multiplication.

MatrixSizeException(A, B, operation)
39    def __init__(self, A, B, operation):
40        super().__init__(
41            "Incompatible matrix sizes {}x{} and {}x{} for {}.".format(
42                A.nr, A.nc, B.nr, B.nc, operation))
Inherited Members
builtins.BaseException
with_traceback
args
class MatrixTypeException(builtins.Exception):
45class MatrixTypeException(Exception):
46    """A `MatrixTypeException` is raised when a matrix does not satisfy
47    the requirements of an attempted operation."""
48
49    def __init__(self, operation, req_type):
50        super().__init__(
51            "The matrix must be {} to perform: {}.".format(
52                req_type, operation))

A MatrixTypeException is raised when a matrix does not satisfy the requirements of an attempted operation.

MatrixTypeException(operation, req_type)
49    def __init__(self, operation, req_type):
50        super().__init__(
51            "The matrix must be {} to perform: {}.".format(
52                req_type, operation))
Inherited Members
builtins.BaseException
with_traceback
args
class Matrix:
 74class Matrix:
 75    """A Matrix object is an immutable representation of a matrix.
 76
 77    In addition to the entries, the Matrix keeps track of
 78    computations which have been performed on it, and reuses
 79    existing computations whenever possible. Thus, a Matrix
 80    object is "aware" of linear algebra theorems.
 81
 82    Each Computable Property can be either:
 83     1. unset (default), in which case the corresponding
 84        function will be called to compute it.
 85     2. set to a callable, in which case that callable
 86        will be called to compute it.
 87     3. set to a value, which will be returned immediately.
 88
 89    List of Computable Properties:
 90     - complex        : `True` if $M$ has complex entries
 91     - conjugate      : The complex conjugate of $M$
 92     - cond_2         : Condition number in 2-norm
 93     - determinant    : `det(M)`
 94     - diagonal       : `True` if $M$ is zero except for main diag.
 95     - diagonalizable : `True` if $M = P D P^{-1}$
 96     - eigenvalues    : The eigenvalues of $M$
 97     - eigenvectors   : The right eigenvectors of M
 98     - hermitian      : `True` if conjugate transpose of $M$ equals $M$
 99     - inverse        : if it exists, M^{-1}
100     - invertible     : `True` if M^{-1} exists
101     - lin_solver     : A function to solve Ax = b for x.
102     - norm_2         : The 2-norm of the matrix
103     - normal         : `True` if $M M^H = M^H M$
104     - nullity        : `dim(Kernel(M))`
105     - orthogonal     : `True` if $M^T = M^{-1}$
106     - posdef         : `True` if $M$ is positive definite
107     - qr             : The `(Q, R)` factorization of $M$
108     - rank           : `dim(Col(M))`
109     - sigmas         : Singular values of $M$
110     - sparsity       : The fraction of zero entries in $M$
111     - square         : `True` if $M$ is square
112     - svd            : The SVD `(U, S, V)` of $M$
113     - symmetric      : `True` if $M = M^T$
114     - trace          : The trace of $M$
115     - transpose      : $M^T$
116     - triangular_L   : `True` if $M$ is lower triangular.
117     - triangular_U   : `True` if $M$ is upper triangular.
118     - unitary        : `True` if $M^H = M^{-1}$
119
120    Non-Mathematical:
121     - sparse         : `True` if the matrix is stored as a
122                        SciPy Sparse object.
123     - to_dense       : A dense representation of $M$
124     - to_sparse      : A sparse representation of $M$
125    """
126
127    def set(self, prop, value):
128        self._computed[prop] = value
129
130    def __init__(self, arr):
131        """Initialize a Matrix object.
132
133        The argument `arr` can be a Numpy array, Scipy sparse matrix, list of
134        Vector objects, or an object convertible to a Numpy array.
135
136        If the resulting array is of dimension 1 in either axis, the
137        Matrix automatically becomes a Vector.
138        """
139        if isinstance(arr, Matrix):
140            arr = arr.entries
141        if issparse(arr):
142            # This is a SciPy sparse object. Preserve it.
143            self.entries = arr
144        elif (type(arr) is list or type(arr) is tuple) and len(arr) > 0 and \
145                isinstance(arr[0], Vector):
146            # This is a list of Vectors. Join them into a Matrix.
147            any_complex = sum(v.is_complex() for v in arr) > 0
148            if arr[0].nr == 1:
149                # Treat all as row vectors
150                self.entries = np.empty(
151                    shape=(len(arr), len(arr[0])),
152                    dtype=np.complex_ if any_complex else np.double)
153                for j in range(len(arr)):
154                    if len(arr[j]) != len(arr[0]):
155                        raise MatrixSizeException(
156                            arr[j], arr[0], "initialize Matrix from rows")
157                    self.entries[j, :] = arr[j].entries.ravel()
158            else:
159                # Treat all as column vectors
160                self.entries = np.empty(
161                    shape=(len(arr[0]), len(arr)),
162                    dtype=np.complex_ if any_complex else np.double)
163                for j in range(len(arr)):
164                    if len(arr[j]) != len(arr[0]):
165                        raise MatrixSizeException(
166                            arr[j], arr[0], "initialize Matrix from rows")
167                    self.entries[:, j] = arr[j].entries.ravel()
168        else:
169            # This is another kind of object, hopefully numpy can handle it.
170            self.entries = np.array(arr)
171        if len(self.entries.shape) == 1:
172            # Reshape ambiguous arrays into column vectors.
173            self.entries = self.entries.reshape((len(self.entries), 1))
174        if min(self.entries.shape) == 1 and not isinstance(self, Vector):
175            # Automatically become a Vector if has a dimension of 1
176            self.__dict__ = Vector(arr).__dict__
177            self.__class__ = Vector
178
179        # Initialize a blank dictionary of Computed Properties
180        self._computed = {}
181
182    """Directly accessible properties of the matrix"""
183
184    @property
185    def size(self) -> tuple:
186        """The shape of the underlying matrix representation."""
187        return self.entries.shape
188
189    @property
190    def nr(self) -> int:
191        """The number of rows in the Matrix."""
192        return self.size[0]
193
194    @property
195    def nc(self) -> int:
196        """The number of columns in the Matrix."""
197        return self.size[1]
198
199    def __str__(self):
200        return str(self.entries)
201
202    def __repr__(self):
203        return "{}x{} Matrix ({} properties)".format(
204            self.nr, self.nc, len(self._computed))
205
206    """Computable Properties corresponding to "checks".
207    All correspond to matrix "types" and thus are queried
208    using a method called `is_<type>()`."""
209
210    @_computable_property("sparse")
211    def is_sparse(self):
212        """`True` if the underlying matrix representation is SciPy Sparse."""
213        return issparse(self.entries)
214
215    @_computable_property("complex")
216    def is_complex(self):
217        """`True` if the matrix has any non-zero complex entry."""
218        return np.iscomplex(self.entries).any()
219
220    @_computable_property("diagonal")
221    def is_diagonal(self, tolerance=1e-10):
222        """`True` if the non-diagonal elements of the matrix are within
223        `tolerance` of zero."""
224        check = False
225        if self._computed.get("triangular_L", False) and \
226                self._computed.get("triangular_U", False):
227            return True
228        if self.is_square():
229            if self.is_sparse():
230                check = (self.entries -
231                         dia_array(
232                             (self.entries.diagonal().T, 0),
233                             shape=(self.nr, self.nc))).count_nonzero() == 0
234            else:
235                # Fast check for square matrices, @Daniel F on Stackoverflow
236                test = self.entries.reshape(-1)[:-1].reshape(
237                    self.nr-1, self.nc+1)
238                check = ~np.any(test[:, 1:])
239        else:
240            check = np.allclose(
241                self.entries - np.diag(self.entries, 0), 0, atol=tolerance)
242        if check:
243            # Automatically convert to a DiagonalMatrix
244            self.entries = dia_array((self.diagonal(0).entries.T, 0),
245                                     shape=(self.nr, self.nc))
246            self.__class__ = DiagonalMatrix
247        return check
248
249    @_computable_property("diagonalizable")
250    def is_diagonalizable(self, tolerance=1e-10):
251        """`True` if the matrix can be diagonalized, $M = P D P^{-1}$.
252        Does not necessarily compute the diagonalization. Densifies."""
253        if not self.is_square():
254            return False
255        if self.is_sparse():
256            return self.to_dense().is_diagonalizable()
257        if "eigenvalues" in self._computed:
258            if len(uniquetol(self.eigenvalues(), tolerance)) == self.nc:
259                # N distinct eigenvalues is sufficient for diagonalizability.
260                return True
261        if self.is_normal():
262            # Normal matrices are diagonalizable.
263            return True
264        # Check the linear independence of the eigenvectors through the SVD
265        return Matrix(self.eigenvectors()).rank(tolerance) == self.nc
266
267    @_computable_property("hermitian")
268    def is_hermitian(self, tolerance=1e-10):
269        """`True` if the matrix is Hermitian, $M^H = M$."""
270        if not self.is_square():
271            return False
272        if self.is_complex():
273            # Call the SciPy ishermitian function
274            return ishermitian(self.entries, atol=tolerance)
275        # For real matrices, equivalent to symmetric
276        return self.is_symmetric()
277
278    @_computable_property("invertible")
279    def is_invertible(self, tolerance=1e-10):
280        """`True` if the matrix has an inverse $M^{-1} M = Id$.
281        Does not compute the inverse."""
282        if "inverse" in self._computed:
283            return True
284        if "sigmas" in self._computed:
285            # No zero singular values => invertible
286            return np.isclose(self.sigmas(), 0, atol=tolerance).sum() == 0
287        if (self.is_triangular_L() or self.is_triangular_U()) or \
288                (not self.is_sparse() and "eigenvalues" in self._computed):
289            # No zero eigenvalues => invertible
290            return np.isclose(self.eigenvalues(), 0, atol=tolerance).sum() == 0
291        # Square matrices with a finite condition number are invertible
292        return self.is_square() and self.cond_2() < 1 / EPS
293
294    @_computable_property("normal")
295    def is_normal(self, tolerance=1e-10):
296        """`True` if the matrix satisfies the normal equation $M^H M = M M^H$.
297        Computes the conjugate transpose."""
298        if not self.is_square():
299            return False
300        CT = self.conjugate().transpose()
301        return (self @ CT).equals((CT @ self), tolerance)
302
303    @_computable_property("orthogonal")
304    def is_orthogonal(self, tolerance=1e-10):
305        """`True` if the matrix is orthogonal, $M^T = M^{-1}$.
306        Computes the transpose and identifies the inverse if `True`."""
307        if not self.is_square():
308            raise MatrixTypeException("orthogonality check", "square")
309        prod = self @ self.transpose()
310        check = prod.equals(Identity(prod.nc), tolerance)
311
312        if check:
313            # The inverse is now known.
314            self.set("inverse", self.transpose())
315            if not self.is_complex():
316                self.set("unitary", True)
317            self.transpose().set("orthogonal", True)
318            self.transpose().set("inverse", self)
319        return check
320
321    @_computable_property("posdef")
322    def is_posdef(self):
323        """`True` if the matrix is positive-definite.
324        Computes the eigenvalues. Densifies."""
325
326        if self.is_sparse():
327            return self.to_dense().is_posdef()
328        return self.is_symmetric() and np.all(self.eigenvalues() > 0)
329
330    def is_square(self):
331        """`True` if the matrix is square."""
332        return self.nr == self.nc
333
334    @_computable_property("symmetric")
335    def is_symmetric(self, tolerance=1e-10):
336        """`True` if the matrix is symmetric, $M^T = M$.
337        Computes the transpose only if the matrix is sparse."""
338        if not self.is_square():
339            return False
340        else:
341            if self.is_sparse():
342                return (abs(self.entries
343                            - self.transpose().entries) > tolerance).nnz == 0
344            # Call SciPy issymmetric
345            return issymmetric(self.entries, atol=tolerance)
346
347    @_computable_property("triangular_L")
348    def is_triangular_L(self, tolerance=1e-10):
349        """`True` if the matrix is lower (left) triangular."""
350        if self.is_sparse():
351            return (abs(self.entries
352                        - sparse_tril(self.entries)) > tolerance).sum() == 0
353        return np.allclose(self.entries, np.tril(self.entries), atol=tolerance)
354
355    @_computable_property("triangular_U")
356    def is_triangular_U(self, tolerance=1e-10):
357        """`True` if the matrix is upper (right) triangular."""
358        if self.is_sparse():
359            return (abs(self.entries
360                        - sparse_triu(self.entries)) > tolerance).sum() == 0
361        return np.allclose(self.entries, np.triu(self.entries), atol=tolerance)
362
363    @_computable_property("unitary")
364    def is_unitary(self, tolerance=1e-10):
365        """`True` if the matrix is unitary, $M^H = M^{-1}$.
366        Computes the conjugate transpose and identifies the inverse if True."""
367        if not self.is_square():
368            raise MatrixTypeException("unitary check", "square")
369        if not self.is_complex():
370            return self.is_orthogonal()
371        CT = self.conjugate().transpose()
372        prod = self @ CT
373        check = prod.equals(Identity(prod.nc), tolerance)
374
375        if check:
376            # The inverse is now known.
377            self.set("inverse", CT)
378            CT.set("unitary", True)
379            CT.set("inverse", self)
380        return check
381
382    """Computable Properties of the matrix that are not checks."""
383
384    @_computable_property("cond_2")
385    def cond_2(self):
386        """Compute the condition number $\\kappa_2 for inversion of the matrix.
387        Computes the singular values."""
388        sigmas = self.sigmas()
389        return sigmas[0] / sigmas[-1]
390
391    @_computable_property("determinant")
392    def determinant(self):
393        """Compute the determinant of the matrix. Densifies.
394        Computes the eigenvalues of triangular matrices."""
395        if not self.is_square():
396            raise MatrixTypeException("determinant", "square")
397        if self.is_sparse():
398            # For now, we have to go to a dense representation
399            return self.to_dense().determinant()
400        else:
401            if "eigenvalues" in self._computed or \
402                    (self._computed.get("triangular_L", False) or
403                     self._computed.get("triangular_U", False)):
404                # Use the eigenvalues if available
405                return np.prod(self.eigenvalues())
406            return np.linalg.det(self.entries)
407
408    @_computable_property("eigenvalues")
409    def eigenvalues(self, tolerance=1e-10):
410        """Compute the eigenvalues of the matrix.
411        Does not compute the eigenvectors, unless the matrix is sparse.
412        Checks whether $M$ is Hermitian, unless the matrix is triangular."""
413        if not self.is_square():
414            raise MatrixTypeException("eigenvalues", "square")
415        if self.is_triangular_L(tolerance) or self.is_triangular_U(tolerance):
416            # Triangular matrix eigenvalues are on the diagonal.
417            return np.sort(self.diagonal(0))
418        if self.is_sparse():
419            # Use sparse algorithms
420            self.eigenvectors(tolerance)
421            return self.eigenvalues()
422        if self.is_hermitian():
423            # Use the specialized algorithm for Hermitian matrices.
424            return eigvalsh(self.entries, check_finite=False)
425        vals = eigvals(self.entries, check_finite=False)
426        if np.iscomplex(vals).any():
427            return np.sort_complex(vals)
428        return np.sort(vals)
429
430    @_computable_property("eigenvectors")
431    def eigenvectors(self, tolerance=1e-10, sparse_k="max"):
432        """Compute the eigenvectors of the matrix.
433        Computes the eigenvalues. Checks whether $M$ is Hermitian.
434
435        Note that if the matrix has a sparse representation, then
436        only `sparse_k` many eigenvalues/vectors can be computed!
437        The `max` is 90% of the dimension of the matrix.
438        """
439        if not self.is_square():
440            raise MatrixTypeException("eigenvectors", "square")
441        if self.is_sparse():
442            sparse_k = min(self.nr - 1, int(0.9 * self.nr))
443            # Use the sparse algorithms
444            if self.is_hermitian():
445                # Use the specialized algorithm for Hermitian matrices
446                vals, vecs = sparse_eigsh(self.entries, k=sparse_k)
447            else:
448                vals, vecs = sparse_eigs(self.entries, k=sparse_k)
449            if not np.iscomplex(vals).any():
450                order = np.argsort(vals)
451                # Report the eigenvalues and vectors in increasing order
452                vals = vals[order]
453                vecs = vecs[:, order]
454        else:
455            if self.is_hermitian():
456                # Use the specialized algorithm for Hermitian matrices.
457                vals, vecs = eigh(self.entries, check_finite=False)
458            else:
459                vals, vecs = eig(self.entries, check_finite=False)
460                if not np.iscomplex(vals).any():
461                    order = np.argsort(vals)
462                    # Report the eigenvalues and vectors in increasing order
463                    vals = vals[order]
464                    vecs = vecs[:, order]
465        self.set("eigenvalues", vals)
466        return [Vector(vecs[:, j]) for j in range(len(vals))]
467
468    @_computable_property("norm_2")
469    def norm_2(self):
470        """Compute the 2-norm of the matrix $\\lVert M \\rVert_2."""
471        if self._computed.get("orthogonal", False) or \
472                self._computed.get("unitary", False):
473            # Unitary and Orthogonal matrices have norm 1.
474            return 1.0
475        if "sigmas" in self._computed:
476            # The norm is the largest singular value.
477            return self.sigmas()[0]
478        if self.is_sparse():
479            return sparse_norm(self.entries, ord=2)
480        return np.linalg.norm(self.entries, ord=2)
481
482    @_computable_property("nullity")
483    def nullity(self, tolerance=1e-10):
484        """Compute the nullity as the number of columns minus the rank."""
485        return self.nc - self.rank()
486
487    @_computable_property("rank")
488    def rank(self, tolerance=1e-10):
489        """Compute the rank of the matrix.
490        Computes the SVD if no inverse or eigenvalues are known."""
491        if "invertible" in self._computed or "inverse" in self._computed:
492            # Invertible => full rank
493            return self.nc
494        if not self.is_sparse() and "eigenvalues" in self._computed:
495            # Number of nonzero eigenvalues
496            return np.logical_not(
497                np.isclose(self.eigenvalues(), 0, atol=tolerance)).sum()
498        # Number of nonzero signular values
499        _, S, _ = factor.svd(self)
500        return np.logical_not(
501            np.isclose(S.diagonal(0).entries, 0, atol=tolerance)).sum()
502
503    @_computable_property("sigmas")
504    def sigmas(self):
505        """Compute the singular values of the matrix.
506        Computes the SVD if not positive-definite."""
507        if "posdef" in self._computed and self.is_posdef():
508            return np.flip(np.sqrt(self.eigenvalues()))
509        factor.svd(self)
510        return self.sigmas()
511
512    @_computable_property("sparsity")
513    def sparsity(self, tolerance=1e-10):
514        """Compute the sparsity of the matrix as the fraction of zero
515        entries out of all entries in the matrix."""
516        if self.is_sparse():
517            # Use the sparse methods if available
518            return len(self.entries.nnz) / (self.nr * self.nc)
519        return np.isclose(
520            self.entries, 0, atol=tolerance).sum() / (self.nr * self.nc)
521
522    @_computable_property("trace")
523    def trace(self):
524        """Compute the trace of the matrix."""
525        if self.is_sparse():
526            return self.entries.trace()
527        if "eigenvalues" in self._computed:
528            return np.sum(self.eigenvalues())
529        return np.trace(self.entries)
530
531    """Getter functions for elements of the matrix."""
532
533    def row(self, number):
534        """Return the 1-indexed row."""
535        if number < 0:
536            return Vector(self.entries[number, :])
537        return Vector(self.entries[number-1, :])
538
539    def column(self, number):
540        """Return the 1-indexed column."""
541        if number < 0:
542            return Vector(self.entries[:, number])
543        return Vector(self.entries[:, number-1])
544
545    def diagonal(self, number):
546        """Return the k-th diagonal."""
547        if self.is_sparse():
548            return Vector(self.entries.diagonal(number))
549        return Vector(np.diagonal(self.entries, number))
550
551    def entry(self, row, col):
552        """Return the 1-indexed entry $M_{ij}$."""
553        return self.entries[row if row < 0 else row - 1,
554                            col if col < 0 else col-1]
555
556    def equals(self, M, tolerance=1e-10):
557        """`True` if the matrix equals another elementwise,
558        within a tolerance."""
559        if type(M) is int or type(M) is float:
560            return np.allclose(self.entries, M, atol=tolerance)
561        if self.is_sparse() or M.is_sparse():
562            # Use subtraction if one or both are sparse
563            return np.allclose(self.entries - M.entries, 0, atol=tolerance)
564        return np.allclose(self.entries, M.entries, atol=tolerance)
565
566    def __eq__(self, M):
567        return self.equals(M)
568
569    """Transformations of the matrix."""
570
571    @_computable_property("dense_mat")
572    def to_dense(self):
573        """Switch from a sparse to a dense repsresentation."""
574        if self.is_sparse():
575            DM = Matrix(self.entries.todense())
576            DM._computed = self._computed
577            # Remove properties that are best recomputed
578            for k in ("cond_2", "dense", "eigenvalues", "eigenvectors", "rank",
579                      "invertible", "inverse", "sigmas", "sparse", "svd",
580                      "transpose"):
581                DM._computed.pop(k, None)
582            DM.set("sparse_mat", self)
583            return DM
584        return self
585
586    @_computable_property("sparse_mat")
587    def to_sparse(self, tolerance=1e-10):
588        """Switch from a dense to a sparse representation."""
589        if self.is_sparse():
590            return self
591        if self.is_diagonal():
592            return DiagonalMatrix(self.diagonal(0))
593        return Matrix(csc_array(self.entries))
594
595    @_computable_property("conjugate")
596    def conjugate(self):
597        """Compute the complex conjugate of the matrix."""
598        if self.is_sparse():
599            Conj = Matrix(self.entries.conjugate())
600        else:
601            Conj = Matrix(np.conjugate(self.entries))
602        Conj.set("conjugate", self)
603        if "triangular_L" in self._computed:
604            Conj.set("triangular_L", self.is_triangular_L())
605        if "triangular_U" in self._computed:
606            Conj.set("triangular_U", self.is_triangular_U())
607        if "diagonal" in self._computed:
608            Conj.set("diagonal", self.is_diagonal())
609        return Conj
610
611    @_computable_property("inverse")
612    def inverse(self):
613        """Compute the inverse of the matrix. Checks invertibility."""
614        if not self.is_invertible():
615            raise MatrixTypeException("inverse", "invertible")
616        if "qr" in self._computed:
617            # Use the QR if it exists
618            Q, R = self.qr()
619            Inv = R.inverse() @ Q.transpose()
620        elif "svd" in self._computed:
621            # Use the SVD if it exists
622            U, S, V = self.svd()
623            Inv = V.conjugate().transpose() @ S.inverse() \
624                @ U.conjugate().transpose()
625        else:
626            # Nothing known, compute inverse
627            Inv = Matrix(inv(self.entries))
628        Inv.set("inverse", self)
629        # Set known properties
630        if "determinant" in self._computed:
631            Inv.set("determinant", 1.0 / self.determinant())
632        if "eigenvalues" in self._computed:
633            Inv.set("eigenvalues", np.flip(1.0 / self.eigenvalues()))
634        # Set the lin solver to the found inverse
635        self.set("lin_solver", lambda v: solve.invert(self, v))
636        return Inv
637
638    @_computable_property("lin_solver")
639    def lin_solver(self):
640        """Return a method which solves $Ax=b$ for $x$.
641        See `solve.automatic`."""
642        return solve.automatic(self, None, get_method=True)
643
644    @_computable_property("qr")
645    def qr(self):
646        """Compute the QR factorization of the matrix. See `factor.qr`."""
647        return factor.qr(self)
648
649    @_computable_property("svd")
650    def svd(self):
651        """Compute the SVD factorization of the matrix. See `factor.svd`."""
652        return factor.svd(self)
653
654    @_computable_property("transpose")
655    def transpose(self):
656        """Compute the transpose of the matrix."""
657        if self.is_sparse():
658            T = Matrix(self.entries.transpose())
659        else:
660            T = Matrix(self.entries.T)
661        # Copy properties that are identical for the transpose
662        T._computed = {prop: self._computed[prop] for prop in [
663            "complex", "cond_2", "determinant", "diagonal"
664            "eigenvalues", "eigenvectors", "invertible", "rank",
665            "sigmas", "symmetric"
666        ] if prop in self._computed}
667        T.set("transpose", self)
668        if "triangular_L" in self._computed:
669            T.set("triangular_U", self.is_triangular_L())
670        if "triangular_U" in self._computed:
671            T.set("triangular_L", self.is_triangular_U())
672        if "svd" in self._computed:
673            U, S, V = self.svd()
674            T.set("svd", (V.transpose(), S, U.transpose()))
675        return T
676
677    """Operation implementations."""
678
679    def __mul__(self, M):
680        """Multiplication by a constant."""
681        if type(M) is int or type(M) is float:
682            return Matrix(M * self.entries)
683        raise TypeError("Use of * to multiply by a {}.".format(type(M)))
684
685    def __rmul__(self, M):
686        return self.__mul__(M)
687
688    def __neg__(self):
689        return self.__mul__(-1)
690
691    def __matmul__(self, M):
692        """Matrix multiplication."""
693        if not isinstance(M, Matrix):
694            # Try to promote to Matrix object
695            M = Matrix(M)
696        if self.nc == M.nr:
697            return Matrix(self.entries @ M.entries)
698        raise MatrixSizeException(self, M, "multiply")
699
700    def __add__(self, M):
701        """Addition"""
702        if self.nc != M.nc or self.nr != M.nr:
703            raise MatrixSizeException(self, M, "addition")
704        if not isinstance(M, Matrix):
705            # Try to promote to Matrix object
706            M = Matrix(M)
707        return Matrix(self.entries + M.entries)
708
709    def __radd__(self, M):
710        return self.__add__(M)
711
712    def __sub__(self, M):
713        """Subtraction"""
714        if self.nc != M.nc or self.nr != M.nr:
715            raise MatrixSizeException(self, M, "subtraction")
716        if not isinstance(M, Matrix):
717            # Try to promote to Matrix object
718            M = Matrix(M)
719        return Matrix(self.entries - M.entries)
720
721    def __rsub__(self, M):
722        return (-self).__add__(M)

A Matrix object is an immutable representation of a matrix.

In addition to the entries, the Matrix keeps track of computations which have been performed on it, and reuses existing computations whenever possible. Thus, a Matrix object is "aware" of linear algebra theorems.

Each Computable Property can be either:

  1. unset (default), in which case the corresponding function will be called to compute it.
  2. set to a callable, in which case that callable will be called to compute it.
  3. set to a value, which will be returned immediately.

List of Computable Properties:

  • complex : True if $M$ has complex entries
  • conjugate : The complex conjugate of $M$
  • cond_2 : Condition number in 2-norm
  • determinant : det(M)
  • diagonal : True if $M$ is zero except for main diag.
  • diagonalizable : True if $M = P D P^{-1}$
  • eigenvalues : The eigenvalues of $M$
  • eigenvectors : The right eigenvectors of M
  • hermitian : True if conjugate transpose of $M$ equals $M$
  • inverse : if it exists, M^{-1}
  • invertible : True if M^{-1} exists
  • lin_solver : A function to solve Ax = b for x.
  • norm_2 : The 2-norm of the matrix
  • normal : True if $M M^H = M^H M$
  • nullity : dim(Kernel(M))
  • orthogonal : True if $M^T = M^{-1}$
  • posdef : True if $M$ is positive definite
  • qr : The (Q, R) factorization of $M$
  • rank : dim(Col(M))
  • sigmas : Singular values of $M$
  • sparsity : The fraction of zero entries in $M$
  • square : True if $M$ is square
  • svd : The SVD (U, S, V) of $M$
  • symmetric : True if $M = M^T$
  • trace : The trace of $M$
  • transpose : $M^T$
  • triangular_L : True if $M$ is lower triangular.
  • triangular_U : True if $M$ is upper triangular.
  • unitary : True if $M^H = M^{-1}$

Non-Mathematical:

  • sparse : True if the matrix is stored as a SciPy Sparse object.
  • to_dense : A dense representation of $M$
  • to_sparse : A sparse representation of $M$
Matrix(arr)
130    def __init__(self, arr):
131        """Initialize a Matrix object.
132
133        The argument `arr` can be a Numpy array, Scipy sparse matrix, list of
134        Vector objects, or an object convertible to a Numpy array.
135
136        If the resulting array is of dimension 1 in either axis, the
137        Matrix automatically becomes a Vector.
138        """
139        if isinstance(arr, Matrix):
140            arr = arr.entries
141        if issparse(arr):
142            # This is a SciPy sparse object. Preserve it.
143            self.entries = arr
144        elif (type(arr) is list or type(arr) is tuple) and len(arr) > 0 and \
145                isinstance(arr[0], Vector):
146            # This is a list of Vectors. Join them into a Matrix.
147            any_complex = sum(v.is_complex() for v in arr) > 0
148            if arr[0].nr == 1:
149                # Treat all as row vectors
150                self.entries = np.empty(
151                    shape=(len(arr), len(arr[0])),
152                    dtype=np.complex_ if any_complex else np.double)
153                for j in range(len(arr)):
154                    if len(arr[j]) != len(arr[0]):
155                        raise MatrixSizeException(
156                            arr[j], arr[0], "initialize Matrix from rows")
157                    self.entries[j, :] = arr[j].entries.ravel()
158            else:
159                # Treat all as column vectors
160                self.entries = np.empty(
161                    shape=(len(arr[0]), len(arr)),
162                    dtype=np.complex_ if any_complex else np.double)
163                for j in range(len(arr)):
164                    if len(arr[j]) != len(arr[0]):
165                        raise MatrixSizeException(
166                            arr[j], arr[0], "initialize Matrix from rows")
167                    self.entries[:, j] = arr[j].entries.ravel()
168        else:
169            # This is another kind of object, hopefully numpy can handle it.
170            self.entries = np.array(arr)
171        if len(self.entries.shape) == 1:
172            # Reshape ambiguous arrays into column vectors.
173            self.entries = self.entries.reshape((len(self.entries), 1))
174        if min(self.entries.shape) == 1 and not isinstance(self, Vector):
175            # Automatically become a Vector if has a dimension of 1
176            self.__dict__ = Vector(arr).__dict__
177            self.__class__ = Vector
178
179        # Initialize a blank dictionary of Computed Properties
180        self._computed = {}

Initialize a Matrix object.

The argument arr can be a Numpy array, Scipy sparse matrix, list of Vector objects, or an object convertible to a Numpy array.

If the resulting array is of dimension 1 in either axis, the Matrix automatically becomes a Vector.

def set(self, prop, value):
127    def set(self, prop, value):
128        self._computed[prop] = value
size: tuple

The shape of the underlying matrix representation.

nr: int

The number of rows in the Matrix.

nc: int

The number of columns in the Matrix.

def is_sparse(self):
210    @_computable_property("sparse")
211    def is_sparse(self):
212        """`True` if the underlying matrix representation is SciPy Sparse."""
213        return issparse(self.entries)

True if the underlying matrix representation is SciPy Sparse.

def is_complex(self):
215    @_computable_property("complex")
216    def is_complex(self):
217        """`True` if the matrix has any non-zero complex entry."""
218        return np.iscomplex(self.entries).any()

True if the matrix has any non-zero complex entry.

def is_diagonal(self, tolerance=1e-10):
220    @_computable_property("diagonal")
221    def is_diagonal(self, tolerance=1e-10):
222        """`True` if the non-diagonal elements of the matrix are within
223        `tolerance` of zero."""
224        check = False
225        if self._computed.get("triangular_L", False) and \
226                self._computed.get("triangular_U", False):
227            return True
228        if self.is_square():
229            if self.is_sparse():
230                check = (self.entries -
231                         dia_array(
232                             (self.entries.diagonal().T, 0),
233                             shape=(self.nr, self.nc))).count_nonzero() == 0
234            else:
235                # Fast check for square matrices, @Daniel F on Stackoverflow
236                test = self.entries.reshape(-1)[:-1].reshape(
237                    self.nr-1, self.nc+1)
238                check = ~np.any(test[:, 1:])
239        else:
240            check = np.allclose(
241                self.entries - np.diag(self.entries, 0), 0, atol=tolerance)
242        if check:
243            # Automatically convert to a DiagonalMatrix
244            self.entries = dia_array((self.diagonal(0).entries.T, 0),
245                                     shape=(self.nr, self.nc))
246            self.__class__ = DiagonalMatrix
247        return check

True if the non-diagonal elements of the matrix are within tolerance of zero.

def is_diagonalizable(self, tolerance=1e-10):
249    @_computable_property("diagonalizable")
250    def is_diagonalizable(self, tolerance=1e-10):
251        """`True` if the matrix can be diagonalized, $M = P D P^{-1}$.
252        Does not necessarily compute the diagonalization. Densifies."""
253        if not self.is_square():
254            return False
255        if self.is_sparse():
256            return self.to_dense().is_diagonalizable()
257        if "eigenvalues" in self._computed:
258            if len(uniquetol(self.eigenvalues(), tolerance)) == self.nc:
259                # N distinct eigenvalues is sufficient for diagonalizability.
260                return True
261        if self.is_normal():
262            # Normal matrices are diagonalizable.
263            return True
264        # Check the linear independence of the eigenvectors through the SVD
265        return Matrix(self.eigenvectors()).rank(tolerance) == self.nc

True if the matrix can be diagonalized, $M = P D P^{-1}$. Does not necessarily compute the diagonalization. Densifies.

def is_hermitian(self, tolerance=1e-10):
267    @_computable_property("hermitian")
268    def is_hermitian(self, tolerance=1e-10):
269        """`True` if the matrix is Hermitian, $M^H = M$."""
270        if not self.is_square():
271            return False
272        if self.is_complex():
273            # Call the SciPy ishermitian function
274            return ishermitian(self.entries, atol=tolerance)
275        # For real matrices, equivalent to symmetric
276        return self.is_symmetric()

True if the matrix is Hermitian, $M^H = M$.

def is_invertible(self, tolerance=1e-10):
278    @_computable_property("invertible")
279    def is_invertible(self, tolerance=1e-10):
280        """`True` if the matrix has an inverse $M^{-1} M = Id$.
281        Does not compute the inverse."""
282        if "inverse" in self._computed:
283            return True
284        if "sigmas" in self._computed:
285            # No zero singular values => invertible
286            return np.isclose(self.sigmas(), 0, atol=tolerance).sum() == 0
287        if (self.is_triangular_L() or self.is_triangular_U()) or \
288                (not self.is_sparse() and "eigenvalues" in self._computed):
289            # No zero eigenvalues => invertible
290            return np.isclose(self.eigenvalues(), 0, atol=tolerance).sum() == 0
291        # Square matrices with a finite condition number are invertible
292        return self.is_square() and self.cond_2() < 1 / EPS

True if the matrix has an inverse $M^{-1} M = Id$. Does not compute the inverse.

def is_normal(self, tolerance=1e-10):
294    @_computable_property("normal")
295    def is_normal(self, tolerance=1e-10):
296        """`True` if the matrix satisfies the normal equation $M^H M = M M^H$.
297        Computes the conjugate transpose."""
298        if not self.is_square():
299            return False
300        CT = self.conjugate().transpose()
301        return (self @ CT).equals((CT @ self), tolerance)

True if the matrix satisfies the normal equation $M^H M = M M^H$. Computes the conjugate transpose.

def is_orthogonal(self, tolerance=1e-10):
303    @_computable_property("orthogonal")
304    def is_orthogonal(self, tolerance=1e-10):
305        """`True` if the matrix is orthogonal, $M^T = M^{-1}$.
306        Computes the transpose and identifies the inverse if `True`."""
307        if not self.is_square():
308            raise MatrixTypeException("orthogonality check", "square")
309        prod = self @ self.transpose()
310        check = prod.equals(Identity(prod.nc), tolerance)
311
312        if check:
313            # The inverse is now known.
314            self.set("inverse", self.transpose())
315            if not self.is_complex():
316                self.set("unitary", True)
317            self.transpose().set("orthogonal", True)
318            self.transpose().set("inverse", self)
319        return check

True if the matrix is orthogonal, $M^T = M^{-1}$. Computes the transpose and identifies the inverse if True.

def is_posdef(self):
321    @_computable_property("posdef")
322    def is_posdef(self):
323        """`True` if the matrix is positive-definite.
324        Computes the eigenvalues. Densifies."""
325
326        if self.is_sparse():
327            return self.to_dense().is_posdef()
328        return self.is_symmetric() and np.all(self.eigenvalues() > 0)

True if the matrix is positive-definite. Computes the eigenvalues. Densifies.

def is_square(self):
330    def is_square(self):
331        """`True` if the matrix is square."""
332        return self.nr == self.nc

True if the matrix is square.

def is_symmetric(self, tolerance=1e-10):
334    @_computable_property("symmetric")
335    def is_symmetric(self, tolerance=1e-10):
336        """`True` if the matrix is symmetric, $M^T = M$.
337        Computes the transpose only if the matrix is sparse."""
338        if not self.is_square():
339            return False
340        else:
341            if self.is_sparse():
342                return (abs(self.entries
343                            - self.transpose().entries) > tolerance).nnz == 0
344            # Call SciPy issymmetric
345            return issymmetric(self.entries, atol=tolerance)

True if the matrix is symmetric, $M^T = M$. Computes the transpose only if the matrix is sparse.

def is_triangular_L(self, tolerance=1e-10):
347    @_computable_property("triangular_L")
348    def is_triangular_L(self, tolerance=1e-10):
349        """`True` if the matrix is lower (left) triangular."""
350        if self.is_sparse():
351            return (abs(self.entries
352                        - sparse_tril(self.entries)) > tolerance).sum() == 0
353        return np.allclose(self.entries, np.tril(self.entries), atol=tolerance)

True if the matrix is lower (left) triangular.

def is_triangular_U(self, tolerance=1e-10):
355    @_computable_property("triangular_U")
356    def is_triangular_U(self, tolerance=1e-10):
357        """`True` if the matrix is upper (right) triangular."""
358        if self.is_sparse():
359            return (abs(self.entries
360                        - sparse_triu(self.entries)) > tolerance).sum() == 0
361        return np.allclose(self.entries, np.triu(self.entries), atol=tolerance)

True if the matrix is upper (right) triangular.

def is_unitary(self, tolerance=1e-10):
363    @_computable_property("unitary")
364    def is_unitary(self, tolerance=1e-10):
365        """`True` if the matrix is unitary, $M^H = M^{-1}$.
366        Computes the conjugate transpose and identifies the inverse if True."""
367        if not self.is_square():
368            raise MatrixTypeException("unitary check", "square")
369        if not self.is_complex():
370            return self.is_orthogonal()
371        CT = self.conjugate().transpose()
372        prod = self @ CT
373        check = prod.equals(Identity(prod.nc), tolerance)
374
375        if check:
376            # The inverse is now known.
377            self.set("inverse", CT)
378            CT.set("unitary", True)
379            CT.set("inverse", self)
380        return check

True if the matrix is unitary, $M^H = M^{-1}$. Computes the conjugate transpose and identifies the inverse if True.

def cond_2(self):
384    @_computable_property("cond_2")
385    def cond_2(self):
386        """Compute the condition number $\\kappa_2 for inversion of the matrix.
387        Computes the singular values."""
388        sigmas = self.sigmas()
389        return sigmas[0] / sigmas[-1]

Compute the condition number $\kappa_2 for inversion of the matrix. Computes the singular values.

def determinant(self):
391    @_computable_property("determinant")
392    def determinant(self):
393        """Compute the determinant of the matrix. Densifies.
394        Computes the eigenvalues of triangular matrices."""
395        if not self.is_square():
396            raise MatrixTypeException("determinant", "square")
397        if self.is_sparse():
398            # For now, we have to go to a dense representation
399            return self.to_dense().determinant()
400        else:
401            if "eigenvalues" in self._computed or \
402                    (self._computed.get("triangular_L", False) or
403                     self._computed.get("triangular_U", False)):
404                # Use the eigenvalues if available
405                return np.prod(self.eigenvalues())
406            return np.linalg.det(self.entries)

Compute the determinant of the matrix. Densifies. Computes the eigenvalues of triangular matrices.

def eigenvalues(self, tolerance=1e-10):
408    @_computable_property("eigenvalues")
409    def eigenvalues(self, tolerance=1e-10):
410        """Compute the eigenvalues of the matrix.
411        Does not compute the eigenvectors, unless the matrix is sparse.
412        Checks whether $M$ is Hermitian, unless the matrix is triangular."""
413        if not self.is_square():
414            raise MatrixTypeException("eigenvalues", "square")
415        if self.is_triangular_L(tolerance) or self.is_triangular_U(tolerance):
416            # Triangular matrix eigenvalues are on the diagonal.
417            return np.sort(self.diagonal(0))
418        if self.is_sparse():
419            # Use sparse algorithms
420            self.eigenvectors(tolerance)
421            return self.eigenvalues()
422        if self.is_hermitian():
423            # Use the specialized algorithm for Hermitian matrices.
424            return eigvalsh(self.entries, check_finite=False)
425        vals = eigvals(self.entries, check_finite=False)
426        if np.iscomplex(vals).any():
427            return np.sort_complex(vals)
428        return np.sort(vals)

Compute the eigenvalues of the matrix. Does not compute the eigenvectors, unless the matrix is sparse. Checks whether $M$ is Hermitian, unless the matrix is triangular.

def eigenvectors(self, tolerance=1e-10, sparse_k='max'):
430    @_computable_property("eigenvectors")
431    def eigenvectors(self, tolerance=1e-10, sparse_k="max"):
432        """Compute the eigenvectors of the matrix.
433        Computes the eigenvalues. Checks whether $M$ is Hermitian.
434
435        Note that if the matrix has a sparse representation, then
436        only `sparse_k` many eigenvalues/vectors can be computed!
437        The `max` is 90% of the dimension of the matrix.
438        """
439        if not self.is_square():
440            raise MatrixTypeException("eigenvectors", "square")
441        if self.is_sparse():
442            sparse_k = min(self.nr - 1, int(0.9 * self.nr))
443            # Use the sparse algorithms
444            if self.is_hermitian():
445                # Use the specialized algorithm for Hermitian matrices
446                vals, vecs = sparse_eigsh(self.entries, k=sparse_k)
447            else:
448                vals, vecs = sparse_eigs(self.entries, k=sparse_k)
449            if not np.iscomplex(vals).any():
450                order = np.argsort(vals)
451                # Report the eigenvalues and vectors in increasing order
452                vals = vals[order]
453                vecs = vecs[:, order]
454        else:
455            if self.is_hermitian():
456                # Use the specialized algorithm for Hermitian matrices.
457                vals, vecs = eigh(self.entries, check_finite=False)
458            else:
459                vals, vecs = eig(self.entries, check_finite=False)
460                if not np.iscomplex(vals).any():
461                    order = np.argsort(vals)
462                    # Report the eigenvalues and vectors in increasing order
463                    vals = vals[order]
464                    vecs = vecs[:, order]
465        self.set("eigenvalues", vals)
466        return [Vector(vecs[:, j]) for j in range(len(vals))]

Compute the eigenvectors of the matrix. Computes the eigenvalues. Checks whether $M$ is Hermitian.

Note that if the matrix has a sparse representation, then only sparse_k many eigenvalues/vectors can be computed! The max is 90% of the dimension of the matrix.

def norm_2(self):
468    @_computable_property("norm_2")
469    def norm_2(self):
470        """Compute the 2-norm of the matrix $\\lVert M \\rVert_2."""
471        if self._computed.get("orthogonal", False) or \
472                self._computed.get("unitary", False):
473            # Unitary and Orthogonal matrices have norm 1.
474            return 1.0
475        if "sigmas" in self._computed:
476            # The norm is the largest singular value.
477            return self.sigmas()[0]
478        if self.is_sparse():
479            return sparse_norm(self.entries, ord=2)
480        return np.linalg.norm(self.entries, ord=2)

Compute the 2-norm of the matrix $\lVert M \rVert_2.

def nullity(self, tolerance=1e-10):
482    @_computable_property("nullity")
483    def nullity(self, tolerance=1e-10):
484        """Compute the nullity as the number of columns minus the rank."""
485        return self.nc - self.rank()

Compute the nullity as the number of columns minus the rank.

def rank(self, tolerance=1e-10):
487    @_computable_property("rank")
488    def rank(self, tolerance=1e-10):
489        """Compute the rank of the matrix.
490        Computes the SVD if no inverse or eigenvalues are known."""
491        if "invertible" in self._computed or "inverse" in self._computed:
492            # Invertible => full rank
493            return self.nc
494        if not self.is_sparse() and "eigenvalues" in self._computed:
495            # Number of nonzero eigenvalues
496            return np.logical_not(
497                np.isclose(self.eigenvalues(), 0, atol=tolerance)).sum()
498        # Number of nonzero signular values
499        _, S, _ = factor.svd(self)
500        return np.logical_not(
501            np.isclose(S.diagonal(0).entries, 0, atol=tolerance)).sum()

Compute the rank of the matrix. Computes the SVD if no inverse or eigenvalues are known.

def sigmas(self):
503    @_computable_property("sigmas")
504    def sigmas(self):
505        """Compute the singular values of the matrix.
506        Computes the SVD if not positive-definite."""
507        if "posdef" in self._computed and self.is_posdef():
508            return np.flip(np.sqrt(self.eigenvalues()))
509        factor.svd(self)
510        return self.sigmas()

Compute the singular values of the matrix. Computes the SVD if not positive-definite.

def sparsity(self, tolerance=1e-10):
512    @_computable_property("sparsity")
513    def sparsity(self, tolerance=1e-10):
514        """Compute the sparsity of the matrix as the fraction of zero
515        entries out of all entries in the matrix."""
516        if self.is_sparse():
517            # Use the sparse methods if available
518            return len(self.entries.nnz) / (self.nr * self.nc)
519        return np.isclose(
520            self.entries, 0, atol=tolerance).sum() / (self.nr * self.nc)

Compute the sparsity of the matrix as the fraction of zero entries out of all entries in the matrix.

def trace(self):
522    @_computable_property("trace")
523    def trace(self):
524        """Compute the trace of the matrix."""
525        if self.is_sparse():
526            return self.entries.trace()
527        if "eigenvalues" in self._computed:
528            return np.sum(self.eigenvalues())
529        return np.trace(self.entries)

Compute the trace of the matrix.

def row(self, number):
533    def row(self, number):
534        """Return the 1-indexed row."""
535        if number < 0:
536            return Vector(self.entries[number, :])
537        return Vector(self.entries[number-1, :])

Return the 1-indexed row.

def column(self, number):
539    def column(self, number):
540        """Return the 1-indexed column."""
541        if number < 0:
542            return Vector(self.entries[:, number])
543        return Vector(self.entries[:, number-1])

Return the 1-indexed column.

def diagonal(self, number):
545    def diagonal(self, number):
546        """Return the k-th diagonal."""
547        if self.is_sparse():
548            return Vector(self.entries.diagonal(number))
549        return Vector(np.diagonal(self.entries, number))

Return the k-th diagonal.

def entry(self, row, col):
551    def entry(self, row, col):
552        """Return the 1-indexed entry $M_{ij}$."""
553        return self.entries[row if row < 0 else row - 1,
554                            col if col < 0 else col-1]

Return the 1-indexed entry $M_{ij}$.

def equals(self, M, tolerance=1e-10):
556    def equals(self, M, tolerance=1e-10):
557        """`True` if the matrix equals another elementwise,
558        within a tolerance."""
559        if type(M) is int or type(M) is float:
560            return np.allclose(self.entries, M, atol=tolerance)
561        if self.is_sparse() or M.is_sparse():
562            # Use subtraction if one or both are sparse
563            return np.allclose(self.entries - M.entries, 0, atol=tolerance)
564        return np.allclose(self.entries, M.entries, atol=tolerance)

True if the matrix equals another elementwise, within a tolerance.

def to_dense(self):
571    @_computable_property("dense_mat")
572    def to_dense(self):
573        """Switch from a sparse to a dense repsresentation."""
574        if self.is_sparse():
575            DM = Matrix(self.entries.todense())
576            DM._computed = self._computed
577            # Remove properties that are best recomputed
578            for k in ("cond_2", "dense", "eigenvalues", "eigenvectors", "rank",
579                      "invertible", "inverse", "sigmas", "sparse", "svd",
580                      "transpose"):
581                DM._computed.pop(k, None)
582            DM.set("sparse_mat", self)
583            return DM
584        return self

Switch from a sparse to a dense repsresentation.

def to_sparse(self, tolerance=1e-10):
586    @_computable_property("sparse_mat")
587    def to_sparse(self, tolerance=1e-10):
588        """Switch from a dense to a sparse representation."""
589        if self.is_sparse():
590            return self
591        if self.is_diagonal():
592            return DiagonalMatrix(self.diagonal(0))
593        return Matrix(csc_array(self.entries))

Switch from a dense to a sparse representation.

def conjugate(self):
595    @_computable_property("conjugate")
596    def conjugate(self):
597        """Compute the complex conjugate of the matrix."""
598        if self.is_sparse():
599            Conj = Matrix(self.entries.conjugate())
600        else:
601            Conj = Matrix(np.conjugate(self.entries))
602        Conj.set("conjugate", self)
603        if "triangular_L" in self._computed:
604            Conj.set("triangular_L", self.is_triangular_L())
605        if "triangular_U" in self._computed:
606            Conj.set("triangular_U", self.is_triangular_U())
607        if "diagonal" in self._computed:
608            Conj.set("diagonal", self.is_diagonal())
609        return Conj

Compute the complex conjugate of the matrix.

def inverse(self):
611    @_computable_property("inverse")
612    def inverse(self):
613        """Compute the inverse of the matrix. Checks invertibility."""
614        if not self.is_invertible():
615            raise MatrixTypeException("inverse", "invertible")
616        if "qr" in self._computed:
617            # Use the QR if it exists
618            Q, R = self.qr()
619            Inv = R.inverse() @ Q.transpose()
620        elif "svd" in self._computed:
621            # Use the SVD if it exists
622            U, S, V = self.svd()
623            Inv = V.conjugate().transpose() @ S.inverse() \
624                @ U.conjugate().transpose()
625        else:
626            # Nothing known, compute inverse
627            Inv = Matrix(inv(self.entries))
628        Inv.set("inverse", self)
629        # Set known properties
630        if "determinant" in self._computed:
631            Inv.set("determinant", 1.0 / self.determinant())
632        if "eigenvalues" in self._computed:
633            Inv.set("eigenvalues", np.flip(1.0 / self.eigenvalues()))
634        # Set the lin solver to the found inverse
635        self.set("lin_solver", lambda v: solve.invert(self, v))
636        return Inv

Compute the inverse of the matrix. Checks invertibility.

def lin_solver(self):
638    @_computable_property("lin_solver")
639    def lin_solver(self):
640        """Return a method which solves $Ax=b$ for $x$.
641        See `solve.automatic`."""
642        return solve.automatic(self, None, get_method=True)

Return a method which solves $Ax=b$ for $x$. See solve.automatic.

def qr(self):
644    @_computable_property("qr")
645    def qr(self):
646        """Compute the QR factorization of the matrix. See `factor.qr`."""
647        return factor.qr(self)

Compute the QR factorization of the matrix. See factor.qr.

def svd(self):
649    @_computable_property("svd")
650    def svd(self):
651        """Compute the SVD factorization of the matrix. See `factor.svd`."""
652        return factor.svd(self)

Compute the SVD factorization of the matrix. See factor.svd.

def transpose(self):
654    @_computable_property("transpose")
655    def transpose(self):
656        """Compute the transpose of the matrix."""
657        if self.is_sparse():
658            T = Matrix(self.entries.transpose())
659        else:
660            T = Matrix(self.entries.T)
661        # Copy properties that are identical for the transpose
662        T._computed = {prop: self._computed[prop] for prop in [
663            "complex", "cond_2", "determinant", "diagonal"
664            "eigenvalues", "eigenvectors", "invertible", "rank",
665            "sigmas", "symmetric"
666        ] if prop in self._computed}
667        T.set("transpose", self)
668        if "triangular_L" in self._computed:
669            T.set("triangular_U", self.is_triangular_L())
670        if "triangular_U" in self._computed:
671            T.set("triangular_L", self.is_triangular_U())
672        if "svd" in self._computed:
673            U, S, V = self.svd()
674            T.set("svd", (V.transpose(), S, U.transpose()))
675        return T

Compute the transpose of the matrix.

class DiagonalMatrix(Matrix):
725class DiagonalMatrix(Matrix):
726    """A DiagonalMatrix is a Matrix whose only nonzero entries
727    are on the primary diagonal."""
728
729    def __init__(self, diagonal):
730        """Initialize a DiagonalMatrix from an array of the diagonal."""
731        if isinstance(diagonal, Vector):
732            diagonal = diagonal.entries
733        # Use the SciPy sparse diagonal implementation.
734        super().__init__(dia_array((diagonal, 0),
735                                   shape=(len(diagonal), len(diagonal))))
736        self.set("diagonal", True)
737
738    def __getitem__(self, number):
739        """Return the 1-indexed entry along the diagonal."""
740        if number < 0:
741            return self.entries.diagonal()[number]
742        return self.entries.diagonal()[number-1]
743
744    @_computable_property("invertible")
745    def is_invertible(self, tolerance=1e-10):
746        """`True` if there are no zeros on the diagonal."""
747        if "inverse" in self._computed:
748            return True
749        return np.isclose(
750            self.entries.diagonal(), 0, atol=tolerance).sum() == 0
751
752    @_computable_property("determinant")
753    def determinant(self):
754        """Compute the determinant as the product of the diagonal."""
755        return np.prod(self.entries.diagonal())
756
757    @_computable_property("eigenvalues")
758    def eigenvalues(self, tolerance=1e-10):
759        """Compute the eigenvalues, equivalent to sorting the diagonal."""
760        return np.sort(self.entries.diagonal())
761
762    @_computable_property("inverse")
763    def inverse(self):
764        """Compute the inverse as the reciprocal of the diagonal."""
765        if self.is_invertible():
766            return DiagonalMatrix(1.0 / self.entries.diagonal())
767        raise MatrixTypeException("inverse", "invertible")
768
769    @_computable_property("trace")
770    def trace(self):
771        """Compute the trace of the matrix."""
772        return np.sum(self.diagonal().entries)

A DiagonalMatrix is a Matrix whose only nonzero entries are on the primary diagonal.

DiagonalMatrix(diagonal)
729    def __init__(self, diagonal):
730        """Initialize a DiagonalMatrix from an array of the diagonal."""
731        if isinstance(diagonal, Vector):
732            diagonal = diagonal.entries
733        # Use the SciPy sparse diagonal implementation.
734        super().__init__(dia_array((diagonal, 0),
735                                   shape=(len(diagonal), len(diagonal))))
736        self.set("diagonal", True)

Initialize a DiagonalMatrix from an array of the diagonal.

def is_invertible(self, tolerance=1e-10):
744    @_computable_property("invertible")
745    def is_invertible(self, tolerance=1e-10):
746        """`True` if there are no zeros on the diagonal."""
747        if "inverse" in self._computed:
748            return True
749        return np.isclose(
750            self.entries.diagonal(), 0, atol=tolerance).sum() == 0

True if there are no zeros on the diagonal.

def determinant(self):
752    @_computable_property("determinant")
753    def determinant(self):
754        """Compute the determinant as the product of the diagonal."""
755        return np.prod(self.entries.diagonal())

Compute the determinant as the product of the diagonal.

def eigenvalues(self, tolerance=1e-10):
757    @_computable_property("eigenvalues")
758    def eigenvalues(self, tolerance=1e-10):
759        """Compute the eigenvalues, equivalent to sorting the diagonal."""
760        return np.sort(self.entries.diagonal())

Compute the eigenvalues, equivalent to sorting the diagonal.

def inverse(self):
762    @_computable_property("inverse")
763    def inverse(self):
764        """Compute the inverse as the reciprocal of the diagonal."""
765        if self.is_invertible():
766            return DiagonalMatrix(1.0 / self.entries.diagonal())
767        raise MatrixTypeException("inverse", "invertible")

Compute the inverse as the reciprocal of the diagonal.

def trace(self):
769    @_computable_property("trace")
770    def trace(self):
771        """Compute the trace of the matrix."""
772        return np.sum(self.diagonal().entries)

Compute the trace of the matrix.

class Identity(DiagonalMatrix):
775class Identity(DiagonalMatrix):
776    """The Identity matrix is a DiagonalMatrix of ones."""
777
778    def __init__(self, n):
779        """Initialize the Identity of size n."""
780        Matrix.__init__(self, identity(n))

The Identity matrix is a DiagonalMatrix of ones.

Identity(n)
778    def __init__(self, n):
779        """Initialize the Identity of size n."""
780        Matrix.__init__(self, identity(n))

Initialize the Identity of size n.

class Vector(Matrix):
783class Vector(Matrix):
784    """A Vector is a Matrix of size 1 in one dimension."""
785
786    def __init__(self, arr, row=False):
787        """Initialize a Vector object from a Numpy array.
788        If the array has only one dimension, defaults to being a column vector.
789        A row vector can be forced by setting row=True."""
790        if isinstance(arr, Matrix):
791            arr = arr.entries
792        arr = np.array(arr)
793        if len(arr.shape) == 1:
794            # One-dimensional array.
795            if row:
796                super().__init__(arr.reshape(1, len(arr)))
797            else:
798                super().__init__(arr.reshape(len(arr), 1))
799        else:
800            # 2-dimensional array, initialize the Matrix.
801            super().__init__(arr)
802
803        if min(self.nr, self.nc) != 1:
804            raise ValueError(
805                "A Vector cannot have size {}x{}".format(self.nr, self.nc))
806
807    def __len__(self):
808        """Return the number of entries in the Vector."""
809        return max(self.nr, self.nc)
810
811    def __getitem__(self, number):
812        """Returns the 1-indexed entry in the Vector."""
813        if number < 0:
814            return self.entries[number]
815        return self.entries[number-1]
816
817    def __repr__(self):
818        return "{}x{} Vector ({} properties)".format(
819            self.nr, self.nc, len(self._computed))
820
821    @_computable_property("unit")
822    def unit(self):
823        """Compute the unit vector in the direction of this Vector.
824        Computes the 2-norm."""
825        return Vector(self.entries / self.norm_2())

A Vector is a Matrix of size 1 in one dimension.

Vector(arr, row=False)
786    def __init__(self, arr, row=False):
787        """Initialize a Vector object from a Numpy array.
788        If the array has only one dimension, defaults to being a column vector.
789        A row vector can be forced by setting row=True."""
790        if isinstance(arr, Matrix):
791            arr = arr.entries
792        arr = np.array(arr)
793        if len(arr.shape) == 1:
794            # One-dimensional array.
795            if row:
796                super().__init__(arr.reshape(1, len(arr)))
797            else:
798                super().__init__(arr.reshape(len(arr), 1))
799        else:
800            # 2-dimensional array, initialize the Matrix.
801            super().__init__(arr)
802
803        if min(self.nr, self.nc) != 1:
804            raise ValueError(
805                "A Vector cannot have size {}x{}".format(self.nr, self.nc))

Initialize a Vector object from a Numpy array. If the array has only one dimension, defaults to being a column vector. A row vector can be forced by setting row=True.

def unit(self):
821    @_computable_property("unit")
822    def unit(self):
823        """Compute the unit vector in the direction of this Vector.
824        Computes the 2-norm."""
825        return Vector(self.entries / self.norm_2())

Compute the unit vector in the direction of this Vector. Computes the 2-norm.