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

New approach to handling randomness in pyMOR #1736

Merged
merged 18 commits into from Oct 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
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
3 changes: 3 additions & 0 deletions .codecov.yml
Expand Up @@ -16,8 +16,11 @@ coverage:
changes: false
project:
pymor:
# since we have no policy on this might as well turn this off entirely
target: 0%
flags:
- gitlab_ci
- github_actions
patch:
pymor:
target: 0%
2 changes: 2 additions & 0 deletions docs/source/substitutions.py
Expand Up @@ -164,6 +164,8 @@

.. |SymplecticBasis| replace:: :class:`~pymor.algorithms.symplectic.SymplecticBasis`
.. |CanonicalSymplecticFormOperator| replace:: :class:`~pymor.operators.symplectic.CanonicalSymplecticFormOperator`

.. |RNG| replace:: :class:`random number generator <pymor.tools.random.RNG>`
'''

substitutions = interfaces + common
Expand Down
9 changes: 4 additions & 5 deletions src/pymor/algorithms/eigs.py
Expand Up @@ -9,10 +9,11 @@
from pymor.core.logger import getLogger
from pymor.operators.constructions import IdentityOperator, InverseOperator
from pymor.operators.interface import Operator
from pymor.tools.random import new_rng


def eigs(A, E=None, k=3, sigma=None, which='LM', b=None, l=None, maxiter=1000, tol=1e-13,
imag_tol=1e-12, complex_pair_tol=1e-12, complex_evp=False, left_evp=False, seed=0):
imag_tol=1e-12, complex_pair_tol=1e-12, complex_evp=False, left_evp=False):
"""Approximate a few eigenvalues of a linear |Operator|.

Computes `k` eigenvalues `w` with corresponding eigenvectors `v` which solve
Expand Down Expand Up @@ -67,9 +68,6 @@ def eigs(A, E=None, k=3, sigma=None, which='LM', b=None, l=None, maxiter=1000, t
are real setting this argument to `False` will increase stability and performance.
left_evp
If set to `True` compute left eigenvectors else compute right eigenvectors.
seed
Random seed which is used for computing the initial vector for the Arnoldi
iteration.

Returns
-------
Expand All @@ -93,7 +91,8 @@ def eigs(A, E=None, k=3, sigma=None, which='LM', b=None, l=None, maxiter=1000, t
assert E.source == A.source

if b is None:
b = A.source.random(seed=seed)
with new_rng(0):
b = A.source.random()
else:
assert b in A.source

Expand Down
7 changes: 4 additions & 3 deletions src/pymor/algorithms/hapod.py
Expand Up @@ -10,6 +10,7 @@

from pymor.algorithms.pod import pod
from pymor.core.logger import getLogger
from pymor.tools.random import spawn_rng


class Node:
Expand Down Expand Up @@ -180,7 +181,7 @@ async def hapod_step(node):

if node.children:
modes, svals, snap_counts = zip(
*await asyncio.gather(*(hapod_step(c) for c in node.children))
*await asyncio.gather(*(spawn_rng(hapod_step(c)) for c in node.children))
)
for m, sv in zip(modes, svals):
m.scal(sv)
Expand Down Expand Up @@ -221,7 +222,7 @@ def main():
nonlocal result
result = asyncio.run(hapod_step(tree))
result = None
hapod_thread = Thread(target=main)
hapod_thread = Thread(target=spawn_rng(main))
hapod_thread.start()
hapod_thread.join()
return result
Expand Down Expand Up @@ -433,7 +434,7 @@ def __init__(self, executor, max_workers=None):

def submit(self, f, *args):
future = asyncio.get_event_loop().create_future()
self.queue.append((future, f, args))
self.queue.append((future, spawn_rng(f), args))
asyncio.get_event_loop().create_task(self.run_task())
return future

Expand Down
13 changes: 5 additions & 8 deletions src/pymor/algorithms/lradi.py
Expand Up @@ -12,18 +12,17 @@
from pymor.core.defaults import defaults
from pymor.core.logger import getLogger
from pymor.operators.constructions import IdentityOperator, InverseOperator
from pymor.tools.random import get_random_state
from pymor.tools.random import new_rng
from pymor.vectorarrays.constructions import cat_arrays


@defaults('lradi_tol', 'lradi_maxiter', 'lradi_shifts', 'projection_shifts_init_maxiter',
'projection_shifts_init_seed', 'projection_shifts_subspace_columns',
'projection_shifts_subspace_columns',
'wachspress_large_ritz_num', 'wachspress_small_ritz_num', 'wachspress_tol')
def lyap_lrcf_solver_options(lradi_tol=1e-10,
lradi_maxiter=500,
lradi_shifts='projection_shifts',
projection_shifts_init_maxiter=20,
projection_shifts_init_seed=None,
projection_shifts_subspace_columns=6,
wachspress_large_ritz_num=50,
wachspress_small_ritz_num=25,
Expand All @@ -40,8 +39,6 @@ def lyap_lrcf_solver_options(lradi_tol=1e-10,
See :func:`solve_lyap_lrcf`.
projection_shifts_init_maxiter
See :func:`projection_shifts_init`.
projection_shifts_init_seed
See :func:`projection_shifts_init`.
projection_shifts_subspace_columns
See :func:`projection_shifts`.
wachspress_large_ritz_num
Expand All @@ -62,7 +59,6 @@ def lyap_lrcf_solver_options(lradi_tol=1e-10,
'shift_options':
{'projection_shifts': {'type': 'projection_shifts',
'init_maxiter': projection_shifts_init_maxiter,
'init_seed': projection_shifts_init_seed,
'subspace_columns': projection_shifts_subspace_columns},
'wachspress_shifts': {'type': 'wachspress_shifts',
'large_ritz_num': wachspress_large_ritz_num,
Expand Down Expand Up @@ -194,14 +190,15 @@ def projection_shifts_init(A, E, B, shift_options):
shifts
A |NumPy array| containing a set of stable shift parameters.
"""
random_state = get_random_state(seed=shift_options['init_seed'])
rng = new_rng(0)
sdrave marked this conversation as resolved.
Show resolved Hide resolved
for i in range(shift_options['init_maxiter']):
Q = gram_schmidt(B, atol=0, rtol=0)
shifts = spla.eigvals(A.apply2(Q, Q), E.apply2(Q, Q))
shifts = shifts[shifts.real < 0]
if shifts.size == 0:
# use random subspace instead of span{B} (with same dimensions)
B = B.random(len(B), distribution='normal', random_state=random_state)
with rng:
B = B.random(len(B), distribution='normal')
else:
return shifts
raise RuntimeError('Could not generate initial shifts for low-rank ADI iteration.')
Expand Down
13 changes: 5 additions & 8 deletions src/pymor/algorithms/lrradi.py
Expand Up @@ -12,16 +12,15 @@
from pymor.core.logger import getLogger
from pymor.operators.constructions import IdentityOperator
from pymor.algorithms.gram_schmidt import gram_schmidt
from pymor.tools.random import get_random_state
from pymor.tools.random import new_rng


@defaults('lrradi_tol', 'lrradi_maxiter', 'lrradi_shifts', 'hamiltonian_shifts_init_maxiter',
'hamiltonian_shifts_init_seed', 'hamiltonian_shifts_subspace_columns')
'hamiltonian_shifts_subspace_columns')
def ricc_lrcf_solver_options(lrradi_tol=1e-10,
lrradi_maxiter=500,
lrradi_shifts='hamiltonian_shifts',
hamiltonian_shifts_init_maxiter=20,
hamiltonian_shifts_init_seed=None,
hamiltonian_shifts_subspace_columns=6):
"""Returns available Riccati equation solvers with default solver options.

Expand All @@ -35,8 +34,6 @@ def ricc_lrcf_solver_options(lrradi_tol=1e-10,
See :func:`solve_ricc_lrcf`.
hamiltonian_shifts_init_maxiter
See :func:`hamiltonian_shifts_init`.
hamiltonian_shifts_init_seed
See :func:`hamiltonian_shifts_init`.
hamiltonian_shifts_subspace_columns
See :func:`hamiltonian_shifts`.

Expand All @@ -51,7 +48,6 @@ def ricc_lrcf_solver_options(lrradi_tol=1e-10,
'shift_options':
{'hamiltonian_shifts': {'type': 'hamiltonian_shifts',
'init_maxiter': hamiltonian_shifts_init_maxiter,
'init_seed': hamiltonian_shifts_init_seed,
'subspace_columns': hamiltonian_shifts_subspace_columns}}}}


Expand Down Expand Up @@ -229,7 +225,7 @@ def hamiltonian_shifts_init(A, E, B, C, shift_options):
shifts
A |NumPy array| containing a set of stable shift parameters.
"""
random_state = get_random_state(seed=shift_options['init_seed'])
rng = new_rng(0)
for _ in range(shift_options['init_maxiter']):
Q = gram_schmidt(C, atol=0, rtol=0)
Ap = A.apply2(Q, Q)
Expand All @@ -249,7 +245,8 @@ def hamiltonian_shifts_init(A, E, B, C, shift_options):
eigpairs = list(filter(lambda e: e[0].real < 0, eigpairs))
if len(eigpairs) == 0:
# use random subspace instead of span{C} (with same dimensions)
C = C.random(len(C), distribution='normal', random_state=random_state)
with rng:
C = C.random(len(C), distribution='normal')
continue
# find shift with most impact on convergence
maxval = -1
Expand Down
15 changes: 7 additions & 8 deletions src/pymor/algorithms/samdp.py
Expand Up @@ -10,12 +10,13 @@
from pymor.core.logger import getLogger
from pymor.operators.constructions import IdentityOperator
from pymor.operators.interface import Operator
from pymor.tools.random import new_rng


@defaults('which', 'tol', 'imagtol', 'conjtol', 'dorqitol', 'rqitol', 'maxrestart', 'krestart', 'init_shifts',
'rqi_maxiter', 'seed')
'rqi_maxiter')
def samdp(A, E, B, C, nwanted, init_shifts=None, which='NR', tol=1e-10, imagtol=1e-6, conjtol=1e-8,
dorqitol=1e-4, rqitol=1e-10, maxrestart=100, krestart=20, rqi_maxiter=10, seed=0):
dorqitol=1e-4, rqitol=1e-10, maxrestart=100, krestart=20, rqi_maxiter=10):
"""Compute the dominant pole triplets and residues of the transfer function of an LTI system.

This function uses the subspace accelerated dominant pole (SAMDP) algorithm as described in
Expand Down Expand Up @@ -70,8 +71,6 @@ def samdp(A, E, B, C, nwanted, init_shifts=None, which='NR', tol=1e-10, imagtol=
Maximum dimension of search space before performing a restart.
rqi_maxiter
Maximum number of iterations for the two-sided Rayleigh quotient iteration.
seed
Random seed which is used for computing the initial shift and random restarts.

Returns
-------
Expand Down Expand Up @@ -106,7 +105,7 @@ def samdp(A, E, B, C, nwanted, init_shifts=None, which='NR', tol=1e-10, imagtol=
k = 0
nrestart = 0
nr_converged = 0
np.random.seed(seed)
rng = new_rng(0)

X = A.source.empty()
Q = A.source.empty()
Expand All @@ -121,7 +120,7 @@ def samdp(A, E, B, C, nwanted, init_shifts=None, which='NR', tol=1e-10, imagtol=
poles = np.empty(0)

if init_shifts is None:
st = np.random.uniform() * 10.j
st = rng.uniform() * 10.j
shift_nr = 0
nr_shifts = 0
else:
Expand Down Expand Up @@ -164,7 +163,7 @@ def samdp(A, E, B, C, nwanted, init_shifts=None, which='NR', tol=1e-10, imagtol=
SH, UR, URt, res = _select_max_eig(H, G, X, V, B_defl, C_defl, which)

if np.all(res < np.finfo(float).eps):
st = np.random.uniform() * 10.j
st = rng.uniform() * 10.j
found = False
else:
found = True
Expand Down Expand Up @@ -287,7 +286,7 @@ def samdp(A, E, B, C, nwanted, init_shifts=None, which='NR', tol=1e-10, imagtol=
if found:
st = SH[0, 0]
else:
st = np.random.uniform() * 10.j
st = rng.uniform() * 10.j

if shift_nr < nr_shifts:
st = shifts[shift_nr]
Expand Down
2 changes: 1 addition & 1 deletion src/pymor/basic.py
Expand Up @@ -76,7 +76,7 @@
SOBTReductor)
from pymor.reductors.sor_irka import SORIRKAReductor

from pymor.tools.random import default_random_state
from pymor.tools.random import get_rng, new_rng

from pymor.vectorarrays.constructions import cat_arrays
from pymor.vectorarrays.list import ListVectorSpace
Expand Down
4 changes: 2 additions & 2 deletions src/pymor/bindings/dunegdt.py
Expand Up @@ -125,8 +125,8 @@ def real_zero_vector(self):
def real_full_vector(self, value):
return DuneXTVector(self.dune_vector_type(self.dim, value))

def real_random_vector(self, distribution, random_state, **kwargs):
values = _create_random_values(self.dim, distribution, random_state, **kwargs)
def real_random_vector(self, distribution, **kwargs):
values = _create_random_values(self.dim, distribution, **kwargs)
return self.real_vector_from_numpy(values)

def real_vector_from_numpy(self, data, ensure_copy=False):
Expand Down
4 changes: 2 additions & 2 deletions src/pymor/bindings/fenics.py
Expand Up @@ -157,9 +157,9 @@ def real_full_vector(self, value):
impl += value
return FenicsVector(impl)

def real_random_vector(self, distribution, random_state, **kwargs):
def real_random_vector(self, distribution, **kwargs):
impl = df.Function(self.V).vector()
values = _create_random_values(impl.local_size(), distribution, random_state, **kwargs)
values = _create_random_values(impl.local_size(), distribution, **kwargs)
impl[:] = np.ascontiguousarray(values)
return FenicsVector(impl)

Expand Down
15 changes: 7 additions & 8 deletions src/pymor/parallel/ipython.py
Expand Up @@ -147,7 +147,8 @@ def __init__(self, num_engines=None, **kwargs):
else:
self.view = self.client[:]
self.logger.info(f'Connected to {len(self.view)} engines')
self.view.map_sync(_setup_worker, range(len(self.view)))
from pymor.tools.random import get_seed_seq
self.view.map_sync(_setup_worker, get_seed_seq().spawn(len(self.view)))
self._remote_objects_created = Counter()

if defaults.defaults_changes() > 0:
Expand Down Expand Up @@ -202,15 +203,13 @@ def _worker_call_function(function, loop, args, kwargs):
return function(*args, **kwargs)


def _setup_worker(worker_id):
def _setup_worker(seed_seq):
global _remote_objects
_remote_objects = {}
# ensure that each worker starts with a different RandomState
from pymor.tools import random
import numpy as np
state = random.default_random_state()
new_state = np.random.RandomState(state.randint(0, 2**16) + worker_id)
random._default_random_state = new_state
# ensure that each worker starts with a different yet deterministically
# initialized rng
from pymor.tools.random import new_rng
new_rng(seed_seq).install()


def _push_object(remote_id, obj):
Expand Down
16 changes: 9 additions & 7 deletions src/pymor/parallel/mpi.py
Expand Up @@ -8,6 +8,7 @@

from pymor.parallel.basic import WorkerPoolBase
from pymor.tools import mpi
from pymor.tools.random import get_seed_seq


class MPIPool(WorkerPoolBase):
Expand All @@ -18,6 +19,7 @@ def __init__(self):
self.logger.info(f'Connected to {mpi.size} ranks')
self._payload = mpi.call(mpi.function_call_manage, _setup_worker)
self._apply(os.chdir, os.getcwd())
self._map(_setup_rng, [[[s] for s in get_seed_seq().spawn(mpi.size)]])

def __del__(self):
mpi.call(mpi.remove_object, self._payload)
Expand Down Expand Up @@ -90,15 +92,15 @@ def _worker_map_function(payload, function, **kwargs):


def _setup_worker():
# ensure that each worker starts with a different RandomState
if not mpi.rank0:
from pymor.tools import random
import numpy as np
state = random.default_random_state()
new_state = np.random.RandomState(state.randint(0, 2**16) + mpi.rank)
random._default_random_state = new_state
return [None]


def _setup_rng(seed_seq):
# ensure that each worker starts with a different yet deterministically
# initialized rng
from pymor.tools.random import new_rng
new_rng(seed_seq).install()


def _push_object(obj):
return obj