Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adding randomized algorithms #665

Merged
merged 3 commits into from May 8, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS.md
Expand Up @@ -14,6 +14,7 @@
* improve projection shifts for low-rank ADI method for Lyapunov equations

* Dennis Eickhorn, d.eickhorn@uni-muenster.de
* randomized range approximation algorithms in algorithms.randrangefinder
* fixed complex norms in vectorarrays.interfaces


Expand Down
10 changes: 10 additions & 0 deletions docs/source/bibliography.rst
Expand Up @@ -33,6 +33,10 @@ Bibliography
Equations, Proceedings of the 11th World Congress on Computational
Mechanics, 2014.

.. [BS18] A. Buhr, K. Smetana,
Randomized Local Model Order Reduction.
SIAM Journal on Scientific Computing, 40(4), A2120–A2151, 2018.

.. [CLVV06] Y. Chahlaoui, D. Lemonnier, A. Vandendorpe, P. Van
Dooren,
Second-order balanced truncation,
Expand All @@ -59,6 +63,12 @@ Bibliography
approximations of parametrized evolution equations,
M2AN 42(2), 277-302, 2008.

.. [HMT11] N. Halko, P. G. Martinsson and J. A. Tropp,
Finding structure with randomness: probabilistic
algorithms for constructing approximate matrix
decompositions,
SIAM Review, 53(2), 217–288, 2011.

.. [HLR18] C. Himpe, T. Leibner, S. Rave,
Hierarchical Approximate Proper Orthogonal Decomposition,
SIAM J. Sci. Comput. 40, A3267-A3292, 2018.
Expand Down
143 changes: 143 additions & 0 deletions src/pymor/algorithms/randrangefinder.py
@@ -0,0 +1,143 @@
# This file is part of the pyMOR project (http://www.pymor.org).
# Copyright 2013-2019 pyMOR developers and contributors. All rights reserved.
# License: BSD 2-Clause License (http://opensource.org/licenses/BSD-2-Clause)

import numpy as np
from scipy.sparse.linalg import eigsh, LinearOperator
from scipy.special import erfinv

from pymor.algorithms.gram_schmidt import gram_schmidt
from pymor.core.defaults import defaults
from pymor.operators.interfaces import OperatorInterface


@defaults('tol', 'failure_tolerance', 'num_testvecs')
def adaptive_rrf(A, source_product=None, range_product=None, tol=1e-4,
failure_tolerance=1e-15, num_testvecs=20, lambda_min=None, iscomplex=False):
r"""Adaptive randomized range approximation of `A`.

This is an implementation of Algorithm 1 in [BS18]_.

Given the |Operator| `A`, the return value of this method is the |VectorArray|
`B` with the property

.. math::
\Vert A - P_{span(B)} A \Vert \leq tol
sdrave marked this conversation as resolved.
Show resolved Hide resolved

with a failure probability smaller than `failure_tolerance`, where the inner product of the
range of `A` is given by `range_product` and the inner product of the source of `A`
is given by `source_product`.

Parameters
----------
A
The |Operator| A.
source_product
Inner product |Operator| of the source of A.
range_product
Inner product |Operator| of the range of A.
tol
Error tolerance for the algorithm.
failure_tolerance
Maximum failure probability.
num_testvecs
Number of test vectors.
lambda_min
The smallest eigenvalue of source_product.
If `None`, the smallest eigenvalue is computed using scipy.
iscomplex
If `True`, the random vectors are chosen complex.

Returns
-------
B
|VectorArray| which contains the basis, whose span approximates the range of A.
"""

assert source_product is None or isinstance(source_product, OperatorInterface)
assert range_product is None or isinstance(range_product, OperatorInterface)
assert isinstance(A, OperatorInterface)

B = A.range.empty()

R = A.source.random(num_testvecs, distribution='normal')
if iscomplex:
R += 1j*A.source.random(num_testvecs, distribution='normal')

if source_product is None:
lambda_min = 1
elif lambda_min is None:
def mv(v):
return source_product.apply(source_product.source.from_numpy(v)).to_numpy()

def mvinv(v):
return source_product.apply_inverse(source_product.range.from_numpy(v)).to_numpy()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is adaptive_rrf actually supposed to work if source_product.space.from_numpy is not implemented? Or is from_numpy always implemented for source_products in applications?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or is from_numpy always implemented for source_products in applications?

No, this cannot be expected. In that case you would need to specify lambda_min. For the future, I would hope that at some point we reimplement something like ARPACK in pyMOR. Not sure how much effort would be required, however.

L = LinearOperator((source_product.source.dim, source_product.range.dim), matvec=mv, matmat=mv)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is matmat=mv really correct? I would expect scipy to use column vectors, but from_numpy/to_numpy uses column vectors (@pmli, I know, I know ...).

Copy link
Contributor Author

@deneick deneick May 7, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just realized scipy does not use matmat in eigsh, but only in svds, so I will leave this out here.

Linv = LinearOperator((source_product.range.dim, source_product.source.dim), matvec=mvinv, matmat=mvinv)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

lambda_min = eigsh(L, sigma=0, which="LM", return_eigenvectors=False, k=1, OPinv=Linv)[0]
sdrave marked this conversation as resolved.
Show resolved Hide resolved

testfail = failure_tolerance / min(A.source.dim, A.range.dim)
testlimit = np.sqrt(2. * lambda_min) * erfinv(testfail**(1. / num_testvecs)) * tol
maxnorm = np.inf
M = A.apply(R)

while(maxnorm > testlimit):
basis_length = len(B)
v = A.source.random(distribution='normal')
if iscomplex:
v += 1j*A.source.random(distribution='normal')
pmli marked this conversation as resolved.
Show resolved Hide resolved
B.append(A.apply(v))
gram_schmidt(B, range_product, atol=0, rtol=0, offset=basis_length, copy=False)
M -= B.lincomb(B.inner(M, range_product).T)
maxnorm = np.max(M.norm(range_product))

return B


@defaults('q', 'l')
def rrf(A, source_product=None, range_product=None, q=2, l=8, iscomplex=False):
"""Randomized range approximation of `A`.

This is an implementation of Algorithm 4.4 in [HMT11]_.

Given the |Operator| `A`, the return value of this method is the |VectorArray|
`Q` whose vectors form an orthonomal basis for the range of `A`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

approximate orthonormal basis


Parameters
----------
A
The |Operator| A.
source_product
Inner product |Operator| of the source of A.
range_product
Inner product |Operator| of the range of A.
q
The number of power iterations.
l
The block size of the normalized power iterations.
iscomplex
If `True`, the random vectors are chosen complex.

Returns
-------
Q
|VectorArray| which contains the basis, whose span approximates the range of A.
"""

assert source_product is None or isinstance(source_product, OperatorInterface)
assert range_product is None or isinstance(range_product, OperatorInterface)
assert isinstance(A, OperatorInterface)

R = A.source.random(l, distribution='normal')
if iscomplex:
R += 1j*A.source.random(l, distribution='normal')
Q = A.apply(R)
gram_schmidt(Q, range_product, atol=0, rtol=0, copy=False)

for i in range(q):
Q = A.apply_adjoint(Q)
gram_schmidt(Q, source_product, atol=0, rtol=0, copy=False)
Q = A.apply(Q)
gram_schmidt(Q, range_product, atol=0, rtol=0, copy=False)

return Q
47 changes: 47 additions & 0 deletions src/pymortests/randrangefinder.py
@@ -0,0 +1,47 @@
# This file is part of the pyMOR project (http://www.pymor.org).
# Copyright 2013-2019 pyMOR developers and contributors. All rights reserved.
# License: BSD 2-Clause License (http://opensource.org/licenses/BSD-2-Clause)

import numpy as np
from numpy.random import uniform

from pymor.algorithms.randrangefinder import rrf, adaptive_rrf
from pymor.operators.numpy import NumpyMatrixOperator
from pymor.operators.constructions import VectorArrayOperator


np.random.seed(0)
A = uniform(low=-1.0, high=1.0, size=(100, 100))
A = A.dot(A.T)
range_product = NumpyMatrixOperator(A)

np.random.seed(1)
A = uniform(low=-1.0, high=1.0, size=(10, 10))
A = A.dot(A.T)
source_product = NumpyMatrixOperator(A)

B = range_product.range.random(10, seed=10)
op = VectorArrayOperator(B)

C = range_product.range.random(10, seed=11)+1j*range_product.range.random(10, seed=12)
op_complex = VectorArrayOperator(C)


def test_rrf():
Q = rrf(op, source_product, range_product)
assert Q in op.range
assert len(Q) == 8

Q = rrf(op_complex, iscomplex=True)
assert np.iscomplexobj(Q.data)
assert Q in op.range
assert len(Q) == 8


def test_adaptive_rrf():
B = adaptive_rrf(op, source_product, range_product)
assert B in op.range

B = adaptive_rrf(op_complex, iscomplex=True)
assert np.iscomplexobj(B.data)
assert B in op.range