├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── grcar.png ├── pseudopy ├── __init__.py ├── demo.py ├── nonnormal.py ├── normal.py ├── tests │ └── test.py └── utils.py ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .*.swp 3 | build 4 | dist 5 | MANIFEST 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.6" 5 | before_install: 6 | - pip install --only-binary=numpy,scipy numpy scipy shapely matplotlib 7 | script: nosetests pseudopy/tests 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 André Gaul 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | recursive-include pseudopy *.py 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | default: 3 | @echo "\"make upload\"?" 4 | 5 | upload: setup.py 6 | python setup.py sdist 7 | twine upload dist/* 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PseudoPy [![Build Status](https://travis-ci.org/andrenarchy/pseudopy.png?branch=master)](https://travis-ci.org/andrenarchy/pseudopy) 2 | 3 | PseudoPy computes and visualizes the pseudospectrum of a matrix. It is a Python version of the original [eigtool](http://www.cs.ox.ac.uk/pseudospectra/eigtool/) by Thomas G. Wright. The algorithms used in this package can be found in the book [Spectra and pseudospectra](http://press.princeton.edu/titles/8113.html) by [Nick Trefethen](http://www.maths.ox.ac.uk/people/profiles/nick.trefethen) and [Mark Embree](http://www.caam.rice.edu/~embree/). 4 | 5 | ## Example 6 | The pseudospectrum of the Grcar matrix looks like this: 7 | 8 | ![Pseudospectrum of Grcar matrix](grcar.png) 9 | 10 | If no knowledge about the location of the pseudospectrum of the given matrix is available, the following lines of code can be used to obtain an approximation: 11 | ```python 12 | from pseudopy import NonnormalAuto, demo 13 | from matplotlib import pyplot 14 | from scipy.linalg import eigvals 15 | 16 | # get Grcar matrix 17 | A = demo.grcar(32).todense() 18 | 19 | # compute pseudospectrum for the levels of interest between [1e-5, 1] 20 | pseudo = NonnormalAuto(A, 1e-5, 1) 21 | 22 | # plot 23 | pseudo.plot([10**k for k in range(-4, 0)], spectrum=eigvals(A)) 24 | pyplot.show() 25 | ``` 26 | 27 | ## Installation 28 | ### Dependencies 29 | PseudoPy depends on numpy, scipy, matplotlib and shapely. If you are on Debian/Ubuntu, you can install these dependencies with 30 | ``` 31 | sudo apt-get install python-numpy python-scipy python-matplotlib python-shapely 32 | ``` 33 | 34 | ### pip 35 | ```pip install pseudopy``` 36 | 37 | Note that you may need to add `sudo` if you want to install it system-wide. 38 | 39 | ## License 40 | PseudoPy is free software licensed under the [MIT License](http://opensource.org/licenses/mit-license.php). 41 | -------------------------------------------------------------------------------- /grcar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrenarchy/pseudopy/6a21511c74d7716d8239ef8b6cbb803e13fd44de/grcar.png -------------------------------------------------------------------------------- /pseudopy/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .nonnormal import NonnormalMeshgrid, NonnormalMeshgridAuto, \ 4 | NonnormalTriang, NonnormalPoints, NonnormalAuto 5 | from .normal import Normal, NormalEvals 6 | from . import demo, utils 7 | 8 | __all__ = ['NonnormalMeshgrid', 'NonnormalMeshgridAuto', 'NonnormalTriang', 9 | 'NonnormalPoints', 'NonnormalAuto', 10 | 'Normal', 'NormalEvals', 11 | 'demo', 'utils'] 12 | -------------------------------------------------------------------------------- /pseudopy/demo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import numpy 4 | from scipy.linalg import toeplitz 5 | from scipy.sparse import csr_matrix 6 | 7 | 8 | def toeplitz1(N): 9 | column = numpy.zeros(N) 10 | column[1] = 0.25 11 | row = numpy.zeros(N) 12 | row[1] = 1 13 | return csr_matrix(toeplitz(column, row)) 14 | 15 | 16 | def grcar(N, k=3): 17 | column = numpy.zeros(N) 18 | column[0:2] = [1, -1] 19 | row = numpy.zeros(N) 20 | row[0:k+1] = 1 21 | return csr_matrix(toeplitz(column, row)) 22 | 23 | 24 | def grcar_demo(): 25 | from pseudopy import NonnormalMeshgrid, demo 26 | from matplotlib import pyplot 27 | from scipy.linalg import eigvals 28 | 29 | # get Grcar matrix 30 | A = demo.grcar(32).todense() 31 | 32 | # compute pseudospectrum 33 | pseudo = NonnormalMeshgrid(A, 34 | real_min=-1, real_max=3, real_n=400, 35 | imag_min=-3.5, imag_max=3.5, imag_n=400) 36 | # plot 37 | pseudo.plot([10**k for k in range(-4, 0)], spectrum=eigvals(A)) 38 | pyplot.show() 39 | -------------------------------------------------------------------------------- /pseudopy/nonnormal.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | from scipy.linalg import svdvals, schur, solve_triangular 3 | from scipy.sparse.linalg import eigsh, LinearOperator 4 | from matplotlib.tri import Triangulation 5 | from matplotlib import pyplot 6 | 7 | from .utils import Path, Paths, plot_finish 8 | 9 | 10 | def inv_resolvent_norm(A, z, method='svd'): 11 | r'''Compute the reciprocal norm of the resolvent 12 | 13 | :param A: the input matrix as a ``numpy.array``, sparse matrix or 14 | ``LinearOperator`` with ``A.shape==(m,n)``, where :math:`m\geq n`. 15 | :param z: a complex number 16 | :param method: (optional) one of 17 | 18 | * ``svd`` (default): computes the minimal singular value of :math:`A-zI`. 19 | This one should be used for dense matrices. 20 | * ``lanczos``: computes the minimal singular value with the Lanczos 21 | iteration on the matrix 22 | :math:`\begin{bmatrix}0&A\\A^*&0\end{bmatrix}` 23 | ''' 24 | if method == 'svd': 25 | return numpy.min(svdvals(A - z*numpy.eye(*A.shape))) 26 | elif method == 'lanczos': 27 | m, n = A.shape 28 | if m > n: 29 | raise ValueError('m > n is not allowed') 30 | AH = A.T.conj() 31 | 32 | def matvec(x): 33 | r'''matrix-vector multiplication 34 | 35 | matrix-vector multiplication with matrix 36 | :math:`\begin{bmatrix}0&A\\A^*&0\end{bmatrix}` 37 | ''' 38 | x1 = x[:m] 39 | x2 = x[m:] 40 | ret1 = AH.dot(x2) - numpy.conj(z)*x2 41 | ret2 = numpy.array(A.dot(x1), dtype=numpy.complex) 42 | ret2[:n] -= z*x1 43 | return numpy.c_[ret1, ret2] 44 | AH_A = LinearOperator(matvec=matvec, dtype=numpy.complex, 45 | shape=(m+n, m+n)) 46 | 47 | evals = eigsh(AH_A, k=2, tol=1e-6, which='SM', maxiter=m+n+1, 48 | ncv=2*(m+n), 49 | return_eigenvectors=False) 50 | 51 | return numpy.min(numpy.abs(evals)) 52 | 53 | 54 | class _Nonnormal(object): 55 | '''Base class for nonnormal pseudospectra''' 56 | def __init__(self, A, points, method='svd'): 57 | '''Evaluates the inverse resolvent norm on the given list of points 58 | 59 | Stores result in self.vals and points in self.points 60 | ''' 61 | self.points = points 62 | if method == 'lanczosinv': 63 | self.vals = [] 64 | 65 | # algorithm from page 375 of Trefethen/Embree 2005 66 | T, _ = schur(A, output='complex') 67 | m, n = A.shape 68 | if m != n: 69 | raise ValueError('m != n is not allowed in dense mode') 70 | for point in points: 71 | M = T - point*numpy.eye(*T.shape) 72 | 73 | def matvec(x): 74 | r'''Matrix-vector multiplication 75 | 76 | Matrix-vector multiplication with matrix 77 | :math:`\begin{bmatrix}0&(A-\lambda I)^{-1}\\(A-\lambda I)^{-1}&0\end{bmatrix}`''' 78 | return solve_triangular( 79 | M, 80 | solve_triangular( 81 | M, 82 | x, 83 | check_finite=False 84 | ), 85 | trans=2, 86 | check_finite=False 87 | ) 88 | MH_M = LinearOperator(matvec=matvec, dtype=numpy.complex, 89 | shape=(n, n)) 90 | 91 | evals = eigsh(MH_M, k=1, tol=1e-3, which='LM', 92 | maxiter=n, 93 | ncv=n, 94 | return_eigenvectors=False) 95 | 96 | self.vals.append(1/numpy.sqrt(numpy.max(numpy.abs(evals)))) 97 | else: 98 | self.vals = [inv_resolvent_norm(A, point, method=method) 99 | for point in points] 100 | 101 | 102 | 103 | class NonnormalMeshgrid(_Nonnormal): 104 | def __init__(self, A, 105 | real_min=-1, real_max=1, real_n=50, 106 | imag_min=-1, imag_max=1, imag_n=50, 107 | method='svd'): 108 | 109 | real = numpy.linspace(real_min, real_max, real_n) 110 | imag = numpy.linspace(imag_min, imag_max, imag_n) 111 | 112 | self.Real, self.Imag = numpy.meshgrid(real, imag) 113 | 114 | # call super constructor 115 | super(NonnormalMeshgrid, self).__init__( 116 | A, self.Real.flatten() + 1j*self.Imag.flatten()) 117 | self.Vals = numpy.array(self.vals).reshape((imag_n, real_n)) 118 | 119 | def plot(self, epsilons, **kwargs): 120 | contours = pyplot.contour(self.Real, self.Imag, self.Vals, 121 | levels=epsilons, 122 | colors=pyplot.rcParams['axes.prop_cycle'].by_key()['color'] 123 | ) 124 | plot_finish(contours, **kwargs) 125 | return contours 126 | 127 | def contour_paths(self, epsilon): 128 | '''Extract the polygon patches for the provided epsilon''' 129 | figure = pyplot.figure() 130 | ax = figure.gca() 131 | contours = ax.contour(self.Real, self.Imag, self.Vals, 132 | levels=[epsilon]) 133 | paths = Paths() 134 | if len(contours.collections) == 0: 135 | return paths 136 | for path in contours.collections[0].get_paths(): 137 | paths.append(Path(path.vertices[:, 0] + 1j*path.vertices[:, 1])) 138 | pyplot.close(figure) 139 | return paths 140 | 141 | 142 | class NonnormalTriang(_Nonnormal): 143 | def __init__(self, A, triang, **kwargs): 144 | self.triang = triang 145 | super(NonnormalTriang, self).__init__( 146 | A, triang.x + 1j*triang.y, **kwargs) 147 | 148 | def plot(self, epsilons, **kwargs): 149 | contours = pyplot.tricontour(self.triang, self.vals, levels=epsilons) 150 | plot_finish(contours, **kwargs) 151 | return contours 152 | 153 | def contour_paths(self, epsilon): 154 | '''Extract the polygon patches for the provided epsilon''' 155 | figure = pyplot.figure() 156 | contours = pyplot.tricontour(self.triang, self.vals, levels=[epsilon]) 157 | paths = Paths() 158 | if len(contours.collections) == 0: 159 | return paths 160 | for path in contours.collections[0].get_paths(): 161 | paths.append(Path(path.vertices[:, 0] + 1j*path.vertices[:, 1])) 162 | pyplot.close(figure) 163 | return paths 164 | 165 | 166 | class NonnormalPoints(NonnormalTriang): 167 | def __init__(self, A, points, **kwargs): 168 | triang = Triangulation(numpy.real(points), numpy.imag(points)) 169 | super(NonnormalPoints, self).__init__(A, triang, **kwargs) 170 | 171 | 172 | class NonnormalMeshgridAuto(NonnormalMeshgrid): 173 | '''Determines rough bounding box of pseudospectrum. 174 | 175 | The bounding box is determined for a diagonalizable matrix via the 176 | condition number of the eigenvector basis (see theorem 2.3 in the book 177 | of Trefethen and Embree). Note that this method produces a bounding box 178 | where the pseudospectrum with eps_max is guaranteed to be contained but 179 | that the bounding box may be overestimated severely. 180 | 181 | :param A: the matrix as numpy array with ``A.shape==(N,N)``. 182 | :param eps_max: maximal value of :math:`\varepsilon` that is of interest. 183 | ''' 184 | def __init__(self, A, eps_max, **kwargs): 185 | from scipy.linalg import eig 186 | evals, evecs = eig(A) 187 | 188 | # compute condition number of eigenvector basis 189 | kappa = numpy.linalg.cond(evecs, 2) 190 | 191 | new_kwargs = {'real_min': numpy.min(evals.real) - eps_max*kappa, 192 | 'real_max': numpy.max(evals.real) + eps_max*kappa, 193 | 'imag_min': numpy.min(evals.imag) - eps_max*kappa, 194 | 'imag_max': numpy.max(evals.imag) + eps_max*kappa 195 | } 196 | new_kwargs.update(kwargs) 197 | super(NonnormalMeshgridAuto, self).__init__(A, **new_kwargs) 198 | 199 | 200 | class NonnormalAuto(NonnormalPoints): 201 | '''Determines pseudospectrum automatically. 202 | 203 | This method automatically determines an inclusion set for the 204 | pseudospectrum. Very useful if you have no idea where the pseudospectrum 205 | lives. 206 | 207 | The computation time is dominated by ``N*(N+1)/2`` Schur decompositions and 208 | ``N*n_circles*n_points`` computations of the norm of the resolvent inverse. 209 | ''' 210 | def __init__(self, A, eps_min, eps_max, 211 | n_circles=20, 212 | n_points=20, 213 | randomize=True, 214 | **kwargs 215 | ): 216 | from scipy.linalg import eig, schur 217 | M = A.copy() 218 | 219 | if eps_min <= 0: 220 | raise ValueError('eps_min > 0 is required') 221 | if eps_min >= eps_max: 222 | raise ValueError('eps_min < eps_max is required') 223 | 224 | midpoints = [] 225 | # compute containment circles with eps_max 226 | radii = [eps_max] 227 | 228 | for i in range(A.shape[0]): 229 | evals, evecs = eig(M) 230 | 231 | # compute condition number of eigenvector basis 232 | evec_cond = numpy.linalg.cond(evecs, 2) 233 | 234 | # try all eigenvalues in top-left position and pick the 235 | # configuration with smallest radius 236 | candidates_midpoints = [] 237 | candidates_radii = [] 238 | candidates_Ms = [] 239 | if len(evals) == 1: 240 | midpoints.append(evals[0]) 241 | radii.append(radii[-1]) 242 | else: 243 | for eval in evals: 244 | dists = numpy.sort(numpy.abs(eval - evals)) 245 | 246 | # get Schur decomposition 247 | def sort(lambd): 248 | return numpy.abs(lambd - eval) <= dists[1] 249 | T, Z, sdim = schur(M, output='complex', sort=sort) 250 | 251 | # T = [eval c^T] 252 | # [0 M ] 253 | # solve Sylvester equation c^T = r^T M - eval*r^T 254 | # <=> r = (M - lambd*I)^{-T} c 255 | c = T[0, 1:] 256 | M_tmp = T[1:, 1:] 257 | candidates_midpoints.append(T[0, 0]) 258 | 259 | r = solve_triangular(M_tmp - T[0, 0]*numpy.eye(*M_tmp.shape), 260 | c, 261 | trans='T' 262 | ) 263 | sep_min = numpy.min(svdvals(M_tmp - T[0, 0]*numpy.eye(*M_tmp.shape))) 264 | sep_max = numpy.min(numpy.abs(T[0, 0] - numpy.diag(M_tmp))) 265 | r_norm = numpy.linalg.norm(r, 2) 266 | p = numpy.sqrt(1. + r_norm**2) 267 | 268 | # Grammont-Largillier bound 269 | g_gram_larg = numpy.sqrt(1. + numpy.linalg.norm(c, 2)/radii[-1]) 270 | 271 | # Demmel 1: g = kappa 272 | g_demmel1 = kappa = p + r_norm 273 | 274 | # Demmel 2 275 | g_demmel2 = numpy.Inf 276 | if radii[-1] <= sep_min/(2*kappa): 277 | g_demmel2 = p + r_norm**2 * radii[-1]/(0.5*sep_min - p*radii[-1]) 278 | 279 | # Michael Karow bound (personal communication) 280 | g_mika = numpy.Inf 281 | if radii[-1] <= sep_min/(2*kappa): 282 | eps_sep = radii[-1]/sep_min 283 | g_mika = (p - eps_sep)/( 284 | 0.5 + numpy.sqrt(0.25 - eps_sep*(p - eps_sep)) 285 | ) 286 | 287 | # use the minimum of the above g's 288 | candidates_radii.append( 289 | radii[-1]*numpy.min([evec_cond, 290 | g_gram_larg, 291 | g_demmel1, 292 | g_demmel2, 293 | g_mika 294 | ]) 295 | ) 296 | candidates_Ms.append(M_tmp) 297 | min_index = numpy.argmin(candidates_radii) 298 | midpoints.append(candidates_midpoints[min_index]) 299 | radii.append(candidates_radii[min_index]) 300 | M = candidates_Ms[min_index] 301 | # remove first radius 302 | radii = radii[1:] 303 | 304 | # construct points for evaluation of resolvent 305 | points = [] 306 | arg = numpy.linspace(0, 2*numpy.pi, n_points, endpoint=False) 307 | for midpoint, radius_max in zip(midpoints, radii): 308 | radius_log = numpy.logspace(numpy.log10(eps_min), 309 | numpy.log10(radius_max), 310 | n_circles 311 | ) 312 | 313 | #radius_lin = numpy.linspace(eps_min, radius_max, n_circles) 314 | for radius in radius_log: 315 | rand = 0. 316 | if randomize: 317 | rand = numpy.random.rand() 318 | 319 | # check that radius is larger than round-off in order to 320 | # avoid duplicate points 321 | if numpy.abs(radius)/numpy.abs(midpoint) > 1e-15: 322 | points.append(midpoint + radius*numpy.exp(1j*(rand+arg))) 323 | points = numpy.concatenate(points) 324 | super(NonnormalAuto, self).__init__(A, points, **kwargs) 325 | -------------------------------------------------------------------------------- /pseudopy/normal.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | from matplotlib import pyplot 3 | import shapely.geometry as geom 4 | from shapely.ops import cascaded_union 5 | 6 | from .utils import get_paths, plot_finish 7 | 8 | 9 | class NormalEvals(object): 10 | def __init__(self, evals): 11 | self.evals = evals 12 | 13 | def plot(self, epsilons, **kwargs): 14 | epsilons = list(numpy.sort(epsilons)) 15 | padepsilons = [epsilons[0]*0.9] + epsilons + [epsilons[-1]*1.1] 16 | X = [] 17 | Y = [] 18 | Z = [] 19 | for epsilon in padepsilons: 20 | paths = self.contour_paths(epsilon) 21 | for path in paths: 22 | X += list(numpy.real(path.vertices[:-1])) 23 | Y += list(numpy.imag(path.vertices[:-1])) 24 | Z += [epsilon] * (len(path.vertices) - 1) 25 | contours = pyplot.tricontour(X, Y, Z, levels=epsilons, 26 | colors=pyplot.rcParams['axes.prop_cycle'].by_key()['color'] 27 | ) 28 | plot_finish(contours, **kwargs) 29 | return contours 30 | 31 | def contour_paths(self, epsilon): 32 | '''Get boundary of union of circles around eigenvalues''' 33 | # create circles 34 | circles = [geom.Point(numpy.real(lamda), numpy.imag(lamda)) 35 | .buffer(epsilon) for lamda in self.evals] 36 | 37 | # pseudospectrum is union of circles 38 | pseudospec = cascaded_union(circles) 39 | 40 | return get_paths(pseudospec) 41 | 42 | 43 | class Normal(NormalEvals): 44 | def __init__(self, A): 45 | from scipy.linalg import eigvals 46 | super(Normal, self).__init__(eigvals(A)) 47 | -------------------------------------------------------------------------------- /pseudopy/tests/test.py: -------------------------------------------------------------------------------- 1 | import matplotlib 2 | matplotlib.use('Agg') 3 | 4 | import numpy 5 | import pseudopy 6 | from itertools import product 7 | 8 | def dict_merge(*dicts): 9 | items = [] 10 | for d in dicts: 11 | items += d.items() 12 | return dict(items) 13 | 14 | 15 | def dict_slicevals(d, keys): 16 | return [d[k] for k in keys] 17 | 18 | 19 | def test(): 20 | n = 10 21 | A = numpy.diag(numpy.ones(n-1), -1) 22 | A[0, -1] = 1 23 | 24 | # compute evals 25 | from scipy.linalg import eigvals 26 | evals = eigvals(A) 27 | 28 | nonnormal_params = {'real_min': -2.2, 'real_max': 2.2, 'real_n': 200, 29 | 'imag_min': -2.2, 'imag_max': 2.2, 'imag_n': 200} 30 | 31 | # compute points 32 | real = numpy.linspace(*dict_slicevals(nonnormal_params, 33 | ['real_min', 'real_max', 'real_n'])) 34 | imag = numpy.linspace(*dict_slicevals(nonnormal_params, 35 | ['imag_min', 'imag_max', 'imag_n'])) 36 | Real, Imag = numpy.meshgrid(real, imag) 37 | points = Real.flatten() + 1j*Imag.flatten() 38 | 39 | # compute triang from points 40 | from matplotlib.tri import Triangulation 41 | triang = Triangulation(numpy.real(points), numpy.imag(points)) 42 | 43 | # define classes to test 44 | classes = { 45 | pseudopy.NonnormalMeshgrid: [dict_merge(nonnormal_params, {'A': A})], 46 | pseudopy.NonnormalTriang: [{'A': A, 'triang': triang}], 47 | pseudopy.NonnormalPoints: [{'A': A, 'points': points}], 48 | pseudopy.Normal: [{'A': A}], 49 | pseudopy.NormalEvals: [{'evals': evals}] 50 | } 51 | 52 | # define epsilons 53 | epsilons = [0.2, 0.7, 1.1] 54 | 55 | for cls, params in classes.items(): 56 | for param in params: 57 | pseudo = cls(**param) 58 | 59 | # test plot 60 | #yield run_plot, pseudo, epsilons 61 | 62 | # test contour_paths 63 | for epsilon in epsilons: 64 | yield run_contour_paths, pseudo, epsilon, evals 65 | 66 | 67 | def run_plot(pseudo, epsilons): 68 | from matplotlib import pyplot 69 | pyplot.figure() 70 | pseudo.plot(epsilons) 71 | pyplot.close() 72 | 73 | 74 | def run_contour_paths(pseudo, epsilon, evals): 75 | # get paths 76 | paths = pseudo.contour_paths(epsilon) 77 | 78 | # check if pseudospectrum is correct by matching the parts of it 79 | import shapely.geometry as geom 80 | from shapely.ops import cascaded_union 81 | # create circles 82 | circles = [geom.Point(numpy.real(lamda), numpy.imag(lamda)) 83 | .buffer(epsilon) for lamda in evals] 84 | exact_pseudo = cascaded_union(circles) 85 | exact_paths = pseudopy.utils.get_paths(exact_pseudo) 86 | 87 | N = len(paths) 88 | assert(N == len(exact_paths)) 89 | 90 | # create polygons 91 | polys = [geom.Polygon([(numpy.real(z), numpy.imag(z)) 92 | for z in path.vertices]) 93 | for path in paths] 94 | exact_polys = [geom.Polygon([(numpy.real(z), numpy.imag(z)) 95 | for z in path.vertices]) 96 | for path in exact_paths] 97 | 98 | # match elements by measuring intersections 99 | M = numpy.zeros((N, N)) 100 | for (i, j) in product(range(N), range(N)): 101 | M[i, j] = exact_polys[i].symmetric_difference(polys[j]).area 102 | for i in range(N): 103 | assert(numpy.min(M[i, :]) < 0.1*epsilon) 104 | 105 | 106 | if __name__ == '__main__': 107 | import nose 108 | nose.main() 109 | -------------------------------------------------------------------------------- /pseudopy/utils.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | from matplotlib import pyplot 3 | 4 | 5 | class Path(object): 6 | def __init__(self, vertices): 7 | self.vertices = numpy.array(vertices) 8 | 9 | def __iter__(self): 10 | return iter(self.vertices) 11 | 12 | def length(self): 13 | return numpy.sum(numpy.abs(self.vertices[1:]-self.vertices[:-1])) 14 | 15 | 16 | class Paths(list): 17 | def length(self): 18 | return numpy.sum([path.length() for path in self]) 19 | 20 | def vertices(self): 21 | verts = [] 22 | for path in self: 23 | verts += list(path.vertices) 24 | return verts 25 | 26 | 27 | def get_paths(obj): 28 | def _get_polygon_paths(polygon): 29 | def _get_points(c): 30 | vertices = numpy.array(c.coords) 31 | return vertices[:, 0] + 1j*vertices[:, 1] 32 | return [Path(_get_points(sub)) 33 | for sub in [polygon.exterior]+list(polygon.interiors)] 34 | 35 | paths = Paths() 36 | import shapely.geometry as geom 37 | if isinstance(obj, geom.polygon.Polygon): 38 | paths += _get_polygon_paths(obj) 39 | elif isinstance(obj, geom.multipolygon.MultiPolygon): 40 | for polygon in obj: 41 | paths += _get_polygon_paths(polygon) 42 | return paths 43 | 44 | 45 | def plot_finish(contours, spectrum=None, contour_labels=True, autofit=True): 46 | # plot spectrum? 47 | if spectrum is not None: 48 | pyplot.plot(numpy.real(spectrum), numpy.imag(spectrum), 'o') 49 | 50 | if autofit: 51 | vertices = [] 52 | for collection in contours.collections: 53 | for path in collection.get_paths(): 54 | vertices.append(path.vertices[:, 0] + 1j*path.vertices[:, 1]) 55 | vertices = numpy.concatenate(vertices) 56 | pyplot.xlim(numpy.min(vertices.real), numpy.max(vertices.real)) 57 | pyplot.ylim(numpy.min(vertices.imag), numpy.max(vertices.imag)) 58 | 59 | # plot contour labels? 60 | from matplotlib.ticker import LogFormatterMathtext 61 | if contour_labels: 62 | pyplot.clabel(contours, inline=1, fmt=LogFormatterMathtext()) 63 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | matplotlib>=1.2 2 | numpy>=1.7 3 | scipy>=0.12 4 | shapely>=1.2 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | from distutils.core import setup 4 | import codecs 5 | 6 | # shamelessly copied from VoroPy 7 | def read(fname): 8 | return codecs.open(os.path.join(os.path.dirname(__file__), fname), encoding='utf-8').read() 9 | 10 | setup(name='pseudopy', 11 | packages=['pseudopy'], 12 | version='1.2.5', 13 | description='Compute and visualize pseudospectra of' 14 | + ' matrices (like eigtool)', 15 | long_description=read('README.md'), 16 | author='André Gaul', 17 | author_email='gaul@web-yard.de', 18 | url='https://github.com/andrenarchy/pseudopy', 19 | install_requires=['matplotlib>=2.0', 'numpy>=1.7', 20 | 'scipy>=0.12', 'shapely>=1.2'], 21 | classifiers=[ 22 | 'Development Status :: 4 - Beta', 23 | 'Intended Audience :: Science/Research', 24 | 'License :: OSI Approved :: MIT License', 25 | 'Operating System :: OS Independent', 26 | 'Programming Language :: Python', 27 | 'Programming Language :: Python :: 3', 28 | 'Topic :: Scientific/Engineering :: Mathematics' 29 | ], 30 | ) 31 | --------------------------------------------------------------------------------