Commit e5620e98 authored by Martin Reinecke's avatar Martin Reinecke

merge attempt

parents c016b764 f3dea2e4
Pipeline #71317 failed with stages
in 47 seconds
Changes since NIFTy 5: Changes since NIFTy 5:
MPI parallelisation over samples in MetricGaussianKL
====================================================
The classes `MetricGaussianKL` and `MetricGaussianKL_MPI` have been unified
into one `MetricGaussianKL` class which has MPI support built in.
New approach for random number generation
=========================================
The code now uses `numpy`'s new `SeedSequence` and `Generator` classes for the
production of random numbers (introduced in numpy 1.17. This greatly simplifies
the generation of reproducible random numbers in the presence of MPI parallelism
and leads to cleaner code overall. Please see the documentation of
`nifty6.random` for details.
Interface Change for non-linear Operators Interface Change for non-linear Operators
========================================= =========================================
...@@ -17,7 +32,6 @@ behaviour since both `Operator._check_input()` and ...@@ -17,7 +32,6 @@ behaviour since both `Operator._check_input()` and
`extra.check_jacobian_consistency()` tests for the new conditions to be `extra.check_jacobian_consistency()` tests for the new conditions to be
fulfilled. fulfilled.
Special functions for complete Field reduction operations Special functions for complete Field reduction operations
========================================================= =========================================================
...@@ -66,12 +80,3 @@ User-visible changes: ...@@ -66,12 +80,3 @@ User-visible changes:
replaced by a single function called `makeField` replaced by a single function called `makeField`
- the property `local_shape` has been removed from `Domain` (and subclasses) - the property `local_shape` has been removed from `Domain` (and subclasses)
and `DomainTuple`. and `DomainTuple`.
Transfer of MPI parallelization into operators:
===============================================
As was already the case with the `MetricGaussianKL_MPI` in NIFTy5, MPI
parallelization in NIFTy6 is handled by specialized MPI-enabled operators.
They are accessible via the `nifty6.mpi` namespace, from which they can be
imported directly: `from nifty6.mpi import MPIenabledOperator`.
...@@ -140,7 +140,6 @@ ...@@ -140,7 +140,6 @@
"outputs": [], "outputs": [],
"source": [ "source": [
"import numpy as np\n", "import numpy as np\n",
"np.random.seed(40)\n",
"import nifty6 as ift\n", "import nifty6 as ift\n",
"import matplotlib.pyplot as plt\n", "import matplotlib.pyplot as plt\n",
"%matplotlib inline" "%matplotlib inline"
......
...@@ -5,8 +5,6 @@ import numpy as np ...@@ -5,8 +5,6 @@ import numpy as np
import nifty6 as ift import nifty6 as ift
np.random.seed(40)
N0s, a0s, b0s, c0s = [], [], [], [] N0s, a0s, b0s, c0s = [], [], [], []
for ii in range(10, 26): for ii in range(10, 26):
...@@ -15,15 +13,16 @@ for ii in range(10, 26): ...@@ -15,15 +13,16 @@ for ii in range(10, 26):
N = int(2**ii) N = int(2**ii)
print('N = {}'.format(N)) print('N = {}'.format(N))
uv = np.random.rand(N, 2) - 0.5 rng = ift.random.current_rng()
vis = np.random.randn(N) + 1j*np.random.randn(N) uv = rng.uniform(-.5, .5, (N,2))
vis = rng.normal(0., 1., N) + 1j*rng.normal(0., 1., N)
uvspace = ift.RGSpace((nu, nv)) uvspace = ift.RGSpace((nu, nv))
visspace = ift.TMP_UnstructuredSpace(N) visspace = ift.TMP_UnstructuredSpace(N)
img = np.random.randn(nu*nv) img = rng.standard_normal((nu, nv))
img = img.reshape((nu, nv)) img = ift.makeField(uvspace, img)
img = ift.makeTMP_fld(uvspace, img) img = ift.makeTMP_fld(uvspace, img)
t0 = time() t0 = time()
......
...@@ -27,8 +27,6 @@ import numpy as np ...@@ -27,8 +27,6 @@ import numpy as np
import nifty6 as ift import nifty6 as ift
if __name__ == '__main__': if __name__ == '__main__':
np.random.seed(41)
# Set up the position space of the signal # Set up the position space of the signal
mode = 2 mode = 2
if mode == 0: if mode == 0:
...@@ -62,7 +60,7 @@ if __name__ == '__main__': ...@@ -62,7 +60,7 @@ if __name__ == '__main__':
p = R(sky) p = R(sky)
mock_position = ift.from_random('normal', harmonic_space) mock_position = ift.from_random('normal', harmonic_space)
tmp = p(mock_position).val.astype(np.float64) tmp = p(mock_position).val.astype(np.float64)
data = np.random.binomial(1, tmp) data = ift.random.current_rng().binomial(1, tmp)
data = ift.TMP_fld.from_raw(R.target, data) data = ift.TMP_fld.from_raw(R.target, data)
# Compute likelihood and Hamiltonian # Compute likelihood and Hamiltonian
......
...@@ -46,8 +46,6 @@ def make_random_mask(): ...@@ -46,8 +46,6 @@ def make_random_mask():
if __name__ == '__main__': if __name__ == '__main__':
np.random.seed(42)
# Choose space on which the signal field is defined # Choose space on which the signal field is defined
if len(sys.argv) == 2: if len(sys.argv) == 2:
mode = int(sys.argv[1]) mode = int(sys.argv[1])
......
...@@ -44,8 +44,6 @@ def exposure_2d(): ...@@ -44,8 +44,6 @@ def exposure_2d():
if __name__ == '__main__': if __name__ == '__main__':
np.random.seed(42)
# Choose space on which the signal field is defined # Choose space on which the signal field is defined
if len(sys.argv) == 2: if len(sys.argv) == 2:
mode = int(sys.argv[1]) mode = int(sys.argv[1])
...@@ -94,7 +92,7 @@ if __name__ == '__main__': ...@@ -94,7 +92,7 @@ if __name__ == '__main__':
lamb = R(sky) lamb = R(sky)
mock_position = ift.from_random('normal', domain) mock_position = ift.from_random('normal', domain)
data = lamb(mock_position) data = lamb(mock_position)
data = np.random.poisson(data.val.astype(np.float64)) data = ift.random.current_rng().poisson(data.val.astype(np.float64))
data = ift.TMP_fld.from_raw(d_space, data) data = ift.TMP_fld.from_raw(d_space, data)
likelihood = ift.PoissonianEnergy(data) @ lamb likelihood = ift.PoissonianEnergy(data) @ lamb
......
...@@ -33,20 +33,18 @@ import nifty6 as ift ...@@ -33,20 +33,18 @@ import nifty6 as ift
def random_los(n_los): def random_los(n_los):
starts = list(np.random.uniform(0, 1, (n_los, 2)).T) starts = list(ift.random.current_rng().random((n_los, 2)).T)
ends = list(np.random.uniform(0, 1, (n_los, 2)).T) ends = list(ift.random.current_rng().random((n_los, 2)).T)
return starts, ends return starts, ends
def radial_los(n_los): def radial_los(n_los):
starts = list(np.random.uniform(0, 1, (n_los, 2)).T) starts = list(ift.random.current_rng().random((n_los, 2)).T)
ends = list(0.5 + 0*np.random.uniform(0, 1, (n_los, 2)).T) ends = list(0.5 + 0*ift.random.current_rng().random((n_los, 2)).T)
return starts, ends return starts, ends
if __name__ == '__main__': if __name__ == '__main__':
np.random.seed(420)
# Choose between random line-of-sight response (mode=0) and radial lines # Choose between random line-of-sight response (mode=0) and radial lines
# of sight (mode=1) # of sight (mode=1)
if len(sys.argv) == 2: if len(sys.argv) == 2:
......
...@@ -44,20 +44,18 @@ class SingleTMP_Space(ift.LinearOperator): ...@@ -44,20 +44,18 @@ class SingleTMP_Space(ift.LinearOperator):
def random_los(n_los): def random_los(n_los):
starts = list(np.random.uniform(0, 1, (n_los, 2)).T) starts = list(ift.random.current_rng().random((n_los, 2)).T)
ends = list(np.random.uniform(0, 1, (n_los, 2)).T) ends = list(ift.random.current_rng().random((n_los, 2)).T)
return starts, ends return starts, ends
def radial_los(n_los): def radial_los(n_los):
starts = list(np.random.uniform(0, 1, (n_los, 2)).T) starts = list(ift.random.current_rng().random((n_los, 2)).T)
ends = list(0.5 + 0*np.random.uniform(0, 1, (n_los, 2)).T) ends = list(0.5 + 0*ift.random.current_rng().random((n_los, 2)).T)
return starts, ends return starts, ends
if __name__ == '__main__': if __name__ == '__main__':
np.random.seed(43)
# Choose between random line-of-sight response (mode=0) and radial lines # Choose between random line-of-sight response (mode=0) and radial lines
# of sight (mode=1) # of sight (mode=1)
if len(sys.argv) == 2: if len(sys.argv) == 2:
......
...@@ -19,7 +19,6 @@ import matplotlib.pyplot as plt ...@@ -19,7 +19,6 @@ import matplotlib.pyplot as plt
import numpy as np import numpy as np
import nifty6 as ift import nifty6 as ift
np.random.seed(12)
def polynomial(coefficients, sampling_points): def polynomial(coefficients, sampling_points):
...@@ -86,7 +85,7 @@ if __name__ == '__main__': ...@@ -86,7 +85,7 @@ if __name__ == '__main__':
N_params = 10 N_params = 10
N_samples = 100 N_samples = 100
size = (12,) size = (12,)
x = np.random.random(size) * 10 x = ift.random.current_rng().random(size) * 10
y = np.sin(x**2) * x**3 y = np.sin(x**2) * x**3
var = np.full_like(y, y.var() / 10) var = np.full_like(y, y.var() / 10)
var[-2] *= 4 var[-2] *= 4
......
from .version import __version__ from .version import __version__
from . import random
from .domains.domain import TMP_Space from .domains.domain import TMP_Space
from .domains.structured_domain import TMP_StructuredSpace from .domains.structured_domain import TMP_StructuredSpace
from .domains.unstructured_domain import TMP_UnstructuredSpace from .domains.unstructured_domain import TMP_UnstructuredSpace
......
...@@ -278,17 +278,21 @@ def check_jacobian_consistency(op, loc, tol=1e-8, ntries=100, perf_check=True): ...@@ -278,17 +278,21 @@ def check_jacobian_consistency(op, loc, tol=1e-8, ntries=100, perf_check=True):
dir = loc2-loc dir = loc2-loc
locnext = loc2 locnext = loc2
dirnorm = dir.norm() dirnorm = dir.norm()
hist = []
for i in range(50): for i in range(50):
locmid = loc + 0.5*dir locmid = loc + 0.5*dir
linmid = op(Linearization.make_var(locmid)) linmid = op(Linearization.make_var(locmid))
dirder = linmid.jac(dir) dirder = linmid.jac(dir)
numgrad = (lin2.val-lin.val) numgrad = (lin2.val-lin.val)
xtol = tol * dirder.norm() / np.sqrt(dirder.size) xtol = tol * dirder.norm() / np.sqrt(dirder.size)
hist.append((numgrad-dirder).norm())
# print(len(hist),hist[-1])
if (abs(numgrad-dirder) <= xtol).s_all(): if (abs(numgrad-dirder) <= xtol).s_all():
break break
dir = dir*0.5 dir = dir*0.5
dirnorm *= 0.5 dirnorm *= 0.5
loc2, lin2 = locmid, linmid loc2, lin2 = locmid, linmid
else: else:
print(hist)
raise ValueError("gradient and value seem inconsistent") raise ValueError("gradient and value seem inconsistent")
loc = locnext loc = locnext
...@@ -25,6 +25,7 @@ from ..field import TMP_fld ...@@ -25,6 +25,7 @@ from ..field import TMP_fld
from ..linearization import Linearization from ..linearization import Linearization
from ..operators.operator import Operator from ..operators.operator import Operator
from ..sugar import makeOp from ..sugar import makeOp
from .. import random
def _f_on_np(f, arr): def _f_on_np(f, arr):
...@@ -67,7 +68,8 @@ class _InterpolationOperator(Operator): ...@@ -67,7 +68,8 @@ class _InterpolationOperator(Operator):
if table_func is not None: if table_func is not None:
if inv_table_func is None: if inv_table_func is None:
raise ValueError raise ValueError
a = func(np.random.randn(10)) # MR FIXME: not sure whether we should have this in production code
a = func(random.current_rng().random(10))
a1 = _f_on_np(lambda x: inv_table_func(table_func(x)), a) a1 = _f_on_np(lambda x: inv_table_func(table_func(x)), a)
np.testing.assert_allclose(a, a1) np.testing.assert_allclose(a, a1)
self._table = _f_on_np(table_func, self._table) self._table = _f_on_np(table_func, self._table)
......
...@@ -11,19 +11,66 @@ ...@@ -11,19 +11,66 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
# Copyright(C) 2013-2019 Max-Planck-Society # Copyright(C) 2013-2020 Max-Planck-Society
# #
# NIFTy is being developed at the Max-Planck-Institut fuer Astrophysik. # NIFTy is being developed at the Max-Planck-Institut fuer Astrophysik.
import numpy as np import numpy as np
from .. import utilities from .. import random, utilities
from ..field import TMP_fld
from ..linearization import Linearization from ..linearization import Linearization
from ..multi_field import TMP_Field
from ..operators.endomorphic_operator import EndomorphicOperator
from ..operators.energy_operators import StandardHamiltonian from ..operators.energy_operators import StandardHamiltonian
from ..probing import approximation2endo from ..probing import approximation2endo
from ..sugar import makeOp from ..sugar import full, makeOp
from .energy import Energy from .energy import Energy
def _shareRange(nwork, nshares, myshare):
nbase = nwork//nshares
additional = nwork % nshares
lo = myshare*nbase + min(myshare, additional)
hi = lo + nbase + int(myshare < additional)
return lo, hi
def _np_allreduce_sum(comm, arr):
if comm is None:
return arr
from mpi4py import MPI
arr = np.array(arr)
res = np.empty_like(arr)
comm.Allreduce(arr, res, MPI.SUM)
return res
def _allreduce_sum_field(comm, fld):
if comm is None:
return fld
if isinstance(fld, TMP_fld):
return TMP_fld(fld.domain, _np_allreduce_sum(fld.val))
res = tuple(
TMP_fld(f.domain, _np_allreduce_sum(comm, f.val))
for f in fld.values())
return TMP_Field(fld.domain, res)
class _KLMetric(EndomorphicOperator):
def __init__(self, KL):
self._KL = KL
self._capability = self.TIMES | self.ADJOINT_TIMES
self._domain = KL.position.domain
def apply(self, x, mode):
self._check_input(x, mode)
return self._KL.apply_metric(x)
def draw_sample(self, from_inverse=False, dtype=np.float64):
return self._KL._metric_sample(from_inverse, dtype)
class MetricGaussianKL(Energy): class MetricGaussianKL(Energy):
"""Provides the sampled Kullback-Leibler divergence between a distribution """Provides the sampled Kullback-Leibler divergence between a distribution
and a Metric Gaussian. and a Metric Gaussian.
...@@ -38,6 +85,7 @@ class MetricGaussianKL(Energy): ...@@ -38,6 +85,7 @@ class MetricGaussianKL(Energy):
typically nonlinear structure of the true distribution these samples have typically nonlinear structure of the true distribution these samples have
to be updated eventually by intantiating `MetricGaussianKL` again. For the to be updated eventually by intantiating `MetricGaussianKL` again. For the
true probability distribution the standard parametrization is assumed. true probability distribution the standard parametrization is assumed.
The samples of this class can be distributed among MPI tasks.
Parameters Parameters
---------- ----------
...@@ -62,6 +110,17 @@ class MetricGaussianKL(Energy): ...@@ -62,6 +110,17 @@ class MetricGaussianKL(Energy):
napprox : int napprox : int
Number of samples for computing preconditioner for sampling. No Number of samples for computing preconditioner for sampling. No
preconditioning is done by default. preconditioning is done by default.
comm : MPI communicator or None
If not None, samples will be distributed as evenly as possible
across this communicator. If `mirror_samples` is set, then a sample and
its mirror image will always reside on the same task.
lh_sampling_dtype : type
Determines which dtype in data space shall be used for drawing samples
from the metric. If the inference is based on complex data,
lh_sampling_dtype shall be set to complex accordingly. The reason for
the presence of this parameter is that metric of the likelihood energy
is just an `Operator` which does not know anything about the dtype of
the fields on which it acts. Default is float64.
_samples : None _samples : None
Only a parameter for internal uses. Typically not to be set by users. Only a parameter for internal uses. Typically not to be set by users.
...@@ -79,7 +138,8 @@ class MetricGaussianKL(Energy): ...@@ -79,7 +138,8 @@ class MetricGaussianKL(Energy):
def __init__(self, mean, hamiltonian, n_samples, constants=[], def __init__(self, mean, hamiltonian, n_samples, constants=[],
point_estimates=[], mirror_samples=False, point_estimates=[], mirror_samples=False,
napprox=0, _samples=None, lh_sampling_dtype=np.float64): napprox=0, comm=None, _samples=None,
lh_sampling_dtype=np.float64):
super(MetricGaussianKL, self).__init__(mean) super(MetricGaussianKL, self).__init__(mean)
if not isinstance(hamiltonian, StandardHamiltonian): if not isinstance(hamiltonian, StandardHamiltonian):
...@@ -88,47 +148,72 @@ class MetricGaussianKL(Energy): ...@@ -88,47 +148,72 @@ class MetricGaussianKL(Energy):
raise ValueError raise ValueError
if not isinstance(n_samples, int): if not isinstance(n_samples, int):
raise TypeError raise TypeError
self._constants = list(constants) self._constants = tuple(constants)
self._point_estimates = list(point_estimates) self._point_estimates = tuple(point_estimates)
if not isinstance(mirror_samples, bool): if not isinstance(mirror_samples, bool):
raise TypeError raise TypeError
self._hamiltonian = hamiltonian self._hamiltonian = hamiltonian
self._n_samples = int(n_samples)
if comm is not None:
self._comm = comm
ntask = self._comm.Get_size()
rank = self._comm.Get_rank()
self._lo, self._hi = _shareRange(self._n_samples, ntask, rank)
else:
self._comm = None
self._lo, self._hi = 0, self._n_samples
self._mirror_samples = bool(mirror_samples)
self._n_eff_samples = self._n_samples
if self._mirror_samples:
self._n_eff_samples *= 2
if _samples is None: if _samples is None:
met = hamiltonian(Linearization.make_partial_var( met = hamiltonian(Linearization.make_partial_var(
mean, point_estimates, True)).metric mean, self._point_estimates, True)).metric
if napprox > 1: if napprox >= 1:
met._approximation = makeOp(approximation2endo(met, napprox)) met._approximation = makeOp(approximation2endo(met, napprox))
_samples = tuple(met.draw_sample(from_inverse=True, _samples = []
dtype=lh_sampling_dtype) sseq = random.spawn_sseq(self._n_samples)
for _ in range(n_samples)) for i in range(self._lo, self._hi):
if mirror_samples: random.push_sseq(sseq[i])
_samples += tuple(-s for s in _samples) _samples.append(met.draw_sample(from_inverse=True,
dtype=lh_sampling_dtype))
random.pop_sseq()
_samples = tuple(_samples)
else:
if len(_samples) != self._hi-self._lo:
raise ValueError("# of samples mismatch")
self._samples = _samples self._samples = _samples
self._lin = Linearization.make_partial_var(mean, self._constants)
# FIXME Use simplify for constant input instead
self._lin = Linearization.make_partial_var(mean, constants)
v, g = None, None v, g = None, None
for s in self._samples: if len(self._samples) == 0: # hack if there are too many MPI tasks
tmp = self._hamiltonian(self._lin+s) tmp = self._hamiltonian(self._lin)
if v is None: v = 0. * tmp.val.val
v = tmp.val.val[()] g = 0. * tmp.gradient
g = tmp.gradient else:
else: for s in self._samples:
v += tmp.val.val[()] tmp = self._hamiltonian(self._lin+s)
g = g + tmp.gradient if self._mirror_samples:
self._val = v / len(self._samples) tmp = tmp + self._hamiltonian(self._lin-s)
self._grad = g * (1./len(self._samples)) if v is None:
v = tmp.val.val_rw()
g = tmp.gradient
else:
v += tmp.val.val
g = g + tmp.gradient
self._val = _np_allreduce_sum(self._comm, v)[()] / self._n_eff_samples
self._grad = _allreduce_sum_field(self._comm, g) / self._n_eff_samples
self._metric = None self._metric = None
self._napprox = napprox
self._sampdt = lh_sampling_dtype self._sampdt = lh_sampling_dtype
def at(self, position): def at(self, position):