Commit 4ed58632 authored by Martin Reinecke's avatar Martin Reinecke
Browse files

Merge branch 'immutable_fields' into 'NIFTy_5'

Immutable fields

See merge request ift/nifty-dev!39
parents 8cf302cb 83400bbe
......@@ -430,7 +430,7 @@
"mask = ift.Field.from_global_data(s_space, mask)\n",
"\n",
"R = ift.DiagonalOperator(mask)*HT\n",
"n = n.to_global_data()\n",
"n = n.to_global_data().copy()\n",
"n[l:h] = 0\n",
"n = ift.Field.from_global_data(s_space, n)\n",
"\n",
......@@ -501,7 +501,7 @@
"m_data = HT(m).to_global_data()\n",
"m_var_data = m_var.to_global_data()\n",
"uncertainty = np.sqrt(m_var_data)\n",
"d_data = d.to_global_data()\n",
"d_data = d.to_global_data().copy()\n",
"\n",
"# Set lost data to NaN for proper plotting\n",
"d_data[d_data == 0] = np.nan"
......@@ -586,7 +586,7 @@
"mask = ift.Field.from_global_data(s_space, mask)\n",
"\n",
"R = ift.DiagonalOperator(mask)*HT\n",
"n = n.to_global_data()\n",
"n = n.to_global_data().copy()\n",
"n[l:h, l:h] = 0\n",
"n = ift.Field.from_global_data(s_space, n)\n",
"curv = Curvature(R=R, N=N, Sh=Sh)\n",
......@@ -731,7 +731,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.6.5"
"version": "3.6.6"
}
},
"nbformat": 4,
......
......@@ -5,7 +5,6 @@ import numpy as np
def get_random_LOS(n_los):
starts = list(np.random.uniform(0, 1, (n_los, 2)).T)
ends = list(np.random.uniform(0, 1, (n_los, 2)).T)
return starts, ends
......@@ -87,20 +86,12 @@ if __name__ == '__main__':
ift.plot([A.at(position).value, A.at(MOCK_POSITION).value],
name='power.pdf')
avrg = 0.
va = 0.
powers = []
sc = ift.StatCalculator()
for sample in samples:
sam = signal.at(sample + position).value
powers.append(A.at(sample+position).value)
avrg += sam
va += sam**2
avrg /= len(samples)
va /= len(samples)
va -= avrg**2
std = ift.sqrt(va)
ift.plot(avrg, name='avrg.pdf')
ift.plot(std, name='std.pdf')
sc.add(signal.at(sample+position).value)
ift.plot(sc.mean, name='avrg.pdf')
ift.plot(ift.sqrt(sc.var), name='std.pdf')
powers = [A.at(s+position).value for s in samples]
ift.plot([A.at(position).value, A.at(MOCK_POSITION).value]+powers,
name='power.pdf')
......@@ -142,12 +142,11 @@ There is also a set of convenience functions to generate fields with constant
values or fields filled with random numbers according to a user-specified
distribution.
Fields are the only fundamental NIFTy objects which can change state after they
have been constructed: while their data type, domain, and array shape cannot
be modified, the actual data content of the array may be manipulated during the
lifetime of the object. This is a slight deviation from the philosophy that all
NIFTy objects should be immutable, but this choice offers considerable
performance benefits.
Like almost all NIFTy objects, fields are immutable: their value or any other
attribute cannot be modified after construction. To manipulate a field in ways
that are not covered by the provided standard operations, its data content must
be extracted first, then changed, and a new field has to be created from the
result.
Linear Operators
......
......@@ -54,7 +54,8 @@ extensions = [
'sphinx.ext.napoleon',
# 'sphinx.ext.coverage',
# 'sphinx.ext.todo',
'sphinx.ext.mathjax',
# 'sphinx.ext.mathjax',
'sphinx.ext.imgmath',
'sphinx.ext.viewcode'
]
......@@ -82,9 +83,9 @@ author = u'Theo Steininger / Martin Reinecke'
# built documents.
#
# The short X.Y version.
version = u'4.0'
version = u'5.0'
# The full version, including alpha/beta/rc tags.
release = u'4.0.0'
release = u'5.0.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
......
......@@ -89,7 +89,7 @@ from .library.bernoulli_energy import BernoulliEnergy
from . import extra
from .utilities import memo
from .utilities import memo, frozendict
from .logger import logger
......
......@@ -16,11 +16,13 @@
# NIFTy is being developed at the Max-Planck-Institut fuer Astrophysik
# and financially supported by the Studienstiftung des deutschen Volkes.
from __future__ import (absolute_import, division, print_function)
from builtins import *
from functools import reduce
import numpy as np
from .random import Random
from mpi4py import MPI
import sys
from functools import reduce
_comm = MPI.COMM_WORLD
ntask = _comm.Get_size()
......@@ -62,6 +64,9 @@ class data_object(object):
if local_shape(self._shape, self._distaxis) != self._data.shape:
raise ValueError("shape mismatch")
def copy(self):
return data_object(self._shape, self._data.copy(), self._distaxis)
# def _sanity_checks(self):
# # check whether the distaxis is consistent
# if self._distaxis < -1 or self._distaxis >= len(self._shape):
......
......@@ -16,6 +16,8 @@
# NIFTy is being developed at the Max-Planck-Institut fuer Astrophysik
# and financially supported by the Studienstiftung des deutschen Volkes.
from __future__ import (absolute_import, division, print_function)
from builtins import *
from functools import reduce
from .domains.domain import Domain
......@@ -104,6 +106,16 @@ class DomainTuple(object):
"""
return self._shape
@property
def local_shape(self):
"""tuple of int: number of pixels along each axis on the local task
The shape of the array-like object required to store information
living on part of the domain which is stored on the local MPI task.
"""
from .dobj import local_shape
return local_shape(self._shape)
@property
def size(self):
"""int : total number of pixels.
......
......@@ -88,6 +88,16 @@ class Domain(NiftyMetaBase()):
"""
raise NotImplementedError
@property
def local_shape(self):
"""tuple of int: number of pixels along each axis on the local task
The shape of the array-like object required to store information
living on part of the domain which is stored on the local MPI task.
"""
from ..dobj import local_shape
return local_shape(self.shape)
@abc.abstractproperty
def size(self):
"""int: total number of pixels.
......
......@@ -101,12 +101,7 @@ class LMSpace(StructuredDomain):
# by Challinor et al.
# http://arxiv.org/abs/astro-ph/0008228
from ..sugar import exp
res = x+1.
res *= x
res *= -0.5*sigma*sigma
exp(res, out=res)
return res
return exp((x+1.) * x * (-0.5*sigma*sigma))
def get_fft_smoothing_kernel_function(self, sigma):
return lambda x: self._kernel(x, sigma)
......
......@@ -3,7 +3,7 @@ from ..sugar import exp
import numpy as np
from ..dobj import ibegin
from .. import dobj
from ..field import Field
from .structured_domain import StructuredDomain
......@@ -62,26 +62,22 @@ class LogRGSpace(StructuredDomain):
np.zeros(len(self.shape)), True)
def get_k_length_array(self):
out = Field(self, dtype=np.float64)
oloc = out.local_data
ib = ibegin(out.val)
res = np.arange(oloc.shape[0], dtype=np.float64) + ib[0]
ib = dobj.ibegin_from_shape(self._shape)
res = np.arange(self.local_shape[0], dtype=np.float64) + ib[0]
res = np.minimum(res, self.shape[0]-res)*self.bindistances[0]
if len(self.shape) == 1:
oloc[()] = res
return out
return Field.from_local_data(self, res)
res *= res
for i in range(1, len(self.shape)):
tmp = np.arange(oloc.shape[i], dtype=np.float64) + ib[i]
tmp = np.arange(self.local_shape[i], dtype=np.float64) + ib[i]
tmp = np.minimum(tmp, self.shape[i]-tmp)*self.bindistances[i]
tmp *= tmp
res = np.add.outer(res, tmp)
oloc[()] = np.sqrt(res)
return out
return Field.from_local_data(self, np.sqrt(res))
def get_expk_length_array(self):
# FIXME This is a hack! Only for plotting. Seems not to be the final version.
out = exp(self.get_k_length_array())
out.val[1:] = out.val[:-1]
out.val[0] = 0
return out
out = exp(self.get_k_length_array()).to_global_data().copy()
out[1:] = out[:-1]
out[0] = 0
return Field.from_global_data(self, out)
......@@ -95,22 +95,18 @@ class RGSpace(StructuredDomain):
def get_k_length_array(self):
if (not self.harmonic):
raise NotImplementedError
out = Field(self, dtype=np.float64)
oloc = out.local_data
ibegin = dobj.ibegin(out.val)
res = np.arange(oloc.shape[0], dtype=np.float64) + ibegin[0]
ibegin = dobj.ibegin_from_shape(self._shape)
res = np.arange(self.local_shape[0], dtype=np.float64) + ibegin[0]
res = np.minimum(res, self.shape[0]-res)*self.distances[0]
if len(self.shape) == 1:
oloc[()] = res
return out
return Field.from_local_data(self, res)
res *= res
for i in range(1, len(self.shape)):
tmp = np.arange(oloc.shape[i], dtype=np.float64) + ibegin[i]
tmp = np.arange(self.local_shape[i], dtype=np.float64) + ibegin[i]
tmp = np.minimum(tmp, self.shape[i]-tmp)*self.distances[i]
tmp *= tmp
res = np.add.outer(res, tmp)
oloc[()] = np.sqrt(res)
return out
return Field.from_local_data(self, np.sqrt(res))
def get_unique_k_lengths(self):
if (not self.harmonic):
......@@ -145,10 +141,7 @@ class RGSpace(StructuredDomain):
@staticmethod
def _kernel(x, sigma):
from ..sugar import exp
tmp = x*x
tmp *= -2.*np.pi*np.pi*sigma*sigma
exp(tmp, out=tmp)
return tmp
return exp(x*x * (-2.*np.pi*np.pi*sigma*sigma))
def get_fft_smoothing_kernel_function(self, sigma):
if (not self.harmonic):
......
......@@ -6,6 +6,7 @@ from ..utilities import memo, my_sum
class SampledKullbachLeiblerDivergence(Energy):
def __init__(self, h, res_samples):
"""
# MR FIXME: does h have to be a Hamiltonian? Couldn't it be any energy?
h: Hamiltonian
N: Number of samples to be used
"""
......
......@@ -16,6 +16,8 @@
# NIFTy is being developed at the Max-Planck-Institut fuer Astrophysik
# and financially supported by the Studienstiftung des deutschen Volkes.
from __future__ import (absolute_import, division, print_function)
from builtins import *
import numpy as np
from ..sugar import from_random
from ..minimization.energy import Energy
......@@ -31,7 +33,7 @@ def _get_acceptable_model(M):
raise ValueError('Initial Model value must be finite')
dir = from_random("normal", M.position.domain)
dirder = M.jacobian(dir)
dir *= val/(dirder).norm()*1e-5
dir = dir * val * (1e-5/dirder.norm())
# Find a step length that leads to a "reasonable" Model
for i in range(50):
try:
......@@ -40,7 +42,7 @@ def _get_acceptable_model(M):
break
except FloatingPointError:
pass
dir *= 0.5
dir = dir*0.5
else:
raise ValueError("could not find a reasonable initial step")
return M2
......@@ -52,7 +54,7 @@ def _get_acceptable_energy(E):
raise ValueError('Initial Energy must be finite')
dir = from_random("normal", E.position.domain)
dirder = E.gradient.vdot(dir)
dir *= np.abs(val)/np.abs(dirder)*1e-5
dir = dir * (np.abs(val)/np.abs(dirder)*1e-5)
# Find a step length that leads to a "reasonable" energy
for i in range(50):
try:
......@@ -61,7 +63,7 @@ def _get_acceptable_energy(E):
break
except FloatingPointError:
pass
dir *= 0.5
dir = dir*0.5
else:
raise ValueError("could not find a reasonable initial step")
return E2
......@@ -92,7 +94,7 @@ def check_value_gradient_consistency(E, tol=1e-8, ntries=100):
xtol = tol*Emid.gradient_norm
if abs(numgrad-dirder) < xtol:
break
dir *= 0.5
dir = dir*0.5
dirnorm *= 0.5
E2 = Emid
else:
......@@ -117,7 +119,7 @@ def check_value_gradient_metric_consistency(E, tol=1e-8, ntries=100):
if abs((E2.value-val)/dirnorm - dirder) < xtol and \
(abs((E2.gradient-E.gradient)/dirnorm-dgrad) < xtol).all():
break
dir *= 0.5
dir = dir*0.5
dirnorm *= 0.5
E2 = Emid
else:
......
......@@ -35,9 +35,9 @@ def adjoint_implementation(op, domain_dtype, target_dtype, atol, rtol):
needed_cap = op.TIMES | op.ADJOINT_TIMES
if (op.capability & needed_cap) != needed_cap:
return
f1 = from_random("normal", op.domain, dtype=domain_dtype).lock()
f2 = from_random("normal", op.target, dtype=target_dtype).lock()
res1 = f1.vdot(op.adjoint_times(f2).lock())
f1 = from_random("normal", op.domain, dtype=domain_dtype)
f2 = from_random("normal", op.target, dtype=target_dtype)
res1 = f1.vdot(op.adjoint_times(f2))
res2 = op.times(f1).vdot(f2)
np.testing.assert_allclose(res1, res2, atol=atol, rtol=rtol)
......@@ -46,12 +46,12 @@ def inverse_implementation(op, domain_dtype, target_dtype, atol, rtol):
needed_cap = op.TIMES | op.INVERSE_TIMES
if (op.capability & needed_cap) != needed_cap:
return
foo = from_random("normal", op.target, dtype=target_dtype).lock()
res = op(op.inverse_times(foo).lock())
foo = from_random("normal", op.target, dtype=target_dtype)
res = op(op.inverse_times(foo))
_assert_allclose(res, foo, atol=atol, rtol=rtol)
foo = from_random("normal", op.domain, dtype=domain_dtype).lock()
res = op.inverse_times(op(foo).lock())
foo = from_random("normal", op.domain, dtype=domain_dtype)
res = op.inverse_times(op(foo))
_assert_allclose(res, foo, atol=atol, rtol=rtol)
......
......@@ -16,8 +16,8 @@
# NIFTy is being developed at the Max-Planck-Institut fuer Astrophysik
# and financially supported by the Studienstiftung des deutschen Volkes.
from __future__ import division
from builtins import range
from __future__ import (absolute_import, division, print_function)
from builtins import *
import numpy as np
from . import utilities
from .domain_tuple import DomainTuple
......@@ -35,7 +35,7 @@ class Field(object):
----------
domain : None, DomainTuple, tuple of Domain, or Domain
val : None, Field, data_object, or scalar
val : Field, data_object or scalar
The values the array should contain after init. A scalar input will
fill the whole array with this scalar. If a data_object is provided,
its dimensions must match the domain's.
......@@ -49,32 +49,30 @@ class Field(object):
many convenience functions for Field conatruction!
"""
def __init__(self, domain=None, val=None, dtype=None, copy=False,
locked=False):
def __init__(self, domain=None, val=None, dtype=None):
self._domain = self._infer_domain(domain=domain, val=val)
dtype = self._infer_dtype(dtype=dtype, val=val)
if isinstance(val, Field):
if self._domain != val._domain:
raise ValueError("Domain mismatch")
self._val = dobj.from_object(val.val, dtype=dtype, copy=copy,
set_locked=locked)
self._val = val._val
elif (np.isscalar(val)):
self._val = dobj.full(self._domain.shape, dtype=dtype,
fill_value=val)
elif isinstance(val, dobj.data_object):
if self._domain.shape == val.shape:
self._val = dobj.from_object(val, dtype=dtype, copy=copy,
set_locked=locked)
if dtype == val.dtype:
self._val = val
else:
self._val = dobj.from_object(val, dtype, True, True)
else:
raise ValueError("Shape mismatch")
elif val is None:
self._val = dobj.empty(self._domain.shape, dtype=dtype)
else:
raise TypeError("unknown source type")
if locked:
dobj.lock(self._val)
dobj.lock(self._val)
# prevent implicit conversion to bool
def __nonzero__(self):
......@@ -84,7 +82,7 @@ class Field(object):
raise TypeError("Field does not support implicit conversion to bool")
@staticmethod
def full(domain, val, dtype=None):
def full(domain, val):
"""Creates a Field with a given domain, filled with a constant value.
Parameters
......@@ -101,11 +99,7 @@ class Field(object):
"""
if not np.isscalar(val):
raise TypeError("val must be a scalar")
return Field(DomainTuple.make(domain), val, dtype)
@staticmethod
def empty(domain, dtype=None):
return Field(DomainTuple.make(domain), None, dtype)
return Field(DomainTuple.make(domain), val)
@staticmethod
def from_global_data(domain, arr, sum_up=False):
......@@ -152,11 +146,6 @@ class Field(object):
Returns a handle to the part of the array data residing on the local
task (or to the entore array if MPI is not active).
Notes
-----
If the field is not locked, the array data can be modified.
Use with care!
"""
return dobj.local_data(self._val)
......@@ -196,8 +185,6 @@ class Field(object):
return dtype
if val is None:
raise ValueError("could not infer dtype")
if isinstance(val, Field):
return val.dtype
return np.result_type(val)
@staticmethod
......@@ -223,41 +210,6 @@ class Field(object):
val=dobj.from_random(random_type, dtype=dtype,
shape=domain.shape, **kwargs))
def fill(self, fill_value):
"""Fill `self` uniformly with `fill_value`
Parameters
----------
fill_value: float or complex or int
The value to fill the field with.
"""
self._val.fill(fill_value)
return self
def lock(self):
"""Write-protect the data content of `self`.
After this call, it will no longer be possible to change the data
entries of `self`. This is convenient if, for example, a
DiagonalOperator wants to ensure that its diagonal cannot be modified
inadvertently, without making copies.
Notes
-----
This will not only prohibit modifications to the entries of `self`, but
also to the entries of any other Field or numpy array pointing to the
same data. If an unlocked instance is needed, use copy().
The fact that there is no `unlock()` method is deliberate.
"""
dobj.lock(self._val)
return self
@property
def locked(self):
"""bool : True iff the field's data content has been locked"""
return dobj.locked(self._val)
@property
def val(self):
"""dobj.data_object : the data object storing the field's entries
......@@ -303,43 +255,6 @@ class Field(object):
raise ValueError(".imag called on a non-complex Field")
return Field(self._domain, self.val.imag)
def copy(self):
""" Returns a full copy of the Field.
The returned object will be an identical copy of the original Field.
The copy will be writeable, even if `self` was locked.
Returns
-------
Field
An identical, but unlocked copy of 'self'.
"""
return Field(val=self, copy=True)
def empty_copy(self):
""" Returns a Field with identical domain and data type, but
uninitialized data.
Returns
-------
Field
A copy of 'self', with uninitialized data.
"""
return Field(self._domain, dtype=self.dtype)
def locked_copy(self):
""" Returns a read-only version of the Field.
If `self` is locked, returns `self`. Otherwise returns a locked copy
of `self`.
Returns
-------
Field
A read-only version of `self`.
"""
return self if self.locked else Field(val=self, copy=True, locked=True)
def scalar_weight(self, spaces=None):
"""Returns the uniform volume element for a sub-domain of `self`.
......@@ -392,7 +307,7 @@ class Field(object):
res *= self._domain[i].total_volume
return res
def weight(self, power=1, spaces=None, out=None):
def weight(self, power=1, spaces=None):
""" Weights the pixels of `self` with their invidual pixel-volume.
Parameters
......@@ -404,21 +319,12 @@ class Field(object):
Determines on which sub-domain the operation takes place.
If None, the entire domain is used.
out : Field or None
if not None, the result is returned in a new Field
otherwise the contents of "out" are overwritten with the result.
"out" may be identical to "self"!
Returns
-------
Field
The weighted field.
"""
if out is None:
out = self.copy()
else:
if out is not self:
out.copy_content_from(self)
aout = self.local_data.copy()
spaces = utilities.parse_spaces(spaces, len(self._domain))
......@@ -435,12 +341,12 @@ class Field(object):
if dobj.distaxis(self._val) >= 0 and ind == 0:
# we need to distribute the weights along axis 0
wgt = dobj.local_data(dobj.from_global_data(wgt))
out.local_data[()] *= wgt**power
aout *= wgt**power
fct = fct**power
if fct != 1.: