summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEdoardo Pasca <edo.paskino@gmail.com>2020-02-21 16:41:57 +0000
committerGitHub <noreply@github.com>2020-02-21 16:41:57 +0000
commitadf4163c145e6ddc16899a92a06c3282f144d88c (patch)
tree1c850418332773b891cb9f3efc4611313be61b36
parent26d16a18000cf96d4540f14b76c0773d6d30bf77 (diff)
downloadframework-adf4163c145e6ddc16899a92a06c3282f144d88c.tar.gz
framework-adf4163c145e6ddc16899a92a06c3282f144d88c.tar.bz2
framework-adf4163c145e6ddc16899a92a06c3282f144d88c.tar.xz
framework-adf4163c145e6ddc16899a92a06c3282f144d88c.zip
Operator composition (#493)
* CompositionOperator and some refactoring * Added SumOperator and CompositionOperator added domain_geometry and optional range_geometry as parameter of Operator. These are saved in _domain_geometry and _range_geometry. Updated all operator and tests to take notice of this change. * fighting with Composition * fix direct and adjoint for CompositeOperator * fix composition Operator * remove target OUTPUT to trigger build * added unit tests * add test for ZeroOperator * fixes * add numba * removed hard coded path * removed comments * fix direct/adjoint with out parameter * removed __rmul__ * use calculate_norm inherited from LinearOperator * removed hard coded version tag * removed cached string version * use add_custom_target instead add_custom_command
-rw-r--r--Wrappers/Python/CMakeLists.txt54
-rw-r--r--Wrappers/Python/ccpi/framework/BlockDataContainer.py2
-rw-r--r--Wrappers/Python/ccpi/optimisation/functions/KullbackLeibler.py140
-rw-r--r--Wrappers/Python/ccpi/optimisation/operators/FiniteDifferenceOperator.py25
-rw-r--r--Wrappers/Python/ccpi/optimisation/operators/GradientOperator.py59
-rw-r--r--Wrappers/Python/ccpi/optimisation/operators/IdentityOperator.py24
-rw-r--r--Wrappers/Python/ccpi/optimisation/operators/LinearOperatorMatrix.py17
-rw-r--r--Wrappers/Python/ccpi/optimisation/operators/Operator.py433
-rw-r--r--Wrappers/Python/ccpi/optimisation/operators/SparseFiniteDiff.py12
-rw-r--r--Wrappers/Python/ccpi/optimisation/operators/SymmetrizedGradientOperator.py42
-rw-r--r--Wrappers/Python/ccpi/optimisation/operators/ZeroOperator.py33
-rwxr-xr-xWrappers/Python/ccpi/optimisation/operators/__init__.py9
-rw-r--r--Wrappers/Python/test/test_BlockOperator.py4
-rw-r--r--Wrappers/Python/test/test_Operator.py264
14 files changed, 932 insertions, 186 deletions
diff --git a/Wrappers/Python/CMakeLists.txt b/Wrappers/Python/CMakeLists.txt
index 79bc912..47c6406 100644
--- a/Wrappers/Python/CMakeLists.txt
+++ b/Wrappers/Python/CMakeLists.txt
@@ -3,7 +3,7 @@ option (BUILD_PYTHON_WRAPPER "Build Python Wrapper" ON)
if (BUILD_PYTHON_WRAPPER)
find_package(PythonInterp REQUIRED)
- set(PYTHON_DEST_DIR "" CACHE PATH "Directory of the Python wrappers")
+ #set(PYTHON_DEST_DIR "" CACHE PATH "Directory of the Python wrappers")
if (PYTHON_DEST_DIR)
set(PYTHON_DEST "${PYTHON_DEST_DIR}")
else()
@@ -34,60 +34,68 @@ if (BUILD_PYTHON_WRAPPER)
if (PYTHONINTERP_FOUND)
message("Python found " ${PYTHON_EXECUTABLE})
set(SETUP_PY_IN "${CMAKE_CURRENT_SOURCE_DIR}/setup-cil.py.in")
- set(SETUP_PY "${CMAKE_CURRENT_BINARY_DIR}/setup.py")
+ set(SETUP_PY "${CMAKE_CURRENT_SOURCE_DIR}/setup.py")
#set(DEPS "${CMAKE_CURRENT_SOURCE_DIR}/module/__init__.py")
- set (DEPS "${CMAKE_BINARY_DIR}/")
- set(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/build/timestamp")
-
- #configure_file(${SETUP_PY_IN} ${SETUP_PY})
-
+ # set (DEPS "${CMAKE_CURRENT_SOURCE_DIR}/ccpi")
+ set(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/timestamp")
+ file(GLOB_RECURSE DEPS ${CMAKE_CURRENT_SOURCE_DIR}/ccpi/*.py )
+
+ # add to add_custom_command DEPENDS the list of python files of the project.
+ # as a hack I remove ${OUTPUT}. This should trigger the new build.
+ file( REMOVE ${OUTPUT} )
+ #file( REMOVE_RECURSE ${CMAKE_CURRENT_BINARY_DIR}/build/ )
+ #message(STATUS "We should have removed the build directory now")
+ #file( COPY ${CMAKE_CURRENT_SOURCE_DIR}/ccpi DESTINATION ${CMAKE_CURRENT_BINARY_DIR} )
+ #file( COPY ${CMAKE_CURRENT_SOURCE_DIR}/test DESTINATION ${CMAKE_CURRENT_BINARY_DIR} )
+ #file( COPY ${CMAKE_CURRENT_SOURCE_DIR}/setup.py DESTINATION ${CMAKE_CURRENT_BINARY_DIR} )
+
if (CONDA_BUILD)
- add_custom_command(OUTPUT ${OUTPUT}
- #COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}
- COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}
+ add_custom_target(pythonsetup ALL
COMMAND ${CMAKE_COMMAND} -E env CIL_VERSION=${CIL_VERSION}
${PYTHON_EXECUTABLE} ${SETUP_PY} -vv install
- #echo "EDO"
- WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
+
+ WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
COMMAND ${CMAKE_COMMAND} -E touch ${OUTPUT}
DEPENDS cilacc)
else()
if (WIN32)
- add_custom_command(OUTPUT ${OUTPUT}
- COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}
+ add_custom_target(pythonsetup ALL
COMMAND ${CMAKE_COMMAND} -E env CIL_VERSION=${CIL_VERSION}
PREFIX=${CMAKE_SOURCE_DIR}/src/
LIBRARY_INC=${CMAKE_SOURCE_DIR}/src/include
LIBRARY_LIB=${CMAKE_BINARY_DIR}/${CMAKE_BUILD_TYPE}
${PYTHON_EXECUTABLE} ${SETUP_PY} build_py
+ WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
COMMAND ${CMAKE_COMMAND} -E touch ${OUTPUT}
DEPENDS cilacc)
else()
- add_custom_command(OUTPUT ${OUTPUT}
- COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}
- COMMAND ${CMAKE_COMMAND} -E env PREFIX=${CMAKE_SOURCE_DIR}/src/
+ add_custom_target(pythonsetup ALL
+ COMMAND ${CMAKE_COMMAND} -E env CIL_VERSION=${CIL_VERSION}
+ PREFIX=${CMAKE_SOURCE_DIR}/src/
LIBRARY_INC=${CMAKE_SOURCE_DIR}/src/include
LIBRARY_LIB=${CMAKE_BINARY_DIR}/
- ${PYTHON_EXECUTABLE} ${SETUP_PY} build_py --build-lib ${CMAKE_CURRENT_BINARY_DIR}/build/lib/
+ ${PYTHON_EXECUTABLE} ${SETUP_PY} build_py --verbose --build-lib=${CMAKE_CURRENT_BINARY_DIR}/build/lib
+ WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
+
COMMAND ${CMAKE_COMMAND} -E touch ${OUTPUT}
- DEPENDS cilacc
+ DEPENDS cilacc
)
endif()
#set (PYTHON_DEST ${CMAKE_INSTALL_PREFIX}/python/)
install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/build/lib/ccpi
-
- DESTINATION ${PYTHON_DEST})
+ DESTINATION ${PYTHON_DEST} )
install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/data/ DESTINATION ${CMAKE_INSTALL_PREFIX}/share/ccpi)
#file(TOUCH ${PYTHON_DEST}/edo/__init__.py)
endif()
- add_custom_target(PythonWrapper ALL DEPENDS ${OUTPUT})
-
+ #add_custom_target(PythonWrapper ALL DEPENDS ${OUTPUT})
+ add_custom_target(PythonWrapper ALL DEPENDS pythonsetup)
+
endif()
endif()
diff --git a/Wrappers/Python/ccpi/framework/BlockDataContainer.py b/Wrappers/Python/ccpi/framework/BlockDataContainer.py
index a0d139b..310132a 100644
--- a/Wrappers/Python/ccpi/framework/BlockDataContainer.py
+++ b/Wrappers/Python/ccpi/framework/BlockDataContainer.py
@@ -261,7 +261,7 @@ class BlockDataContainer(object):
kw['out'] = out.get_item(i)
if operation == BlockDataContainer.AXPBY:
kw['y'] = ot
- el.axpby(kw['a'], kw['b'], kw['y'], kw['out'], kw['dtype'], kw['num_threads'])
+ el.axpby(kw['a'], kw['b'], kw['y'], kw['out'], dtype=kw['dtype'], num_threads=kw['num_threads'])
else:
op(ot, *args, **kw)
else:
diff --git a/Wrappers/Python/ccpi/optimisation/functions/KullbackLeibler.py b/Wrappers/Python/ccpi/optimisation/functions/KullbackLeibler.py
index 39bd983..8fb7ab7 100644
--- a/Wrappers/Python/ccpi/optimisation/functions/KullbackLeibler.py
+++ b/Wrappers/Python/ccpi/optimisation/functions/KullbackLeibler.py
@@ -26,6 +26,66 @@ from ccpi.optimisation.functions import Function
import functools
import scipy.special
+try:
+ import numba
+ from numba import jit, prange
+ has_numba = True
+ '''Some parallelisation of KL calls'''
+ @jit(nopython=True)
+ def kl_proximal(x,b, bnoise, tau, out, eta):
+ for i in prange(x.size):
+ out.flat[i] = 0.5 * (
+ ( x.flat[i] + eta.flat[i] - bnoise.flat[i] - tau ) +\
+ numpy.sqrt( (x.flat[i] + eta.flat[i] + bnoise.flat[i] - tau)**2. + \
+ (4. * tau * b.flat[i])
+ )
+ )
+ @jit(nopython=True)
+ def kl_proximal_conjugate(x, b, bnoise, tau, out):
+ #z = x + tau * self.bnoise
+ #return 0.5*((z + 1) - ((z-1)**2 + 4 * tau * self.b).sqrt())
+
+ for i in prange(x.size):
+ z = x.flat[i] + ( tau * bnoise.flat[i] )
+ out.flat[i] = 0.5 * (
+ (z + 1) - numpy.sqrt((z-1)*(z-1) + 4 * tau * b.flat[i])
+ )
+ @jit(nopython=True)
+ def kl_gradient(x, b, bnoise, out, eta):
+ for i in prange(x.size):
+ out.flat[i] = 1 - b.flat[i]/(x.flat[i] + bnoise.flat[i] + eta.flat[i])
+
+ @jit(nopython=True)
+ def kl_div(x, y, eta):
+ accumulator = 0.
+ for i in prange(x.size):
+ X = x.flat[i]
+ Y = y.flat[i] + eta.flat[i]
+ if X > 0 and Y > 0:
+ # out.flat[i] = X * numpy.log(X/Y) - X + Y
+ accumulator += X * numpy.log(X/Y) - X + Y
+ elif X == 0 and Y >= 0:
+ # out.flat[i] = Y
+ accumulator += Y
+ else:
+ # out.flat[i] = numpy.inf
+ return numpy.inf
+ return accumulator
+
+ # force a jit
+ x = numpy.asarray(numpy.random.random((10,10)), dtype=numpy.float32)
+ b = numpy.asarray(numpy.random.random((10,10)), dtype=numpy.float32)
+ bnoise = numpy.zeros_like(x)
+ eta = numpy.zeros_like(x)
+ out = numpy.empty_like(x)
+ tau = 1.
+ kl_div(b,x,eta)
+ kl_gradient(x,b,bnoise,out, eta)
+ kl_proximal(x,b, bnoise, tau, out, eta)
+ kl_proximal_conjugate(x,b, bnoise, tau, out)
+
+except ImportError as ie:
+ has_numba = False
class KullbackLeibler(Function):
@@ -84,11 +144,14 @@ class KullbackLeibler(Function):
r"""Returns the value of the KullbackLeibler function at :math:`(b, x + \eta)`.
To avoid infinity values, we consider only pixels/voxels for :math:`x+\eta\geq0`.
"""
-
- tmp_sum = (x + self.eta).as_array()
- ind = tmp_sum >= 0
- tmp = scipy.special.kl_div(self.b.as_array()[ind], tmp_sum[ind])
- return numpy.sum(tmp)
+ if has_numba:
+ # tmp = numpy.empty_like(x.as_array())
+ return kl_div(self.b.as_array(), x.as_array(), self.eta.as_array())
+ else:
+ tmp_sum = (x + self.eta).as_array()
+ ind = tmp_sum >= 0
+ tmp = scipy.special.kl_div(self.b.as_array()[ind], tmp_sum[ind])
+ return numpy.sum(tmp)
def log(self, datacontainer):
'''calculates the in-place log of the datacontainer'''
@@ -106,15 +169,26 @@ class KullbackLeibler(Function):
We require the :math:`x+\eta>0` otherwise we have inf values.
"""
-
- tmp_sum_array = (x + self.eta).as_array()
- if out is None:
- tmp_out = x.geometry.allocate()
- tmp_out.as_array()[tmp_sum_array>0] = 1 - self.b.as_array()[tmp_sum_array>0]/tmp_sum_array[tmp_sum_array>0]
- return tmp_out
- else:
- x.add(self.eta, out=out)
- out.as_array()[tmp_sum_array>0] = 1 - self.b.as_array()[tmp_sum_array>0]/tmp_sum_array[tmp_sum_array>0]
+ if has_numba:
+ if out is None:
+ out = (x * 0.)
+ out_np = out.as_array()
+ kl_gradient(x.as_array(), self.b.as_array(), self.bnoise.as_array(), out_np, self.eta.as_array())
+ # out.fill(out_np)
+ return out
+ else:
+ out_np = out.as_array()
+ kl_gradient(x.as_array(), self.b.as_array(), self.bnoise.as_array(), out_np, self.eta.as_array())
+ # out.fill(out_np)
+ else:
+ tmp_sum_array = (x + self.eta).as_array()
+ if out is None:
+ tmp_out = x.geometry.allocate()
+ tmp_out.as_array()[tmp_sum_array>0] = 1 - self.b.as_array()[tmp_sum_array>0]/tmp_sum_array[tmp_sum_array>0]
+ return tmp_out
+ else:
+ x.add(self.eta, out=out)
+ out.as_array()[tmp_sum_array>0] = 1 - self.b.as_array()[tmp_sum_array>0]/tmp_sum_array[tmp_sum_array>0]
def convex_conjugate(self, x):
@@ -142,18 +216,30 @@ class KullbackLeibler(Function):
where :math:`z = x + \tau \eta`
"""
-
- if out is None:
- return 0.5 *( (x - self.eta - tau) + ( (x + self.eta - tau)**2 + 4*tau*self.b ) .sqrt() )
- else:
- x.add(self.eta, out=out)
- out -= tau
- out *= out
- out.add(self.b * (4 * tau), out=out)
- out.sqrt(out=out)
- out.subtract(tau, out = out)
- out.add(x, out=out)
- out *= 0.5
+ if has_numba:
+ if out is None:
+ out = (x * 0.)
+ # out_np = numpy.empty_like(out.as_array(), dtype=numpy.float64)
+ out_np = out.as_array()
+ kl_proximal(x.as_array(), self.b.as_array(), self.bnoise.as_array(), tau, out_np, self.eta.as_array())
+ out.fill(out_np)
+ return out
+ else:
+ out_np = out.as_array()
+ kl_proximal(x.as_array(), self.b.as_array(), self.bnoise.as_array(), tau, out_np, self.eta.as_array())
+ # out.fill(out_np)
+ else:
+ if out is None:
+ return 0.5 *( (x - self.eta - tau) + ( (x + self.eta - tau)**2 + 4*tau*self.b ) .sqrt() )
+ else:
+ x.add(self.eta, out=out)
+ out -= tau
+ out *= out
+ out.add(self.b * (4 * tau), out=out)
+ out.sqrt(out=out)
+ out.subtract(tau, out = out)
+ out.add(x, out=out)
+ out *= 0.5
def proximal_conjugate(self, x, tau, out=None):
@@ -174,7 +260,7 @@ class KullbackLeibler(Function):
self.b.multiply(4*tau, out=out)
- out.add((tmp)**2, out=out)
+ out.add(tmp.power(2), out=out)
out.sqrt(out=out)
out *= -1
tmp += 2
diff --git a/Wrappers/Python/ccpi/optimisation/operators/FiniteDifferenceOperator.py b/Wrappers/Python/ccpi/optimisation/operators/FiniteDifferenceOperator.py
index 1df7745..f8aef7d 100644
--- a/Wrappers/Python/ccpi/optimisation/operators/FiniteDifferenceOperator.py
+++ b/Wrappers/Python/ccpi/optimisation/operators/FiniteDifferenceOperator.py
@@ -56,8 +56,7 @@ class FiniteDiff(LinearOperator):
:type bnd_cond: str, default :code:`Neumann`
'''
- super(FiniteDiff, self).__init__()
-
+
self.gm_domain = gm_domain
self.gm_range = gm_range
@@ -75,6 +74,8 @@ class FiniteDiff(LinearOperator):
#self.voxel_size = kwargs.get('voxel_size',1)
# this wrongly assumes a homogeneous voxel size
# self.voxel_size = self.gm_domain.voxel_size_x
+ super(FiniteDiff, self).__init__(domain_geometry=gm_domain,
+ range_geometry=self.gm_range)
def direct(self, x, out=None):
@@ -350,24 +351,24 @@ class FiniteDiff(LinearOperator):
#else:
# out.fill(outa)
- def range_geometry(self):
+ # def range_geometry(self):
- '''
+ # '''
- Returns the range_geometry of FiniteDiff
+ # Returns the range_geometry of FiniteDiff
- '''
+ # '''
- return self.gm_range
+ # return self.gm_range
- def domain_geometry(self):
+ # def domain_geometry(self):
- '''
+ # '''
- Returns the domain_geometry of FiniteDiff
+ # Returns the domain_geometry of FiniteDiff
- '''
- return self.gm_domain
+ # '''
+ # return self.gm_domain
if __name__ == '__main__':
diff --git a/Wrappers/Python/ccpi/optimisation/operators/GradientOperator.py b/Wrappers/Python/ccpi/optimisation/operators/GradientOperator.py
index a5feca3..fd51873 100644
--- a/Wrappers/Python/ccpi/optimisation/operators/GradientOperator.py
+++ b/Wrappers/Python/ccpi/optimisation/operators/GradientOperator.py
@@ -75,24 +75,29 @@ class Gradient(LinearOperator):
CORRELATION_SPACE = CORRELATION_SPACE
CORRELATION_SPACECHANNEL = CORRELATION_SPACECHANNEL
- def __init__(self, gm_domain, bnd_cond = 'Neumann', **kwargs):
+ def __init__(self, domain_geometry, bnd_cond = 'Neumann', **kwargs):
"""Constructor method
"""
- super(Gradient, self).__init__()
-
+
backend = kwargs.get('backend',C)
correlation = kwargs.get('correlation',CORRELATION_SPACE)
- if correlation == CORRELATION_SPACE and gm_domain.channels > 1:
+ if correlation == CORRELATION_SPACE and domain_geometry.channels > 1:
#numpy implementation only for now
backend = NUMPY
warnings.warn("Warning: correlation='Space' on multi-channel dataset will use `numpy` backend")
if backend == NUMPY:
- self.operator = Gradient_numpy(gm_domain, bnd_cond=bnd_cond, **kwargs)
+ self.operator = Gradient_numpy(domain_geometry, bnd_cond=bnd_cond, **kwargs)
else:
- self.operator = Gradient_C(gm_domain, bnd_cond=bnd_cond, **kwargs)
+ self.operator = Gradient_C(domain_geometry, bnd_cond=bnd_cond, **kwargs)
+
+ super(Gradient, self).__init__(domain_geometry=domain_geometry,
+ range_geometry=self.operator.range_geometry())
+
+ self.gm_range = self.range_geometry()
+ self.gm_domain = self.domain_geometry()
def direct(self, x, out=None):
@@ -120,16 +125,16 @@ class Gradient(LinearOperator):
"""
return self.operator.adjoint(x, out=out)
- def domain_geometry(self):
- '''Returns domain_geometry of Gradient'''
+ # def domain_geometry(self):
+ # '''Returns domain_geometry of Gradient'''
- return self.operator.gm_domain
+ # return self.operator.gm_domain
- def range_geometry(self):
+ # def range_geometry(self):
- '''Returns range_geometry of Gradient'''
+ # '''Returns range_geometry of Gradient'''
- return self.operator.gm_range
+ # return self.operator.gm_range
class Gradient_numpy(LinearOperator):
@@ -143,7 +148,6 @@ class Gradient_numpy(LinearOperator):
:param correlation: optional, :code:`SpaceChannel` or :code:`Space`
:type correlation: str, optional, default :code:`Space`
'''
- super(Gradient_numpy, self).__init__()
self.gm_domain = gm_domain # Domain of Grad Operator
@@ -190,6 +194,10 @@ class Gradient_numpy(LinearOperator):
# Call FiniteDiff operator
self.FD = FiniteDiff(self.gm_domain, direction = 0, bnd_cond = self.bnd_cond)
+
+ super(Gradient_numpy, self).__init__(domain_geometry=self.gm_domain,
+ range_geometry=self.gm_range)
+
print("Initialised GradientOperator with numpy backend")
def direct(self, x, out=None):
@@ -240,14 +248,6 @@ class Gradient_numpy(LinearOperator):
return self.gm_range
- def __rmul__(self, scalar):
-
- '''Multiplication of Gradient with a scalar
-
- Returns: ScaledOperator
- '''
-
- return ScaledOperator(self, scalar)
###########################################################################
############### For preconditioning ######################################
@@ -348,8 +348,6 @@ class Gradient_C(LinearOperator):
def __init__(self, gm_domain, gm_range=None, bnd_cond = NEUMANN, **kwargs):
- super(Gradient_C, self).__init__()
-
self.num_threads = kwargs.get('num_threads',NUM_THREADS)
self.gm_domain = gm_domain
@@ -375,6 +373,9 @@ class Gradient_C(LinearOperator):
else:
raise ValueError('Number of dimensions not supported, expected 2, 3 or 4, got {}'.format(len(gm_domain.shape)))
#self.num_threads
+ # super(Gradient_C, self).__init__()
+ super(Gradient_C, self).__init__(domain_geometry=self.gm_domain,
+ range_geometry=self.gm_range)
print("Initialised GradientOperator with C backend running with ", cilacc.openMPtest(self.num_threads)," threads")
@staticmethod
@@ -418,17 +419,17 @@ class Gradient_C(LinearOperator):
if return_val is True:
return out
- def domain_geometry(self):
+ # def domain_geometry(self):
- '''Returns domain_geometry of Gradient'''
+ # '''Returns domain_geometry of Gradient'''
- return self.gm_domain
+ # return self.gm_domain
- def range_geometry(self):
+ # def range_geometry(self):
- '''Returns range_geometry of Gradient'''
+ # '''Returns range_geometry of Gradient'''
- return self.gm_range
+ # return self.gm_range
if __name__ == '__main__':
diff --git a/Wrappers/Python/ccpi/optimisation/operators/IdentityOperator.py b/Wrappers/Python/ccpi/optimisation/operators/IdentityOperator.py
index a4c6ae3..2c14c5d 100644
--- a/Wrappers/Python/ccpi/optimisation/operators/IdentityOperator.py
+++ b/Wrappers/Python/ccpi/optimisation/operators/IdentityOperator.py
@@ -35,14 +35,14 @@ class Identity(LinearOperator):
'''
- def __init__(self, gm_domain, gm_range=None):
+ def __init__(self, domain_geometry, range_geometry=None):
- self.gm_domain = gm_domain
- self.gm_range = gm_range
- if self.gm_range is None:
- self.gm_range = self.gm_domain
- super(Identity, self).__init__()
+ if range_geometry is None:
+ range_geometry = domain_geometry
+
+ super(Identity, self).__init__(domain_geometry=domain_geometry,
+ range_geometry=range_geometry)
def direct(self,x,out=None):
@@ -68,18 +68,6 @@ class Identity(LinearOperator):
'''Evaluates operator norm of Identity'''
return 1.0
-
- def domain_geometry(self):
-
- '''Returns domain_geometry of Identity'''
-
- return self.gm_domain
-
- def range_geometry(self):
-
- '''Returns range_geometry of Identity'''
-
- return self.gm_range
###########################################################################
diff --git a/Wrappers/Python/ccpi/optimisation/operators/LinearOperatorMatrix.py b/Wrappers/Python/ccpi/optimisation/operators/LinearOperatorMatrix.py
index 6f13532..95bcc92 100644
--- a/Wrappers/Python/ccpi/optimisation/operators/LinearOperatorMatrix.py
+++ b/Wrappers/Python/ccpi/optimisation/operators/LinearOperatorMatrix.py
@@ -39,15 +39,16 @@ class LinearOperatorMatrix(LinearOperator):
'''
self.A = A
M_A, N_A = self.A.shape
- self.gm_domain = VectorGeometry(N_A)
- self.gm_range = VectorGeometry(M_A)
+ domain_geometry = VectorGeometry(N_A)
+ range_geometry = VectorGeometry(M_A)
self.s1 = None # Largest singular value, initially unknown
- super(LinearOperatorMatrix, self).__init__()
+ super(LinearOperatorMatrix, self).__init__(domain_geometry=domain_geometry,
+ range_geometry=range_geometry)
def direct(self,x, out=None):
if out is None:
- tmp = self.gm_range.allocate()
+ tmp = self.range_geometry().allocate()
tmp.fill(numpy.dot(self.A,x.as_array()))
return tmp
else:
@@ -58,7 +59,7 @@ class LinearOperatorMatrix(LinearOperator):
def adjoint(self,x, out=None):
if out is None:
- tmp = self.gm_domain.allocate()
+ tmp = self.domain_geometry().allocate()
tmp.fill(numpy.dot(self.A.transpose(),x.as_array()))
return tmp
else:
@@ -70,8 +71,4 @@ class LinearOperatorMatrix(LinearOperator):
def calculate_norm(self, **kwargs):
# If unknown, compute and store. If known, simply return it.
return svds(self.A,1,return_singular_vectors=False)[0]
-
- def domain_geometry(self):
- return self.gm_domain
- def range_geometry(self):
- return self.gm_range
+
diff --git a/Wrappers/Python/ccpi/optimisation/operators/Operator.py b/Wrappers/Python/ccpi/optimisation/operators/Operator.py
index 14d53e8..d49bc1a 100644
--- a/Wrappers/Python/ccpi/optimisation/operators/Operator.py
+++ b/Wrappers/Python/ccpi/optimisation/operators/Operator.py
@@ -18,13 +18,23 @@
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
-
-from ccpi.optimisation.operators.ScaledOperator import ScaledOperator
+from numbers import Number
+import numpy
+import functools
class Operator(object):
'''Operator that maps from a space X -> Y'''
- def __init__(self, **kwargs):
- self.__norm = None
+ def __init__(self, domain_geometry, **kwargs):
+ r'''
+ Creator
+
+ :param domain_geometry: domain of the operator
+ :param range_geometry: range of the operator
+ :type range_geometry: optional, default None
+ '''
+ self._norm = None
+ self._domain_geometry = domain_geometry
+ self._range_geometry = kwargs.get('range_geometry', None)
def is_linear(self):
'''Returns if the operator is linear'''
@@ -51,20 +61,425 @@ class Operator(object):
:parameter force: forces the recalculation of the norm
:type force: boolean, default :code:`False`
'''
- if self.__norm is None or kwargs.get('force', False):
- self.__norm = self.calculate_norm(**kwargs)
- return self.__norm
+ if self._norm is None or kwargs.get('force', False):
+ self._norm = self.calculate_norm(**kwargs)
+ return self._norm
def calculate_norm(self, **kwargs):
'''Calculates the norm of the Operator'''
raise NotImplementedError
def range_geometry(self):
'''Returns the range of the Operator: Y space'''
- raise NotImplementedError
+ return self._range_geometry
def domain_geometry(self):
'''Returns the domain of the Operator: X space'''
- raise NotImplementedError
+ return self._domain_geometry
def __rmul__(self, scalar):
'''Defines the multiplication by a scalar on the left
returns a ScaledOperator'''
return ScaledOperator(self, scalar)
+
+ def compose(self, *other, **kwargs):
+ # TODO: check equality of domain and range of operators
+ #if self.operator2.range_geometry != self.operator1.domain_geometry:
+ # raise ValueError('Cannot compose operators, check domain geometry of {} and range geometry of {}'.format(self.operato1,self.operator2))
+
+ return CompositionOperator(self, *other, **kwargs)
+
+ def __add__(self, other):
+ return SumOperator(self, other)
+
+ def __mul__(self, scalar):
+ return self.__rmul__(scalar)
+
+ def __neg__(self):
+ """ Return -self """
+ return -1 * self
+
+ def __sub__(self, other):
+ """ Returns the subtraction of the operators."""
+ return self + (-1) * other
+
+
+class LinearOperator(Operator):
+ '''A Linear Operator that maps from a space X <-> Y'''
+ def __init__(self, domain_geometry, **kwargs):
+ super(LinearOperator, self).__init__(domain_geometry, **kwargs)
+ def is_linear(self):
+ '''Returns if the operator is linear'''
+ return True
+ def adjoint(self,x, out=None):
+ '''returns the adjoint/inverse operation
+
+ only available to linear operators'''
+ raise NotImplementedError
+
+ @staticmethod
+ def PowerMethod(operator, iterations, x_init=None):
+ '''Power method to calculate iteratively the Lipschitz constant
+
+ :param operator: input operator
+ :type operator: :code:`LinearOperator`
+ :param iterations: number of iterations to run
+ :type iteration: int
+ :param x_init: starting point for the iteration in the operator domain
+ :returns: tuple with: L, list of L at each iteration, the data the iteration worked on.
+ '''
+
+ # Initialise random
+ if x_init is None:
+ x0 = operator.domain_geometry().allocate('random')
+ else:
+ x0 = x_init.copy()
+
+ x1 = operator.domain_geometry().allocate()
+ y_tmp = operator.range_geometry().allocate()
+ s = numpy.zeros(iterations)
+ # Loop
+ for it in numpy.arange(iterations):
+ operator.direct(x0,out=y_tmp)
+ operator.adjoint(y_tmp,out=x1)
+ x1norm = x1.norm()
+ if hasattr(x0, 'squared_norm'):
+ s[it] = x1.dot(x0) / x0.squared_norm()
+ else:
+ x0norm = x0.norm()
+ s[it] = x1.dot(x0) / (x0norm * x0norm)
+ x1.multiply((1.0/x1norm), out=x0)
+ return numpy.sqrt(s[-1]), numpy.sqrt(s), x0
+
+ def calculate_norm(self, **kwargs):
+ '''Returns the norm of the LinearOperator as calculated by the PowerMethod
+
+ :param iterations: number of iterations to run
+ :type iterations: int, optional, default = 25
+ :param x_init: starting point for the iteration in the operator domain
+ :type x_init: same type as domain, a subclass of :code:`DataContainer`, optional, None
+ :parameter force: forces the recalculation of the norm
+ :type force: boolean, default :code:`False`
+ '''
+ x0 = kwargs.get('x_init', None)
+ iterations = kwargs.get('iterations', 25)
+ s1, sall, svec = LinearOperator.PowerMethod(self, iterations, x_init=x0)
+ return s1
+
+ @staticmethod
+ def dot_test(operator, domain_init=None, range_init=None, verbose=False, **kwargs):
+ r'''Does a dot linearity test on the operator
+
+ Evaluates if the following equivalence holds
+
+ .. math::
+
+ Ax\times y = y \times A^Tx
+
+ The equivalence is tested within a user specified precision
+
+ .. code::
+
+ abs(desired-actual) < 1.5 * 10**(-decimal)
+
+ :param operator: operator to test
+ :param range_init: optional initialisation container in the operator range
+ :param domain_init: optional initialisation container in the operator domain
+ :returns: boolean, True if the test is passed.
+ :param decimal: desired precision
+ :type decimal: int, optional, default 4
+ '''
+ if range_init is None:
+ y = operator.range_geometry().allocate('random_int')
+ else:
+ y = range_init
+ if domain_init is None:
+ x = operator.domain_geometry().allocate('random_int')
+ else:
+ x = domain_init
+
+ fx = operator.direct(x)
+ by = operator.adjoint(y)
+ a = fx.dot(y)
+ b = by.dot(x)
+ if verbose:
+ print ('Left hand side {}, \nRight hand side {}'.format(a, b))
+ print ("decimal ", kwargs.get('decimal', 4))
+ try:
+ numpy.testing.assert_almost_equal(abs((a-b)/a), 0., decimal=kwargs.get('decimal',4))
+ return True
+ except AssertionError as ae:
+ print (ae)
+ return False
+
+
+class ScaledOperator(Operator):
+
+
+ '''ScaledOperator
+
+ A class to represent the scalar multiplication of an Operator with a scalar.
+ It holds an operator and a scalar. Basically it returns the multiplication
+ of the result of direct and adjoint of the operator with the scalar.
+ For the rest it behaves like the operator it holds.
+
+ :param operator: a Operator or LinearOperator
+ :param scalar: a scalar multiplier
+
+ Example:
+ The scaled operator behaves like the following:
+
+ .. code-block:: python
+
+ sop = ScaledOperator(operator, scalar)
+ sop.direct(x) = scalar * operator.direct(x)
+ sop.adjoint(x) = scalar * operator.adjoint(x)
+ sop.norm() = operator.norm()
+ sop.range_geometry() = operator.range_geometry()
+ sop.domain_geometry() = operator.domain_geometry()
+
+ '''
+
+ def __init__(self, operator, scalar, **kwargs):
+ '''creator
+
+ :param operator: a Operator or LinearOperator
+ :param scalar: a scalar multiplier
+ :type scalar: float'''
+
+ super(ScaledOperator, self).__init__(domain_geometry=operator.domain_geometry(),
+ range_geometry=operator.range_geometry())
+ if not isinstance (scalar, Number):
+ raise TypeError('expected scalar: got {}'.format(type(scalar)))
+ self.scalar = scalar
+ self.operator = operator
+ def direct(self, x, out=None):
+ '''direct method'''
+ if out is None:
+ return self.scalar * self.operator.direct(x, out=out)
+ else:
+ self.operator.direct(x, out=out)
+ out *= self.scalar
+ def adjoint(self, x, out=None):
+ '''adjoint method'''
+ if self.operator.is_linear():
+ if out is None:
+ return self.scalar * self.operator.adjoint(x, out=out)
+ else:
+ self.operator.adjoint(x, out=out)
+ out *= self.scalar
+ else:
+ raise TypeError('Operator is not linear')
+ def norm(self, **kwargs):
+ '''norm of the operator'''
+ return numpy.abs(self.scalar) * self.operator.norm(**kwargs)
+ # def range_geometry(self):
+ # '''range of the operator'''
+ # return self.operator.range_geometry()
+ # def domain_geometry(self):
+ # '''domain of the operator'''
+ # return self.operator.domain_geometry()
+ def is_linear(self):
+ '''returns whether the operator is linear
+
+ :returns: boolean '''
+ return self.operator.is_linear()
+
+
+###############################################################################
+################ SumOperator ###########################################
+###############################################################################
+
+class SumOperator(Operator):
+
+
+ def __init__(self, operator1, operator2):
+
+ self.operator1 = operator1
+ self.operator2 = operator2
+
+ # if self.operator1.domain_geometry() != self.operator2.domain_geometry():
+ # raise ValueError('Domain geometry of {} is not equal with domain geometry of {}'.format(self.operator1.__class__.__name__,self.operator2.__class__.__name__))
+
+ # if self.operator1.range_geometry() != self.operator2.range_geometry():
+ # raise ValueError('Range geometry of {} is not equal with range geometry of {}'.format(self.operator1.__class__.__name__,self.operator2.__class__.__name__))
+
+ self.linear_flag = self.operator1.is_linear() and self.operator2.is_linear()
+
+ super(SumOperator, self).__init__(domain_geometry=self.operator1.domain_geometry(),
+ range_geometry=self.operator1.range_geometry())
+
+ def direct(self, x, out=None):
+
+ if out is None:
+ return self.operator1.direct(x) + self.operator2.direct(x)
+ else:
+ #TODO check if allcating with tmp is faster
+ self.operator1.direct(x, out=out)
+ out.add(self.operator2.direct(x), out=out)
+
+ def adjoint(self, x, out=None):
+
+ if self.linear_flag:
+ if out is None:
+ return self.operator1.adjoint(x) + self.operator2.adjoint(x)
+ else:
+ #TODO check if allcating with tmp is faster
+ self.operator1.adjoint(x, out=out)
+ out.add(self.operator2.adjoint(x), out=out)
+ else:
+ raise ValueError('No adjoint operation with non-linear operators')
+
+ def is_linear(self):
+ return self.linear_flag
+
+ def calculate_norm(self, **kwargs):
+ if self.is_linear():
+ return LinearOperator.calculate_norm(self, **kwargs)
+
+###############################################################################
+################ Composition ###########################################
+###############################################################################
+
+class Composition2Operator(Operator):
+
+ def __init__(self, operator1, operator2):
+
+ self.operator1 = operator1
+ self.operator2 = operator2
+
+ self.linear_flag = self.operator1.is_linear() and self.operator2.is_linear()
+
+ if self.operator2.range_geometry() != self.operator1.domain_geometry():
+ raise ValueError('Domain geometry of {} is not equal with range geometry of {}'.format(self.operator1.__class__.__name__,self.operator2.__class__.__name__))
+
+ super(Composition2Operator, self).__init__(self.operator1.domain_geometry(),
+ self.operator2.range_geometry())
+
+ def direct(self, x, out = None):
+
+ if out is None:
+ return self.operator1.direct(self.operator2.direct(x))
+ else:
+ tmp = self.operator2.range_geometry().allocate()
+ self.operator2.direct(x, out = tmp)
+ self.operator1.direct(tmp, out = out)
+
+ def adjoint(self, x, out = None):
+
+ if self.linear_flag:
+
+ if out is None:
+ return self.operator2.adjoint(self.operator1.adjoint(x))
+ else:
+
+ tmp = self.operator1.domain_geometry().allocate()
+ self.operator1.adjoint(x, out=tmp)
+ self.operator2.adjoint(tmp, out=out)
+ else:
+ raise ValueError('No adjoint operation with non-linear operators')
+
+
+ def is_linear(self):
+ return self.linear_flag
+
+ def calculate_norm(self, **kwargs):
+ if self.is_linear():
+ return LinearOperator.calculate_norm(self, **kwargs)
+
+class CompositionOperator(Operator):
+
+ def __init__(self, *operators, **kwargs):
+
+ # get a reference to the operators
+ self.operators = operators
+
+ self.linear_flag = functools.reduce(lambda x,y: x and y.is_linear(),
+ self.operators, True)
+ self.preallocate = kwargs.get('preallocate', False)
+ if self.preallocate:
+ self.tmp_domain = [op.domain_geometry().allocate() for op in self.operators[:-1]]
+ self.tmp_range = [op.range_geometry().allocate() for op in self.operators[1:]]
+ # pass
+
+ # TODO address the equality of geometries
+ # if self.operator2.range_geometry() != self.operator1.domain_geometry():
+ # raise ValueError('Domain geometry of {} is not equal with range geometry of {}'.format(self.operator1.__class__.__name__,self.operator2.__class__.__name__))
+
+ super(CompositionOperator, self).__init__(
+ domain_geometry=self.operators[-1].domain_geometry(),
+ range_geometry=self.operators[0].range_geometry())
+
+ def direct(self, x, out = None):
+
+ if out is None:
+ #return self.operator1.direct(self.operator2.direct(x))
+ # return functools.reduce(lambda X,operator: operator.direct(X),
+ # self.operators[::-1][1:],
+ # self.operators[-1].direct(x))
+ for i,operator in enumerate(self.operators[::-1]):
+ if i == 0:
+ step = operator.direct(x)
+ else:
+ step = operator.direct(step)
+ return step
+
+ else:
+ # tmp = self.operator2.range_geometry().allocate()
+ # self.operator2.direct(x, out = tmp)
+ # self.operator1.direct(tmp, out = out)
+
+ # out.fill (
+ # functools.reduce(lambda X,operator: operator.direct(X),
+ # self.operators[::-1][1:],
+ # self.operators[-1].direct(x))
+ # )
+
+ # TODO this is a bit silly but will handle the pre allocation later
+
+ for i,operator in enumerate(self.operators[::-1]):
+ if i == 0:
+ operator.direct(x, out=self.tmp_range[i])
+ elif i == len(self.operators) - 1:
+ operator.direct(self.tmp_range[i-1], out=out)
+ else:
+ operator.direct(self.tmp_range[i-1], out=self.tmp_range[i])
+
+
+ def adjoint(self, x, out = None):
+
+ if self.linear_flag:
+
+ if out is not None:
+ # return self.operator2.adjoint(self.operator1.adjoint(x))
+ # return functools.reduce(lambda X,operator: operator.adjoint(X),
+ # self.operators[1:],
+ # self.operators[0].adjoint(x))
+
+ for i,operator in enumerate(self.operators):
+ if i == 0:
+ operator.adjoint(x, out=self.tmp_domain[i])
+ elif i == len(self.operators) - 1:
+ step = operator.adjoint(self.tmp_domain[i-1], out=out)
+ else:
+ operator.adjoint(self.tmp_domain[i-1], out=self.tmp_domain[i])
+ return
+
+ else:
+ for i,operator in enumerate(self.operators):
+ if i == 0:
+ step = operator.adjoint(x)
+ else:
+ step = operator.adjoint(step)
+
+ return step
+ else:
+ raise ValueError('No adjoint operation with non-linear operators')
+
+
+ def is_linear(self):
+ return self.linear_flag
+
+ def calculate_norm(self, **kwargs):
+ if self.is_linear():
+ return LinearOperator.calculate_norm(self, **kwargs)
+
+
+
diff --git a/Wrappers/Python/ccpi/optimisation/operators/SparseFiniteDiff.py b/Wrappers/Python/ccpi/optimisation/operators/SparseFiniteDiff.py
index 747db65..310d4e8 100644
--- a/Wrappers/Python/ccpi/optimisation/operators/SparseFiniteDiff.py
+++ b/Wrappers/Python/ccpi/optimisation/operators/SparseFiniteDiff.py
@@ -29,16 +29,16 @@ class SparseFiniteDiff(object):
'''Create Sparse Matrices for the Finite Difference Operator'''
- def __init__(self, gm_domain, gm_range=None, direction=0, bnd_cond = 'Neumann'):
+ def __init__(self, domain_geometry, range_geometry=None,
+ direction=0, bnd_cond = 'Neumann'):
- super(SparseFiniteDiff, self).__init__()
- self.gm_domain = gm_domain
- self.gm_range = gm_range
+ super(SparseFiniteDiff, self).__init__(domain_geometry=domain_geometry,
+ range_geometry=range_geometry)
self.direction = direction
self.bnd_cond = bnd_cond
- if self.gm_range is None:
- self.gm_range = self.gm_domain
+ if self.range_geometry is None:
+ self.range_geometry = self.domain_geometry
self.get_dims = [i for i in gm_domain.shape]
diff --git a/Wrappers/Python/ccpi/optimisation/operators/SymmetrizedGradientOperator.py b/Wrappers/Python/ccpi/optimisation/operators/SymmetrizedGradientOperator.py
index 6fd2e86..9c2300f 100644
--- a/Wrappers/Python/ccpi/optimisation/operators/SymmetrizedGradientOperator.py
+++ b/Wrappers/Python/ccpi/optimisation/operators/SymmetrizedGradientOperator.py
@@ -48,33 +48,36 @@ class SymmetrizedGradient(LinearOperator):
CORRELATION_SPACE = "Space"
CORRELATION_SPACECHANNEL = "SpaceChannels"
- def __init__(self, gm_domain, bnd_cond = 'Neumann', **kwargs):
+ def __init__(self, domain_geometry, bnd_cond = 'Neumann', **kwargs):
'''creator
- :param gm_domain: domain of the operator
+ :param domain_geometry: domain of the operator
:param bnd_cond: boundary condition, either :code:`Neumann` or :code:`Periodic`.
:type bnd_cond: str, optional, default :code:`Neumann`
:param correlation: :code:`SpaceChannel` or :code:`Channel`
:type correlation: str, optional, default :code:`Channel`
'''
- super(SymmetrizedGradient, self).__init__()
+ # super(SymmetrizedGradient, self).__init__(domain_geometry=domain_geometry)
- self.gm_domain = gm_domain
self.bnd_cond = bnd_cond
self.correlation = kwargs.get('correlation',SymmetrizedGradient.CORRELATION_SPACE)
- tmp_gm = len(self.gm_domain.geometries)*self.gm_domain.geometries
+ tmp_gm = len(domain_geometry.geometries)*domain_geometry.geometries
- self.gm_range = BlockGeometry(*tmp_gm)
# Define FD operator. We need one geometry from the BlockGeometry of the domain
- self.FD = FiniteDiff(self.gm_domain.get_item(0), direction = 0, bnd_cond = self.bnd_cond)
+ self.FD = FiniteDiff(domain_geometry.get_item(0), direction = 0,
+ bnd_cond = self.bnd_cond)
- if self.gm_domain.shape[0]==2:
+ if domain_geometry.shape[0]==2:
self.order_ind = [0,2,1,3]
else:
self.order_ind = [0,3,6,1,4,7,2,5,8]
-
+
+ super(SymmetrizedGradient, self).__init__(
+ domain_geometry=domain_geometry,
+ range_geometry=BlockGeometry(*tmp_gm))
+
def direct(self, x, out=None):
@@ -83,7 +86,7 @@ class SymmetrizedGradient(LinearOperator):
if out is None:
tmp = []
- for i in range(self.gm_domain.shape[0]):
+ for i in range(self.domain_geometry().shape[0]):
for j in range(x.shape[0]):
self.FD.direction = i
tmp.append(self.FD.adjoint(x.get_item(j)))
@@ -97,7 +100,7 @@ class SymmetrizedGradient(LinearOperator):
else:
ind = 0
- for i in range(self.gm_domain.shape[0]):
+ for i in range(self.domain_geometry().shape[0]):
for j in range(x.shape[0]):
self.FD.direction = i
self.FD.adjoint(x.get_item(j), out=out[ind])
@@ -110,12 +113,12 @@ class SymmetrizedGradient(LinearOperator):
if out is None:
- tmp = [None]*self.gm_domain.shape[0]
+ tmp = [None]*self.domain_geometry().shape[0]
i = 0
- for k in range(self.gm_domain.shape[0]):
+ for k in range(self.domain_geometry().shape[0]):
tmp1 = 0
- for j in range(self.gm_domain.shape[0]):
+ for j in range(self.domain_geometry().shape[0]):
self.FD.direction = j
tmp1 += self.FD.direct(x[i])
i+=1
@@ -125,23 +128,18 @@ class SymmetrizedGradient(LinearOperator):
else:
- tmp = self.gm_domain.allocate()
+ tmp = self.domain_geometry().allocate()
i = 0
- for k in range(self.gm_domain.shape[0]):
+ for k in range(self.domain_geometry().shape[0]):
tmp1 = 0
- for j in range(self.gm_domain.shape[0]):
+ for j in range(self.domain_geometry().shape[0]):
self.FD.direction = j
self.FD.direct(x[i], out=tmp[j])
i+=1
tmp1+=tmp[j]
out[k].fill(tmp1)
-
- def domain_geometry(self):
- return self.gm_domain
- def range_geometry(self):
- return self.gm_range
if __name__ == '__main__':
diff --git a/Wrappers/Python/ccpi/optimisation/operators/ZeroOperator.py b/Wrappers/Python/ccpi/optimisation/operators/ZeroOperator.py
index 0feafd8..bc4f08e 100644
--- a/Wrappers/Python/ccpi/optimisation/operators/ZeroOperator.py
+++ b/Wrappers/Python/ccpi/optimisation/operators/ZeroOperator.py
@@ -41,14 +41,12 @@ class ZeroOperator(LinearOperator):
'''
- def __init__(self, gm_domain, gm_range=None):
-
- super(ZeroOperator, self).__init__()
+ def __init__(self, domain_geometry, range_geometry=None):
+ if range_geometry is None:
+ range_geometry = domain_geometry.clone()
+ super(ZeroOperator, self).__init__(domain_geometry=domain_geometry,
+ range_geometry=range_geometry)
- self.gm_domain = gm_domain
- self.gm_range = gm_range
- if self.gm_range is None:
- self.gm_range = self.gm_domain
def direct(self,x,out=None):
@@ -57,18 +55,18 @@ class ZeroOperator(LinearOperator):
if out is None:
- return self.gm_range.allocate()
+ return self.range_geometry().allocate()
else:
- out.fill(self.gm_range.allocate())
+ out.fill(self.range_geometry.allocate())
def adjoint(self,x, out=None):
'''Returns O^{*}(y)'''
if out is None:
- return self.gm_domain.allocate()
+ return self.domain_geometry().allocate()
else:
- out.fill(self.gm_domain.allocate())
+ out.fill(self.domain_geometry().allocate())
def calculate_norm(self, **kwargs):
@@ -76,15 +74,4 @@ class ZeroOperator(LinearOperator):
return 0
- def domain_geometry(self):
-
- '''Returns domain_geometry of ZeroOperator'''
-
-
- return self.gm_domain
-
- def range_geometry(self):
-
- '''Returns domain_geometry of ZeroOperator'''
-
- return self.gm_range
+ \ No newline at end of file
diff --git a/Wrappers/Python/ccpi/optimisation/operators/__init__.py b/Wrappers/Python/ccpi/optimisation/operators/__init__.py
index 15c8932..cc25c92 100755
--- a/Wrappers/Python/ccpi/optimisation/operators/__init__.py
+++ b/Wrappers/Python/ccpi/optimisation/operators/__init__.py
@@ -16,11 +16,12 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-from .Operator import Operator
-from .LinearOperator import LinearOperator
-from .ScaledOperator import ScaledOperator
+from .Operator import Operator, LinearOperator, ScaledOperator, SumOperator,\
+ CompositionOperator, Composition2Operator
+#from .LinearOperator import LinearOperator
+#from .ScaledOperator import ScaledOperator
from .BlockOperator import BlockOperator
-from .BlockScaledOperator import BlockScaledOperator
+# from .BlockScaledOperator import BlockScaledOperator
from .SparseFiniteDiff import SparseFiniteDiff
from .ShrinkageOperator import ShrinkageOperator
diff --git a/Wrappers/Python/test/test_BlockOperator.py b/Wrappers/Python/test/test_BlockOperator.py
index d0f568b..2f33f08 100644
--- a/Wrappers/Python/test/test_BlockOperator.py
+++ b/Wrappers/Python/test/test_BlockOperator.py
@@ -81,8 +81,8 @@ class TestBlockOperator(unittest.TestCase):
ImageGeometry(10,22,31) , \
ImageGeometry(10,20,31) ]
x = [ g.allocate() for g in ig ]
- ops = [ Identity(g, gm_range=r) for g,r in zip(ig, rg0) ]
- ops += [ Identity(g, gm_range=r) for g,r in zip(ig, rg1) ]
+ ops = [ Identity(g, range_geometry=r) for g,r in zip(ig, rg0) ]
+ ops += [ Identity(g, range_geometry=r) for g,r in zip(ig, rg1) ]
K = BlockOperator(*ops, shape=(2,3))
print ("K col comp? " , K.column_wise_compatible())
diff --git a/Wrappers/Python/test/test_Operator.py b/Wrappers/Python/test/test_Operator.py
index 57dd41f..8312c6d 100644
--- a/Wrappers/Python/test/test_Operator.py
+++ b/Wrappers/Python/test/test_Operator.py
@@ -23,7 +23,12 @@ import numpy
from timeit import default_timer as timer
from ccpi.optimisation.operators import Gradient, Identity, SparseFiniteDiff
from ccpi.optimisation.operators import LinearOperator, LinearOperatorMatrix
+import numpy
+from ccpi.optimisation.operators import SumOperator, Gradient,\
+ ZeroOperator, SymmetrizedGradient, CompositionOperator
+from ccpi.framework import TestData
+import os
def dt(steps):
return steps[-1] - steps[-2]
@@ -221,6 +226,10 @@ class TestOperator(CCPiTestClass):
for n in [norm, norm2, norm3, norm4, norm5]:
print ("norm {}", format(n))
+
+
+
+
class TestGradients(CCPiTestClass):
def setUp(self):
@@ -339,6 +348,7 @@ class TestGradients(CCPiTestClass):
+
class TestBlockOperator(unittest.TestCase):
def assertBlockDataContainerEqual(self, container1, container2):
print ("assert Block Data Container Equal")
@@ -645,6 +655,260 @@ class TestBlockOperator(unittest.TestCase):
u1 = B.adjoint(w)
self.assertEqual((w * w1).sum() , (u1*u).sum())
+class TestOperatorCompositionSum(unittest.TestCase):
+ def setUp(self):
+
+ self.data = TestData().load(TestData.BOAT, size=(128,128))
+ self.ig = self.data.geometry
+
+ def test_SumOperator(self):
+
+ # numpy.random.seed(1)
+ ig = self.ig
+ data = self.data
+
+ Id1 = 2 * Identity(ig)
+ Id2 = Identity(ig)
+ # z = ZeroOperator(domain_geometry=ig)
+ # sym = SymmetrizedGradient(domain_geometry=ig)
+
+ c = SumOperator(Id1,Id2)
+ out = c.direct(data)
+
+ numpy.testing.assert_array_almost_equal(out.as_array(),3 * data.as_array())
+
+
+ def test_CompositionOperator_direct1(self):
+ ig = self.ig
+ data = self.data
+ G = Gradient(domain_geometry=ig)
+
+
+ Id1 = 2 * Identity(ig)
+ Id2 = Identity(ig)
+
+ d = CompositionOperator(G, Id2)
+
+ out1 = G.direct(data)
+ out2 = d.direct(data)
+
+
+ numpy.testing.assert_array_almost_equal(out2.get_item(0).as_array(), out1.get_item(0).as_array())
+ numpy.testing.assert_array_almost_equal(out2.get_item(1).as_array(), out1.get_item(1).as_array())
+
+ def test_CompositionOperator_direct2(self):
+ ig = self.ig
+ data = self.data
+ G = Gradient(domain_geometry=ig)
+
+
+ Id1 = 2 * Identity(ig)
+ Id2 = Identity(ig)
+
+ d = CompositionOperator(G, Id2)
+
+ out1 = G.direct(data)
+
+ d_out = d.direct(data)
+
+ d1 = Id2.direct(data)
+ d2 = G.direct(d1)
+
+ numpy.testing.assert_array_almost_equal(d2.get_item(0).as_array(),
+ d_out.get_item(0).as_array())
+ numpy.testing.assert_array_almost_equal(d2.get_item(1).as_array(),
+ d_out.get_item(1).as_array())
+
+ def test_CompositionOperator_direct3(self):
+ ig = self.ig
+ data = self.data
+ G = Gradient(domain_geometry=ig)
+
+
+ Id1 = 2 * Identity(ig)
+ Id2 = Identity(ig)
+
+ d = CompositionOperator(G, Id2)
+
+ out1 = G.direct(data)
+
+ d_out = d.direct(data)
+
+ d1 = Id2.direct(data)
+ d2 = G.direct(d1)
+
+ numpy.testing.assert_array_almost_equal(d2.get_item(0).as_array(),
+ d_out.get_item(0).as_array())
+ numpy.testing.assert_array_almost_equal(d2.get_item(1).as_array(),
+ d_out.get_item(1).as_array())
+
+ G2Id = G.compose(2*Id2)
+ d2g = G2Id.direct(data)
+
+ numpy.testing.assert_array_almost_equal(d2g.get_item(0).as_array(),
+ 2 * d_out.get_item(0).as_array())
+ numpy.testing.assert_array_almost_equal(d2g.get_item(1).as_array(),
+ 2 * d_out.get_item(1).as_array())
+
+ def test_CompositionOperator_direct4(self):
+ ig = self.ig
+ data = self.data
+ G = Gradient(domain_geometry=ig)
+
+
+ Id1 = 2 * Identity(ig)
+ Id2 = Identity(ig)
+
+ d = CompositionOperator(G, Id1, Id2)
+
+ out1 = G.direct(data)
+
+ d_out = d.direct(data)
+
+ numpy.testing.assert_array_almost_equal(d_out.get_item(0).as_array(),
+ 2 * out1.get_item(0).as_array())
+ numpy.testing.assert_array_almost_equal(d_out.get_item(1).as_array(),
+ 2 * out1.get_item(1).as_array())
+
+ def test_CompositionOperator_adjoint1(self):
+ ig = self.ig
+ data = self.data
+ G = Gradient(domain_geometry=ig)
+
+
+ Id1 = 2 * Identity(ig)
+ Id2 = Identity(ig)
+
+ d = CompositionOperator(G, Id2)
+ da = d.direct(data)
+
+ out1 = G.adjoint(da)
+ out2 = d.adjoint(da)
+
+ numpy.testing.assert_array_almost_equal(out2.as_array(), out1.as_array())
+
+ def test_CompositionOperator_adjoint2(self):
+ ig = self.ig
+ data = self.data
+ G = Gradient(domain_geometry=ig)
+
+
+ Id1 = 2 * Identity(ig)
+ Id2 = Identity(ig)
+
+ d = CompositionOperator(G, Id1)
+ da = d.direct(data)
+
+ out1 = G.adjoint(da)
+ out2 = d.adjoint(da)
+
+ numpy.testing.assert_array_almost_equal(out2.as_array(), 2 * out1.as_array())
+ def test_CompositionOperator_adjoint3(self):
+ ig = self.ig
+ data = self.data
+ G = Gradient(domain_geometry=ig)
+
+
+ Id1 = 2 * Identity(ig)
+ Id2 = Identity(ig)
+
+ d = G.compose(Id1)
+ da = d.direct(data)
+
+ out1 = G.adjoint(da)
+ out2 = d.adjoint(da)
+
+ numpy.testing.assert_array_almost_equal(out2.as_array(), 2 * out1.as_array())
+
+
+ def test_CompositionOperator_adjoint4(self):
+ ig = self.ig
+ data = self.data
+ G = Gradient(domain_geometry=ig)
+
+
+ Id1 = 2 * Identity(ig)
+
+ d = G.compose(-Id1)
+ da = d.direct(data)
+
+ out1 = G.adjoint(da)
+ out2 = d.adjoint(da)
+
+ numpy.testing.assert_array_almost_equal(out2.as_array(), - 2 * out1.as_array())
+ def test_CompositionOperator_adjoint5(self):
+ ig = self.ig
+ data = self.data
+ G = Gradient(domain_geometry=ig)
+
+
+ Id1 = 3 * Identity(ig)
+ Id = Id1 - Identity(ig)
+ d = G.compose(Id)
+ da = d.direct(data)
+
+ out1 = G.adjoint(da)
+ out2 = d.adjoint(da)
+
+ numpy.testing.assert_array_almost_equal(out2.as_array(), 2 * out1.as_array())
+
+ def test_CompositionOperator_adjoint6(self):
+ ig = self.ig
+ data = self.data
+ G = Gradient(domain_geometry=ig)
+
+
+ Id1 = 3 * Identity(ig)
+ Id = ZeroOperator(ig)
+ d = G.compose(Id)
+ da = d.direct(data)
+
+ out1 = G.adjoint(da)
+ out2 = d.adjoint(da)
+
+ numpy.testing.assert_array_almost_equal(out2.as_array(), 0 * out1.as_array())
+
+ def stest_CompositionOperator_direct4(self):
+ ig = self.ig
+ data = self.data
+ G = Gradient(domain_geometry=ig)
+
+
+ sym = SymmetrizedGradient(domain_geometry=ig)
+ Id2 = Identity(ig)
+
+ d = CompositionOperator(sym, Id2)
+
+ out1 = G.direct(data)
+ out2 = d.direct(data)
+
+
+ numpy.testing.assert_array_almost_equal(out2.get_item(0).as_array(), out1.get_item(0).as_array())
+ numpy.testing.assert_array_almost_equal(out2.get_item(1).as_array(), out1.get_item(1).as_array())
+
+ def test_CompositionOperator_adjoint7(self):
+ ig = self.ig
+ data = self.data
+ G = Gradient(domain_geometry=ig)
+
+
+ Id1 = 2 * Identity(ig)
+ Id2 = Identity(ig)
+
+ d = CompositionOperator(G, Id1, Id2)
+
+ out1 = G.direct(data)
+ out2 = G.adjoint(out1)
+
+ d_out = d.adjoint(out1)
+
+ numpy.testing.assert_array_almost_equal(d_out.as_array(),
+ 2 * out2.as_array())
+ numpy.testing.assert_array_almost_equal(d_out.as_array(),
+ 2 * out2.as_array())
+
+
+