summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEdoardo Pasca <edo.paskino@gmail.com>2019-06-14 13:45:59 +0100
committerGitHub <noreply@github.com>2019-06-14 13:45:59 +0100
commit8ef0e4372624b4a19a7ad69626949760b938eb99 (patch)
tree2cf6e46c707e05ee1280eea092dd03e1f8ffe8e6
parentd108cd7e9652992001a116347941eda7e75b3301 (diff)
parent33140f96f2482701f044f1de65f9dfcc48a1a7f5 (diff)
downloadframework-8ef0e4372624b4a19a7ad69626949760b938eb99.tar.gz
framework-8ef0e4372624b4a19a7ad69626949760b938eb99.tar.bz2
framework-8ef0e4372624b4a19a7ad69626949760b938eb99.tar.xz
framework-8ef0e4372624b4a19a7ad69626949760b938eb99.zip
Merge pull request #207 from vais-ral/composite_operator_datacontainer
Block operator and datacontainer
-rw-r--r--NOTICE.txt15
-rw-r--r--README.md8
-rw-r--r--Wrappers/Python/ccpi/contrib/__init__.py0
-rw-r--r--Wrappers/Python/ccpi/contrib/optimisation/__init__.py0
-rw-r--r--Wrappers/Python/ccpi/contrib/optimisation/algorithms/__init__.py0
-rwxr-xr-xWrappers/Python/ccpi/contrib/optimisation/algorithms/spdhg.py (renamed from Wrappers/Python/ccpi/optimisation/spdhg.py)0
-rwxr-xr-xWrappers/Python/ccpi/framework/BlockDataContainer.py496
-rwxr-xr-xWrappers/Python/ccpi/framework/BlockGeometry.py86
-rwxr-xr-xWrappers/Python/ccpi/framework/TestData.py121
-rwxr-xr-xWrappers/Python/ccpi/framework/Vector.py96
-rwxr-xr-xWrappers/Python/ccpi/framework/__init__.py28
-rwxr-xr-x[-rw-r--r--]Wrappers/Python/ccpi/framework/framework.py (renamed from Wrappers/Python/ccpi/framework.py)743
-rw-r--r--Wrappers/Python/ccpi/io/NEXUSDataReader.py158
-rw-r--r--Wrappers/Python/ccpi/io/NEXUSDataWriter.py164
-rw-r--r--Wrappers/Python/ccpi/io/NikonDataReader.py281
-rw-r--r--Wrappers/Python/ccpi/io/__init__.py6
-rw-r--r--Wrappers/Python/ccpi/io/reader.py21
-rwxr-xr-xWrappers/Python/ccpi/optimisation/algorithms/Algorithm.py73
-rwxr-xr-xWrappers/Python/ccpi/optimisation/algorithms/CGLS.py55
-rw-r--r--Wrappers/Python/ccpi/optimisation/algorithms/FBPD.py86
-rwxr-xr-xWrappers/Python/ccpi/optimisation/algorithms/FISTA.py123
-rwxr-xr-xWrappers/Python/ccpi/optimisation/algorithms/GradientDescent.py17
-rw-r--r--Wrappers/Python/ccpi/optimisation/algorithms/PDHG.py186
-rw-r--r--Wrappers/Python/ccpi/optimisation/algorithms/SIRT.py76
-rw-r--r--Wrappers/Python/ccpi/optimisation/algorithms/__init__.py4
-rwxr-xr-xWrappers/Python/ccpi/optimisation/algs.py319
-rwxr-xr-xWrappers/Python/ccpi/optimisation/funcs.py302
-rw-r--r--Wrappers/Python/ccpi/optimisation/functions/BlockFunction.py223
-rw-r--r--Wrappers/Python/ccpi/optimisation/functions/Function.py69
-rw-r--r--Wrappers/Python/ccpi/optimisation/functions/FunctionOperatorComposition.py110
-rw-r--r--Wrappers/Python/ccpi/optimisation/functions/FunctionOperatorComposition_old.py85
-rwxr-xr-xWrappers/Python/ccpi/optimisation/functions/IndicatorBox.py134
-rw-r--r--Wrappers/Python/ccpi/optimisation/functions/KullbackLeibler.py231
-rw-r--r--Wrappers/Python/ccpi/optimisation/functions/L1Norm.py162
-rw-r--r--Wrappers/Python/ccpi/optimisation/functions/L2NormSquared.py286
-rwxr-xr-xWrappers/Python/ccpi/optimisation/functions/MixedL21Norm.py159
-rwxr-xr-xWrappers/Python/ccpi/optimisation/functions/Norm2Sq.py97
-rwxr-xr-xWrappers/Python/ccpi/optimisation/functions/ScaledFunction.py151
-rw-r--r--Wrappers/Python/ccpi/optimisation/functions/ZeroFunction.py55
-rw-r--r--Wrappers/Python/ccpi/optimisation/functions/__init__.py13
-rwxr-xr-xWrappers/Python/ccpi/optimisation/operators/BlockOperator.py417
-rw-r--r--Wrappers/Python/ccpi/optimisation/operators/BlockScaledOperator.py67
-rw-r--r--Wrappers/Python/ccpi/optimisation/operators/FiniteDifferenceOperator.py367
-rw-r--r--Wrappers/Python/ccpi/optimisation/operators/GradientOperator.py267
-rw-r--r--Wrappers/Python/ccpi/optimisation/operators/IdentityOperator.py79
-rwxr-xr-xWrappers/Python/ccpi/optimisation/operators/LinearOperator.py55
-rw-r--r--Wrappers/Python/ccpi/optimisation/operators/LinearOperatorMatrix.py41
-rwxr-xr-xWrappers/Python/ccpi/optimisation/operators/Operator.py38
-rw-r--r--Wrappers/Python/ccpi/optimisation/operators/ScaledOperator.py51
-rw-r--r--Wrappers/Python/ccpi/optimisation/operators/ShrinkageOperator.py19
-rw-r--r--Wrappers/Python/ccpi/optimisation/operators/SparseFiniteDiff.py144
-rw-r--r--Wrappers/Python/ccpi/optimisation/operators/SymmetrizedGradientOperator.py233
-rw-r--r--Wrappers/Python/ccpi/optimisation/operators/ZeroOperator.py44
-rwxr-xr-xWrappers/Python/ccpi/optimisation/operators/__init__.py23
-rwxr-xr-xWrappers/Python/ccpi/optimisation/ops.py289
-rwxr-xr-xWrappers/Python/ccpi/processors/CenterOfRotationFinder.py (renamed from Wrappers/Python/ccpi/processors.py)920
-rwxr-xr-xWrappers/Python/ccpi/processors/Normalizer.py124
-rwxr-xr-xWrappers/Python/ccpi/processors/Resizer.py262
-rwxr-xr-xWrappers/Python/ccpi/processors/__init__.py10
-rw-r--r--Wrappers/Python/conda-recipe/conda_build_config.yaml4
-rw-r--r--Wrappers/Python/conda-recipe/meta.yaml5
-rw-r--r--Wrappers/Python/data/24737_fd_normalised.nxsbin0 -> 7873212 bytes
-rw-r--r--Wrappers/Python/data/boat.tiffbin0 -> 262278 bytes
-rw-r--r--Wrappers/Python/data/camera.pngbin0 -> 114228 bytes
-rw-r--r--Wrappers/Python/data/peppers.tiffbin0 -> 786572 bytes
-rwxr-xr-xWrappers/Python/data/resolution_chart.tiffbin0 -> 65670 bytes
-rw-r--r--Wrappers/Python/data/shapes.pngbin0 -> 19339 bytes
-rw-r--r--Wrappers/Python/data/test_show_data.py30
-rw-r--r--Wrappers/Python/demos/CGLS_examples/CGLS_Tikhonov.py106
-rw-r--r--Wrappers/Python/demos/CompareAlgorithms/CGLS_FISTA_PDHG_LeastSquares.py189
-rw-r--r--Wrappers/Python/demos/CompareAlgorithms/CGLS_PDHG_Tikhonov.py133
-rw-r--r--Wrappers/Python/demos/CompareAlgorithms/FISTA_vs_PDHG.py124
-rw-r--r--Wrappers/Python/demos/FISTA_examples/FISTA_CGLS.py215
-rw-r--r--Wrappers/Python/demos/FISTA_examples/FISTA_Tikhonov_Poisson_Denoising.py201
-rw-r--r--Wrappers/Python/demos/PDHG_examples/ColorbayDemo.py273
-rwxr-xr-xWrappers/Python/demos/PDHG_examples/GatherAll/PDHG_TGV_Denoising.py282
-rwxr-xr-xWrappers/Python/demos/PDHG_examples/GatherAll/PDHG_TV_Denoising.py280
-rw-r--r--Wrappers/Python/demos/PDHG_examples/GatherAll/PDHG_TV_Denoising_2D_time.py243
-rw-r--r--Wrappers/Python/demos/PDHG_examples/GatherAll/PDHG_TV_Denoising_Gaussian_3D.py147
-rw-r--r--Wrappers/Python/demos/PDHG_examples/GatherAll/PDHG_TV_Tomo2D.py173
-rw-r--r--Wrappers/Python/demos/PDHG_examples/GatherAll/PDHG_Tikhonov_Denoising.py258
-rwxr-xr-xWrappers/Python/demos/PDHG_examples/GatherAll/phantom.matbin0 -> 5583 bytes
-rw-r--r--Wrappers/Python/demos/PDHG_examples/IMATDemo.py339
-rw-r--r--Wrappers/Python/demos/PDHG_examples/MultiChannel/PDHG_2D_time_denoising.py169
-rw-r--r--Wrappers/Python/demos/PDHG_examples/MultiChannel/PDHG_TV_Tomo2D_time.py169
-rw-r--r--Wrappers/Python/demos/PDHG_examples/PDHG_TV_Color_Denoising.py115
-rw-r--r--Wrappers/Python/demos/PDHG_examples/TV_Denoising/PDHG_TV_Denoising_2D_time.py192
-rw-r--r--Wrappers/Python/demos/PDHG_examples/TV_Denoising/PDHG_TV_Denoising_Gaussian_3D.py181
-rw-r--r--Wrappers/Python/demos/PDHG_examples/Tomo/PDHG_Tikhonov_Tomo2D.py156
-rw-r--r--Wrappers/Python/demos/pdhg_TV_tomography2Dccpi.py238
-rw-r--r--Wrappers/Python/environment.yml11
-rw-r--r--Wrappers/Python/setup.py18
-rwxr-xr-xWrappers/Python/test/test_BlockDataContainer.py468
-rw-r--r--Wrappers/Python/test/test_BlockOperator.py359
-rwxr-xr-xWrappers/Python/test/test_DataContainer.py173
-rwxr-xr-xWrappers/Python/test/test_DataProcessor.py24
-rwxr-xr-xWrappers/Python/test/test_Gradient.py101
-rwxr-xr-xWrappers/Python/test/test_NexusReader.py26
-rw-r--r--Wrappers/Python/test/test_Operator.py466
-rwxr-xr-xWrappers/Python/test/test_algorithms.py39
-rw-r--r--Wrappers/Python/test/test_functions.py367
-rwxr-xr-xWrappers/Python/test/test_run_test.py105
-rw-r--r--Wrappers/Python/wip/CGLS_tikhonov.py196
-rw-r--r--Wrappers/Python/wip/CreatePhantom.py242
-rw-r--r--Wrappers/Python/wip/Demos/FISTA_vs_CGLS.py119
-rw-r--r--Wrappers/Python/wip/Demos/FISTA_vs_PDHG.py120
-rw-r--r--Wrappers/Python/wip/Demos/IMAT_Reconstruction/TV_WhiteBeam_reconstruction.py164
-rw-r--r--Wrappers/Python/wip/Demos/IMAT_Reconstruction/golden_angles.txt186
-rw-r--r--Wrappers/Python/wip/Demos/LeastSq_CGLS_FISTA_PDHG.py154
-rw-r--r--Wrappers/Python/wip/Demos/PDHG_TGV_Denoising_SaltPepper.py194
-rw-r--r--Wrappers/Python/wip/Demos/PDHG_TV_Denoising_Gaussian_DiagPrecond.py208
-rw-r--r--Wrappers/Python/wip/Demos/PDHG_TV_Tomo2D.py245
-rw-r--r--Wrappers/Python/wip/Demos/PDHG_TV_Tomo2D_time.py169
-rw-r--r--Wrappers/Python/wip/Demos/PDHG_Tikhonov_Tomo2D.py108
-rw-r--r--Wrappers/Python/wip/Demos/PDHG_vs_CGLS.py127
-rw-r--r--Wrappers/Python/wip/Demos/check_blockOperator_sum_row_cols.py89
-rw-r--r--Wrappers/Python/wip/Demos/check_precond.py182
-rw-r--r--Wrappers/Python/wip/compare_CGLS_algos.py133
-rw-r--r--Wrappers/Python/wip/demo_SIRT.py205
-rw-r--r--Wrappers/Python/wip/demo_box_constraints_FISTA.py158
-rw-r--r--Wrappers/Python/wip/demo_colourbay.py2
-rwxr-xr-xWrappers/Python/wip/fix_test.py208
-rw-r--r--Wrappers/Python/wip/old_demos/demo_colourbay.py137
-rw-r--r--Wrappers/Python/wip/old_demos/demo_compare_cvx.py306
-rwxr-xr-xWrappers/Python/wip/old_demos/demo_gradient_descent.py295
-rw-r--r--Wrappers/Python/wip/old_demos/demo_imat_multichan_RGLTK.py151
-rw-r--r--Wrappers/Python/wip/old_demos/demo_imat_whitebeam.py138
-rwxr-xr-xWrappers/Python/wip/old_demos/demo_memhandle.py193
-rw-r--r--Wrappers/Python/wip/old_demos/demo_test_sirt.py (renamed from Wrappers/Python/wip/demo_test_sirt.py)0
-rwxr-xr-xWrappers/Python/wip/old_demos/multifile_nexus.py307
-rw-r--r--Wrappers/Python/wip/pdhg_TV_denoising_precond.py156
131 files changed, 17893 insertions, 2067 deletions
diff --git a/NOTICE.txt b/NOTICE.txt
new file mode 100644
index 0000000..c107329
--- /dev/null
+++ b/NOTICE.txt
@@ -0,0 +1,15 @@
+CCPi Core Imaging Library (CIL).
+Copyright 2017 Rutherford Appleton Laboratory STFC
+Copyright 2017 University of Manchester
+
+This software product is developed for the Collaborative Computational
+Project in Tomographic Imaging CCPi (http://www.ccpi.ac.uk/) at RAL STFC (http://www.stfc.ac.uk), University of Manchester
+and other contributing institutions.
+
+Main contributors in alphabetical order:
+Evelina Ametova (UoM)
+Jakob Jorgensen (UoM)
+Daniil Kazantsev (Diamond Light Source)
+Srikanth Nagella (STFC)
+Edoardo Pasca (STFC)
+Evangelos Papoutsellis (UoM)
diff --git a/README.md b/README.md
index 3c360f0..e28bdfe 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,7 @@ Some concepts are so much overlapping with the CCPPETMR project that we have cho
This package consists of the following Python modules:
1. `ccpi.framework`
2. `ccpi.optimisation`
+3. `ccpi.io`
### `ccpi.framework`
@@ -25,6 +26,7 @@ In `ccpi.framework` we define a number of common classes normally used in tomogr
* `DataSetProcessor`
* `ImageData`
* `AcquisitionData`
+ * `BlockDataContainer`
#### `DataContainer`
Generic class to hold data. Currently the data is currently held in a numpy arrays, but we are currently planning to create a `GPUDataContainer` and `BoostDataContainer` which will hold the data in an array on GPU or in a boost multidimensional array respectively.
@@ -79,9 +81,9 @@ In `ccpi.framework` we define a number of common classes normally used in tomogr
Fixed parameters can be passed in during the creation of the `function` object.
The methods of the `function` reflect the properties of it, for example,
if the function represented is differentiable
- the `function` should contain a method `grad` which should return the gradient of the function evaluated at
+ the `function` should contain a method `gradient` which should return the gradient of the function evaluated at
an input point. If the function is not differentiable but allows a simple proximal operator, the method
- `prox` should return the proxial operator evaluated at an input point. The function value
+ `proximal` should return the proxial operator evaluated at an input point. The function value
is evaluated by calling the function itself, e.g. `f(x)` for a `function`
`f` and input point `x`.
@@ -91,7 +93,7 @@ In `ccpi.framework` we define a number of common classes normally used in tomogr
is designed for a particular generic optimisation problem accepts and number of `Function`s and/or
`Operator`s as input to define a specific instance of the generic optimisation problem to be solved.
- They are iterable objects which can be run in a `for` loop. The user can provide a stopping cryterion different than the default max_iteration.
+ They are iterable objects which can be run in a `for` loop. The user can provide a stopping criterion different than the default max_iteration. `Algorithm`s provide a courtesy method `run(number_of_iterations, verbose)` which allows the user to easily run a `number_of_iterations` and receive a print to screen.
New algorithms can be easily created by extending the `Algorithm` class. The user is required to implement only 4 methods: `set_up`, `__init__`, `update` and `update_objective`.
diff --git a/Wrappers/Python/ccpi/contrib/__init__.py b/Wrappers/Python/ccpi/contrib/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/Wrappers/Python/ccpi/contrib/__init__.py
diff --git a/Wrappers/Python/ccpi/contrib/optimisation/__init__.py b/Wrappers/Python/ccpi/contrib/optimisation/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/Wrappers/Python/ccpi/contrib/optimisation/__init__.py
diff --git a/Wrappers/Python/ccpi/contrib/optimisation/algorithms/__init__.py b/Wrappers/Python/ccpi/contrib/optimisation/algorithms/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/Wrappers/Python/ccpi/contrib/optimisation/algorithms/__init__.py
diff --git a/Wrappers/Python/ccpi/optimisation/spdhg.py b/Wrappers/Python/ccpi/contrib/optimisation/algorithms/spdhg.py
index 263a7cd..263a7cd 100755
--- a/Wrappers/Python/ccpi/optimisation/spdhg.py
+++ b/Wrappers/Python/ccpi/contrib/optimisation/algorithms/spdhg.py
diff --git a/Wrappers/Python/ccpi/framework/BlockDataContainer.py b/Wrappers/Python/ccpi/framework/BlockDataContainer.py
new file mode 100755
index 0000000..670e214
--- /dev/null
+++ b/Wrappers/Python/ccpi/framework/BlockDataContainer.py
@@ -0,0 +1,496 @@
+ # -*- coding: utf-8 -*-
+"""
+Created on Tue Mar 5 16:04:45 2019
+
+@author: ofn77899
+"""
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+import numpy
+from numbers import Number
+import functools
+from ccpi.framework import DataContainer
+#from ccpi.framework import AcquisitionData, ImageData
+#from ccpi.optimisation.operators import Operator, LinearOperator
+
+class BlockDataContainer(object):
+ '''Class to hold DataContainers as column vector
+
+ Provides basic algebra between BlockDataContainer's, DataContainer's and
+ subclasses and Numbers
+
+ 1) algebra between `BlockDataContainer`s will be element-wise, only if
+ the shape of the 2 `BlockDataContainer`s is the same, otherwise it
+ will fail
+ 2) algebra between `BlockDataContainer`s and `list` or `numpy array` will
+ work as long as the number of `rows` and element of the arrays match,
+ indipendently on the fact that the `BlockDataContainer` could be nested
+ 3) algebra between `BlockDataContainer` and one `DataContainer` is possible.
+ It will require that all the `DataContainers` in the block to be
+ compatible with the `DataContainer` we want to algebra with. Should we
+ require that the `DataContainer` is the same type? Like `ImageData` or `AcquisitionData`?
+ 4) algebra between `BlockDataContainer` and a `Number` is possible and it
+ will be done with each element of the `BlockDataContainer` even if nested
+
+ A = [ [B,C] , D]
+ A * 3 = [ 3 * [B,C] , 3* D] = [ [ 3*B, 3*C] , 3*D ]
+
+ '''
+ ADD = 'add'
+ SUBTRACT = 'subtract'
+ MULTIPLY = 'multiply'
+ DIVIDE = 'divide'
+ POWER = 'power'
+ __array_priority__ = 1
+ __container_priority__ = 2
+ def __init__(self, *args, **kwargs):
+ ''''''
+ self.containers = args
+ self.index = 0
+ shape = kwargs.get('shape', None)
+ if shape is None:
+ shape = (len(args),1)
+# shape = (len(args),1)
+ self.shape = shape
+
+ n_elements = functools.reduce(lambda x,y: x*y, shape, 1)
+ if len(args) != n_elements:
+ raise ValueError(
+ 'Dimension and size do not match: expected {} got {}'
+ .format(n_elements, len(args)))
+
+
+ def __iter__(self):
+ '''BlockDataContainer is Iterable'''
+ return self
+ def next(self):
+ '''python2 backwards compatibility'''
+ return self.__next__()
+ def __next__(self):
+ try:
+ out = self[self.index]
+ except IndexError as ie:
+ raise StopIteration()
+ self.index+=1
+ return out
+
+ def is_compatible(self, other):
+ '''basic check if the size of the 2 objects fit'''
+
+ if isinstance(other, Number):
+ return True
+ elif isinstance(other, (list, numpy.ndarray)) :
+ for ot in other:
+ if not isinstance(ot, (Number,\
+ numpy.int, numpy.int8, numpy.int16, numpy.int32, numpy.int64,\
+ numpy.float, numpy.float16, numpy.float32, numpy.float64, \
+ numpy.complex)):
+ raise ValueError('List/ numpy array can only contain numbers {}'\
+ .format(type(ot)))
+ return len(self.containers) == len(other)
+ elif issubclass(other.__class__, DataContainer):
+ ret = True
+ for i, el in enumerate(self.containers):
+ if isinstance(el, BlockDataContainer):
+ a = el.is_compatible(other)
+ else:
+ a = el.shape == other.shape
+ ret = ret and a
+ return ret
+ #return self.get_item(0).shape == other.shape
+ return len(self.containers) == len(other.containers)
+
+ def get_item(self, row):
+ if row > self.shape[0]:
+ raise ValueError('Requested row {} > max {}'.format(row, self.shape[0]))
+ return self.containers[row]
+
+ def __getitem__(self, row):
+ return self.get_item(row)
+
+ def add(self, other, *args, **kwargs):
+ '''Algebra: add method of BlockDataContainer with number/DataContainer or BlockDataContainer
+
+ :param: other (number, DataContainer or subclasses or BlockDataContainer
+ :param: out (optional): provides a placehold for the resul.
+ '''
+ out = kwargs.get('out', None)
+ if out is not None:
+ self.binary_operations(BlockDataContainer.ADD, other, *args, **kwargs)
+ else:
+ return self.binary_operations(BlockDataContainer.ADD, other, *args, **kwargs)
+ def subtract(self, other, *args, **kwargs):
+ '''Algebra: subtract method of BlockDataContainer with number/DataContainer or BlockDataContainer
+
+ :param: other (number, DataContainer or subclasses or BlockDataContainer
+ :param: out (optional): provides a placehold for the resul.
+ '''
+ out = kwargs.get('out', None)
+ if out is not None:
+ self.binary_operations(BlockDataContainer.SUBTRACT, other, *args, **kwargs)
+ else:
+ return self.binary_operations(BlockDataContainer.SUBTRACT, other, *args, **kwargs)
+ def multiply(self, other, *args, **kwargs):
+ '''Algebra: multiply method of BlockDataContainer with number/DataContainer or BlockDataContainer
+
+ :param: other (number, DataContainer or subclasses or BlockDataContainer
+ :param: out (optional): provides a placehold for the resul.
+ '''
+ out = kwargs.get('out', None)
+ if out is not None:
+ self.binary_operations(BlockDataContainer.MULTIPLY, other, *args, **kwargs)
+ else:
+ return self.binary_operations(BlockDataContainer.MULTIPLY, other, *args, **kwargs)
+ def divide(self, other, *args, **kwargs):
+ '''Algebra: divide method of BlockDataContainer with number/DataContainer or BlockDataContainer
+
+ :param: other (number, DataContainer or subclasses or BlockDataContainer
+ :param: out (optional): provides a placehold for the resul.
+ '''
+ out = kwargs.get('out', None)
+ if out is not None:
+ self.binary_operations(BlockDataContainer.DIVIDE, other, *args, **kwargs)
+ else:
+ return self.binary_operations(BlockDataContainer.DIVIDE, other, *args, **kwargs)
+
+
+ def binary_operations(self, operation, other, *args, **kwargs):
+ '''Algebra: generic method of algebric operation with BlockDataContainer with number/DataContainer or BlockDataContainer
+
+ Provides commutativity with DataContainer and subclasses, i.e. this
+ class's reverse algebric methods take precedence w.r.t. direct algebric
+ methods of DataContainer and subclasses.
+
+ This method is not to be used directly
+ '''
+ if not self.is_compatible(other):
+ raise ValueError('Incompatible for divide')
+ out = kwargs.get('out', None)
+ if isinstance(other, Number) or issubclass(other.__class__, DataContainer):
+ # try to do algebra with one DataContainer. Will raise error if not compatible
+ kw = kwargs.copy()
+ res = []
+ for i,el in enumerate(self.containers):
+ if operation == BlockDataContainer.ADD:
+ op = el.add
+ elif operation == BlockDataContainer.SUBTRACT:
+ op = el.subtract
+ elif operation == BlockDataContainer.MULTIPLY:
+ op = el.multiply
+ elif operation == BlockDataContainer.DIVIDE:
+ op = el.divide
+ elif operation == BlockDataContainer.POWER:
+ op = el.power
+ else:
+ raise ValueError('Unsupported operation', operation)
+ if out is not None:
+ kw['out'] = out.get_item(i)
+ op(other, *args, **kw)
+ else:
+ res.append(op(other, *args, **kw))
+ if out is not None:
+ return
+ else:
+ return type(self)(*res, shape=self.shape)
+ elif isinstance(other, (list, numpy.ndarray, BlockDataContainer)):
+ # try to do algebra with one DataContainer. Will raise error if not compatible
+ kw = kwargs.copy()
+ res = []
+ if isinstance(other, BlockDataContainer):
+ the_other = other.containers
+ else:
+ the_other = other
+ for i,zel in enumerate(zip ( self.containers, the_other) ):
+ el = zel[0]
+ ot = zel[1]
+ if operation == BlockDataContainer.ADD:
+ op = el.add
+ elif operation == BlockDataContainer.SUBTRACT:
+ op = el.subtract
+ elif operation == BlockDataContainer.MULTIPLY:
+ op = el.multiply
+ elif operation == BlockDataContainer.DIVIDE:
+ op = el.divide
+ elif operation == BlockDataContainer.POWER:
+ op = el.power
+ else:
+ raise ValueError('Unsupported operation', operation)
+ if out is not None:
+ kw['out'] = out.get_item(i)
+ op(ot, *args, **kw)
+ else:
+ res.append(op(ot, *args, **kw))
+ if out is not None:
+ return
+ else:
+ return type(self)(*res, shape=self.shape)
+ return type(self)(*[ operation(ot, *args, **kwargs) for el,ot in zip(self.containers,other)], shape=self.shape)
+ else:
+ raise ValueError('Incompatible type {}'.format(type(other)))
+
+
+ def power(self, other, *args, **kwargs):
+ if not self.is_compatible(other):
+ raise ValueError('Incompatible for power')
+ out = kwargs.get('out', None)
+ if isinstance(other, Number):
+ return type(self)(*[ el.power(other, *args, **kwargs) for el in self.containers], shape=self.shape)
+ elif isinstance(other, list) or isinstance(other, numpy.ndarray):
+ return type(self)(*[ el.power(ot, *args, **kwargs) for el,ot in zip(self.containers,other)], shape=self.shape)
+ return type(self)(*[ el.power(ot, *args, **kwargs) for el,ot in zip(self.containers,other.containers)], shape=self.shape)
+
+ def maximum(self,other, *args, **kwargs):
+ if not self.is_compatible(other):
+ raise ValueError('Incompatible for maximum')
+ out = kwargs.get('out', None)
+ if isinstance(other, Number):
+ return type(self)(*[ el.maximum(other, *args, **kwargs) for el in self.containers], shape=self.shape)
+ elif isinstance(other, list) or isinstance(other, numpy.ndarray):
+ return type(self)(*[ el.maximum(ot, *args, **kwargs) for el,ot in zip(self.containers,other)], shape=self.shape)
+ return type(self)(*[ el.maximum(ot, *args, **kwargs) for el,ot in zip(self.containers,other.containers)], shape=self.shape)
+
+
+ def minimum(self,other, *args, **kwargs):
+ if not self.is_compatible(other):
+ raise ValueError('Incompatible for maximum')
+ out = kwargs.get('out', None)
+ if isinstance(other, Number):
+ return type(self)(*[ el.minimum(other, *args, **kwargs) for el in self.containers], shape=self.shape)
+ elif isinstance(other, list) or isinstance(other, numpy.ndarray):
+ return type(self)(*[ el.minimum(ot, *args, **kwargs) for el,ot in zip(self.containers,other)], shape=self.shape)
+ return type(self)(*[ el.minimum(ot, *args, **kwargs) for el,ot in zip(self.containers,other.containers)], shape=self.shape)
+
+
+ ## unary operations
+ def abs(self, *args, **kwargs):
+ return type(self)(*[ el.abs(*args, **kwargs) for el in self.containers], shape=self.shape)
+ def sign(self, *args, **kwargs):
+ return type(self)(*[ el.sign(*args, **kwargs) for el in self.containers], shape=self.shape)
+ def sqrt(self, *args, **kwargs):
+ return type(self)(*[ el.sqrt(*args, **kwargs) for el in self.containers], shape=self.shape)
+ def conjugate(self, out=None):
+ return type(self)(*[el.conjugate() for el in self.containers], shape=self.shape)
+
+ ## reductions
+
+ def sum(self, *args, **kwargs):
+ return numpy.sum([ el.sum(*args, **kwargs) for el in self.containers])
+
+ def squared_norm(self):
+ y = numpy.asarray([el.squared_norm() for el in self.containers])
+ return y.sum()
+
+
+ def norm(self):
+ return numpy.sqrt(self.squared_norm())
+
+ def pnorm(self, p=2):
+
+ if p==1:
+ return sum(self.abs())
+ elif p==2:
+ return sum([el*el for el in self.containers]).sqrt()
+ else:
+ return ValueError('Not implemented')
+
+ def copy(self):
+ '''alias of clone'''
+ return self.clone()
+ def clone(self):
+ return type(self)(*[el.copy() for el in self.containers], shape=self.shape)
+ def fill(self, other):
+ if isinstance (other, BlockDataContainer):
+ if not self.is_compatible(other):
+ raise ValueError('Incompatible containers')
+ for el,ot in zip(self.containers, other.containers):
+ el.fill(ot)
+ else:
+ return ValueError('Cannot fill with object provided {}'.format(type(other)))
+
+ def __add__(self, other):
+ return self.add( other )
+ # __radd__
+
+ def __sub__(self, other):
+ return self.subtract( other )
+ # __rsub__
+
+ def __mul__(self, other):
+ return self.multiply(other)
+ # __rmul__
+
+ def __div__(self, other):
+ return self.divide(other)
+ # __rdiv__
+ def __truediv__(self, other):
+ return self.divide(other)
+
+ def __pow__(self, other):
+ return self.power(other)
+ # reverse operand
+ def __radd__(self, other):
+ '''Reverse addition
+
+ to make sure that this method is called rather than the __mul__ of a numpy array
+ the class constant __array_priority__ must be set > 0
+ https://docs.scipy.org/doc/numpy-1.15.1/reference/arrays.classes.html#numpy.class.__array_priority__
+ '''
+ return self + other
+ # __radd__
+
+ def __rsub__(self, other):
+ '''Reverse subtraction
+
+ to make sure that this method is called rather than the __mul__ of a numpy array
+ the class constant __array_priority__ must be set > 0
+ https://docs.scipy.org/doc/numpy-1.15.1/reference/arrays.classes.html#numpy.class.__array_priority__
+ '''
+ return (-1 * self) + other
+ # __rsub__
+
+ def __rmul__(self, other):
+ '''Reverse multiplication
+
+ to make sure that this method is called rather than the __mul__ of a numpy array
+ the class constant __array_priority__ must be set > 0
+ https://docs.scipy.org/doc/numpy-1.15.1/reference/arrays.classes.html#numpy.class.__array_priority__
+ '''
+ return self * other
+ # __rmul__
+
+ def __rdiv__(self, other):
+ '''Reverse division
+
+ to make sure that this method is called rather than the __mul__ of a numpy array
+ the class constant __array_priority__ must be set > 0
+ https://docs.scipy.org/doc/numpy-1.15.1/reference/arrays.classes.html#numpy.class.__array_priority__
+ '''
+ return pow(self / other, -1)
+ # __rdiv__
+ def __rtruediv__(self, other):
+ '''Reverse truedivision
+
+ to make sure that this method is called rather than the __mul__ of a numpy array
+ the class constant __array_priority__ must be set > 0
+ https://docs.scipy.org/doc/numpy-1.15.1/reference/arrays.classes.html#numpy.class.__array_priority__
+ '''
+ return self.__rdiv__(other)
+
+ def __rpow__(self, other):
+ '''Reverse power
+
+ to make sure that this method is called rather than the __mul__ of a numpy array
+ the class constant __array_priority__ must be set > 0
+ https://docs.scipy.org/doc/numpy-1.15.1/reference/arrays.classes.html#numpy.class.__array_priority__
+ '''
+ return other.power(self)
+
+ def __iadd__(self, other):
+ '''Inline addition'''
+ if isinstance (other, BlockDataContainer):
+ for el,ot in zip(self.containers, other.containers):
+ el += ot
+ elif isinstance(other, Number):
+ for el in self.containers:
+ el += other
+ elif isinstance(other, list) or isinstance(other, numpy.ndarray):
+ if not self.is_compatible(other):
+ raise ValueError('Incompatible for __iadd__')
+ for el,ot in zip(self.containers, other):
+ el += ot
+ return self
+ # __iadd__
+
+ def __isub__(self, other):
+ '''Inline subtraction'''
+ if isinstance (other, BlockDataContainer):
+ for el,ot in zip(self.containers, other.containers):
+ el -= ot
+ elif isinstance(other, Number):
+ for el in self.containers:
+ el -= other
+ elif isinstance(other, list) or isinstance(other, numpy.ndarray):
+ if not self.is_compatible(other):
+ raise ValueError('Incompatible for __isub__')
+ for el,ot in zip(self.containers, other):
+ el -= ot
+ return self
+ # __isub__
+
+ def __imul__(self, other):
+ '''Inline multiplication'''
+ if isinstance (other, BlockDataContainer):
+ for el,ot in zip(self.containers, other.containers):
+ el *= ot
+ elif isinstance(other, Number):
+ for el in self.containers:
+ el *= other
+ elif isinstance(other, list) or isinstance(other, numpy.ndarray):
+ if not self.is_compatible(other):
+ raise ValueError('Incompatible for __imul__')
+ for el,ot in zip(self.containers, other):
+ el *= ot
+ return self
+ # __imul__
+
+ def __idiv__(self, other):
+ '''Inline division'''
+ if isinstance (other, BlockDataContainer):
+ for el,ot in zip(self.containers, other.containers):
+ el /= ot
+ elif isinstance(other, Number):
+ for el in self.containers:
+ el /= other
+ elif isinstance(other, list) or isinstance(other, numpy.ndarray):
+ if not self.is_compatible(other):
+ raise ValueError('Incompatible for __idiv__')
+ for el,ot in zip(self.containers, other):
+ el /= ot
+ return self
+ # __rdiv__
+ def __itruediv__(self, other):
+ '''Inline truedivision'''
+ return self.__idiv__(other)
+
+ def dot(self, other):
+#
+ tmp = [ self.containers[i].dot(other.containers[i]) for i in range(self.shape[0])]
+ return sum(tmp)
+
+
+
+if __name__ == '__main__':
+
+ from ccpi.framework import ImageGeometry, BlockGeometry
+ import numpy
+
+ N, M = 2, 3
+ ig = ImageGeometry(N, M)
+ BG = BlockGeometry(ig, ig)
+
+ U = BG.allocate('random_int')
+ V = BG.allocate('random_int')
+
+
+ print ("test sum BDC " )
+ w = U[0].as_array() + U[1].as_array()
+ w1 = sum(U).as_array()
+ numpy.testing.assert_array_equal(w, w1)
+
+ print ("test sum BDC " )
+ z = numpy.sqrt(U[0].as_array()**2 + U[1].as_array()**2)
+ z1 = sum(U**2).sqrt().as_array()
+ numpy.testing.assert_array_equal(z, z1)
+
+ z2 = U.pnorm(2)
+
+ zzz = U.dot(V)
+
+
+
+
+
+
diff --git a/Wrappers/Python/ccpi/framework/BlockGeometry.py b/Wrappers/Python/ccpi/framework/BlockGeometry.py
new file mode 100755
index 0000000..e58035b
--- /dev/null
+++ b/Wrappers/Python/ccpi/framework/BlockGeometry.py
@@ -0,0 +1,86 @@
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+import numpy
+from numbers import Number
+import functools
+from ccpi.framework import BlockDataContainer
+#from ccpi.optimisation.operators import Operator, LinearOperator
+
+class BlockGeometry(object):
+
+ RANDOM = 'random'
+ RANDOM_INT = 'random_int'
+
+
+
+ '''Class to hold Geometry as column vector'''
+ #__array_priority__ = 1
+ def __init__(self, *args, **kwargs):
+ ''''''
+ self.geometries = args
+ self.index = 0
+
+ shape = (len(args),1)
+ self.shape = shape
+
+ n_elements = functools.reduce(lambda x,y: x*y, shape, 1)
+ if len(args) != n_elements:
+ raise ValueError(
+ 'Dimension and size do not match: expected {} got {}'
+ .format(n_elements, len(args)))
+
+
+ def get_item(self, index):
+ '''returns the Geometry in the BlockGeometry located at position index'''
+ return self.geometries[index]
+
+ def allocate(self, value=0, dimension_labels=None, **kwargs):
+
+ symmetry = kwargs.get('symmetry',False)
+ containers = [geom.allocate(value) for geom in self.geometries]
+
+ if symmetry == True:
+
+ # for 2x2
+ # [ ig11, ig12\
+ # ig21, ig22]
+
+ # Row-wise Order
+
+ if len(containers)==4:
+ containers[1]=containers[2]
+
+ # for 3x3
+ # [ ig11, ig12, ig13\
+ # ig21, ig22, ig23\
+ # ig31, ig32, ig33]
+
+ elif len(containers)==9:
+ containers[1]=containers[3]
+ containers[2]=containers[6]
+ containers[5]=containers[7]
+
+ # for 4x4
+ # [ ig11, ig12, ig13, ig14\
+ # ig21, ig22, ig23, ig24\
+ # ig31, ig32, ig33, ig34
+ # ig41, ig42, ig43, ig44]
+
+ elif len(containers) == 16:
+ containers[1]=containers[4]
+ containers[2]=containers[8]
+ containers[3]=containers[12]
+ containers[6]=containers[9]
+ containers[7]=containers[10]
+ containers[11]=containers[15]
+
+
+
+
+ return BlockDataContainer(*containers)
+
+
+
diff --git a/Wrappers/Python/ccpi/framework/TestData.py b/Wrappers/Python/ccpi/framework/TestData.py
new file mode 100755
index 0000000..e7dc908
--- /dev/null
+++ b/Wrappers/Python/ccpi/framework/TestData.py
@@ -0,0 +1,121 @@
+# -*- coding: utf-8 -*-
+from ccpi.framework import ImageData, ImageGeometry
+import numpy
+from PIL import Image
+import os
+import os.path
+
+data_dir = os.path.abspath(os.path.join(
+ os.path.dirname(__file__),
+ '../data/')
+)
+
+class TestData(object):
+ BOAT = 'boat.tiff'
+ CAMERA = 'camera.png'
+ PEPPERS = 'peppers.tiff'
+ RESOLUTION_CHART = 'resolution_chart.tiff'
+ SIMPLE_PHANTOM_2D = 'hotdog'
+ SHAPES = 'shapes.png'
+
+ def __init__(self, **kwargs):
+ self.data_dir = kwargs.get('data_dir', data_dir)
+
+ def load(self, which, size=(512,512), scale=(0,1), **kwargs):
+ if which not in [TestData.BOAT, TestData.CAMERA,
+ TestData.PEPPERS, TestData.RESOLUTION_CHART,
+ TestData.SIMPLE_PHANTOM_2D, TestData.SHAPES]:
+ raise ValueError('Unknown TestData {}.'.format(which))
+ if which == TestData.SIMPLE_PHANTOM_2D:
+ N = size[0]
+ M = size[1]
+ sdata = numpy.zeros((N,M))
+ sdata[round(N/4):round(3*N/4),round(N/4):round(3*N/4)] = 0.5
+ sdata[round(M/8):round(7*M/8),round(3*M/8):round(5*M/8)] = 1
+ ig = ImageGeometry(voxel_num_x = N, voxel_num_y = M, dimension_labels=[ImageGeometry.HORIZONTAL_X, ImageGeometry.HORIZONTAL_Y])
+ data = ig.allocate()
+ data.fill(sdata)
+
+ elif which == TestData.SHAPES:
+
+ tmp = numpy.array(Image.open(os.path.join(self.data_dir, which)).convert('L'))
+ N = 200
+ M = 300
+ ig = ImageGeometry(voxel_num_x = N, voxel_num_y = M, dimension_labels=[ImageGeometry.HORIZONTAL_X, ImageGeometry.HORIZONTAL_Y])
+ data = ig.allocate()
+ data.fill(tmp/numpy.max(tmp))
+
+ else:
+ tmp = Image.open(os.path.join(self.data_dir, which))
+ print (tmp)
+ bands = tmp.getbands()
+ if len(bands) > 1:
+ ig = ImageGeometry(voxel_num_x=size[0], voxel_num_y=size[1], channels=len(bands),
+ dimension_labels=[ImageGeometry.HORIZONTAL_X, ImageGeometry.HORIZONTAL_Y, ImageGeometry.CHANNEL])
+ data = ig.allocate()
+ else:
+ ig = ImageGeometry(voxel_num_x = size[0], voxel_num_y = size[1], dimension_labels=[ImageGeometry.HORIZONTAL_X, ImageGeometry.HORIZONTAL_Y])
+ data = ig.allocate()
+ data.fill(numpy.array(tmp.resize((size[1],size[0]))))
+ if scale is not None:
+ dmax = data.as_array().max()
+ dmin = data.as_array().min()
+ # scale 0,1
+ data = (data -dmin) / (dmax - dmin)
+ if scale != (0,1):
+ #data = (data-dmin)/(dmax-dmin) * (scale[1]-scale[0]) +scale[0])
+ data *= (scale[1]-scale[0])
+ data += scale[0]
+ print ("data.geometry", data.geometry)
+ return data
+
+ def camera(**kwargs):
+
+ tmp = Image.open(os.path.join(data_dir, 'camera.png'))
+
+ size = kwargs.get('size',(512, 512))
+
+ data = numpy.array(tmp.resize(size))
+
+ data = data/data.max()
+
+ return ImageData(data)
+
+
+ def boat(**kwargs):
+
+ tmp = Image.open(os.path.join(data_dir, 'boat.tiff'))
+
+ size = kwargs.get('size',(512, 512))
+
+ data = numpy.array(tmp.resize(size))
+
+ data = data/data.max()
+
+ return ImageData(data)
+
+
+ def peppers(**kwargs):
+
+ tmp = Image.open(os.path.join(data_dir, 'peppers.tiff'))
+
+ size = kwargs.get('size',(512, 512))
+
+ data = numpy.array(tmp.resize(size))
+
+ data = data/data.max()
+
+ return ImageData(data)
+
+ def shapes(**kwargs):
+
+ tmp = Image.open(os.path.join(data_dir, 'shapes.png')).convert('LA')
+
+ size = kwargs.get('size',(300, 200))
+
+ data = numpy.array(tmp.resize(size))
+
+ data = data/data.max()
+
+ return ImageData(data)
+
diff --git a/Wrappers/Python/ccpi/framework/Vector.py b/Wrappers/Python/ccpi/framework/Vector.py
new file mode 100755
index 0000000..542cb7a
--- /dev/null
+++ b/Wrappers/Python/ccpi/framework/Vector.py
@@ -0,0 +1,96 @@
+# -*- coding: utf-8 -*-
+# This work is part of the Core Imaging Library developed by
+# Visual Analytics and Imaging System Group of the Science Technology
+# Facilities Council, STFC
+
+# Copyright 2018-2019 Edoardo Pasca
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+import numpy
+import sys
+from datetime import timedelta, datetime
+import warnings
+from functools import reduce
+from numbers import Number
+from ccpi.framework import DataContainer
+
+class VectorData(DataContainer):
+ def __init__(self, array=None, **kwargs):
+ self.geometry = kwargs.get('geometry', None)
+ self.dtype = kwargs.get('dtype', numpy.float32)
+
+ if self.geometry is None:
+ if array is None:
+ raise ValueError('Please specify either a geometry or an array')
+ else:
+ if len(array.shape) > 1:
+ raise ValueError('Incompatible size: expected 1D got {}'.format(array.shape))
+ out = array
+ self.geometry = VectorGeometry(array.shape[0])
+ self.length = self.geometry.length
+ else:
+ self.length = self.geometry.length
+
+ if array is None:
+ out = numpy.zeros((self.length,), dtype=self.dtype)
+ else:
+ if self.length == array.shape[0]:
+ out = array
+ else:
+ raise ValueError('Incompatible size: expecting {} got {}'.format((self.length,), array.shape))
+ deep_copy = True
+ super(VectorData, self).__init__(out, deep_copy, None)
+
+class VectorGeometry(object):
+ RANDOM = 'random'
+ RANDOM_INT = 'random_int'
+
+ def __init__(self,
+ length):
+
+ self.length = length
+ self.shape = (length, )
+
+
+ def clone(self):
+ '''returns a copy of VectorGeometry'''
+ return VectorGeometry(self.length)
+
+ def allocate(self, value=0, **kwargs):
+ '''allocates an VectorData according to the size expressed in the instance'''
+ self.dtype = kwargs.get('dtype', numpy.float32)
+ out = VectorData(geometry=self, dtype=self.dtype)
+ if isinstance(value, Number):
+ if value != 0:
+ out += value
+ else:
+ if value == VectorGeometry.RANDOM:
+ seed = kwargs.get('seed', None)
+ if seed is not None:
+ numpy.random.seed(seed)
+ out.fill(numpy.random.random_sample(self.shape))
+ elif value == VectorGeometry.RANDOM_INT:
+ seed = kwargs.get('seed', None)
+ if seed is not None:
+ numpy.random.seed(seed)
+ max_value = kwargs.get('max_value', 100)
+ out.fill(numpy.random.randint(max_value,size=self.shape))
+ else:
+ raise ValueError('Value {} unknown'.format(value))
+ return out
diff --git a/Wrappers/Python/ccpi/framework/__init__.py b/Wrappers/Python/ccpi/framework/__init__.py
new file mode 100755
index 0000000..3de27ed
--- /dev/null
+++ b/Wrappers/Python/ccpi/framework/__init__.py
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+"""
+Created on Tue Mar 5 16:00:18 2019
+
+@author: ofn77899
+"""
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+import numpy
+import sys
+from datetime import timedelta, datetime
+import warnings
+from functools import reduce
+
+
+from .framework import DataContainer
+from .framework import ImageData, AcquisitionData
+from .framework import ImageGeometry, AcquisitionGeometry
+from .framework import find_key, message
+from .framework import DataProcessor
+from .framework import AX, PixelByPixelDataProcessor, CastDataContainer
+from .BlockDataContainer import BlockDataContainer
+from .BlockGeometry import BlockGeometry
+from .TestData import TestData
+from .Vector import VectorGeometry, VectorData
diff --git a/Wrappers/Python/ccpi/framework.py b/Wrappers/Python/ccpi/framework/framework.py
index 24f4ca6..caea1e1 100644..100755
--- a/Wrappers/Python/ccpi/framework.py
+++ b/Wrappers/Python/ccpi/framework/framework.py
@@ -27,6 +27,7 @@ import sys
from datetime import timedelta, datetime
import warnings
from functools import reduce
+from numbers import Number
def find_key(dic, val):
"""return the key of dictionary dic given the value"""
@@ -43,6 +44,13 @@ def message(cls, msg, *args):
class ImageGeometry(object):
+ RANDOM = 'random'
+ RANDOM_INT = 'random_int'
+ CHANNEL = 'channel'
+ ANGLE = 'angle'
+ VERTICAL = 'vertical'
+ HORIZONTAL_X = 'horizontal_x'
+ HORIZONTAL_Y = 'horizontal_y'
def __init__(self,
voxel_num_x=0,
@@ -54,7 +62,8 @@ class ImageGeometry(object):
center_x=0,
center_y=0,
center_z=0,
- channels=1):
+ channels=1,
+ **kwargs):
self.voxel_num_x = voxel_num_x
self.voxel_num_y = voxel_num_y
@@ -67,6 +76,49 @@ class ImageGeometry(object):
self.center_z = center_z
self.channels = channels
+ # this is some code repetition
+ if self.channels > 1:
+ if self.voxel_num_z>1:
+ self.length = 4
+ shape = (self.channels, self.voxel_num_z, self.voxel_num_y, self.voxel_num_x)
+ dim_labels = [ImageGeometry.CHANNEL, ImageGeometry.VERTICAL,
+ ImageGeometry.HORIZONTAL_Y, ImageGeometry.HORIZONTAL_X]
+ else:
+ self.length = 3
+ shape = (self.channels, self.voxel_num_y, self.voxel_num_x)
+ dim_labels = [ImageGeometry.CHANNEL, ImageGeometry.HORIZONTAL_Y, ImageGeometry.HORIZONTAL_X]
+ else:
+ if self.voxel_num_z>1:
+ self.length = 3
+ shape = (self.voxel_num_z, self.voxel_num_y, self.voxel_num_x)
+ dim_labels = [ImageGeometry.VERTICAL, ImageGeometry.HORIZONTAL_Y,
+ ImageGeometry.HORIZONTAL_X]
+ else:
+ self.length = 2
+ shape = (self.voxel_num_y, self.voxel_num_x)
+ dim_labels = [ImageGeometry.HORIZONTAL_Y, ImageGeometry.HORIZONTAL_X]
+
+ labels = kwargs.get('dimension_labels', None)
+ if labels is None:
+ self.shape = shape
+ self.dimension_labels = dim_labels
+ else:
+ order = self.get_order_by_label(labels, dim_labels)
+ if order != [0,1,2]:
+ # resort
+ self.shape = tuple([shape[i] for i in order])
+ self.dimension_labels = labels
+
+ def get_order_by_label(self, dimension_labels, default_dimension_labels):
+ order = []
+ for i, el in enumerate(dimension_labels):
+ for j, ek in enumerate(default_dimension_labels):
+ if el == ek:
+ order.append(j)
+ break
+ return order
+
+
def get_min_x(self):
return self.center_x - 0.5*self.voxel_num_x*self.voxel_size_x
@@ -111,14 +163,52 @@ class ImageGeometry(object):
repres += "voxel_size : x{0},y{1},z{2}\n".format(self.voxel_size_x, self.voxel_size_y, self.voxel_size_z)
repres += "center : x{0},y{1},z{2}\n".format(self.center_x, self.center_y, self.center_z)
return repres
- def allocate(self, value=0, dimension_labels=None):
+ def allocate(self, value=0, dimension_labels=None, **kwargs):
'''allocates an ImageData according to the size expressed in the instance'''
- out = ImageData(geometry=self, dimension_labels=dimension_labels)
- if value != 0:
- out += value
+ if dimension_labels is None:
+ out = ImageData(geometry=self, dimension_labels=self.dimension_labels)
+ else:
+ out = ImageData(geometry=self, dimension_labels=dimension_labels)
+ if isinstance(value, Number):
+ if value != 0:
+ out += value
+ else:
+ if value == ImageGeometry.RANDOM:
+ seed = kwargs.get('seed', None)
+ if seed is not None:
+ numpy.random.seed(seed)
+ out.fill(numpy.random.random_sample(self.shape))
+ elif value == ImageGeometry.RANDOM_INT:
+ seed = kwargs.get('seed', None)
+ if seed is not None:
+ numpy.random.seed(seed)
+ max_value = kwargs.get('max_value', 100)
+ out.fill(numpy.random.randint(max_value,size=self.shape))
+ else:
+ raise ValueError('Value {} unknown'.format(value))
+
return out
+ # The following methods return 2 members of the class, therefore I
+ # don't think we need to implement them.
+ # Additionally using __len__ is confusing as one would think this is
+ # an iterable.
+ #def __len__(self):
+ # '''returns the length of the geometry'''
+ # return self.length
+ #def shape(self):
+ # '''Returns the shape of the array of the ImageData it describes'''
+ # return self.shape
+
class AcquisitionGeometry(object):
-
+ RANDOM = 'random'
+ RANDOM_INT = 'random_int'
+ ANGLE_UNIT = 'angle_unit'
+ DEGREE = 'degree'
+ RADIAN = 'radian'
+ CHANNEL = 'channel'
+ ANGLE = 'angle'
+ VERTICAL = 'vertical'
+ HORIZONTAL = 'horizontal'
def __init__(self,
geom_type,
dimension,
@@ -130,7 +220,7 @@ class AcquisitionGeometry(object):
dist_source_center=None,
dist_center_detector=None,
channels=1,
- angle_unit='degree'
+ **kwargs
):
"""
General inputs for standard type projection geometries
@@ -155,12 +245,16 @@ class AcquisitionGeometry(object):
source_to_center_dist (if parallel: NaN)
center_to_detector_dist (if parallel: NaN)
standard or nonstandard (vec) geometry
- angles
+ angles is expected numpy array, dtype - float32
angles_format radians or degrees
"""
self.geom_type = geom_type # 'parallel' or 'cone'
self.dimension = dimension # 2D or 3D
- self.angles = angles
+ if isinstance(angles, numpy.ndarray):
+ self.angles = angles
+ else:
+ raise ValueError('numpy array is expected')
+ num_of_angles = len (angles)
self.dist_source_center = dist_source_center
self.dist_center_detector = dist_center_detector
@@ -171,6 +265,53 @@ class AcquisitionGeometry(object):
self.pixel_size_v = pixel_size_v
self.channels = channels
+ self.angle_unit=kwargs.get(AcquisitionGeometry.ANGLE_UNIT,
+ AcquisitionGeometry.DEGREE)
+
+ # default labels
+ if channels > 1:
+ if pixel_num_v > 1:
+ shape = (channels, num_of_angles , pixel_num_v, pixel_num_h)
+ dim_labels = [AcquisitionGeometry.CHANNEL ,
+ AcquisitionGeometry.ANGLE , AcquisitionGeometry.VERTICAL ,
+ AcquisitionGeometry.HORIZONTAL]
+ else:
+ shape = (channels , num_of_angles, pixel_num_h)
+ dim_labels = [AcquisitionGeometry.CHANNEL ,
+ AcquisitionGeometry.ANGLE, AcquisitionGeometry.HORIZONTAL]
+ else:
+ if pixel_num_v > 1:
+ shape = (num_of_angles, pixel_num_v, pixel_num_h)
+ dim_labels = [AcquisitionGeometry.ANGLE , AcquisitionGeometry.VERTICAL ,
+ AcquisitionGeometry.HORIZONTAL]
+ else:
+ shape = (num_of_angles, pixel_num_h)
+ dim_labels = [AcquisitionGeometry.ANGLE, AcquisitionGeometry.HORIZONTAL]
+
+ labels = kwargs.get('dimension_labels', None)
+ if labels is None:
+ self.shape = shape
+ self.dimension_labels = dim_labels
+ else:
+ if len(labels) != len(dim_labels):
+ raise ValueError('Wrong number of labels. Expected {} got {}'.format(len(dim_labels), len(labels)))
+ order = self.get_order_by_label(labels, dim_labels)
+ if order != [0,1,2]:
+ # resort
+ self.shape = tuple([shape[i] for i in order])
+ self.dimension_labels = labels
+
+ def get_order_by_label(self, dimension_labels, default_dimension_labels):
+ order = []
+ for i, el in enumerate(dimension_labels):
+ for j, ek in enumerate(default_dimension_labels):
+ if el == ek:
+ order.append(j)
+ break
+ return order
+
+
+
def clone(self):
'''returns a copy of the AcquisitionGeometry'''
@@ -198,15 +339,36 @@ class AcquisitionGeometry(object):
return repres
def allocate(self, value=0, dimension_labels=None):
'''allocates an AcquisitionData according to the size expressed in the instance'''
- out = AcquisitionData(geometry=self, dimension_labels=dimension_labels)
- if value != 0:
- out += value
+ if dimension_labels is None:
+ out = AcquisitionData(geometry=self, dimension_labels=self.dimension_labels)
+ else:
+ out = AcquisitionData(geometry=self, dimension_labels=dimension_labels)
+ if isinstance(value, Number):
+ if value != 0:
+ out += value
+ else:
+ if value == AcquisitionData.RANDOM:
+ seed = kwargs.get('seed', None)
+ if seed is not None:
+ numpy.random.seed(seed)
+ out.fill(numpy.random.random_sample(self.shape))
+ elif value == AcquisitionData.RANDOM_INT:
+ seed = kwargs.get('seed', None)
+ if seed is not None:
+ numpy.random.seed(seed)
+ max_value = kwargs.get('max_value', 100)
+ out.fill(numpy.random.randint(max_value,size=self.shape))
+ else:
+ raise ValueError('Value {} unknown'.format(value))
+
return out
+
class DataContainer(object):
'''Generic class to hold data
Data is currently held in a numpy arrays'''
+ __container_priority__ = 1
def __init__ (self, array, deep_copy=True, dimension_labels=None,
**kwargs):
'''Holds the data'''
@@ -382,116 +544,21 @@ class DataContainer(object):
return self.shape == other.shape
## algebra
- def __add__(self, other , out=None, *args, **kwargs):
- if issubclass(type(other), DataContainer):
- if self.check_dimensions(other):
- out = self.as_array() + other.as_array()
- return type(self)(out,
- deep_copy=True,
- dimension_labels=self.dimension_labels,
- geometry=self.geometry)
- else:
- raise ValueError('Wrong shape: {0} and {1}'.format(self.shape,
- other.shape))
- elif isinstance(other, (int, float, complex)):
- return type(self)(
- self.as_array() + other,
- deep_copy=True,
- dimension_labels=self.dimension_labels,
- geometry=self.geometry)
- else:
- raise TypeError('Cannot {0} DataContainer with {1}'.format("add" ,
- type(other)))
- # __add__
+ def __add__(self, other):
+ return self.add(other)
+ def __mul__(self, other):
+ return self.multiply(other)
def __sub__(self, other):
- if issubclass(type(other), DataContainer):
- if self.check_dimensions(other):
- out = self.as_array() - other.as_array()
- return type(self)(out,
- deep_copy=True,
- dimension_labels=self.dimension_labels,
- geometry=self.geometry)
- else:
- raise ValueError('__sub__ Wrong shape: {0} and {1}'.format(self.shape,
- other.shape))
- elif isinstance(other, (int, float, complex)):
- return type(self)(self.as_array() - other,
- deep_copy=True,
- dimension_labels=self.dimension_labels,
- geometry=self.geometry)
- else:
- raise TypeError('Cannot {0} DataContainer with {1}'.format("subtract" ,
- type(other)))
- # __sub__
- def __truediv__(self,other):
- return self.__div__(other)
-
+ return self.subtract(other)
def __div__(self, other):
- if issubclass(type(other), DataContainer):
- if self.check_dimensions(other):
- out = self.as_array() / other.as_array()
- return type(self)(out,
- deep_copy=True,
- dimension_labels=self.dimension_labels,
- geometry=self.geometry)
- else:
- raise ValueError('__div__ Wrong shape: {0} and {1}'.format(self.shape,
- other.shape))
- elif isinstance(other, (int, float, complex)):
- return type(self)(self.as_array() / other,
- deep_copy=True,
- dimension_labels=self.dimension_labels,
- geometry=self.geometry)
- else:
- raise TypeError('Cannot {0} DataContainer with {1}'.format("divide" ,
- type(other)))
- # __div__
-
+ return self.divide(other)
+ def __truediv__(self, other):
+ return self.divide(other)
def __pow__(self, other):
- if issubclass(type(other), DataContainer):
- if self.check_dimensions(other):
- out = self.as_array() ** other.as_array()
- return type(self)(out,
- deep_copy=True,
- dimension_labels=self.dimension_labels,
- geometry=self.geometry)
- else:
- raise ValueError('__pow__ Wrong shape: {0} and {1}'.format(self.shape,
- other.shape))
- elif isinstance(other, (int, float, complex)):
- return type(self)(self.as_array() ** other,
- deep_copy=True,
- dimension_labels=self.dimension_labels,
- geometry=self.geometry)
- else:
- raise TypeError('pow: Cannot {0} DataContainer with {1}'.format("power" ,
- type(other)))
- # __pow__
+ return self.power(other)
+
- def __mul__(self, other):
- if issubclass(type(other), DataContainer):
- if self.check_dimensions(other):
- out = self.as_array() * other.as_array()
- return type(self)(out,
- deep_copy=True,
- dimension_labels=self.dimension_labels,
- geometry=self.geometry)
- else:
- raise ValueError('*:Wrong shape: {0} and {1}'.format(self.shape,
- other.shape))
- elif isinstance(other, (int, float, complex,\
- numpy.int, numpy.int8, numpy.int16, numpy.int32, numpy.int64,\
- numpy.float, numpy.float16, numpy.float32, numpy.float64, \
- numpy.complex)):
- return type(self)(self.as_array() * other,
- deep_copy=True,
- dimension_labels=self.dimension_labels,
- geometry=self.geometry)
- else:
- raise TypeError('Cannot {0} DataContainer with {1}'.format("multiply" ,
- type(other)))
- # __mul__
# reverse operand
def __radd__(self, other):
@@ -532,54 +599,27 @@ class DataContainer(object):
# (+=, -=, *=, /= , //=,
# must return self
-
-
def __iadd__(self, other):
- if isinstance(other, (int, float)) :
- numpy.add(self.array, other, out=self.array)
- elif issubclass(type(other), DataContainer):
- if self.check_dimensions(other):
- numpy.add(self.array, other.array, out=self.array)
- else:
- raise ValueError('Dimensions do not match')
- return self
- # __iadd__
+ kw = {'out':self}
+ return self.add(other, **kw)
def __imul__(self, other):
- if isinstance(other, (int, float)) :
- arr = self.as_array()
- numpy.multiply(arr, other, out=arr)
- elif issubclass(type(other), DataContainer):
- if self.check_dimensions(other):
- numpy.multiply(self.array, other.array, out=self.array)
- else:
- raise ValueError('Dimensions do not match')
- return self
- # __imul__
+ kw = {'out':self}
+ return self.multiply(other, **kw)
def __isub__(self, other):
- if isinstance(other, (int, float)) :
- numpy.subtract(self.array, other, out=self.array)
- elif issubclass(type(other), DataContainer):
- if self.check_dimensions(other):
- numpy.subtract(self.array, other.array, out=self.array)
- else:
- raise ValueError('Dimensions do not match')
- return self
- # __isub__
+ kw = {'out':self}
+ return self.subtract(other, **kw)
def __idiv__(self, other):
- return self.__itruediv__(other)
+ kw = {'out':self}
+ return self.divide(other, **kw)
+
def __itruediv__(self, other):
- if isinstance(other, (int, float)) :
- numpy.divide(self.array, other, out=self.array)
- elif issubclass(type(other), DataContainer):
- if self.check_dimensions(other):
- numpy.divide(self.array, other.array, out=self.array)
- else:
- raise ValueError('Dimensions do not match')
- return self
- # __idiv__
+ kw = {'out':self}
+ return self.divide(other, **kw)
+
+
def __str__ (self, representation=False):
repres = ""
@@ -639,8 +679,9 @@ class DataContainer(object):
## binary operations
- def pixel_wise_binary(self,pwop, x2 , out=None, *args, **kwargs):
-
+ def pixel_wise_binary(self, pwop, x2, *args, **kwargs):
+ out = kwargs.get('out', None)
+
if out is None:
if isinstance(x2, (int, float, complex)):
out = pwop(self.as_array() , x2 , *args, **kwargs )
@@ -658,24 +699,30 @@ class DataContainer(object):
elif issubclass(type(out), DataContainer) and issubclass(type(x2), DataContainer):
if self.check_dimensions(out) and self.check_dimensions(x2):
- pwop(self.as_array(), x2.as_array(), out=out.as_array(), *args, **kwargs )
+ kwargs['out'] = out.as_array()
+ pwop(self.as_array(), x2.as_array(), *args, **kwargs )
#return type(self)(out.as_array(),
# deep_copy=False,
# dimension_labels=self.dimension_labels,
# geometry=self.geometry)
return out
else:
- raise ValueError(message(type(self),"Wrong size for data memory: ", out.shape,self.shape))
- elif issubclass(type(out), DataContainer) and isinstance(x2, (int,float,complex)):
+ raise ValueError(message(type(self),"Wrong size for data memory: out {} x2 {} expected {}".format( out.shape,x2.shape ,self.shape)))
+ elif issubclass(type(out), DataContainer) and \
+ isinstance(x2, (int,float,complex, numpy.int, numpy.int8, \
+ numpy.int16, numpy.int32, numpy.int64,\
+ numpy.float, numpy.float16, numpy.float32,\
+ numpy.float64, numpy.complex)):
if self.check_dimensions(out):
-
- pwop(self.as_array(), x2, out=out.as_array(), *args, **kwargs )
+ kwargs['out']=out.as_array()
+ pwop(self.as_array(), x2, *args, **kwargs )
return out
else:
raise ValueError(message(type(self),"Wrong size for data memory: ", out.shape,self.shape))
elif issubclass(type(out), numpy.ndarray):
if self.array.shape == out.shape and self.array.dtype == out.dtype:
- pwop(self.as_array(), x2 , out=out, *args, **kwargs)
+ kwargs['out'] = out
+ pwop(self.as_array(), x2, *args, **kwargs)
#return type(self)(out,
# deep_copy=False,
# dimension_labels=self.dimension_labels,
@@ -683,26 +730,43 @@ class DataContainer(object):
else:
raise ValueError (message(type(self), "incompatible class:" , pwop.__name__, type(out)))
- def add(self, other , out=None, *args, **kwargs):
- return self.pixel_wise_binary(numpy.add, other, out=out, *args, **kwargs)
-
- def subtract(self, other, out=None , *args, **kwargs):
- return self.pixel_wise_binary(numpy.subtract, other, out=out, *args, **kwargs)
+ def add(self, other, *args, **kwargs):
+ if hasattr(other, '__container_priority__') and \
+ self.__class__.__container_priority__ < other.__class__.__container_priority__:
+ return other.add(self, *args, **kwargs)
+ return self.pixel_wise_binary(numpy.add, other, *args, **kwargs)
+
+ def subtract(self, other, *args, **kwargs):
+ if hasattr(other, '__container_priority__') and \
+ self.__class__.__container_priority__ < other.__class__.__container_priority__:
+ return other.subtract(self, *args, **kwargs)
+ return self.pixel_wise_binary(numpy.subtract, other, *args, **kwargs)
+
+ def multiply(self, other, *args, **kwargs):
+ if hasattr(other, '__container_priority__') and \
+ self.__class__.__container_priority__ < other.__class__.__container_priority__:
+ return other.multiply(self, *args, **kwargs)
+ return self.pixel_wise_binary(numpy.multiply, other, *args, **kwargs)
+
+ def divide(self, other, *args, **kwargs):
+ if hasattr(other, '__container_priority__') and \
+ self.__class__.__container_priority__ < other.__class__.__container_priority__:
+ return other.divide(self, *args, **kwargs)
+ return self.pixel_wise_binary(numpy.divide, other, *args, **kwargs)
+
+ def power(self, other, *args, **kwargs):
+ return self.pixel_wise_binary(numpy.power, other, *args, **kwargs)
+
+ def maximum(self, x2, *args, **kwargs):
+ return self.pixel_wise_binary(numpy.maximum, x2, *args, **kwargs)
+
+ def minimum(self,x2, out=None, *args, **kwargs):
+ return self.pixel_wise_binary(numpy.minimum, x2=x2, out=out, *args, **kwargs)
- def multiply(self, other , out=None, *args, **kwargs):
- return self.pixel_wise_binary(numpy.multiply, other, out=out, *args, **kwargs)
-
- def divide(self, other , out=None ,*args, **kwargs):
- return self.pixel_wise_binary(numpy.divide, other, out=out, *args, **kwargs)
-
- def power(self, other , out=None, *args, **kwargs):
- return self.pixel_wise_binary(numpy.power, other, out=out, *args, **kwargs)
-
- def maximum(self,x2, out=None, *args, **kwargs):
- return self.pixel_wise_binary(numpy.maximum, x2=x2, out=out, *args, **kwargs)
## unary operations
- def pixel_wise_unary(self,pwop, out=None, *args, **kwargs):
+ def pixel_wise_unary(self, pwop, *args, **kwargs):
+ out = kwargs.get('out', None)
if out is None:
out = pwop(self.as_array() , *args, **kwargs )
return type(self)(out,
@@ -711,104 +775,90 @@ class DataContainer(object):
geometry=self.geometry)
elif issubclass(type(out), DataContainer):
if self.check_dimensions(out):
- pwop(self.as_array(), out=out.as_array(), *args, **kwargs )
+ kwargs['out'] = out.as_array()
+ pwop(self.as_array(), *args, **kwargs )
else:
raise ValueError(message(type(self),"Wrong size for data memory: ", out.shape,self.shape))
elif issubclass(type(out), numpy.ndarray):
if self.array.shape == out.shape and self.array.dtype == out.dtype:
- pwop(self.as_array(), out=out, *args, **kwargs)
+ kwargs['out'] = out
+ pwop(self.as_array(), *args, **kwargs)
else:
raise ValueError (message(type(self), "incompatible class:" , pwop.__name__, type(out)))
- def abs(self, out=None, *args, **kwargs):
- return self.pixel_wise_unary(numpy.abs, out=out, *args, **kwargs)
-
- def sign(self, out=None, *args, **kwargs):
- return self.pixel_wise_unary(numpy.sign , out=out, *args, **kwargs)
+ def abs(self, *args, **kwargs):
+ return self.pixel_wise_unary(numpy.abs, *args, **kwargs)
- def sqrt(self, out=None, *args, **kwargs):
- return self.pixel_wise_unary(numpy.sqrt, out=out, *args, **kwargs)
+ def sign(self, *args, **kwargs):
+ return self.pixel_wise_unary(numpy.sign, *args, **kwargs)
+ def sqrt(self, *args, **kwargs):
+ return self.pixel_wise_unary(numpy.sqrt, *args, **kwargs)
+
+ def conjugate(self, *args, **kwargs):
+ return self.pixel_wise_unary(numpy.conjugate, *args, **kwargs)
#def __abs__(self):
# operation = FM.OPERATION.ABS
# return self.callFieldMath(operation, None, self.mask, self.maskOnValue)
# __abs__
## reductions
- def sum(self, out=None, *args, **kwargs):
+ def sum(self, *args, **kwargs):
return self.as_array().sum(*args, **kwargs)
def squared_norm(self):
'''return the squared euclidean norm of the DataContainer viewed as a vector'''
- shape = self.shape
- size = reduce(lambda x,y:x*y, shape, 1)
- y = numpy.reshape(self.as_array(), (size, ))
- return numpy.dot(y, y.conjugate())
+ #shape = self.shape
+ #size = reduce(lambda x,y:x*y, shape, 1)
+ #y = numpy.reshape(self.as_array(), (size, ))
+ return self.dot(self.conjugate())
+ #return self.dot(self)
def norm(self):
'''return the euclidean norm of the DataContainer viewed as a vector'''
return numpy.sqrt(self.squared_norm())
+
+
def dot(self, other, *args, **kwargs):
'''return the inner product of 2 DataContainers viewed as vectors'''
+ method = kwargs.get('method', 'numpy')
+ if method not in ['numpy','reduce']:
+ raise ValueError('dot: specified method not valid. Expecting numpy or reduce got {} '.format(
+ method))
+
if self.shape == other.shape:
- return numpy.dot(self.as_array().ravel(), other.as_array().ravel())
+ # return (self*other).sum()
+ if method == 'numpy':
+ return numpy.dot(self.as_array().ravel(), other.as_array().ravel())
+ elif method == 'reduce':
+ # see https://github.com/vais-ral/CCPi-Framework/pull/273
+ # notice that Python seems to be smart enough to use
+ # the appropriate type to hold the result of the reduction
+ sf = reduce(lambda x,y: x + y[0]*y[1],
+ zip(self.as_array().ravel(),
+ other.as_array().ravel()),
+ 0)
+ return sf
else:
raise ValueError('Shapes are not aligned: {} != {}'.format(self.shape, other.shape))
-
+
+
class ImageData(DataContainer):
'''DataContainer for holding 2D or 3D DataContainer'''
+ __container_priority__ = 1
+
+
def __init__(self,
array = None,
deep_copy=False,
dimension_labels=None,
**kwargs):
- self.geometry = None
+ self.geometry = kwargs.get('geometry', None)
if array is None:
- if 'geometry' in kwargs.keys():
- geometry = kwargs['geometry']
- self.geometry = geometry
- channels = geometry.channels
- horiz_x = geometry.voxel_num_x
- horiz_y = geometry.voxel_num_y
- vert = 1 if geometry.voxel_num_z is None\
- else geometry.voxel_num_z # this should be 1 for 2D
- if dimension_labels is None:
- if channels > 1:
- if vert > 1:
- shape = (channels, vert, horiz_y, horiz_x)
- dim_labels = ['channel' ,'vertical' , 'horizontal_y' ,
- 'horizontal_x']
- else:
- shape = (channels , horiz_y, horiz_x)
- dim_labels = ['channel' , 'horizontal_y' ,
- 'horizontal_x']
- else:
- if vert > 1:
- shape = (vert, horiz_y, horiz_x)
- dim_labels = ['vertical' , 'horizontal_y' ,
- 'horizontal_x']
- else:
- shape = (horiz_y, horiz_x)
- dim_labels = ['horizontal_y' ,
- 'horizontal_x']
- dimension_labels = dim_labels
- else:
- shape = []
- for dim in dimension_labels:
- if dim == 'channel':
- shape.append(channels)
- elif dim == 'horizontal_y':
- shape.append(horiz_y)
- elif dim == 'vertical':
- shape.append(vert)
- elif dim == 'horizontal_x':
- shape.append(horiz_x)
- if len(shape) != len(dimension_labels):
- raise ValueError('Missing {0} axes'.format(
- len(dimension_labels) - len(shape)))
- shape = tuple(shape)
+ if self.geometry is not None:
+ shape, dimension_labels = self.get_shape_labels(self.geometry, dimension_labels)
array = numpy.zeros( shape , dtype=numpy.float32)
super(ImageData, self).__init__(array, deep_copy,
@@ -818,6 +868,11 @@ class ImageData(DataContainer):
raise ValueError('Please pass either a DataContainer, ' +\
'a numpy array or a geometry')
else:
+ if self.geometry is not None:
+ shape, labels = self.get_shape_labels(self.geometry, dimension_labels)
+ if array.shape != shape:
+ raise ValueError('Shape mismatch {} {}'.format(shape, array.shape))
+
if issubclass(type(array) , DataContainer):
# if the array is a DataContainer get the info from there
if not ( array.number_of_dimensions == 2 or \
@@ -838,14 +893,17 @@ class ImageData(DataContainer):
if dimension_labels is None:
if array.ndim == 4:
- dimension_labels = ['channel' ,'vertical' , 'horizontal_y' ,
- 'horizontal_x']
+ dimension_labels = [ImageGeometry.CHANNEL,
+ ImageGeometry.VERTICAL,
+ ImageGeometry.HORIZONTAL_Y,
+ ImageGeometry.HORIZONTAL_X]
elif array.ndim == 3:
- dimension_labels = ['vertical' , 'horizontal_y' ,
- 'horizontal_x']
+ dimension_labels = [ImageGeometry.VERTICAL,
+ ImageGeometry.HORIZONTAL_Y,
+ ImageGeometry.HORIZONTAL_X]
else:
- dimension_labels = ['horizontal_y' ,
- 'horizontal_x']
+ dimension_labels = [ ImageGeometry.HORIZONTAL_Y,
+ ImageGeometry.HORIZONTAL_X]
#DataContainer.__init__(self, array, deep_copy, dimension_labels, **kwargs)
super(ImageData, self).__init__(array, deep_copy,
@@ -861,75 +919,93 @@ class ImageData(DataContainer):
self.spacing = value
def subset(self, dimensions=None, **kw):
+ # FIXME: this is clearly not rigth
+ # it should be something like
+ # out = DataContainer.subset(self, dimensions, **kw)
+ # followed by regeneration of the proper geometry.
out = super(ImageData, self).subset(dimensions, **kw)
#out.geometry = self.recalculate_geometry(dimensions , **kw)
out.geometry = self.geometry
return out
-
+
+ def get_shape_labels(self, geometry, dimension_labels=None):
+ channels = geometry.channels
+ horiz_x = geometry.voxel_num_x
+ horiz_y = geometry.voxel_num_y
+ vert = 1 if geometry.voxel_num_z is None\
+ else geometry.voxel_num_z # this should be 1 for 2D
+ if dimension_labels is None:
+ if channels > 1:
+ if vert > 1:
+ shape = (channels, vert, horiz_y, horiz_x)
+ dim_labels = [ImageGeometry.CHANNEL,
+ ImageGeometry.VERTICAL,
+ ImageGeometry.HORIZONTAL_Y,
+ ImageGeometry.HORIZONTAL_X]
+ else:
+ shape = (channels , horiz_y, horiz_x)
+ dim_labels = [ImageGeometry.CHANNEL,
+ ImageGeometry.HORIZONTAL_Y,
+ ImageGeometry.HORIZONTAL_X]
+ else:
+ if vert > 1:
+ shape = (vert, horiz_y, horiz_x)
+ dim_labels = [ImageGeometry.VERTICAL,
+ ImageGeometry.HORIZONTAL_Y,
+ ImageGeometry.HORIZONTAL_X]
+ else:
+ shape = (horiz_y, horiz_x)
+ dim_labels = [ImageGeometry.HORIZONTAL_Y,
+ ImageGeometry.HORIZONTAL_X]
+ dimension_labels = dim_labels
+ else:
+ shape = []
+ for i in range(len(dimension_labels)):
+ dim = dimension_labels[i]
+ if dim == ImageGeometry.CHANNEL:
+ shape.append(channels)
+ elif dim == ImageGeometry.HORIZONTAL_Y:
+ shape.append(horiz_y)
+ elif dim == ImageGeometry.VERTICAL:
+ shape.append(vert)
+ elif dim == ImageGeometry.HORIZONTAL_X:
+ shape.append(horiz_x)
+ if len(shape) != len(dimension_labels):
+ raise ValueError('Missing {0} axes {1} shape {2}'.format(
+ len(dimension_labels) - len(shape), dimension_labels, shape))
+ shape = tuple(shape)
+
+ return (shape, dimension_labels)
+
class AcquisitionData(DataContainer):
'''DataContainer for holding 2D or 3D sinogram'''
+ __container_priority__ = 1
+
+
def __init__(self,
array = None,
deep_copy=True,
dimension_labels=None,
**kwargs):
- self.geometry = None
+ self.geometry = kwargs.get('geometry', None)
if array is None:
if 'geometry' in kwargs.keys():
geometry = kwargs['geometry']
self.geometry = geometry
- channels = geometry.channels
- horiz = geometry.pixel_num_h
- vert = geometry.pixel_num_v
- angles = geometry.angles
- num_of_angles = numpy.shape(angles)[0]
- if dimension_labels is None:
- if channels > 1:
- if vert > 1:
- shape = (channels, num_of_angles , vert, horiz)
- dim_labels = ['channel' , ' angle' ,
- 'vertical' , 'horizontal']
- else:
- shape = (channels , num_of_angles, horiz)
- dim_labels = ['channel' , 'angle' ,
- 'horizontal']
- else:
- if vert > 1:
- shape = (num_of_angles, vert, horiz)
- dim_labels = ['angle' , 'vertical' ,
- 'horizontal']
- else:
- shape = (num_of_angles, horiz)
- dim_labels = ['angle' ,
- 'horizontal']
-
- dimension_labels = dim_labels
- else:
- shape = []
- for dim in dimension_labels:
- if dim == 'channel':
- shape.append(channels)
- elif dim == 'angle':
- shape.append(num_of_angles)
- elif dim == 'vertical':
- shape.append(vert)
- elif dim == 'horizontal':
- shape.append(horiz)
- if len(shape) != len(dimension_labels):
- raise ValueError('Missing {0} axes.\nExpected{1} got {2}}'\
- .format(
- len(dimension_labels) - len(shape),
- dimension_labels, shape)
- )
- shape = tuple(shape)
+ shape, dimension_labels = self.get_shape_labels(geometry, dimension_labels)
+
array = numpy.zeros( shape , dtype=numpy.float32)
super(AcquisitionData, self).__init__(array, deep_copy,
dimension_labels, **kwargs)
else:
-
+ if self.geometry is not None:
+ shape, labels = self.get_shape_labels(self.geometry, dimension_labels)
+ if array.shape != shape:
+ raise ValueError('Shape mismatch {} {}'.format(shape, array.shape))
+
if issubclass(type(array) ,DataContainer):
# if the array is a DataContainer get the info from there
if not ( array.number_of_dimensions == 2 or \
@@ -950,21 +1026,81 @@ class AcquisitionData(DataContainer):
if dimension_labels is None:
if array.ndim == 4:
- dimension_labels = ['channel' ,'angle' , 'vertical' ,
- 'horizontal']
+ dimension_labels = [AcquisitionGeometry.CHANNEL,
+ AcquisitionGeometry.ANGLE,
+ AcquisitionGeometry.VERTICAL,
+ AcquisitionGeometry.HORIZONTAL]
elif array.ndim == 3:
- dimension_labels = ['angle' , 'vertical' ,
- 'horizontal']
+ dimension_labels = [AcquisitionGeometry.ANGLE,
+ AcquisitionGeometry.VERTICAL,
+ AcquisitionGeometry.HORIZONTAL]
else:
- dimension_labels = ['angle' ,
- 'horizontal']
-
- #DataContainer.__init__(self, array, deep_copy, dimension_labels, **kwargs)
+ dimension_labels = [AcquisitionGeometry.ANGLE,
+ AcquisitionGeometry.HORIZONTAL]
+
super(AcquisitionData, self).__init__(array, deep_copy,
dimension_labels, **kwargs)
+ def get_shape_labels(self, geometry, dimension_labels=None):
+ channels = geometry.channels
+ horiz = geometry.pixel_num_h
+ vert = geometry.pixel_num_v
+ angles = geometry.angles
+ num_of_angles = numpy.shape(angles)[0]
+
+ if dimension_labels is None:
+ if channels > 1:
+ if vert > 1:
+ shape = (channels, num_of_angles , vert, horiz)
+ dim_labels = [AcquisitionGeometry.CHANNEL,
+ AcquisitionGeometry.ANGLE,
+ AcquisitionGeometry.VERTICAL,
+ AcquisitionGeometry.HORIZONTAL]
+ else:
+ shape = (channels , num_of_angles, horiz)
+ dim_labels = [AcquisitionGeometry.CHANNEL,
+ AcquisitionGeometry.ANGLE,
+ AcquisitionGeometry.HORIZONTAL]
+ else:
+ if vert > 1:
+ shape = (num_of_angles, vert, horiz)
+ dim_labels = [AcquisitionGeometry.ANGLE,
+ AcquisitionGeometry.VERTICAL,
+ AcquisitionGeometry.HORIZONTAL
+ ]
+ else:
+ shape = (num_of_angles, horiz)
+ dim_labels = [AcquisitionGeometry.ANGLE,
+ AcquisitionGeometry.HORIZONTAL
+ ]
+
+ dimension_labels = dim_labels
+ else:
+ shape = []
+ for i in range(len(dimension_labels)):
+ dim = dimension_labels[i]
+
+ if dim == AcquisitionGeometry.CHANNEL:
+ shape.append(channels)
+ elif dim == AcquisitionGeometry.ANGLE:
+ shape.append(num_of_angles)
+ elif dim == AcquisitionGeometry.VERTICAL:
+ shape.append(vert)
+ elif dim == AcquisitionGeometry.HORIZONTAL:
+ shape.append(horiz)
+ if len(shape) != len(dimension_labels):
+ raise ValueError('Missing {0} axes.\nExpected{1} got {2}'\
+ .format(
+ len(dimension_labels) - len(shape),
+ dimension_labels, shape)
+ )
+ shape = tuple(shape)
+ return (shape, dimension_labels)
+
+
class DataProcessor(object):
+
'''Defines a generic DataContainer processor
accepts DataContainer as inputs and
@@ -1010,6 +1146,7 @@ class DataProcessor(object):
raise NotImplementedError('Implement basic checks for input DataContainer')
def get_output(self, out=None):
+
for k,v in self.__dict__.items():
if v is None and k != 'output':
raise ValueError('Key {0} is None'.format(k))
diff --git a/Wrappers/Python/ccpi/io/NEXUSDataReader.py b/Wrappers/Python/ccpi/io/NEXUSDataReader.py
new file mode 100644
index 0000000..e6d4d3b
--- /dev/null
+++ b/Wrappers/Python/ccpi/io/NEXUSDataReader.py
@@ -0,0 +1,158 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Created on Wed Apr 3 10:30:25 2019
+
+@author: evelina
+"""
+
+
+import numpy
+import os
+from ccpi.framework import AcquisitionData, AcquisitionGeometry, ImageData, ImageGeometry
+
+
+h5pyAvailable = True
+try:
+ import h5py
+except:
+ h5pyAvailable = False
+
+
+class NEXUSDataReader(object):
+
+ def __init__(self,
+ **kwargs):
+
+ '''
+ Constructor
+
+ Input:
+
+ nexus_file full path to NEXUS file
+ '''
+
+ self.nexus_file = kwargs.get('nexus_file', None)
+
+ if self.nexus_file is not None:
+ self.set_up(nexus_file = self.nexus_file)
+
+ def set_up(self,
+ nexus_file = None):
+
+ self.nexus_file = nexus_file
+
+ # check that h5py library is installed
+ if (h5pyAvailable == False):
+ raise Exception('h5py is not available, cannot load NEXUS files.')
+
+ if self.nexus_file == None:
+ raise Exception('Path to nexus file is required.')
+
+ # check if nexus file exists
+ if not(os.path.isfile(self.nexus_file)):
+ raise Exception('File\n {}\n does not exist.'.format(self.nexus_file))
+
+ def load_data(self):
+
+ '''
+ Parse NEXUS file and returns either ImageData or Acquisition Data
+ depending on file content
+ '''
+
+ try:
+ with h5py.File(self.nexus_file,'r') as file:
+
+ if (file.attrs['creator'] != 'NEXUSDataWriter.py'):
+ raise Exception('We can parse only files created by NEXUSDataWriter.py')
+
+ ds_data = file['entry1/tomo_entry/data/data']
+ data = numpy.array(ds_data, dtype = 'float32')
+
+ dimension_labels = []
+
+ for i in range(data.ndim):
+ dimension_labels.append(ds_data.attrs['dim{}'.format(i)])
+
+ if ds_data.attrs['data_type'] == 'ImageData':
+ self._geometry = ImageGeometry(voxel_num_x = ds_data.attrs['voxel_num_x'],
+ voxel_num_y = ds_data.attrs['voxel_num_y'],
+ voxel_num_z = ds_data.attrs['voxel_num_z'],
+ voxel_size_x = ds_data.attrs['voxel_size_x'],
+ voxel_size_y = ds_data.attrs['voxel_size_y'],
+ voxel_size_z = ds_data.attrs['voxel_size_z'],
+ center_x = ds_data.attrs['center_x'],
+ center_y = ds_data.attrs['center_y'],
+ center_z = ds_data.attrs['center_z'],
+ channels = ds_data.attrs['channels'])
+
+ return ImageData(array = data,
+ deep_copy = False,
+ geometry = self._geometry,
+ dimension_labels = dimension_labels)
+
+ else: # AcquisitionData
+ if ds_data.attrs.__contains__('dist_source_center'):
+ dist_source_center = ds_data.attrs['dist_source_center']
+ else:
+ dist_source_center = None
+
+ if ds_data.attrs.__contains__('dist_center_detector'):
+ dist_center_detector = ds_data.attrs['dist_center_detector']
+ else:
+ dist_center_detector = None
+
+ self._geometry = AcquisitionGeometry(geom_type = ds_data.attrs['geom_type'],
+ dimension = ds_data.attrs['dimension'],
+ dist_source_center = dist_source_center,
+ dist_center_detector = dist_center_detector,
+ pixel_num_h = ds_data.attrs['pixel_num_h'],
+ pixel_size_h = ds_data.attrs['pixel_size_h'],
+ pixel_num_v = ds_data.attrs['pixel_num_v'],
+ pixel_size_v = ds_data.attrs['pixel_size_v'],
+ channels = ds_data.attrs['channels'],
+ angles = numpy.array(file['entry1/tomo_entry/data/rotation_angle'], dtype = 'float32'))
+ #angle_unit = file['entry1/tomo_entry/data/rotation_angle'].attrs['units'])
+
+ return AcquisitionData(array = data,
+ deep_copy = False,
+ geometry = self._geometry,
+ dimension_labels = dimension_labels)
+
+ except:
+ print("Error reading nexus file")
+ raise
+
+ def get_geometry(self):
+
+ '''
+ Return either ImageGeometry or AcquisitionGeometry
+ depepnding on file content
+ '''
+
+ return self._geometry
+
+
+'''
+# usage example
+reader = NEXUSDataReader()
+reader.set_up(nexus_file = '/home/evelina/test_nexus.nxs')
+acquisition_data = reader.load_data()
+print(acquisition_data)
+ag = reader.get_geometry()
+print(ag)
+
+reader = NEXUSDataReader()
+reader.set_up(nexus_file = '/home/evelina/test_nexus_im.nxs')
+image_data = reader.load_data()
+print(image_data)
+ig = reader.get_geometry()
+print(ig)
+
+reader = NEXUSDataReader()
+reader.set_up(nexus_file = '/home/evelina/test_nexus_ag.nxs')
+ad = reader.load_data()
+print(ad)
+ad = reader.get_geometry()
+print(ad)
+'''
diff --git a/Wrappers/Python/ccpi/io/NEXUSDataWriter.py b/Wrappers/Python/ccpi/io/NEXUSDataWriter.py
new file mode 100644
index 0000000..6f5c0b2
--- /dev/null
+++ b/Wrappers/Python/ccpi/io/NEXUSDataWriter.py
@@ -0,0 +1,164 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Created on Thu May 2 10:11:20 2019
+
+@author: evelina
+"""
+
+
+import numpy
+import os
+from ccpi.framework import AcquisitionData, AcquisitionGeometry, ImageData, ImageGeometry
+import datetime
+
+
+h5pyAvailable = True
+try:
+ import h5py
+except:
+ h5pyAvailable = False
+
+
+class NEXUSDataWriter(object):
+
+ def __init__(self,
+ **kwargs):
+
+ self.data_container = kwargs.get('data_container', None)
+ self.file_name = kwargs.get('file_name', None)
+
+ if ((self.data_container is not None) and (self.file_name is not None)):
+ self.set_up(data_container = self.data_container,
+ file_name = self.file_name)
+
+ def set_up(self,
+ data_container = None,
+ file_name = None):
+
+ self.data_container = data_container
+ self.file_name = file_name
+
+ if not ((isinstance(self.data_container, ImageData)) or
+ (isinstance(self.data_container, AcquisitionData))):
+ raise Exception('Writer supports only following data types:\n' +
+ ' - ImageData\n - AcquisitionData')
+
+ # check that h5py library is installed
+ if (h5pyAvailable == False):
+ raise Exception('h5py is not available, cannot load NEXUS files.')
+
+ def write_file(self):
+
+ # if the folder does not exist, create the folder
+ if not os.path.isdir(os.path.dirname(self.file_name)):
+ os.mkdir(os.path.dirname(self.file_name))
+
+ # create the file
+ with h5py.File(self.file_name, 'w') as f:
+
+ # give the file some important attributes
+ f.attrs['file_name'] = self.file_name
+ f.attrs['file_time'] = str(datetime.datetime.utcnow())
+ f.attrs['creator'] = 'NEXUSDataWriter.py'
+ f.attrs['NeXus_version'] = '4.3.0'
+ f.attrs['HDF5_Version'] = h5py.version.hdf5_version
+ f.attrs['h5py_version'] = h5py.version.version
+
+ # create the NXentry group
+ nxentry = f.create_group('entry1/tomo_entry')
+ nxentry.attrs['NX_class'] = 'NXentry'
+
+ # create dataset to store data
+ ds_data = f.create_dataset('entry1/tomo_entry/data/data',
+ (self.data_container.as_array().shape),
+ dtype = 'float32',
+ data = self.data_container.as_array())
+
+ # set up dataset attributes
+ if (isinstance(self.data_container, ImageData)):
+ ds_data.attrs['data_type'] = 'ImageData'
+ else:
+ ds_data.attrs['data_type'] = 'AcquisitionData'
+
+ for i in range(self.data_container.as_array().ndim):
+ ds_data.attrs['dim{}'.format(i)] = self.data_container.dimension_labels[i]
+
+ if (isinstance(self.data_container, AcquisitionData)):
+ ds_data.attrs['geom_type'] = self.data_container.geometry.geom_type
+ ds_data.attrs['dimension'] = self.data_container.geometry.dimension
+ if self.data_container.geometry.dist_source_center is not None:
+ ds_data.attrs['dist_source_center'] = self.data_container.geometry.dist_source_center
+ if self.data_container.geometry.dist_center_detector is not None:
+ ds_data.attrs['dist_center_detector'] = self.data_container.geometry.dist_center_detector
+ ds_data.attrs['pixel_num_h'] = self.data_container.geometry.pixel_num_h
+ ds_data.attrs['pixel_size_h'] = self.data_container.geometry.pixel_size_h
+ ds_data.attrs['pixel_num_v'] = self.data_container.geometry.pixel_num_v
+ ds_data.attrs['pixel_size_v'] = self.data_container.geometry.pixel_size_v
+ ds_data.attrs['channels'] = self.data_container.geometry.channels
+ ds_data.attrs['n_angles'] = self.data_container.geometry.angles.shape[0]
+
+ # create the dataset to store rotation angles
+ ds_angles = f.create_dataset('entry1/tomo_entry/data/rotation_angle',
+ (self.data_container.geometry.angles.shape),
+ dtype = 'float32',
+ data = self.data_container.geometry.angles)
+
+ #ds_angles.attrs['units'] = self.data_container.geometry.angle_unit
+
+ else: # ImageData
+
+ ds_data.attrs['voxel_num_x'] = self.data_container.geometry.voxel_num_x
+ ds_data.attrs['voxel_num_y'] = self.data_container.geometry.voxel_num_y
+ ds_data.attrs['voxel_num_z'] = self.data_container.geometry.voxel_num_z
+ ds_data.attrs['voxel_size_x'] = self.data_container.geometry.voxel_size_x
+ ds_data.attrs['voxel_size_y'] = self.data_container.geometry.voxel_size_y
+ ds_data.attrs['voxel_size_z'] = self.data_container.geometry.voxel_size_z
+ ds_data.attrs['center_x'] = self.data_container.geometry.center_x
+ ds_data.attrs['center_y'] = self.data_container.geometry.center_y
+ ds_data.attrs['center_z'] = self.data_container.geometry.center_z
+ ds_data.attrs['channels'] = self.data_container.geometry.channels
+
+
+'''
+# usage example
+xtek_file = '/home/evelina/nikon_data/SophiaBeads_256_averaged.xtekct'
+reader = NikonDataReader()
+reader.set_up(xtek_file = xtek_file,
+ binning = [3, 1],
+ roi = [200, 500, 1500, 2000],
+ normalize = True)
+
+data = reader.load_projections()
+ag = reader.get_geometry()
+
+writer = NEXUSDataWriter()
+writer.set_up(file_name = '/home/evelina/test_nexus.nxs',
+ data_container = data)
+
+writer.write_file()
+
+ig = ImageGeometry(voxel_num_x = 100,
+ voxel_num_y = 100)
+im = ImageData(array = numpy.zeros((100, 100), dtype = 'float'),
+ geometry = ig)
+im_writer = NEXUSDataWriter()
+
+im_writer.set_up(file_name = '/home/evelina/test_nexus_im.nxs',
+ data_container = im)
+im_writer.write_file()
+
+ag = AcquisitionGeometry(geom_type = 'parallel',
+ dimension = '2D',
+ angles = numpy.array([0, 1]),
+ pixel_num_h = 200,
+ pixel_size_h = 1,
+ pixel_num_v = 100,
+ pixel_size_v = 1)
+
+ad = ag.allocate()
+ag_writer = NEXUSDataWriter()
+ag_writer.set_up(file_name = '/home/evelina/test_nexus_ag.nxs',
+ data_container = ad)
+ag_writer.write_file()
+''' \ No newline at end of file
diff --git a/Wrappers/Python/ccpi/io/NikonDataReader.py b/Wrappers/Python/ccpi/io/NikonDataReader.py
new file mode 100644
index 0000000..703b65b
--- /dev/null
+++ b/Wrappers/Python/ccpi/io/NikonDataReader.py
@@ -0,0 +1,281 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Created on Wed Apr 3 10:30:25 2019
+
+@author: evelina
+"""
+
+from ccpi.framework import AcquisitionData, AcquisitionGeometry
+import numpy
+import os
+
+
+pilAvailable = True
+try:
+ from PIL import Image
+except:
+ pilAvailable = False
+
+
+class NikonDataReader(object):
+
+ def __init__(self,
+ **kwargs):
+ '''
+ Constructor
+
+ Input:
+
+ xtek_file full path to .xtexct file
+
+ roi region-of-interest to load. If roi = -1 (default),
+ full projections will be loaded. Otherwise roi is
+ given by [(row0, row1), (column0, column1)], where
+ row0, column0 are coordinates of top left corner and
+ row1, column1 are coordinates of bottom right corner.
+
+ binning number of pixels to bin (combine) along 0 (column)
+ and 1 (row) dimension. If binning = [1, 1] (default),
+ projections in original resolution are loaded. Note,
+ if binning[0] != binning[1], then loaded projections
+ will have anisotropic pixels, which are currently not
+ supported by the Framework
+
+ normalize normalize loaded projections by detector
+ white level (I_0). Default value is False,
+ i.e. no normalization.
+
+ flip default = False, flip projections in the left-right direction
+
+ '''
+
+ self.xtek_file = kwargs.get('xtek_file', None)
+ self.roi = kwargs.get('roi', -1)
+ self.binning = kwargs.get('binning', [1, 1])
+ self.normalize = kwargs.get('normalize', False)
+ self.flip = kwargs.get('flip', False)
+
+ if self.xtek_file is not None:
+ self.set_up(xtek_file = self.xtek_file,
+ roi = self.roi,
+ binning = self.binning,
+ normalize = self.normalize,
+ flip = self.flip)
+
+ def set_up(self,
+ xtek_file = None,
+ roi = -1,
+ binning = [1, 1],
+ normalize = False,
+ flip = False):
+
+ self.xtek_file = xtek_file
+ self.roi = roi
+ self.binning = binning
+ self.normalize = normalize
+ self.flip = flip
+
+ if self.xtek_file == None:
+ raise Exception('Path to xtek file is required.')
+
+ # check if xtek file exists
+ if not(os.path.isfile(self.xtek_file)):
+ raise Exception('File\n {}\n does not exist.'.format(self.xtek_file))
+
+ # check that PIL library is installed
+ if (pilAvailable == False):
+ raise Exception("PIL (pillow) is not available, cannot load TIFF files.")
+
+ # parse xtek file
+ with open(self.xtek_file, 'r') as f:
+ content = f.readlines()
+
+ content = [x.strip() for x in content]
+
+ for line in content:
+ # filename of TIFF files
+ if line.startswith("Name"):
+ self._experiment_name = line.split('=')[1]
+ # number of projections
+ elif line.startswith("Projections"):
+ num_projections = int(line.split('=')[1])
+ # white level - used for normalization
+ elif line.startswith("WhiteLevel"):
+ self._white_level = float(line.split('=')[1])
+ # number of pixels along Y axis
+ elif line.startswith("DetectorPixelsY"):
+ pixel_num_v_0 = int(line.split('=')[1])
+ # number of pixels along X axis
+ elif line.startswith("DetectorPixelsX"):
+ pixel_num_h_0 = int(line.split('=')[1])
+ # pixel size along X axis
+ elif line.startswith("DetectorPixelSizeX"):
+ pixel_size_h_0 = float(line.split('=')[1])
+ # pixel size along Y axis
+ elif line.startswith("DetectorPixelSizeY"):
+ pixel_size_v_0 = float(line.split('=')[1])
+ # source to center of rotation distance
+ elif line.startswith("SrcToObject"):
+ source_x = float(line.split('=')[1])
+ # source to detector distance
+ elif line.startswith("SrcToDetector"):
+ detector_x = float(line.split('=')[1])
+ # initial angular position of a rotation stage
+ elif line.startswith("InitialAngle"):
+ initial_angle = float(line.split('=')[1])
+ # angular increment (in degrees)
+ elif line.startswith("AngularStep"):
+ angular_step = float(line.split('=')[1])
+
+ if self.roi == -1:
+ self._roi_par = [(0, pixel_num_v_0), \
+ (0, pixel_num_h_0)]
+ else:
+ self._roi_par = self.roi.copy()
+ if self._roi_par[0] == -1:
+ self._roi_par[0] = (0, pixel_num_v_0)
+ if self._roi_par[1] == -1:
+ self._roi_par[1] = (0, pixel_num_h_0)
+
+ # calculate number of pixels and pixel size
+ if (self.binning == [1, 1]):
+ pixel_num_v = self._roi_par[0][1] - self._roi_par[0][0]
+ pixel_num_h = self._roi_par[1][1] - self._roi_par[1][0]
+ pixel_size_v = pixel_size_v_0
+ pixel_size_h = pixel_size_h_0
+ else:
+ pixel_num_v = (self._roi_par[0][1] - self._roi_par[0][0]) // self.binning[0]
+ pixel_num_h = (self._roi_par[1][1] - self._roi_par[1][0]) // self.binning[1]
+ pixel_size_v = pixel_size_v_0 * self.binning[0]
+ pixel_size_h = pixel_size_h_0 * self.binning[1]
+
+ '''
+ Parse the angles file .ang or _ctdata.txt file and returns the angles
+ as an numpy array.
+ '''
+ input_path = os.path.dirname(self.xtek_file)
+ angles_ctdata_file = os.path.join(input_path, '_ctdata.txt')
+ angles_named_file = os.path.join(input_path, self._experiment_name+'.ang')
+ angles = numpy.zeros(num_projections, dtype = 'float')
+
+ # look for _ctdata.txt
+ if os.path.exists(angles_ctdata_file):
+ # read txt file with angles
+ with open(angles_ctdata_file) as f:
+ content = f.readlines()
+ # skip firt three lines
+ # read the middle value of 3 values in each line as angles in degrees
+ index = 0
+ for line in content[3:]:
+ angles[index] = float(line.split(' ')[1])
+ index += 1
+ angles = angles + initial_angle
+
+ # look for ang file
+ elif os.path.exists(angles_named_file):
+ # read the angles file which is text with first line as header
+ with open(angles_named_file) as f:
+ content = f.readlines()
+ # skip first line
+ index = 0
+ for line in content[1:]:
+ angles[index] = float(line.split(':')[1])
+ index += 1
+ angles = numpy.flipud(angles + initial_angle) # angles are in the reverse order
+
+ else: # calculate angles based on xtek file
+ angles = initial_angle + angular_step * range(num_projections)
+
+ # fill in metadata
+ self._ag = AcquisitionGeometry(geom_type = 'cone',
+ dimension = '3D',
+ angles = angles,
+ pixel_num_h = pixel_num_h,
+ pixel_size_h = pixel_size_h,
+ pixel_num_v = pixel_num_v,
+ pixel_size_v = pixel_size_v,
+ dist_source_center = source_x,
+ dist_center_detector = detector_x - source_x,
+ channels = 1,
+ angle_unit = 'degree')
+
+ def get_geometry(self):
+
+ '''
+ Return AcquisitionGeometry object
+ '''
+
+ return self._ag
+
+ def load_projections(self):
+
+ '''
+ Load projections and return AcquisitionData container
+ '''
+
+ # get path to projections
+ path_projection = os.path.dirname(self.xtek_file)
+
+ # get number of projections
+ num_projections = numpy.shape(self._ag.angles)[0]
+
+ # allocate array to store projections
+ data = numpy.zeros((num_projections, self._ag.pixel_num_v, self._ag.pixel_num_h), dtype = float)
+
+ for i in range(num_projections):
+
+ filename = (path_projection + '/' + self._experiment_name + '_{:04d}.tif').format(i + 1)
+
+ try:
+ tmp = numpy.asarray(Image.open(filename), dtype = float)
+ except:
+ print('Error reading\n {}\n file.'.format(filename))
+ raise
+
+ if (self.binning == [1, 1]):
+ data[i, :, :] = tmp[self._roi_par[0][0]:self._roi_par[0][1], self._roi_par[1][0]:self._roi_par[1][1]]
+ else:
+ shape = (self._ag.pixel_num_v, self.binning[0],
+ self._ag.pixel_num_h, self.binning[1])
+ data[i, :, :] = tmp[self._roi_par[0][0]:(self._roi_par[0][0] + (((self._roi_par[0][1] - self._roi_par[0][0]) // self.binning[0]) * self.binning[0])), \
+ self._roi_par[1][0]:(self._roi_par[1][0] + (((self._roi_par[1][1] - self._roi_par[1][0]) // self.binning[1]) * self.binning[1]))].reshape(shape).mean(-1).mean(1)
+
+ if (self.normalize):
+ data /= self._white_level
+ data[data > 1] = 1
+
+ if self.flip:
+ return AcquisitionData(array = data[:, :, ::-1],
+ deep_copy = False,
+ geometry = self._ag,
+ dimension_labels = ['angle', \
+ 'vertical', \
+ 'horizontal'])
+ else:
+ return AcquisitionData(array = data,
+ deep_copy = False,
+ geometry = self._ag,
+ dimension_labels = ['angle', \
+ 'vertical', \
+ 'horizontal'])
+
+
+'''
+# usage example
+xtek_file = '/home/evelina/nikon_data/SophiaBeads_256_averaged.xtekct'
+reader = NikonDataReader()
+reader.set_up(xtek_file = xtek_file,
+ binning = [1, 1],
+ roi = -1,
+ normalize = True,
+ flip = True)
+
+data = reader.load_projections()
+print(data)
+ag = reader.get_geometry()
+print(ag)
+
+plt.imshow(data.as_array()[1, :, :])
+plt.show()
+'''
diff --git a/Wrappers/Python/ccpi/io/__init__.py b/Wrappers/Python/ccpi/io/__init__.py
index 9233d7a..455faba 100644
--- a/Wrappers/Python/ccpi/io/__init__.py
+++ b/Wrappers/Python/ccpi/io/__init__.py
@@ -15,4 +15,8 @@
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
-# limitations under the License. \ No newline at end of file
+# limitations under the License.
+
+from .NEXUSDataReader import NEXUSDataReader
+from .NEXUSDataWriter import NEXUSDataWriter
+from .NikonDataReader import NikonDataReader
diff --git a/Wrappers/Python/ccpi/io/reader.py b/Wrappers/Python/ccpi/io/reader.py
index 856f5e0..07e3bf9 100644
--- a/Wrappers/Python/ccpi/io/reader.py
+++ b/Wrappers/Python/ccpi/io/reader.py
@@ -241,26 +241,37 @@ class NexusReader(object):
pass
dims = file[self.data_path].shape
if ymin is None and ymax is None:
- data = np.array(file[self.data_path])
+
+ try:
+ image_keys = self.get_image_keys()
+ print ("image_keys", image_keys)
+ projections = np.array(file[self.data_path])
+ data = projections[image_keys==0]
+ except KeyError as ke:
+ print (ke)
+ data = np.array(file[self.data_path])
+
else:
+ image_keys = self.get_image_keys()
+ print ("image_keys", image_keys)
+ projections = np.array(file[self.data_path])[image_keys==0]
if ymin is None:
ymin = 0
if ymax > dims[1]:
raise ValueError('ymax out of range')
- data = np.array(file[self.data_path][:,:ymax,:])
+ data = projections[:,:ymax,:]
elif ymax is None:
ymax = dims[1]
if ymin < 0:
raise ValueError('ymin out of range')
- data = np.array(file[self.data_path][:,ymin:,:])
+ data = projections[:,ymin:,:]
else:
if ymax > dims[1]:
raise ValueError('ymax out of range')
if ymin < 0:
raise ValueError('ymin out of range')
- data = np.array(file[self.data_path]
- [: , ymin:ymax , :] )
+ data = projections[: , ymin:ymax , :]
except:
print("Error reading nexus file")
diff --git a/Wrappers/Python/ccpi/optimisation/algorithms/Algorithm.py b/Wrappers/Python/ccpi/optimisation/algorithms/Algorithm.py
index 680b268..c62d0ea 100755
--- a/Wrappers/Python/ccpi/optimisation/algorithms/Algorithm.py
+++ b/Wrappers/Python/ccpi/optimisation/algorithms/Algorithm.py
@@ -16,7 +16,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
-import time
+import time, functools
from numbers import Integral
class Algorithm(object):
@@ -34,7 +34,7 @@ class Algorithm(object):
method will stop when the stopping cryterion is met.
'''
- def __init__(self):
+ def __init__(self, **kwargs):
'''Constructor
Set the minimal number of parameters:
@@ -48,11 +48,12 @@ class Algorithm(object):
when evaluating the objective is computationally expensive.
'''
self.iteration = 0
- self.__max_iteration = 0
+ self.__max_iteration = kwargs.get('max_iteration', 0)
self.__loss = []
self.memopt = False
+ self.configured = False
self.timing = []
- self.update_objective_interval = 1
+ self.update_objective_interval = kwargs.get('update_objective_interval', 1)
def set_up(self, *args, **kwargs):
'''Set up the algorithm'''
raise NotImplementedError()
@@ -86,14 +87,18 @@ class Algorithm(object):
raise StopIteration()
else:
time0 = time.time()
+ if not self.configured:
+ raise ValueError('Algorithm not configured correctly. Please run set_up.')
self.update()
self.timing.append( time.time() - time0 )
if self.iteration % self.update_objective_interval == 0:
self.update_objective()
self.iteration += 1
+
def get_output(self):
'''Returns the solution found'''
return self.x
+
def get_last_loss(self):
'''Returns the last stored value of the loss function
@@ -140,18 +145,64 @@ class Algorithm(object):
raise ValueError('Update objective interval must be an integer >= 1')
else:
raise ValueError('Update objective interval must be an integer >= 1')
- def run(self, iterations, verbose=False, callback=None):
+ def run(self, iterations, verbose=True, callback=None):
'''run n iterations and update the user with the callback if specified'''
if self.should_stop():
print ("Stop cryterion has been reached.")
i = 0
for _ in self:
- if verbose:
- print ("Iteration {}/{}, objective {}".format(self.iteration,
- self.max_iteration, self.get_last_objective()) )
- if callback is not None:
- callback(self.iteration, self.get_last_objective())
+ if i == 0 and verbose:
+ print (self.verbose_header())
+ if (self.iteration -1) % self.update_objective_interval == 0:
+ if verbose:
+ print (self.verbose_output())
+ if callback is not None:
+ callback(self.iteration -1, self.get_last_objective(), self.x)
i += 1
if i == iterations:
break
-
+
+ def verbose_output(self):
+ '''Creates a nice tabulated output'''
+ timing = self.timing[-self.update_objective_interval-1:-1]
+ if len (timing) == 0:
+ t = 0
+ else:
+ t = sum(timing)/len(timing)
+ out = "{:>9} {:>10} {:>13} {}".format(
+ self.iteration-1,
+ self.max_iteration,
+ "{:.3f}".format(t),
+ self.objective_to_string()
+ )
+ return out
+
+ def objective_to_string(self):
+ el = self.get_last_objective()
+ if type(el) == list:
+ string = functools.reduce(lambda x,y: x+' {:>13.5e}'.format(y), el[:-1],'')
+ string += '{:>15.5e}'.format(el[-1])
+ else:
+ string = "{:>20.5e}".format(el)
+ return string
+ def verbose_header(self):
+ el = self.get_last_objective()
+ if type(el) == list:
+ out = "{:>9} {:>10} {:>13} {:>13} {:>13} {:>15}\n".format('Iter',
+ 'Max Iter',
+ 'Time/Iter',
+ 'Primal' , 'Dual', 'Primal-Dual')
+ out += "{:>9} {:>10} {:>13} {:>13} {:>13} {:>15}".format('',
+ '',
+ '[s]',
+ 'Objective' , 'Objective', 'Gap')
+ else:
+ out = "{:>9} {:>10} {:>13} {:>20}\n".format('Iter',
+ 'Max Iter',
+ 'Time/Iter',
+ 'Objective')
+ out += "{:>9} {:>10} {:>13} {:>20}".format('',
+ '',
+ '[s]',
+ '')
+ return out
diff --git a/Wrappers/Python/ccpi/optimisation/algorithms/CGLS.py b/Wrappers/Python/ccpi/optimisation/algorithms/CGLS.py
index 7194eb8..15acc31 100755
--- a/Wrappers/Python/ccpi/optimisation/algorithms/CGLS.py
+++ b/Wrappers/Python/ccpi/optimisation/algorithms/CGLS.py
@@ -23,7 +23,9 @@ Created on Thu Feb 21 11:11:23 2019
"""
from ccpi.optimisation.algorithms import Algorithm
-#from collections.abc import Iterable
+from ccpi.optimisation.functions import Norm2Sq
+import numpy
+
class CGLS(Algorithm):
'''Conjugate Gradient Least Squares algorithm
@@ -48,20 +50,26 @@ class CGLS(Algorithm):
def set_up(self, x_init, operator , data ):
self.r = data.copy()
- self.x = x_init.copy()
+ self.x = x_init * 0
self.operator = operator
self.d = operator.adjoint(self.r)
self.normr2 = self.d.squared_norm()
+
+ self.s = self.operator.domain_geometry().allocate()
#if isinstance(self.normr2, Iterable):
# self.normr2 = sum(self.normr2)
#self.normr2 = numpy.sqrt(self.normr2)
#print ("set_up" , self.normr2)
+ n = Norm2Sq(operator, self.data)
+ self.loss.append(n(x_init))
+ self.configured = True
def update(self):
-
+ self.update_new()
+ def update_old(self):
Ad = self.operator.direct(self.d)
#norm = (Ad*Ad).sum()
#if isinstance(norm, Iterable):
@@ -83,5 +91,44 @@ class CGLS(Algorithm):
self.normr2 = normr2_new
self.d = s + beta*self.d
+ def update_new(self):
+
+ Ad = self.operator.direct(self.d)
+ norm = Ad.squared_norm()
+ if norm == 0.:
+ print ('norm = 0, cannot update solution')
+ print ("self.d norm", self.d.squared_norm(), self.d.as_array())
+ raise StopIteration()
+ alpha = self.normr2/norm
+ if alpha == 0.:
+ print ('alpha = 0, cannot update solution')
+ raise StopIteration()
+ self.d *= alpha
+ Ad *= alpha
+ self.r -= Ad
+
+ self.x += self.d
+
+ self.operator.adjoint(self.r, out=self.s)
+ s = self.s
+
+ normr2_new = s.squared_norm()
+
+ beta = normr2_new/self.normr2
+ self.normr2 = normr2_new
+ self.d *= (beta/alpha)
+ self.d += s
+
def update_objective(self):
- self.loss.append(self.r.squared_norm()) \ No newline at end of file
+ a = self.r.squared_norm()
+ if a is numpy.nan:
+ raise StopIteration()
+ self.loss.append(a)
+
+# def should_stop(self):
+# if self.iteration > 0:
+# x = self.get_last_objective()
+# a = x > 0
+# return self.max_iteration_stop_cryterion() or (not a)
+# else:
+# return False
diff --git a/Wrappers/Python/ccpi/optimisation/algorithms/FBPD.py b/Wrappers/Python/ccpi/optimisation/algorithms/FBPD.py
deleted file mode 100644
index 798fb61..0000000
--- a/Wrappers/Python/ccpi/optimisation/algorithms/FBPD.py
+++ /dev/null
@@ -1,86 +0,0 @@
-# -*- coding: utf-8 -*-
-# This work is part of the Core Imaging Library developed by
-# Visual Analytics and Imaging System Group of the Science Technology
-# Facilities Council, STFC
-
-# Copyright 2019 Edoardo Pasca
-
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-
-# http://www.apache.org/licenses/LICENSE-2.0
-
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-"""
-Created on Thu Feb 21 11:09:03 2019
-
-@author: ofn77899
-"""
-
-from ccpi.optimisation.algorithms import Algorithm
-from ccpi.optimisation.funcs import ZeroFun
-
-class FBPD(Algorithm):
- '''FBPD Algorithm
-
- Parameters:
- x_init: initial guess
- f: constraint
- g: data fidelity
- h: regularizer
- opt: additional algorithm
- '''
- constraint = None
- data_fidelity = None
- regulariser = None
- def __init__(self, **kwargs):
- pass
- def set_up(self, x_init, operator=None, constraint=None, data_fidelity=None,\
- regulariser=None, opt=None):
-
- # default inputs
- if constraint is None:
- self.constraint = ZeroFun()
- else:
- self.constraint = constraint
- if data_fidelity is None:
- data_fidelity = ZeroFun()
- else:
- self.data_fidelity = data_fidelity
- if regulariser is None:
- self.regulariser = ZeroFun()
- else:
- self.regulariser = regulariser
-
- # algorithmic parameters
-
-
- # step-sizes
- self.tau = 2 / (self.data_fidelity.L + 2)
- self.sigma = (1/self.tau - self.data_fidelity.L/2) / self.regulariser.L
-
- self.inv_sigma = 1/self.sigma
-
- # initialization
- self.x = x_init
- self.y = operator.direct(self.x)
-
-
- def update(self):
-
- # primal forward-backward step
- x_old = self.x
- self.x = self.x - self.tau * ( self.data_fidelity.grad(self.x) + self.operator.adjoint(self.y) )
- self.x = self.constraint.prox(self.x, self.tau);
-
- # dual forward-backward step
- self.y = self.y + self.sigma * self.operator.direct(2*self.x - x_old);
- self.y = self.y - self.sigma * self.regulariser.prox(self.inv_sigma*self.y, self.inv_sigma);
-
- # time and criterion
- self.loss = self.constraint(self.x) + self.data_fidelity(self.x) + self.regulariser(self.operator.direct(self.x))
diff --git a/Wrappers/Python/ccpi/optimisation/algorithms/FISTA.py b/Wrappers/Python/ccpi/optimisation/algorithms/FISTA.py
index bc4489e..c8fd0d4 100755
--- a/Wrappers/Python/ccpi/optimisation/algorithms/FISTA.py
+++ b/Wrappers/Python/ccpi/optimisation/algorithms/FISTA.py
@@ -6,7 +6,7 @@ Created on Thu Feb 21 11:07:30 2019
"""
from ccpi.optimisation.algorithms import Algorithm
-from ccpi.optimisation.funcs import ZeroFun
+from ccpi.optimisation.functions import ZeroFunction
import numpy
class FISTA(Algorithm):
@@ -20,102 +20,65 @@ class FISTA(Algorithm):
x_init: initial guess
f: data fidelity
g: regularizer
- h:
- opt: additional algorithm
+ opt: additional options
'''
-
+
+
def __init__(self, **kwargs):
'''initialisation can be done at creation time if all
proper variables are passed or later with set_up'''
super(FISTA, self).__init__()
- self.f = None
- self.g = None
+ self.f = kwargs.get('f', None)
+ self.g = kwargs.get('g', ZeroFunction())
+ self.x_init = kwargs.get('x_init',None)
self.invL = None
self.t_old = 1
- args = ['x_init', 'f', 'g', 'opt']
- for k,v in kwargs.items():
- if k in args:
- args.pop(args.index(k))
- if len(args) == 0:
- return self.set_up(kwargs['x_init'],
- f=kwargs['f'],
- g=kwargs['g'],
- opt=kwargs['opt'])
+ if self.x_init is not None and \
+ self.f is not None and self.g is not None:
+ print ("FISTA set_up called from creator")
+ self.set_up(self.x_init, self.f, self.g)
+
- def set_up(self, x_init, f=None, g=None, opt=None):
+ def set_up(self, x_init, f, g, opt=None, **kwargs):
- # default inputs
- if f is None:
- self.f = ZeroFun()
- else:
- self.f = f
- if g is None:
- g = ZeroFun()
- self.g = g
- else:
- self.g = g
+ self.f = f
+ self.g = g
# algorithmic parameters
if opt is None:
- opt = {'tol': 1e-4, 'memopt':False}
-
- self.tol = opt['tol'] if 'tol' in opt.keys() else 1e-4
- memopt = opt['memopt'] if 'memopt' in opt.keys() else False
- self.memopt = memopt
-
- # initialization
- if memopt:
- self.y = x_init.clone()
- self.x_old = x_init.clone()
- self.x = x_init.clone()
- self.u = x_init.clone()
- else:
- self.x_old = x_init.copy()
- self.y = x_init.copy()
-
- #timing = numpy.zeros(max_iter)
- #criter = numpy.zeros(max_iter)
+ opt = {'tol': 1e-4}
-
+ self.y = x_init.copy()
+ self.x_old = x_init.copy()
+ self.x = x_init.copy()
+ self.u = x_init.copy()
+
+
self.invL = 1/f.L
self.t_old = 1
+ self.update_objective()
+ self.configured = True
def update(self):
- # algorithm loop
- #for it in range(0, max_iter):
-
- if self.memopt:
- # u = y - invL*f.grad(y)
- # store the result in x_old
- self.f.gradient(self.y, out=self.u)
- self.u.__imul__( -self.invL )
- self.u.__iadd__( self.y )
- # x = g.prox(u,invL)
- self.g.proximal(self.u, self.invL, out=self.x)
-
- self.t = 0.5*(1 + numpy.sqrt(1 + 4*(self.t_old**2)))
-
- # y = x + (t_old-1)/t*(x-x_old)
- self.x.subtract(self.x_old, out=self.y)
- self.y.__imul__ ((self.t_old-1)/self.t)
- self.y.__iadd__( self.x )
-
- self.x_old.fill(self.x)
- self.t_old = self.t
-
-
- else:
- u = self.y - self.invL*self.f.grad(self.y)
-
- self.x = self.g.prox(u,self.invL)
-
- self.t = 0.5*(1 + numpy.sqrt(1 + 4*(self.t_old**2)))
-
- self.y = self.x + (self.t_old-1)/self.t*(self.x-self.x_old)
-
- self.x_old = self.x.copy()
- self.t_old = self.t
+
+ self.f.gradient(self.y, out=self.u)
+ self.u.__imul__( -self.invL )
+ self.u.__iadd__( self.y )
+ # x = g.prox(u,invL)
+ self.g.proximal(self.u, self.invL, out=self.x)
+
+ self.t = 0.5*(1 + numpy.sqrt(1 + 4*(self.t_old**2)))
+
+# self.x.subtract(self.x_old, out=self.y)
+ self.y = self.x - self.x_old
+ self.y.__imul__ ((self.t_old-1)/self.t)
+ self.y.__iadd__( self.x )
+
+ self.x_old.fill(self.x)
+ self.t_old = self.t
def update_objective(self):
- self.loss.append( self.f(self.x) + self.g(self.x) ) \ No newline at end of file
+ self.loss.append( self.f(self.x) + self.g(self.x) )
+
+
diff --git a/Wrappers/Python/ccpi/optimisation/algorithms/GradientDescent.py b/Wrappers/Python/ccpi/optimisation/algorithms/GradientDescent.py
index 7794b4d..34bf954 100755
--- a/Wrappers/Python/ccpi/optimisation/algorithms/GradientDescent.py
+++ b/Wrappers/Python/ccpi/optimisation/algorithms/GradientDescent.py
@@ -40,7 +40,7 @@ class GradientDescent(Algorithm):
if k in args:
args.pop(args.index(k))
if len(args) == 0:
- return self.set_up(x_init=kwargs['x_init'],
+ self.set_up(x_init=kwargs['x_init'],
objective_function=kwargs['objective_function'],
rate=kwargs['rate'])
@@ -51,13 +51,18 @@ class GradientDescent(Algorithm):
def set_up(self, x_init, objective_function, rate):
'''initialisation of the algorithm'''
self.x = x_init.copy()
- if self.memopt:
- self.x_update = x_init.copy()
self.objective_function = objective_function
self.rate = rate
self.loss.append(objective_function(x_init))
self.iteration = 0
-
+ try:
+ self.memopt = self.objective_function.memopt
+ except AttributeError as ae:
+ self.memopt = False
+ if self.memopt:
+ self.x_update = x_init.copy()
+ self.configured = True
+
def update(self):
'''Single iteration'''
if self.memopt:
@@ -65,8 +70,8 @@ class GradientDescent(Algorithm):
self.x_update *= -self.rate
self.x += self.x_update
else:
- self.x += -self.rate * self.objective_function.grad(self.x)
+ self.x += -self.rate * self.objective_function.gradient(self.x)
def update_objective(self):
self.loss.append(self.objective_function(self.x))
- \ No newline at end of file
+
diff --git a/Wrappers/Python/ccpi/optimisation/algorithms/PDHG.py b/Wrappers/Python/ccpi/optimisation/algorithms/PDHG.py
new file mode 100644
index 0000000..3afd8b0
--- /dev/null
+++ b/Wrappers/Python/ccpi/optimisation/algorithms/PDHG.py
@@ -0,0 +1,186 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Created on Mon Feb 4 16:18:06 2019
+
+@author: evangelos
+"""
+from ccpi.optimisation.algorithms import Algorithm
+from ccpi.framework import ImageData, DataContainer
+import numpy as np
+import numpy
+import time
+from ccpi.optimisation.operators import BlockOperator
+from ccpi.framework import BlockDataContainer
+from ccpi.optimisation.functions import FunctionOperatorComposition
+
+class PDHG(Algorithm):
+ '''Primal Dual Hybrid Gradient'''
+
+ def __init__(self, **kwargs):
+ super(PDHG, self).__init__(max_iteration=kwargs.get('max_iteration',0))
+ self.f = kwargs.get('f', None)
+ self.operator = kwargs.get('operator', None)
+ self.g = kwargs.get('g', None)
+ self.tau = kwargs.get('tau', None)
+ self.sigma = kwargs.get('sigma', 1.)
+
+
+ if self.f is not None and self.operator is not None and \
+ self.g is not None:
+ if self.tau is None:
+ # Compute operator Norm
+ normK = self.operator.norm()
+ # Primal & dual stepsizes
+ self.tau = 1/(self.sigma*normK**2)
+ print ("Calling from creator")
+ self.set_up(self.f,
+ self.g,
+ self.operator,
+ self.tau,
+ self.sigma)
+
+ def set_up(self, f, g, operator, tau = None, sigma = None, opt = None, **kwargs):
+ # algorithmic parameters
+ self.operator = operator
+ self.f = f
+ self.g = g
+ self.tau = tau
+ self.sigma = sigma
+ self.opt = opt
+ if sigma is None and tau is None:
+ raise ValueError('Need sigma*tau||K||^2<1')
+
+ self.x_old = self.operator.domain_geometry().allocate()
+ self.x_tmp = self.x_old.copy()
+ self.x = self.x_old.copy()
+
+ self.y_old = self.operator.range_geometry().allocate()
+ self.y_tmp = self.y_old.copy()
+ self.y = self.y_old.copy()
+
+ self.xbar = self.x_old.copy()
+
+ # relaxation parameter
+ self.theta = 1
+ self.update_objective()
+ self.configured = True
+
+ def update(self):
+
+ # Gradient descent, Dual problem solution
+ self.operator.direct(self.xbar, out=self.y_tmp)
+ self.y_tmp *= self.sigma
+ self.y_tmp += self.y_old
+
+ #self.y = self.f.proximal_conjugate(self.y_old, self.sigma)
+ self.f.proximal_conjugate(self.y_tmp, self.sigma, out=self.y)
+
+ # Gradient ascent, Primal problem solution
+ self.operator.adjoint(self.y, out=self.x_tmp)
+ self.x_tmp *= -1*self.tau
+ self.x_tmp += self.x_old
+
+
+ self.g.proximal(self.x_tmp, self.tau, out=self.x)
+
+ #Update
+ self.x.subtract(self.x_old, out=self.xbar)
+ self.xbar *= self.theta
+ self.xbar += self.x
+
+ self.x_old.fill(self.x)
+ self.y_old.fill(self.y)
+
+ def update_objective(self):
+
+ p1 = self.f(self.operator.direct(self.x)) + self.g(self.x)
+ d1 = -(self.f.convex_conjugate(self.y) + self.g.convex_conjugate(-1*self.operator.adjoint(self.y)))
+
+ self.loss.append([p1,d1,p1-d1])
+
+
+
+def PDHG_old(f, g, operator, tau = None, sigma = None, opt = None, **kwargs):
+
+ # algorithmic parameters
+ if opt is None:
+ opt = {'tol': 1e-6, 'niter': 500, 'show_iter': 100, \
+ 'memopt': False}
+
+ if sigma is None and tau is None:
+ raise ValueError('Need sigma*tau||K||^2<1')
+
+ niter = opt['niter'] if 'niter' in opt.keys() else 1000
+ tol = opt['tol'] if 'tol' in opt.keys() else 1e-4
+ memopt = opt['memopt'] if 'memopt' in opt.keys() else False
+ show_iter = opt['show_iter'] if 'show_iter' in opt.keys() else False
+ stop_crit = opt['stop_crit'] if 'stop_crit' in opt.keys() else False
+
+ x_old = operator.domain_geometry().allocate()
+ y_old = operator.range_geometry().allocate()
+
+ xbar = x_old.copy()
+ x_tmp = x_old.copy()
+ x = x_old.copy()
+
+ y_tmp = y_old.copy()
+ y = y_tmp.copy()
+
+
+ # relaxation parameter
+ theta = 1
+
+ t = time.time()
+
+ primal = []
+ dual = []
+ pdgap = []
+
+
+ for i in range(niter):
+
+
+
+ if memopt:
+ operator.direct(xbar, out = y_tmp)
+ y_tmp *= sigma
+ y_tmp += y_old
+ else:
+ y_tmp = y_old + sigma * operator.direct(xbar)
+
+ f.proximal_conjugate(y_tmp, sigma, out=y)
+
+ if memopt:
+ operator.adjoint(y, out = x_tmp)
+ x_tmp *= -1*tau
+ x_tmp += x_old
+ else:
+ x_tmp = x_old - tau*operator.adjoint(y)
+
+ g.proximal(x_tmp, tau, out=x)
+
+ x.subtract(x_old, out=xbar)
+ xbar *= theta
+ xbar += x
+
+ x_old.fill(x)
+ y_old.fill(y)
+
+ if i%10==0:
+
+ p1 = f(operator.direct(x)) + g(x)
+ d1 = - ( f.convex_conjugate(y) + g.convex_conjugate(-1*operator.adjoint(y)) )
+ primal.append(p1)
+ dual.append(d1)
+ pdgap.append(p1-d1)
+ print(p1, d1, p1-d1)
+
+
+
+ t_end = time.time()
+
+ return x, t_end - t, primal, dual, pdgap
+
+
+
diff --git a/Wrappers/Python/ccpi/optimisation/algorithms/SIRT.py b/Wrappers/Python/ccpi/optimisation/algorithms/SIRT.py
new file mode 100644
index 0000000..c73d323
--- /dev/null
+++ b/Wrappers/Python/ccpi/optimisation/algorithms/SIRT.py
@@ -0,0 +1,76 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# This work is part of the Core Imaging Library developed by
+# Visual Analytics and Imaging System Group of the Science Technology
+# Facilities Council, STFC
+
+# Copyright 2018 Edoardo Pasca
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from ccpi.optimisation.algorithms import Algorithm
+
+class SIRT(Algorithm):
+
+ '''Simultaneous Iterative Reconstruction Technique
+
+ Parameters:
+ x_init: initial guess
+ operator: operator for forward/backward projections
+ data: data to operate on
+ constraint: Function with prox-method, for example IndicatorBox to
+ enforce box constraints, default is None).
+ '''
+ def __init__(self, **kwargs):
+ super(SIRT, self).__init__()
+ self.x = kwargs.get('x_init', None)
+ self.operator = kwargs.get('operator', None)
+ self.data = kwargs.get('data', None)
+ self.constraint = kwargs.get('constraint', None)
+ if self.x is not None and self.operator is not None and \
+ self.data is not None:
+ print ("Calling from creator")
+ self.set_up(x_init=kwargs['x_init'],
+ operator=kwargs['operator'],
+ data=kwargs['data'],
+ constraint=kwargs['constraint'])
+
+ def set_up(self, x_init, operator , data, constraint=None ):
+
+ self.x = x_init.copy()
+ self.operator = operator
+ self.data = data
+ self.constraint = constraint
+
+ self.r = data.copy()
+
+ self.relax_par = 1.0
+
+ # Set up scaling matrices D and M.
+ self.M = 1/self.operator.direct(self.operator.domain_geometry().allocate(value=1.0))
+ self.D = 1/self.operator.adjoint(self.operator.range_geometry().allocate(value=1.0))
+ self.configured = True
+
+
+ def update(self):
+
+ self.r = self.data - self.operator.direct(self.x)
+
+ self.x += self.relax_par * (self.D*self.operator.adjoint(self.M*self.r))
+
+ if self.constraint is not None:
+ self.x = self.constraint.proximal(self.x,None)
+ # self.constraint.proximal(self.x,None, out=self.x)
+
+ def update_objective(self):
+ self.loss.append(self.r.squared_norm())
diff --git a/Wrappers/Python/ccpi/optimisation/algorithms/__init__.py b/Wrappers/Python/ccpi/optimisation/algorithms/__init__.py
index 903bc30..8f255f3 100644
--- a/Wrappers/Python/ccpi/optimisation/algorithms/__init__.py
+++ b/Wrappers/Python/ccpi/optimisation/algorithms/__init__.py
@@ -24,6 +24,8 @@ Created on Thu Feb 21 11:03:13 2019
from .Algorithm import Algorithm
from .CGLS import CGLS
+from .SIRT import SIRT
from .GradientDescent import GradientDescent
from .FISTA import FISTA
-from .FBPD import FBPD
+from .PDHG import PDHG
+
diff --git a/Wrappers/Python/ccpi/optimisation/algs.py b/Wrappers/Python/ccpi/optimisation/algs.py
deleted file mode 100755
index 15638a9..0000000
--- a/Wrappers/Python/ccpi/optimisation/algs.py
+++ /dev/null
@@ -1,319 +0,0 @@
-# -*- coding: utf-8 -*-
-# This work is part of the Core Imaging Library developed by
-# Visual Analytics and Imaging System Group of the Science Technology
-# Facilities Council, STFC
-
-# Copyright 2018 Jakob Jorgensen, Daniil Kazantsev and Edoardo Pasca
-
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-
-# http://www.apache.org/licenses/LICENSE-2.0
-
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import numpy
-import time
-
-from ccpi.optimisation.funcs import Function
-from ccpi.optimisation.funcs import ZeroFun
-from ccpi.framework import ImageData
-from ccpi.framework import AcquisitionData
-from ccpi.optimisation.spdhg import spdhg
-from ccpi.optimisation.spdhg import KullbackLeibler
-from ccpi.optimisation.spdhg import KullbackLeiblerConvexConjugate
-
-def FISTA(x_init, f=None, g=None, opt=None):
- '''Fast Iterative Shrinkage-Thresholding Algorithm
-
- Beck, A. and Teboulle, M., 2009. A fast iterative shrinkage-thresholding
- algorithm for linear inverse problems.
- SIAM journal on imaging sciences,2(1), pp.183-202.
-
- Parameters:
- x_init: initial guess
- f: data fidelity
- g: regularizer
- h:
- opt: additional algorithm
- '''
- # default inputs
- if f is None: f = ZeroFun()
- if g is None: g = ZeroFun()
-
- # algorithmic parameters
- if opt is None:
- opt = {'tol': 1e-4, 'iter': 1000, 'memopt':False}
-
- max_iter = opt['iter'] if 'iter' in opt.keys() else 1000
- tol = opt['tol'] if 'tol' in opt.keys() else 1e-4
- memopt = opt['memopt'] if 'memopt' in opt.keys() else False
-
-
- # initialization
- if memopt:
- y = x_init.clone()
- x_old = x_init.clone()
- x = x_init.clone()
- u = x_init.clone()
- else:
- x_old = x_init
- y = x_init;
-
- timing = numpy.zeros(max_iter)
- criter = numpy.zeros(max_iter)
-
- invL = 1/f.L
-
- t_old = 1
-
- c = f(x_init) + g(x_init)
-
- # algorithm loop
- for it in range(0, max_iter):
-
- time0 = time.time()
- if memopt:
- # u = y - invL*f.grad(y)
- # store the result in x_old
- f.gradient(y, out=u)
- u.__imul__( -invL )
- u.__iadd__( y )
- # x = g.prox(u,invL)
- g.proximal(u, invL, out=x)
-
- t = 0.5*(1 + numpy.sqrt(1 + 4*(t_old**2)))
-
- # y = x + (t_old-1)/t*(x-x_old)
- x.subtract(x_old, out=y)
- y.__imul__ ((t_old-1)/t)
- y.__iadd__( x )
-
- x_old.fill(x)
- t_old = t
-
-
- else:
- u = y - invL*f.grad(y)
-
- x = g.prox(u,invL)
-
- t = 0.5*(1 + numpy.sqrt(1 + 4*(t_old**2)))
-
- y = x + (t_old-1)/t*(x-x_old)
-
- x_old = x.copy()
- t_old = t
-
- # time and criterion
- timing[it] = time.time() - time0
- criter[it] = f(x) + g(x);
-
- # stopping rule
- #if np.linalg.norm(x - x_old) < tol * np.linalg.norm(x_old) and it > 10:
- # break
-
- #print(it, 'out of', 10, 'iterations', end='\r');
-
- #criter = criter[0:it+1];
- timing = numpy.cumsum(timing[0:it+1]);
-
- return x, it, timing, criter
-
-def FBPD(x_init, operator=None, constraint=None, data_fidelity=None,\
- regulariser=None, opt=None):
- '''FBPD Algorithm
-
- Parameters:
- x_init: initial guess
- f: constraint
- g: data fidelity
- h: regularizer
- opt: additional algorithm
- '''
- # default inputs
- if constraint is None: constraint = ZeroFun()
- if data_fidelity is None: data_fidelity = ZeroFun()
- if regulariser is None: regulariser = ZeroFun()
-
- # algorithmic parameters
- if opt is None:
- opt = {'tol': 1e-4, 'iter': 1000}
- else:
- try:
- max_iter = opt['iter']
- except KeyError as ke:
- opt[ke] = 1000
- try:
- opt['tol'] = 1000
- except KeyError as ke:
- opt[ke] = 1e-4
- tol = opt['tol']
- max_iter = opt['iter']
- memopt = opt['memopts'] if 'memopts' in opt.keys() else False
-
- # step-sizes
- tau = 2 / (data_fidelity.L + 2)
- sigma = (1/tau - data_fidelity.L/2) / regulariser.L
- inv_sigma = 1/sigma
-
- # initialization
- x = x_init
- y = operator.direct(x);
-
- timing = numpy.zeros(max_iter)
- criter = numpy.zeros(max_iter)
-
-
-
-
- # algorithm loop
- for it in range(0, max_iter):
-
- t = time.time()
-
- # primal forward-backward step
- x_old = x;
- x = x - tau * ( data_fidelity.grad(x) + operator.adjoint(y) );
- x = constraint.prox(x, tau);
-
- # dual forward-backward step
- y = y + sigma * operator.direct(2*x - x_old);
- y = y - sigma * regulariser.prox(inv_sigma*y, inv_sigma);
-
- # time and criterion
- timing[it] = time.time() - t
- criter[it] = constraint(x) + data_fidelity(x) + regulariser(operator.direct(x))
-
- # stopping rule
- #if np.linalg.norm(x - x_old) < tol * np.linalg.norm(x_old) and it > 10:
- # break
-
- criter = criter[0:it+1]
- timing = numpy.cumsum(timing[0:it+1])
-
- return x, it, timing, criter
-
-def CGLS(x_init, operator , data , opt=None):
- '''Conjugate Gradient Least Squares algorithm
-
- Parameters:
- x_init: initial guess
- operator: operator for forward/backward projections
- data: data to operate on
- opt: additional algorithm
- '''
-
- if opt is None:
- opt = {'tol': 1e-4, 'iter': 1000}
- else:
- try:
- max_iter = opt['iter']
- except KeyError as ke:
- opt[ke] = 1000
- try:
- opt['tol'] = 1000
- except KeyError as ke:
- opt[ke] = 1e-4
- tol = opt['tol']
- max_iter = opt['iter']
-
- r = data.copy()
- x = x_init.copy()
-
- d = operator.adjoint(r)
-
- normr2 = (d**2).sum()
-
- timing = numpy.zeros(max_iter)
- criter = numpy.zeros(max_iter)
-
- # algorithm loop
- for it in range(0, max_iter):
-
- t = time.time()
-
- Ad = operator.direct(d)
- alpha = normr2/( (Ad**2).sum() )
- x = x + alpha*d
- r = r - alpha*Ad
- s = operator.adjoint(r)
-
- normr2_new = (s**2).sum()
- beta = normr2_new/normr2
- normr2 = normr2_new
- d = s + beta*d
-
- # time and criterion
- timing[it] = time.time() - t
- criter[it] = (r**2).sum()
-
- return x, it, timing, criter
-
-def SIRT(x_init, operator , data , opt=None, constraint=None):
- '''Simultaneous Iterative Reconstruction Technique
-
- Parameters:
- x_init: initial guess
- operator: operator for forward/backward projections
- data: data to operate on
- opt: additional algorithm
- constraint: func of Indicator type specifying convex constraint.
- '''
-
- if opt is None:
- opt = {'tol': 1e-4, 'iter': 1000}
- else:
- try:
- max_iter = opt['iter']
- except KeyError as ke:
- opt[ke] = 1000
- try:
- opt['tol'] = 1000
- except KeyError as ke:
- opt[ke] = 1e-4
- tol = opt['tol']
- max_iter = opt['iter']
-
- # Set default constraint to unconstrained
- if constraint==None:
- constraint = Function()
-
- x = x_init.clone()
-
- timing = numpy.zeros(max_iter)
- criter = numpy.zeros(max_iter)
-
- # Relaxation parameter must be strictly between 0 and 2. For now fix at 1.0
- relax_par = 1.0
-
- # Set up scaling matrices D and M.
- im1 = ImageData(geometry=x_init.geometry)
- im1.array[:] = 1.0
- M = 1/operator.direct(im1)
- del im1
- aq1 = AcquisitionData(geometry=M.geometry)
- aq1.array[:] = 1.0
- D = 1/operator.adjoint(aq1)
- del aq1
-
- # algorithm loop
- for it in range(0, max_iter):
- t = time.time()
- r = data - operator.direct(x)
-
- x = constraint.prox(x + relax_par * (D*operator.adjoint(M*r)),None)
-
- timing[it] = time.time() - t
- if it > 0:
- criter[it-1] = (r**2).sum()
-
- r = data - operator.direct(x)
- criter[it] = (r**2).sum()
- return x, it, timing, criter
-
diff --git a/Wrappers/Python/ccpi/optimisation/funcs.py b/Wrappers/Python/ccpi/optimisation/funcs.py
deleted file mode 100755
index 47ee810..0000000
--- a/Wrappers/Python/ccpi/optimisation/funcs.py
+++ /dev/null
@@ -1,302 +0,0 @@
-# -*- coding: utf-8 -*-
-# This work is part of the Core Imaging Library developed by
-# Visual Analytics and Imaging System Group of the Science Technology
-# Facilities Council, STFC
-
-# Copyright 2018 Jakob Jorgensen, Daniil Kazantsev and Edoardo Pasca
-
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-
-# http://www.apache.org/licenses/LICENSE-2.0
-
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-from ccpi.optimisation.ops import Identity, FiniteDiff2D
-import numpy
-from ccpi.framework import DataContainer
-
-
-def isSizeCorrect(data1 ,data2):
- if issubclass(type(data1), DataContainer) and \
- issubclass(type(data2), DataContainer):
- # check dimensionality
- if data1.check_dimensions(data2):
- return True
- elif issubclass(type(data1) , numpy.ndarray) and \
- issubclass(type(data2) , numpy.ndarray):
- return data1.shape == data2.shape
- else:
- raise ValueError("{0}: getting two incompatible types: {1} {2}"\
- .format('Function', type(data1), type(data2)))
- return False
-
-class Function(object):
- def __init__(self):
- self.L = None
- def __call__(self,x, out=None): raise NotImplementedError
- def grad(self, x): raise NotImplementedError
- def prox(self, x, tau): raise NotImplementedError
- def gradient(self, x, out=None): raise NotImplementedError
- def proximal(self, x, tau, out=None): raise NotImplementedError
-
-
-class Norm2(Function):
-
- def __init__(self,
- gamma=1.0,
- direction=None):
- super(Norm2, self).__init__()
- self.gamma = gamma;
- self.direction = direction;
-
- def __call__(self, x, out=None):
-
- if out is None:
- xx = numpy.sqrt(numpy.sum(numpy.square(x.as_array()), self.direction,
- keepdims=True))
- else:
- if isSizeCorrect(out, x):
- # check dimensionality
- if issubclass(type(out), DataContainer):
- arr = out.as_array()
- numpy.square(x.as_array(), out=arr)
- xx = numpy.sqrt(numpy.sum(arr, self.direction, keepdims=True))
-
- elif issubclass(type(out) , numpy.ndarray):
- numpy.square(x.as_array(), out=out)
- xx = numpy.sqrt(numpy.sum(out, self.direction, keepdims=True))
- else:
- raise ValueError ('Wrong size: x{0} out{1}'.format(x.shape,out.shape) )
-
- p = numpy.sum(self.gamma*xx)
-
- return p
-
- def prox(self, x, tau):
-
- xx = numpy.sqrt(numpy.sum( numpy.square(x.as_array()), self.direction,
- keepdims=True ))
- xx = numpy.maximum(0, 1 - tau*self.gamma / xx)
- p = x.as_array() * xx
-
- return type(x)(p,geometry=x.geometry)
- def proximal(self, x, tau, out=None):
- if out is None:
- return self.prox(x,tau)
- else:
- if isSizeCorrect(out, x):
- # check dimensionality
- if issubclass(type(out), DataContainer):
- numpy.square(x.as_array(), out = out.as_array())
- xx = numpy.sqrt(numpy.sum( out.as_array() , self.direction,
- keepdims=True ))
- xx = numpy.maximum(0, 1 - tau*self.gamma / xx)
- x.multiply(xx, out= out.as_array())
-
-
- elif issubclass(type(out) , numpy.ndarray):
- numpy.square(x.as_array(), out=out)
- xx = numpy.sqrt(numpy.sum(out, self.direction, keepdims=True))
-
- xx = numpy.maximum(0, 1 - tau*self.gamma / xx)
- x.multiply(xx, out= out)
- else:
- raise ValueError ('Wrong size: x{0} out{1}'.format(x.shape,out.shape) )
-
-
-class TV2D(Norm2):
-
- def __init__(self, gamma):
- super(TV2D,self).__init__(gamma, 0)
- self.op = FiniteDiff2D()
- self.L = self.op.get_max_sing_val()
-
-
-# Define a class for squared 2-norm
-class Norm2sq(Function):
- '''
- f(x) = c*||A*x-b||_2^2
-
- which has
-
- grad[f](x) = 2*c*A^T*(A*x-b)
-
- and Lipschitz constant
-
- L = 2*c*||A||_2^2 = 2*s1(A)^2
-
- where s1(A) is the largest singular value of A.
-
- '''
-
- def __init__(self,A,b,c=1.0,memopt=False):
- super(Norm2sq, self).__init__()
-
- self.A = A # Should be an operator, default identity
- self.b = b # Default zero DataSet?
- self.c = c # Default 1.
- self.memopt = memopt
- if memopt:
- #self.direct_placehold = A.adjoint(b)
- self.direct_placehold = A.allocate_direct()
- self.adjoint_placehold = A.allocate_adjoint()
-
-
- # Compute the Lipschitz parameter from the operator if possible
- # Leave it initialised to None otherwise
- try:
- self.L = 2.0*self.c*(self.A.get_max_sing_val()**2)
- except AttributeError as ae:
- pass
-
- def grad(self,x):
- #return 2*self.c*self.A.adjoint( self.A.direct(x) - self.b )
- return (2.0*self.c)*self.A.adjoint( self.A.direct(x) - self.b )
-
- def __call__(self,x):
- #return self.c* np.sum(np.square((self.A.direct(x) - self.b).ravel()))
- #if out is None:
- # return self.c*( ( (self.A.direct(x)-self.b)**2).sum() )
- #else:
- y = self.A.direct(x)
- y.__isub__(self.b)
- #y.__imul__(y)
- #return y.sum() * self.c
- try:
- return y.squared_norm() * self.c
- except AttributeError as ae:
- # added for compatibility with SIRF
- return (y.norm()**2) * self.c
-
- def gradient(self, x, out = None):
- if self.memopt:
- #return 2.0*self.c*self.A.adjoint( self.A.direct(x) - self.b )
-
- self.A.direct(x, out=self.adjoint_placehold)
- self.adjoint_placehold.__isub__( self.b )
- self.A.adjoint(self.adjoint_placehold, out=self.direct_placehold)
- self.direct_placehold.__imul__(2.0 * self.c)
- # can this be avoided?
- out.fill(self.direct_placehold)
- else:
- return self.grad(x)
-
-
-
-class ZeroFun(Function):
-
- def __init__(self,gamma=0,L=1):
- self.gamma = gamma
- self.L = L
- super(ZeroFun, self).__init__()
-
- def __call__(self,x):
- return 0
-
- def prox(self,x,tau):
- return x.copy()
-
- def proximal(self, x, tau, out=None):
- if out is None:
- return self.prox(x, tau)
- else:
- if isSizeCorrect(out, x):
- # check dimensionality
- if issubclass(type(out), DataContainer):
- out.fill(x)
-
- elif issubclass(type(out) , numpy.ndarray):
- out[:] = x
- else:
- raise ValueError ('Wrong size: x{0} out{1}'
- .format(x.shape,out.shape) )
-
-# A more interesting example, least squares plus 1-norm minimization.
-# Define class to represent 1-norm including prox function
-class Norm1(Function):
-
- def __init__(self,gamma):
- super(Norm1, self).__init__()
- self.gamma = gamma
- self.L = 1
- self.sign_x = None
-
- def __call__(self,x,out=None):
- if out is None:
- return self.gamma*(x.abs().sum())
- else:
- if not x.shape == out.shape:
- raise ValueError('Norm1 Incompatible size:',
- x.shape, out.shape)
- x.abs(out=out)
- return out.sum() * self.gamma
-
- def prox(self,x,tau):
- return (x.abs() - tau*self.gamma).maximum(0) * x.sign()
-
- def proximal(self, x, tau, out=None):
- if out is None:
- return self.prox(x, tau)
- else:
- if isSizeCorrect(x,out):
- # check dimensionality
- if issubclass(type(out), DataContainer):
- v = (x.abs() - tau*self.gamma).maximum(0)
- x.sign(out=out)
- out *= v
- #out.fill(self.prox(x,tau))
- elif issubclass(type(out) , numpy.ndarray):
- v = (x.abs() - tau*self.gamma).maximum(0)
- out[:] = x.sign()
- out *= v
- #out[:] = self.prox(x,tau)
- else:
- raise ValueError ('Wrong size: x{0} out{1}'.format(x.shape,out.shape) )
-
-# Box constraints indicator function. Calling returns 0 if argument is within
-# the box. The prox operator is projection onto the box. Only implements one
-# scalar lower and one upper as constraint on all elements. Should generalise
-# to vectors to allow different constraints one elements.
-class IndicatorBox(Function):
-
- def __init__(self,lower=-numpy.inf,upper=numpy.inf):
- # Do nothing
- self.lower = lower
- self.upper = upper
- super(IndicatorBox, self).__init__()
-
- def __call__(self,x):
-
- if (numpy.all(x.array>=self.lower) and
- numpy.all(x.array <= self.upper) ):
- val = 0
- else:
- val = numpy.inf
- return val
-
- def prox(self,x,tau=None):
- return (x.maximum(self.lower)).minimum(self.upper)
-
- def proximal(self, x, tau, out=None):
- if out is None:
- return self.prox(x, tau)
- else:
- if not x.shape == out.shape:
- raise ValueError('Norm1 Incompatible size:',
- x.shape, out.shape)
- #(x.abs() - tau*self.gamma).maximum(0) * x.sign()
- x.abs(out = out)
- out.__isub__(tau*self.gamma)
- out.maximum(0, out=out)
- if self.sign_x is None or not x.shape == self.sign_x.shape:
- self.sign_x = x.sign()
- else:
- x.sign(out=self.sign_x)
-
- out.__imul__( self.sign_x )
diff --git a/Wrappers/Python/ccpi/optimisation/functions/BlockFunction.py b/Wrappers/Python/ccpi/optimisation/functions/BlockFunction.py
new file mode 100644
index 0000000..3765685
--- /dev/null
+++ b/Wrappers/Python/ccpi/optimisation/functions/BlockFunction.py
@@ -0,0 +1,223 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Created on Fri Mar 8 10:01:31 2019
+
+@author: evangelos
+"""
+
+from ccpi.optimisation.functions import Function
+from ccpi.framework import BlockDataContainer
+from numbers import Number
+
+class BlockFunction(Function):
+
+ '''BlockFunction acts as a separable sum function, i.e.,
+
+ f = [f_1,...,f_n]
+
+ f([x_1,...,x_n]) = f_1(x_1) + .... + f_n(x_n)
+
+ '''
+ def __init__(self, *functions):
+
+ super(BlockFunction, self).__init__()
+ self.functions = functions
+ self.length = len(self.functions)
+
+
+
+
+ def __call__(self, x):
+
+ '''Evaluates the BlockFunction at a BlockDataContainer x
+
+ :param: x (BlockDataContainer): must have as many rows as self.length
+
+ returns sum(f_i(x_i))
+ '''
+
+ if self.length != x.shape[0]:
+ raise ValueError('BlockFunction and BlockDataContainer have incompatible size')
+ t = 0
+ for i in range(x.shape[0]):
+ t += self.functions[i](x.get_item(i))
+ return t
+
+ def convex_conjugate(self, x):
+
+ ''' Evaluate convex conjugate of BlockFunction at x
+
+ returns sum(f_i^{*}(x_i))
+
+ '''
+ t = 0
+ for i in range(x.shape[0]):
+ t += self.functions[i].convex_conjugate(x.get_item(i))
+ return t
+
+
+ def proximal_conjugate(self, x, tau, out = None):
+
+ ''' Evaluate Proximal Operator of tau * f(\cdot) at x
+
+ prox_{tau*f}(x) = sum_{i} prox_{tau*f_{i}}(x_{i})
+
+
+ '''
+
+ if out is not None:
+ if isinstance(tau, Number):
+ for i in range(self.length):
+ self.functions[i].proximal_conjugate(x.get_item(i), tau, out=out.get_item(i))
+ else:
+ for i in range(self.length):
+ self.functions[i].proximal_conjugate(x.get_item(i), tau.get_item(i),out=out.get_item(i))
+
+ else:
+
+ out = [None]*self.length
+ if isinstance(tau, Number):
+ for i in range(self.length):
+ out[i] = self.functions[i].proximal_conjugate(x.get_item(i), tau)
+ else:
+ for i in range(self.length):
+ out[i] = self.functions[i].proximal_conjugate(x.get_item(i), tau.get_item(i))
+
+ return BlockDataContainer(*out)
+
+
+ def proximal(self, x, tau, out = None):
+
+ ''' Evaluate Proximal Operator of tau * f^{*}(\cdot) at x
+
+ prox_{tau*f^{*}}(x) = sum_{i} prox_{tau*f^{*}_{i}}(x_{i})
+
+
+ '''
+
+ if out is None:
+
+ out = [None]*self.length
+ if isinstance(tau, Number):
+ for i in range(self.length):
+ out[i] = self.functions[i].proximal(x.get_item(i), tau)
+ else:
+ for i in range(self.length):
+ out[i] = self.functions[i].proximal(x.get_item(i), tau.get_item(i))
+
+ return BlockDataContainer(*out)
+
+ else:
+ if isinstance(tau, Number):
+ for i in range(self.length):
+ self.functions[i].proximal(x.get_item(i), tau, out[i])
+ else:
+ for i in range(self.length):
+ self.functions[i].proximal(x.get_item(i), tau.get_item(i), out[i])
+
+
+
+ def gradient(self,x, out=None):
+
+ ''' Evaluate gradient of f at x: f'(x)
+
+ returns: BlockDataContainer [f_{1}'(x_{1}), ... , f_{n}'(x_{n})]
+
+ '''
+
+ out = [None]*self.length
+ for i in range(self.length):
+ out[i] = self.functions[i].gradient(x.get_item(i))
+
+ return BlockDataContainer(*out)
+
+
+
+if __name__ == '__main__':
+
+ M, N, K = 2,3,5
+
+ from ccpi.optimisation.functions import L2NormSquared, MixedL21Norm
+ from ccpi.framework import ImageGeometry, BlockGeometry
+ from ccpi.optimisation.operators import Gradient, Identity, BlockOperator
+ import numpy
+ import numpy as np
+
+
+ ig = ImageGeometry(M, N)
+ BG = BlockGeometry(ig, ig)
+
+ u = ig.allocate('random_int')
+ B = BlockOperator( Gradient(ig), Identity(ig) )
+
+ U = B.direct(u)
+ b = ig.allocate('random_int')
+
+ f1 = 10 * MixedL21Norm()
+ f2 = 0.5 * L2NormSquared(b=b)
+
+ f = BlockFunction(f1, f2)
+ tau = 0.3
+
+ print( " without out " )
+ res_no_out = f.proximal_conjugate( U, tau)
+ res_out = B.range_geometry().allocate()
+ f.proximal_conjugate( U, tau, out = res_out)
+
+ numpy.testing.assert_array_almost_equal(res_no_out[0][0].as_array(), \
+ res_out[0][0].as_array(), decimal=4)
+
+ numpy.testing.assert_array_almost_equal(res_no_out[0][1].as_array(), \
+ res_out[0][1].as_array(), decimal=4)
+
+ numpy.testing.assert_array_almost_equal(res_no_out[1].as_array(), \
+ res_out[1].as_array(), decimal=4)
+
+
+
+ ##########################################################################
+
+
+
+
+
+
+
+# zzz = B.range_geometry().allocate('random_int')
+# www = B.range_geometry().allocate()
+# www.fill(zzz)
+
+# res[0].fill(z)
+
+
+
+
+# f.proximal_conjugate(z, sigma, out = res)
+
+# print(z1[0][0].as_array())
+# print(res[0][0].as_array())
+
+
+
+
+# U = BG.allocate('random_int')
+# RES = BG.allocate()
+# f = BlockFunction(f1, f2)
+#
+# z = f.proximal_conjugate(U, 0.2)
+# f.proximal_conjugate(U, 0.2, out = RES)
+#
+# print(z[0].as_array())
+# print(RES[0].as_array())
+#
+# print(z[1].as_array())
+# print(RES[1].as_array())
+
+
+
+
+
+
+
+ \ No newline at end of file
diff --git a/Wrappers/Python/ccpi/optimisation/functions/Function.py b/Wrappers/Python/ccpi/optimisation/functions/Function.py
new file mode 100644
index 0000000..ba33666
--- /dev/null
+++ b/Wrappers/Python/ccpi/optimisation/functions/Function.py
@@ -0,0 +1,69 @@
+# -*- coding: utf-8 -*-
+# This work is part of the Core Imaging Library developed by
+# Visual Analytics and Imaging System Group of the Science Technology
+# Facilities Council, STFC
+
+# Copyright 2018-2019 Jakob Jorgensen, Daniil Kazantsev and Edoardo Pasca
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import warnings
+from ccpi.optimisation.functions.ScaledFunction import ScaledFunction
+
+class Function(object):
+ '''Abstract class representing a function
+
+ Members:
+ L is the Lipschitz constant of the gradient of the Function
+ '''
+ def __init__(self):
+ self.L = None
+
+ def __call__(self,x, out=None):
+ '''Evaluates the function at x '''
+ raise NotImplementedError
+
+ def gradient(self, x, out=None):
+ '''Returns the gradient of the function at x, if the function is differentiable'''
+ raise NotImplementedError
+
+ def proximal(self, x, tau, out=None):
+ '''This returns the proximal operator for the function at x, tau'''
+ raise NotImplementedError
+
+ def convex_conjugate(self, x, out=None):
+ '''This evaluates the convex conjugate of the function at x'''
+ raise NotImplementedError
+
+ def proximal_conjugate(self, x, tau, out = None):
+ '''This returns the proximal operator for the convex conjugate of the function at x, tau'''
+ raise NotImplementedError
+
+ def grad(self, x):
+ '''Alias of gradient(x,None)'''
+ warnings.warn('''This method will disappear in following
+ versions of the CIL. Use gradient instead''', DeprecationWarning)
+ return self.gradient(x, out=None)
+
+ def prox(self, x, tau):
+ '''Alias of proximal(x, tau, None)'''
+ warnings.warn('''This method will disappear in following
+ versions of the CIL. Use proximal instead''', DeprecationWarning)
+ return self.proximal(x, tau, out=None)
+
+ def __rmul__(self, scalar):
+ '''Defines the multiplication by a scalar on the left
+
+ returns a ScaledFunction'''
+ return ScaledFunction(self, scalar)
+
diff --git a/Wrappers/Python/ccpi/optimisation/functions/FunctionOperatorComposition.py b/Wrappers/Python/ccpi/optimisation/functions/FunctionOperatorComposition.py
new file mode 100644
index 0000000..a2445cd
--- /dev/null
+++ b/Wrappers/Python/ccpi/optimisation/functions/FunctionOperatorComposition.py
@@ -0,0 +1,110 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Created on Fri Mar 8 09:55:36 2019
+
+@author: evangelos
+"""
+
+from ccpi.optimisation.functions import Function
+from ccpi.optimisation.functions import ScaledFunction
+
+
+class FunctionOperatorComposition(Function):
+
+ ''' Function composition with Operator, i.e., f(Ax)
+
+ A: operator
+ f: function
+
+ '''
+
+ def __init__(self, function, operator):
+
+ super(FunctionOperatorComposition, self).__init__()
+
+ self.function = function
+ self.operator = operator
+ self.L = function.L * operator.norm()**2
+
+
+ def __call__(self, x):
+
+ ''' Evaluate FunctionOperatorComposition at x
+
+ returns f(Ax)
+
+ '''
+
+ return self.function(self.operator.direct(x))
+
+ def gradient(self, x, out=None):
+#
+ ''' Gradient takes into account the Operator'''
+ if out is None:
+ return self.operator.adjoint(self.function.gradient(self.operator.direct(x)))
+ else:
+ tmp = self.operator.range_geometry().allocate()
+ self.operator.direct(x, out=tmp)
+ self.function.gradient(tmp, out=tmp)
+ self.operator.adjoint(tmp, out=out)
+
+
+
+
+if __name__ == '__main__':
+
+ from ccpi.framework import ImageGeometry, AcquisitionGeometry
+ from ccpi.optimisation.operators import Gradient
+ from ccpi.optimisation.functions import L2NormSquared
+ from ccpi.astra.ops import AstraProjectorSimple
+ import numpy as np
+
+ M, N= 50, 50
+ ig = ImageGeometry(voxel_num_x=M, voxel_num_y = N)
+
+ detectors = N
+ angles_num = N
+ det_w = 1.0
+
+ angles = np.linspace(0, np.pi, angles_num, endpoint=False)
+ ag = AcquisitionGeometry('parallel',
+ '2D',
+ angles,
+ detectors,det_w)
+
+
+ Aop = AstraProjectorSimple(ig, ag, 'cpu')
+
+ u = ig.allocate('random_int', seed=15)
+ u1 = ig.allocate('random_int', seed=10)
+ b = Aop.direct(u1)
+
+
+# G = Gradient(ig)
+ alpha = 0.5
+
+ f1 = alpha * L2NormSquared(b=b)
+
+ f_comp = FunctionOperatorComposition(f1, Aop)
+
+ print(f_comp(u))
+
+
+ z1 = Aop.direct(u)
+ tmp = 0.5 * ((z1 - b)**2).sum()
+
+
+ print(tmp)
+
+
+
+
+
+
+
+
+
+
+
+ \ No newline at end of file
diff --git a/Wrappers/Python/ccpi/optimisation/functions/FunctionOperatorComposition_old.py b/Wrappers/Python/ccpi/optimisation/functions/FunctionOperatorComposition_old.py
new file mode 100644
index 0000000..70511bb
--- /dev/null
+++ b/Wrappers/Python/ccpi/optimisation/functions/FunctionOperatorComposition_old.py
@@ -0,0 +1,85 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Created on Fri Mar 8 09:55:36 2019
+
+@author: evangelos
+"""
+
+from ccpi.optimisation.functions import Function
+from ccpi.optimisation.functions import ScaledFunction
+
+
+class FunctionOperatorComposition(Function):
+
+ ''' Function composition with Operator, i.e., f(Ax)
+
+ A: operator
+ f: function
+
+ '''
+
+ def __init__(self, operator, function):
+
+ super(FunctionOperatorComposition, self).__init__()
+ self.function = function
+ self.operator = operator
+ alpha = 1
+
+ if isinstance (function, ScaledFunction):
+ alpha = function.scalar
+ self.L = 2 * alpha * operator.norm()**2
+
+
+ def __call__(self, x):
+
+ ''' Evaluate FunctionOperatorComposition at x
+
+ returns f(Ax)
+
+ '''
+
+ return self.function(self.operator.direct(x))
+
+ #TODO do not know if we need it
+ def call_adjoint(self, x):
+
+ return self.function(self.operator.adjoint(x))
+
+
+ def convex_conjugate(self, x):
+
+ ''' convex_conjugate does not take into account the Operator'''
+ return self.function.convex_conjugate(x)
+
+ def proximal(self, x, tau, out=None):
+
+ '''proximal does not take into account the Operator'''
+ if out is None:
+ return self.function.proximal(x, tau)
+ else:
+ self.function.proximal(x, tau, out=out)
+
+
+ def proximal_conjugate(self, x, tau, out=None):
+
+ ''' proximal conjugate does not take into account the Operator'''
+ if out is None:
+ return self.function.proximal_conjugate(x, tau)
+ else:
+ self.function.proximal_conjugate(x, tau, out=out)
+
+ def gradient(self, x, out=None):
+
+ ''' Gradient takes into account the Operator'''
+ if out is None:
+ return self.operator.adjoint(
+ self.function.gradient(self.operator.direct(x))
+ )
+ else:
+ self.operator.adjoint(
+ self.function.gradient(self.operator.direct(x),
+ out=out)
+ )
+
+ \ No newline at end of file
diff --git a/Wrappers/Python/ccpi/optimisation/functions/IndicatorBox.py b/Wrappers/Python/ccpi/optimisation/functions/IndicatorBox.py
new file mode 100755
index 0000000..7fec65e
--- /dev/null
+++ b/Wrappers/Python/ccpi/optimisation/functions/IndicatorBox.py
@@ -0,0 +1,134 @@
+#========================================================================
+# Copyright 2019 Science Technology Facilities Council
+# Copyright 2019 University of Manchester
+#
+# This work is part of the Core Imaging Library developed by Science Technology
+# Facilities Council and University of Manchester
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0.txt
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+#=========================================================================
+
+
+from ccpi.optimisation.functions import Function
+import numpy
+from ccpi.framework import ImageData
+
+class IndicatorBox(Function):
+ '''Box constraints indicator function.
+
+ Calling returns 0 if argument is within the box. The prox operator is projection onto the box.
+ Only implements one scalar lower and one upper as constraint on all elements. Should generalise
+ to vectors to allow different constraints one elements.
+'''
+
+ def __init__(self,lower=-numpy.inf,upper=numpy.inf):
+ # Do nothing
+ super(IndicatorBox, self).__init__()
+ self.lower = lower
+ self.upper = upper
+
+
+ def __call__(self,x):
+
+ if (numpy.all(x.array>=self.lower) and
+ numpy.all(x.array <= self.upper) ):
+ val = 0
+ else:
+ val = numpy.inf
+ return val
+
+ def gradient(self,x):
+ return ValueError('Not Differentiable')
+
+ def convex_conjugate(self,x):
+ # support function sup <x, z>, z \in [lower, upper]
+ # ????
+ return x.maximum(0).sum()
+
+ def proximal(self, x, tau, out=None):
+
+ if out is None:
+ return (x.maximum(self.lower)).minimum(self.upper)
+ else:
+ x.maximum(self.lower, out=out)
+ out.minimum(self.upper, out=out)
+
+ def proximal_conjugate(self, x, tau, out=None):
+
+ if out is None:
+
+ return x - tau * self.proximal(x/tau, tau)
+
+ else:
+
+ self.proximal(x/tau, tau, out=out)
+ out *= -1*tau
+ out += x
+
+
+
+if __name__ == '__main__':
+
+ from ccpi.framework import ImageGeometry, BlockDataContainer
+
+ N, M = 2,3
+ ig = ImageGeometry(voxel_num_x = N, voxel_num_y = M)
+
+ u = ig.allocate('random_int')
+ tau = 2
+
+ f = IndicatorBox(2, 3)
+
+ lower = 10
+ upper = 30
+
+ z1 = f.proximal(u, tau)
+
+ z2 = f.proximal_conjugate(u/tau, 1/tau)
+
+ z = z1 + tau * z2
+
+ numpy.testing.assert_array_equal(z.as_array(), u.as_array())
+
+ out1 = ig.allocate()
+ out2 = ig.allocate()
+
+ f.proximal(u, tau, out=out1)
+ f.proximal_conjugate(u/tau, 1/tau, out = out2)
+
+ p = out1 + tau * out2
+
+ numpy.testing.assert_array_equal(p.as_array(), u.as_array())
+
+ d = f.convex_conjugate(u)
+ print(d)
+
+
+
+ # what about n-dimensional Block
+ #uB = BlockDataContainer(u,u,u)
+ #lowerB = BlockDataContainer(1,2,3)
+ #upperB = BlockDataContainer(10,21,30)
+
+ #fB = IndicatorBox(lowerB, upperB)
+
+ #z1B = fB.proximal(uB, tau)
+
+
+
+
+
+
+
+ \ No newline at end of file
diff --git a/Wrappers/Python/ccpi/optimisation/functions/KullbackLeibler.py b/Wrappers/Python/ccpi/optimisation/functions/KullbackLeibler.py
new file mode 100644
index 0000000..6920829
--- /dev/null
+++ b/Wrappers/Python/ccpi/optimisation/functions/KullbackLeibler.py
@@ -0,0 +1,231 @@
+# -*- coding: utf-8 -*-
+# This work is part of the Core Imaging Library developed by
+# Visual Analytics and Imaging System Group of the Science Technology
+# Facilities Council, STFC
+
+# Copyright 2018-2019 Evangelos Papoutsellis and Edoardo Pasca
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import numpy
+from ccpi.optimisation.functions import Function
+from ccpi.optimisation.functions.ScaledFunction import ScaledFunction
+import functools
+import scipy.special
+
+class KullbackLeibler(Function):
+
+ '''
+
+ KL_div(x, y + back) = int x * log(x/(y+back)) - x + (y+back)
+
+ Assumption: y>=0
+ back>=0
+
+ '''
+
+ def __init__(self, data, **kwargs):
+
+ super(KullbackLeibler, self).__init__()
+
+ self.b = data
+ self.bnoise = 0
+
+
+ def __call__(self, x):
+
+
+ '''
+
+ x - y * log( x + bnoise) + y * log(y) - y + bnoise
+
+
+ '''
+
+ ind = x.as_array()>0
+ tmp = scipy.special.kl_div(self.b.as_array()[ind], x.as_array()[ind])
+ return numpy.sum(tmp)
+
+
+ def log(self, datacontainer):
+ '''calculates the in-place log of the datacontainer'''
+ if not functools.reduce(lambda x,y: x and y>0,
+ datacontainer.as_array().ravel(), True):
+ raise ValueError('KullbackLeibler. Cannot calculate log of negative number')
+ datacontainer.fill( numpy.log(datacontainer.as_array()) )
+
+
+ def gradient(self, x, out=None):
+
+ if out is None:
+ return 1 - self.b/(x + self.bnoise)
+ else:
+
+ x.add(self.bnoise, out=out)
+ self.b.divide(out, out=out)
+ out.subtract(1, out=out)
+ out *= -1
+
+ def convex_conjugate(self, x):
+
+ xlogy = - scipy.special.xlogy(self.b.as_array(), 1 - x.as_array())
+ return numpy.sum(xlogy)
+
+ def proximal(self, x, tau, out=None):
+
+ if out is None:
+ return 0.5 *( (x - self.bnoise - tau) + ( (x + self.bnoise - tau)**2 + 4*tau*self.b ) .sqrt() )
+ else:
+
+ tmp = 0.5 *( (x - self.bnoise - tau) +
+ ( (x + self.bnoise - tau)**2 + 4*tau*self.b ) .sqrt()
+ )
+ x.add(self.bnoise, out=out)
+ out -= tau
+ out *= out
+ tmp = self.b * (4 * tau)
+ out.add(tmp, out=out)
+ out.sqrt(out=out)
+
+ x.subtract(self.bnoise, out=tmp)
+ tmp -= tau
+
+ out += tmp
+
+ out *= 0.5
+
+ def proximal_conjugate(self, x, tau, out=None):
+
+
+ if out is None:
+ z = x + tau * self.bnoise
+ return 0.5*((z + 1) - ((z-1)**2 + 4 * tau * self.b).sqrt())
+ else:
+
+ #tmp = x + tau * self.bnoise
+ tmp = tau * self.bnoise
+ tmp += x
+ tmp -= 1
+
+ self.b.multiply(4*tau, out=out)
+
+ out.add((tmp)**2, out=out)
+ out.sqrt(out=out)
+ out *= -1
+ tmp += 2
+ out += tmp
+ out *= 0.5
+
+ def __rmul__(self, scalar):
+
+ ''' Multiplication of L2NormSquared with a scalar
+
+ Returns: ScaledFunction
+
+ '''
+
+ return ScaledFunction(self, scalar)
+
+
+if __name__ == '__main__':
+
+ from ccpi.framework import ImageGeometry
+ import numpy
+
+ M, N = 2,3
+ ig = ImageGeometry(voxel_num_x=M, voxel_num_y = N)
+ u = ig.allocate('random_int')
+ b = ig.allocate('random_int')
+ u.as_array()[1,1]=0
+ u.as_array()[2,0]=0
+ b.as_array()[1,1]=0
+ b.as_array()[2,0]=0
+
+ f = KullbackLeibler(b)
+
+
+# longest = reduce(lambda x, y: len(x) if len(x) > len(y) else len(y), strings)
+
+
+# tmp = functools.reduce(lambda x, y: \
+# 0 if x==0 and not numpy.isnan(y) else x * numpy.log(y), \
+# zip(b.as_array().ravel(), u.as_array().ravel()),0)
+
+
+# np.multiply.reduce(X, 0)
+
+
+# sf = reduce(lambda x,y: x + y[0]*y[1],
+# zip(self.as_array().ravel(),
+# other.as_array().ravel()),
+# 0)
+#cdef inline number_t xlogy(number_t x, number_t y) nogil:
+# if x == 0 and not zisnan(y):
+# return 0
+# else:
+# return x * zlog(y)
+
+# if npy_isnan(x):
+# return x
+# elif x > 0:
+# return -x * log(x)
+# elif x == 0:
+# return 0
+# else:
+# return -inf
+
+# cdef inline double kl_div(double x, double y) nogil:
+# if npy_isnan(x) or npy_isnan(y):
+# return nan
+# elif x > 0 and y > 0:
+# return x * log(x / y) - x + y
+# elif x == 0 and y >= 0:
+# return y
+# else:
+# return inf
+
+
+
+
+# def xlogy(self, dc1, dc2):
+
+# return numpy.sum(numpy.where(dc1.as_array() != 0, dc2.as_array() * numpy.log(dc2.as_array() / dc1.as_array()), 0))
+
+
+
+# f.xlog(u, b)
+
+
+
+
+# tmp1 = b.as_array()
+# tmp2 = u.as_array()
+#
+# zz = scipy.special.xlogy(tmp1, tmp2)
+#
+# print(np.sum(zz))
+
+
+# ww = f.xlogy(b, u)
+
+# print(ww)
+
+
+#cdef inline double kl_div(double x, double y) nogil:
+
+
+
+
+
+
+
diff --git a/Wrappers/Python/ccpi/optimisation/functions/L1Norm.py b/Wrappers/Python/ccpi/optimisation/functions/L1Norm.py
new file mode 100644
index 0000000..37c2016
--- /dev/null
+++ b/Wrappers/Python/ccpi/optimisation/functions/L1Norm.py
@@ -0,0 +1,162 @@
+#========================================================================
+# Copyright 2019 Science Technology Facilities Council
+# Copyright 2019 University of Manchester
+#
+# This work is part of the Core Imaging Library developed by Science Technology
+# Facilities Council and University of Manchester
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0.txt
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+#=========================================================================
+
+
+from ccpi.optimisation.functions import Function
+from ccpi.optimisation.functions.ScaledFunction import ScaledFunction
+from ccpi.optimisation.operators import ShrinkageOperator
+
+
+class L1Norm(Function):
+
+ '''
+
+ Class: L1Norm
+
+ Cases: a) f(x) = ||x||_{1}
+
+ b) f(x) = ||x - b||_{1}
+
+ '''
+
+ def __init__(self, **kwargs):
+
+ super(L1Norm, self).__init__()
+ self.b = kwargs.get('b',None)
+
+ def __call__(self, x):
+
+ ''' Evaluate L1Norm at x: f(x) '''
+
+ y = x
+ if self.b is not None:
+ y = x - self.b
+ return y.abs().sum()
+
+ def gradient(self,x):
+ #TODO implement subgradient???
+ return ValueError('Not Differentiable')
+
+ def convex_conjugate(self,x):
+ #TODO implement Indicator infty???
+
+ y = 0
+ if self.b is not None:
+ y = 0 + self.b.dot(x)
+ return y
+
+ def proximal(self, x, tau, out=None):
+
+ # TODO implement shrinkage operator, we will need it later e.g SplitBregman
+
+ if out is None:
+ if self.b is not None:
+ return self.b + ShrinkageOperator.__call__(self, x - self.b, tau)
+ else:
+ return ShrinkageOperator.__call__(self, x, tau)
+ else:
+ if self.b is not None:
+ out.fill(self.b + ShrinkageOperator.__call__(self, x - self.b, tau))
+ else:
+ out.fill(ShrinkageOperator.__call__(self, x, tau))
+
+ def proximal_conjugate(self, x, tau, out=None):
+
+ if out is None:
+ if self.b is not None:
+ return (x - tau*self.b).divide((x - tau*self.b).abs().maximum(1.0))
+ else:
+ return x.divide(x.abs().maximum(1.0))
+ else:
+ if self.b is not None:
+ out.fill((x - tau*self.b).divide((x - tau*self.b).abs().maximum(1.0)))
+ else:
+ out.fill(x.divide(x.abs().maximum(1.0)) )
+
+ def __rmul__(self, scalar):
+ return ScaledFunction(self, scalar)
+
+
+if __name__ == '__main__':
+
+ from ccpi.framework import ImageGeometry
+ import numpy
+ N, M = 400,400
+ ig = ImageGeometry(N, M)
+ scalar = 10
+ b = ig.allocate('random')
+ u = ig.allocate('random')
+
+ f = L1Norm()
+ f_scaled = scalar * L1Norm()
+
+ f_b = L1Norm(b=b)
+ f_scaled_b = scalar * L1Norm(b=b)
+
+ # call
+
+ a1 = f(u)
+ a2 = f_scaled(u)
+ numpy.testing.assert_equal(scalar * a1, a2)
+
+ a3 = f_b(u)
+ a4 = f_scaled_b(u)
+ numpy.testing.assert_equal(scalar * a3, a4)
+
+ # proximal
+ tau = 0.4
+ b1 = f.proximal(u, tau*scalar)
+ b2 = f_scaled.proximal(u, tau)
+
+ numpy.testing.assert_array_almost_equal(b1.as_array(), b2.as_array(), decimal=4)
+
+ b3 = f_b.proximal(u, tau*scalar)
+ b4 = f_scaled_b.proximal(u, tau)
+
+ z1 = b + (u-b).sign() * ((u-b).abs() - tau * scalar).maximum(0)
+
+ numpy.testing.assert_array_almost_equal(b3.as_array(), b4.as_array(), decimal=4)
+#
+# #proximal conjugate
+#
+ c1 = f_scaled.proximal_conjugate(u, tau)
+ c2 = u.divide((u.abs()/scalar).maximum(1.0))
+
+ numpy.testing.assert_array_almost_equal(c1.as_array(), c2.as_array(), decimal=4)
+
+ c3 = f_scaled_b.proximal_conjugate(u, tau)
+ c4 = (u - tau*b).divide( ((u-tau*b).abs()/scalar).maximum(1.0) )
+
+ numpy.testing.assert_array_almost_equal(c3.as_array(), c4.as_array(), decimal=4)
+
+
+
+
+
+
+
+
+
+
+
+
+
+ \ No newline at end of file
diff --git a/Wrappers/Python/ccpi/optimisation/functions/L2NormSquared.py b/Wrappers/Python/ccpi/optimisation/functions/L2NormSquared.py
new file mode 100644
index 0000000..2f05119
--- /dev/null
+++ b/Wrappers/Python/ccpi/optimisation/functions/L2NormSquared.py
@@ -0,0 +1,286 @@
+# -*- coding: utf-8 -*-
+# This work is part of the Core Imaging Library developed by
+# Visual Analytics and Imaging System Group of the Science Technology
+# Facilities Council, STFC
+
+# Copyright 2018-2019 Evangelos Papoutsellis and Edoardo Pasca
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from ccpi.optimisation.functions import Function
+from ccpi.optimisation.functions.ScaledFunction import ScaledFunction
+from ccpi.optimisation.functions import FunctionOperatorComposition
+
+class L2NormSquared(Function):
+
+ '''
+
+ Cases: a) f(x) = \|x\|^{2}_{2}
+
+ b) f(x) = ||x - b||^{2}_{2}
+
+ '''
+
+ def __init__(self, **kwargs):
+
+ super(L2NormSquared, self).__init__()
+ self.b = kwargs.get('b',None)
+ self.L = 2
+
+ def __call__(self, x):
+
+ ''' Evaluate L2NormSquared at x: f(x) '''
+
+ y = x
+ if self.b is not None:
+ y = x - self.b
+ try:
+ return y.squared_norm()
+ except AttributeError as ae:
+ # added for compatibility with SIRF
+ return (y.norm()**2)
+
+ def gradient(self, x, out=None):
+
+ ''' Evaluate gradient of L2NormSquared at x: f'(x) '''
+
+ if out is not None:
+
+ out.fill(x)
+ if self.b is not None:
+ out -= self.b
+ out *= 2
+
+ else:
+
+ y = x
+ if self.b is not None:
+ y = x - self.b
+ return 2*y
+
+
+ def convex_conjugate(self, x):
+
+ ''' Evaluate convex conjugate of L2NormSquared at x: f^{*}(x)'''
+
+ tmp = 0
+
+ if self.b is not None:
+ tmp = x.dot(self.b) #(x * self.b).sum()
+
+ return (1./4.) * x.squared_norm() + tmp
+
+
+ def proximal(self, x, tau, out = None):
+
+ ''' Evaluate Proximal Operator of tau * f(\cdot) at x:
+
+ prox_{tau*f(\cdot)}(x) = \argmin_{z} \frac{1}{2}|| z - x ||^{2}_{2} + tau * f(z)
+
+ '''
+
+ if out is None:
+
+ if self.b is None:
+ return x/(1+2*tau)
+ else:
+ tmp = x.subtract(self.b)
+ tmp /= (1+2*tau)
+ tmp += self.b
+ return tmp
+
+ else:
+ if self.b is not None:
+ x.subtract(self.b, out=out)
+ out /= (1+2*tau)
+ out += self.b
+ else:
+ x.divide((1+2*tau), out=out)
+
+
+ def proximal_conjugate(self, x, tau, out=None):
+
+ ''' Evaluate Proximal Operator of tau * f^{*}(\cdot) at x (i.e., the convex conjugate of f) :
+
+ prox_{tau*f(\cdot)}(x) = \argmin_{z} \frac{1}{2}|| z - x ||^{2}_{2} + tau * f^{*}(z)
+
+ '''
+
+ if out is None:
+ if self.b is not None:
+ return (x - tau*self.b)/(1 + tau/2)
+ else:
+ return x/(1 + tau/2)
+ else:
+ if self.b is not None:
+ x.subtract(tau*self.b, out=out)
+ out.divide(1+tau/2, out=out)
+ else:
+ x.divide(1 + tau/2, out=out)
+
+ def __rmul__(self, scalar):
+
+ ''' Multiplication of L2NormSquared with a scalar
+
+ Returns: ScaledFunction
+
+ '''
+
+ return ScaledFunction(self, scalar)
+
+
+ def composition(self, operator):
+
+ return FunctionOperatorComposition(operator)
+
+
+
+if __name__ == '__main__':
+
+ from ccpi.framework import ImageGeometry
+ import numpy
+ # TESTS for L2 and scalar * L2
+
+ M, N, K = 20,30,50
+ ig = ImageGeometry(voxel_num_x=M, voxel_num_y = N, voxel_num_z = K)
+ u = ig.allocate('random_int')
+ b = ig.allocate('random_int')
+
+ # check grad/call no data
+ f = L2NormSquared()
+ a1 = f.gradient(u)
+ a2 = 2 * u
+ numpy.testing.assert_array_almost_equal(a1.as_array(), a2.as_array(), decimal=4)
+ numpy.testing.assert_equal(f(u), u.squared_norm())
+
+ # check grad/call with data
+ f1 = L2NormSquared(b=b)
+ b1 = f1.gradient(u)
+ b2 = 2 * (u-b)
+
+ numpy.testing.assert_array_almost_equal(b1.as_array(), b2.as_array(), decimal=4)
+ numpy.testing.assert_equal(f1(u), (u-b).squared_norm())
+
+ #check convex conjuagate no data
+ c1 = f.convex_conjugate(u)
+ c2 = 1/4 * u.squared_norm()
+ numpy.testing.assert_equal(c1, c2)
+
+ #check convex conjuagate with data
+ d1 = f1.convex_conjugate(u)
+ d2 = (1/4) * u.squared_norm() + u.dot(b)
+ numpy.testing.assert_equal(d1, d2)
+
+ # check proximal no data
+ tau = 5
+ e1 = f.proximal(u, tau)
+ e2 = u/(1+2*tau)
+ numpy.testing.assert_array_almost_equal(e1.as_array(), e2.as_array(), decimal=4)
+
+ # check proximal with data
+ tau = 5
+ h1 = f1.proximal(u, tau)
+ h2 = (u-b)/(1+2*tau) + b
+ numpy.testing.assert_array_almost_equal(h1.as_array(), h2.as_array(), decimal=4)
+
+ # check proximal conjugate no data
+ tau = 0.2
+ k1 = f.proximal_conjugate(u, tau)
+ k2 = u/(1 + tau/2 )
+ numpy.testing.assert_array_almost_equal(k1.as_array(), k2.as_array(), decimal=4)
+
+ # check proximal conjugate with data
+ l1 = f1.proximal_conjugate(u, tau)
+ l2 = (u - tau * b)/(1 + tau/2 )
+ numpy.testing.assert_array_almost_equal(l1.as_array(), l2.as_array(), decimal=4)
+
+
+ # check scaled function properties
+
+ # scalar
+ scalar = 100
+ f_scaled_no_data = scalar * L2NormSquared()
+ f_scaled_data = scalar * L2NormSquared(b=b)
+
+ # call
+ numpy.testing.assert_equal(f_scaled_no_data(u), scalar*f(u))
+ numpy.testing.assert_equal(f_scaled_data(u), scalar*f1(u))
+
+ # grad
+ numpy.testing.assert_array_almost_equal(f_scaled_no_data.gradient(u).as_array(), scalar*f.gradient(u).as_array(), decimal=4)
+ numpy.testing.assert_array_almost_equal(f_scaled_data.gradient(u).as_array(), scalar*f1.gradient(u).as_array(), decimal=4)
+
+ # conj
+ numpy.testing.assert_almost_equal(f_scaled_no_data.convex_conjugate(u), \
+ f.convex_conjugate(u/scalar) * scalar, decimal=4)
+
+ numpy.testing.assert_almost_equal(f_scaled_data.convex_conjugate(u), \
+ scalar * f1.convex_conjugate(u/scalar), decimal=4)
+
+ # proximal
+ numpy.testing.assert_array_almost_equal(f_scaled_no_data.proximal(u, tau).as_array(), \
+ f.proximal(u, tau*scalar).as_array())
+
+
+ numpy.testing.assert_array_almost_equal(f_scaled_data.proximal(u, tau).as_array(), \
+ f1.proximal(u, tau*scalar).as_array())
+
+
+ # proximal conjugate
+ numpy.testing.assert_array_almost_equal(f_scaled_no_data.proximal_conjugate(u, tau).as_array(), \
+ (u/(1 + tau/(2*scalar) )).as_array(), decimal=4)
+
+ numpy.testing.assert_array_almost_equal(f_scaled_data.proximal_conjugate(u, tau).as_array(), \
+ ((u - tau * b)/(1 + tau/(2*scalar) )).as_array(), decimal=4)
+
+
+
+ print( " ####### check without out ######### " )
+
+
+ u_out_no_out = ig.allocate('random_int')
+ res_no_out = f_scaled_data.proximal_conjugate(u_out_no_out, 0.5)
+ print(res_no_out.as_array())
+
+ print( " ####### check with out ######### " )
+
+ res_out = ig.allocate()
+ f_scaled_data.proximal_conjugate(u_out_no_out, 0.5, out = res_out)
+
+ print(res_out.as_array())
+
+ numpy.testing.assert_array_almost_equal(res_no_out.as_array(), \
+ res_out.as_array(), decimal=4)
+
+
+
+ ig1 = ImageGeometry(2,3)
+
+ tau = 0.1
+
+ u = ig1.allocate('random_int')
+ b = ig1.allocate('random_int')
+
+ scalar = 0.5
+ f_scaled = scalar * L2NormSquared(b=b)
+ f_noscaled = L2NormSquared(b=b)
+
+
+ res1 = f_scaled.proximal(u, tau)
+ res2 = f_noscaled.proximal(u, tau*scalar)
+
+# res2 = (u + tau*b)/(1+tau)
+
+ numpy.testing.assert_array_almost_equal(res1.as_array(), \
+ res2.as_array(), decimal=4)
+
diff --git a/Wrappers/Python/ccpi/optimisation/functions/MixedL21Norm.py b/Wrappers/Python/ccpi/optimisation/functions/MixedL21Norm.py
new file mode 100755
index 0000000..e8f6da4
--- /dev/null
+++ b/Wrappers/Python/ccpi/optimisation/functions/MixedL21Norm.py
@@ -0,0 +1,159 @@
+# -*- coding: utf-8 -*-
+# This work is part of the Core Imaging Library developed by
+# Visual Analytics and Imaging System Group of the Science Technology
+# Facilities Council, STFC
+
+# Copyright 2018-2019 Evangelos Papoutsellis and Edoardo Pasca
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from ccpi.optimisation.functions import Function, ScaledFunction
+from ccpi.framework import BlockDataContainer
+
+import functools
+
+class MixedL21Norm(Function):
+
+
+ '''
+ f(x) = ||x||_{2,1} = \sum |x|_{2}
+ '''
+
+ def __init__(self, **kwargs):
+
+ super(MixedL21Norm, self).__init__()
+ self.SymTensor = kwargs.get('SymTensor',False)
+
+ def __call__(self, x):
+
+ ''' Evaluates L2,1Norm at point x
+
+ :param: x is a BlockDataContainer
+
+ '''
+ if not isinstance(x, BlockDataContainer):
+ raise ValueError('__call__ expected BlockDataContainer, got {}'.format(type(x)))
+
+ tmp = [ el**2 for el in x.containers ]
+ res = sum(tmp).sqrt().sum()
+
+ return res
+
+ def gradient(self, x, out=None):
+ return ValueError('Not Differentiable')
+
+ def convex_conjugate(self,x):
+
+ ''' This is the Indicator function of ||\cdot||_{2, \infty}
+ which is either 0 if ||x||_{2, \infty} or \infty
+ '''
+
+ return 0.0
+
+ #tmp = [ el**2 for el in x.containers ]
+ #print(sum(tmp).sqrt().as_array().max())
+ #return sum(tmp).sqrt().as_array().max()
+
+ def proximal(self, x, tau, out=None):
+
+ '''
+ For this we need to define a MixedL2,2 norm acting on BDC,
+ different form L2NormSquared which acts on DC
+
+ '''
+ pass
+
+ def proximal_conjugate(self, x, tau, out=None):
+
+
+ if out is None:
+ tmp = [ el*el for el in x.containers]
+ res = sum(tmp).sqrt().maximum(1.0)
+ frac = [el/res for el in x.containers]
+ return BlockDataContainer(*frac)
+
+
+ #TODO this is slow, why???
+# return x.divide(x.pnorm().maximum(1.0))
+ else:
+
+ res1 = functools.reduce(lambda a,b: a + b*b, x.containers, x.get_item(0) * 0 )
+ res = res1.sqrt().maximum(1.0)
+ x.divide(res, out=out)
+
+# x.divide(sum([el*el for el in x.containers]).sqrt().maximum(1.0), out=out)
+ #TODO this is slow, why ???
+# x.divide(x.pnorm().maximum(1.0), out=out)
+
+
+ def __rmul__(self, scalar):
+
+ ''' Multiplication of L2NormSquared with a scalar
+
+ Returns: ScaledFunction
+
+ '''
+ return ScaledFunction(self, scalar)
+
+
+#
+if __name__ == '__main__':
+
+ M, N, K = 2,3,5
+ from ccpi.framework import BlockGeometry
+ import numpy
+
+ ig = ImageGeometry(M, N)
+
+ BG = BlockGeometry(ig, ig)
+
+ U = BG.allocate('random_int')
+
+ # Define no scale and scaled
+ f_no_scaled = MixedL21Norm()
+ f_scaled = 0.5 * MixedL21Norm()
+
+ # call
+
+ a1 = f_no_scaled(U)
+ a2 = f_scaled(U)
+ print(a1, 2*a2)
+
+
+ print( " ####### check without out ######### " )
+
+
+ u_out_no_out = BG.allocate('random_int')
+ res_no_out = f_scaled.proximal_conjugate(u_out_no_out, 0.5)
+ print(res_no_out[0].as_array())
+
+ print( " ####### check with out ######### " )
+#
+ res_out = BG.allocate()
+ f_scaled.proximal_conjugate(u_out_no_out, 0.5, out = res_out)
+#
+ print(res_out[0].as_array())
+#
+ numpy.testing.assert_array_almost_equal(res_no_out[0].as_array(), \
+ res_out[0].as_array(), decimal=4)
+
+ numpy.testing.assert_array_almost_equal(res_no_out[1].as_array(), \
+ res_out[1].as_array(), decimal=4)
+#
+
+
+
+
+
+
+
diff --git a/Wrappers/Python/ccpi/optimisation/functions/Norm2Sq.py b/Wrappers/Python/ccpi/optimisation/functions/Norm2Sq.py
new file mode 100755
index 0000000..8e77f56
--- /dev/null
+++ b/Wrappers/Python/ccpi/optimisation/functions/Norm2Sq.py
@@ -0,0 +1,97 @@
+# -*- coding: utf-8 -*-
+# This work is part of the Core Imaging Library developed by
+# Visual Analytics and Imaging System Group of the Science Technology
+# Facilities Council, STFC
+
+# Copyright 2018-2019 Jakob Jorgensen, Daniil Kazantsev and Edoardo Pasca
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from ccpi.optimisation.functions import Function
+import numpy
+import warnings
+
+# Define a class for squared 2-norm
+class Norm2Sq(Function):
+ '''
+ f(x) = c*||A*x-b||_2^2
+
+ which has
+
+ grad[f](x) = 2*c*A^T*(A*x-b)
+
+ and Lipschitz constant
+
+ L = 2*c*||A||_2^2 = 2*s1(A)^2
+
+ where s1(A) is the largest singular value of A.
+
+ '''
+
+ def __init__(self,A,b,c=1.0,memopt=False):
+ super(Norm2Sq, self).__init__()
+
+ self.A = A # Should be an operator, default identity
+ self.b = b # Default zero DataSet?
+ self.c = c # Default 1.
+ if memopt:
+ try:
+ self.range_tmp = A.range_geometry().allocate()
+ self.domain_tmp = A.domain_geometry().allocate()
+ self.memopt = True
+ except NameError as ne:
+ warnings.warn(str(ne))
+ self.memopt = False
+ except NotImplementedError as nie:
+ print (nie)
+ warnings.warn(str(nie))
+ self.memopt = False
+ else:
+ self.memopt = False
+
+ # Compute the Lipschitz parameter from the operator if possible
+ # Leave it initialised to None otherwise
+ try:
+ self.L = 2.0*self.c*(self.A.norm()**2)
+ except AttributeError as ae:
+ pass
+ except NotImplementedError as noe:
+ pass
+
+ #def grad(self,x):
+ # return self.gradient(x, out=None)
+
+ def __call__(self,x):
+ #return self.c* np.sum(np.square((self.A.direct(x) - self.b).ravel()))
+ #if out is None:
+ # return self.c*( ( (self.A.direct(x)-self.b)**2).sum() )
+ #else:
+ y = self.A.direct(x)
+ y.__isub__(self.b)
+ #y.__imul__(y)
+ #return y.sum() * self.c
+ try:
+ return y.squared_norm() * self.c
+ except AttributeError as ae:
+ # added for compatibility with SIRF
+ return (y.norm()**2) * self.c
+
+ def gradient(self, x, out = None):
+ if self.memopt:
+ #return 2.0*self.c*self.A.adjoint( self.A.direct(x) - self.b )
+ self.A.direct(x, out=self.range_tmp)
+ self.range_tmp -= self.b
+ self.A.adjoint(self.range_tmp, out=out)
+ #self.direct_placehold.multiply(2.0*self.c, out=out)
+ out *= (self.c * 2.0)
+ else:
+ return (2.0*self.c)*self.A.adjoint( self.A.direct(x) - self.b )
diff --git a/Wrappers/Python/ccpi/optimisation/functions/ScaledFunction.py b/Wrappers/Python/ccpi/optimisation/functions/ScaledFunction.py
new file mode 100755
index 0000000..8bf502a
--- /dev/null
+++ b/Wrappers/Python/ccpi/optimisation/functions/ScaledFunction.py
@@ -0,0 +1,151 @@
+# -*- coding: utf-8 -*-
+# This work is part of the Core Imaging Library developed by
+# Visual Analytics and Imaging System Group of the Science Technology
+# Facilities Council, STFC
+
+# Copyright 2018-2019 Evangelos Papoutsellis and Edoardo Pasca
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from numbers import Number
+import numpy
+import warnings
+
+class ScaledFunction(object):
+
+ '''ScaledFunction
+
+ A class to represent the scalar multiplication of an Function with a scalar.
+ It holds a function and a scalar. Basically it returns the multiplication
+ of the product of the function __call__, convex_conjugate and gradient with the scalar.
+ For the rest it behaves like the function it holds.
+
+ Args:
+ function (Function): a Function or BlockOperator
+ scalar (Number): a scalar multiplier
+ Example:
+ The scaled operator behaves like the following:
+
+ '''
+ def __init__(self, function, scalar):
+ super(ScaledFunction, self).__init__()
+
+ if not isinstance (scalar, Number):
+ raise TypeError('expected scalar: got {}'.format(type(scalar)))
+ self.scalar = scalar
+ self.function = function
+
+ if self.function.L is not None:
+ self.L = self.scalar * self.function.L
+
+ def __call__(self,x, out=None):
+ '''Evaluates the function at x '''
+ return self.scalar * self.function(x)
+
+ def convex_conjugate(self, x):
+ '''returns the convex_conjugate of the scaled function '''
+ return self.scalar * self.function.convex_conjugate(x/self.scalar)
+
+ def gradient(self, x, out=None):
+ '''Returns the gradient of the function at x, if the function is differentiable'''
+ if out is None:
+ return self.scalar * self.function.gradient(x)
+ else:
+ self.function.gradient(x, out=out)
+ out *= self.scalar
+
+ def proximal(self, x, tau, out=None):
+ '''This returns the proximal operator for the function at x, tau
+ '''
+ if out is None:
+ return self.function.proximal(x, tau*self.scalar)
+ else:
+ self.function.proximal(x, tau*self.scalar, out = out)
+
+ def proximal_conjugate(self, x, tau, out = None):
+ '''This returns the proximal operator for the function at x, tau
+ '''
+ if out is None:
+ return self.scalar * self.function.proximal_conjugate(x/self.scalar, tau/self.scalar)
+ else:
+ self.function.proximal_conjugate(x/self.scalar, tau/self.scalar, out=out)
+ out *= self.scalar
+
+ def grad(self, x):
+ '''Alias of gradient(x,None)'''
+ warnings.warn('''This method will disappear in following
+ versions of the CIL. Use gradient instead''', DeprecationWarning)
+ return self.gradient(x, out=None)
+
+ def prox(self, x, tau):
+ '''Alias of proximal(x, tau, None)'''
+ warnings.warn('''This method will disappear in following
+ versions of the CIL. Use proximal instead''', DeprecationWarning)
+ return self.proximal(x, tau, out=None)
+
+
+
+if __name__ == '__main__':
+
+ from ccpi.optimisation.functions import L2NormSquared, MixedL21Norm
+ from ccpi.framework import ImageGeometry, BlockGeometry
+
+ M, N, K = 2,3,5
+ ig = ImageGeometry(voxel_num_x=M, voxel_num_y = N, voxel_num_z = K)
+
+ u = ig.allocate('random_int')
+ b = ig.allocate('random_int')
+
+ BG = BlockGeometry(ig, ig)
+ U = BG.allocate('random_int')
+
+ f2 = 0.5 * L2NormSquared(b=b)
+ f1 = 30 * MixedL21Norm()
+ tau = 0.355
+
+ res_no_out1 = f1.proximal_conjugate(U, tau)
+ res_no_out2 = f2.proximal_conjugate(u, tau)
+
+
+# print( " ######## with out ######## ")
+ res_out1 = BG.allocate()
+ res_out2 = ig.allocate()
+
+ f1.proximal_conjugate(U, tau, out = res_out1)
+ f2.proximal_conjugate(u, tau, out = res_out2)
+
+
+ numpy.testing.assert_array_almost_equal(res_no_out1[0].as_array(), \
+ res_out1[0].as_array(), decimal=4)
+
+ numpy.testing.assert_array_almost_equal(res_no_out2.as_array(), \
+ res_out2.as_array(), decimal=4)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Wrappers/Python/ccpi/optimisation/functions/ZeroFunction.py b/Wrappers/Python/ccpi/optimisation/functions/ZeroFunction.py
new file mode 100644
index 0000000..a019815
--- /dev/null
+++ b/Wrappers/Python/ccpi/optimisation/functions/ZeroFunction.py
@@ -0,0 +1,55 @@
+# -*- coding: utf-8 -*-
+# This work is part of the Core Imaging Library developed by
+# Visual Analytics and Imaging System Group of the Science Technology
+# Facilities Council, STFC
+
+# Copyright 2018-2019 Evangelos Papoutsellis and Edoardo Pasca
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from ccpi.optimisation.functions import Function
+from ccpi.framework import BlockDataContainer
+
+class ZeroFunction(Function):
+
+ ''' ZeroFunction: f(x) = 0
+
+
+ '''
+
+ def __init__(self):
+ super(ZeroFunction, self).__init__()
+
+ def __call__(self,x):
+ return 0
+
+ def convex_conjugate(self, x):
+
+ ''' This is the support function sup <x, x^{*}> which in fact is the
+ indicator function for the set = {0}
+ So 0 if x=0, or inf if x neq 0
+ '''
+ return x.maximum(0).sum()
+
+
+ def proximal(self, x, tau, out=None):
+ if out is None:
+ return x.copy()
+ else:
+ out.fill(x)
+
+ def proximal_conjugate(self, x, tau, out = None):
+ if out is None:
+ return 0
+ else:
+ return 0
diff --git a/Wrappers/Python/ccpi/optimisation/functions/__init__.py b/Wrappers/Python/ccpi/optimisation/functions/__init__.py
new file mode 100644
index 0000000..c0eab31
--- /dev/null
+++ b/Wrappers/Python/ccpi/optimisation/functions/__init__.py
@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-
+
+from .Function import Function
+from .ZeroFunction import ZeroFunction
+from .L1Norm import L1Norm
+from .L2NormSquared import L2NormSquared
+from .ScaledFunction import ScaledFunction
+from .BlockFunction import BlockFunction
+from .FunctionOperatorComposition import FunctionOperatorComposition
+from .MixedL21Norm import MixedL21Norm
+from .IndicatorBox import IndicatorBox
+from .KullbackLeibler import KullbackLeibler
+from .Norm2Sq import Norm2Sq
diff --git a/Wrappers/Python/ccpi/optimisation/operators/BlockOperator.py b/Wrappers/Python/ccpi/optimisation/operators/BlockOperator.py
new file mode 100755
index 0000000..cbdc420
--- /dev/null
+++ b/Wrappers/Python/ccpi/optimisation/operators/BlockOperator.py
@@ -0,0 +1,417 @@
+# -*- coding: utf-8 -*-
+"""
+Created on Thu Feb 14 12:36:40 2019
+
+@author: ofn77899
+"""
+#from ccpi.optimisation.ops import Operator
+import numpy
+from numbers import Number
+import functools
+from ccpi.framework import AcquisitionData, ImageData, BlockDataContainer, DataContainer
+from ccpi.optimisation.operators import Operator, LinearOperator
+from ccpi.optimisation.operators.BlockScaledOperator import BlockScaledOperator
+from ccpi.framework import BlockGeometry
+
+class BlockOperator(Operator):
+ '''A Block matrix containing Operators
+
+ The Block Framework is a generic strategy to treat variational problems in the
+ following form:
+
+ .. math::
+
+ min Regulariser + Fidelity
+
+
+ BlockOperators have a generic shape M x N, and when applied on an
+ Nx1 BlockDataContainer, will yield and Mx1 BlockDataContainer.
+ Notice: BlockDatacontainer are only allowed to have the shape of N x 1, with
+ N rows and 1 column.
+
+ User may specify the shape of the block, by default is a row vector
+
+ Operators in a Block are required to have the same domain column-wise and the
+ same range row-wise.
+ '''
+ __array_priority__ = 1
+ def __init__(self, *args, **kwargs):
+ '''
+ Class creator
+
+ Note:
+ Do not include the `self` parameter in the ``Args`` section.
+
+ Args:
+ :param: vararg (Operator): Operators in the block.
+ :param: shape (:obj:`tuple`, optional): If shape is passed the Operators in
+ vararg are considered input in a row-by-row fashion.
+ Shape and number of Operators must match.
+
+ Example:
+ BlockOperator(op0,op1) results in a row block
+ BlockOperator(op0,op1,shape=(1,2)) results in a column block
+ '''
+ self.operators = args
+ shape = kwargs.get('shape', None)
+ if shape is None:
+ shape = (len(args),1)
+ self.shape = shape
+ n_elements = functools.reduce(lambda x,y: x*y, shape, 1)
+ if len(args) != n_elements:
+ raise ValueError(
+ 'Dimension and size do not match: expected {} got {}'
+ .format(n_elements,len(args)))
+ # test if operators are compatible
+ if not self.column_wise_compatible():
+ raise ValueError('Operators in each column must have the same domain')
+ if not self.row_wise_compatible():
+ raise ValueError('Operators in each row must have the same range')
+
+ def column_wise_compatible(self):
+ '''Operators in a Block should have the same domain per column'''
+ rows, cols = self.shape
+ compatible = True
+ for col in range(cols):
+ column_compatible = True
+ for row in range(1,rows):
+ dg0 = self.get_item(row-1,col).domain_geometry()
+ dg1 = self.get_item(row,col).domain_geometry()
+ column_compatible = dg0.__dict__ == dg1.__dict__ and column_compatible
+ compatible = compatible and column_compatible
+ return compatible
+
+ def row_wise_compatible(self):
+ '''Operators in a Block should have the same range per row'''
+ rows, cols = self.shape
+ compatible = True
+ for row in range(rows):
+ row_compatible = True
+ for col in range(1,cols):
+ dg0 = self.get_item(row,col-1).range_geometry()
+ dg1 = self.get_item(row,col).range_geometry()
+ row_compatible = dg0.__dict__ == dg1.__dict__ and row_compatible
+ compatible = compatible and row_compatible
+ return compatible
+
+ def get_item(self, row, col):
+ '''returns the Operator at specified row and col'''
+ if row > self.shape[0]:
+ raise ValueError('Requested row {} > max {}'.format(row, self.shape[0]))
+ if col > self.shape[1]:
+ raise ValueError('Requested col {} > max {}'.format(col, self.shape[1]))
+
+ index = row*self.shape[1]+col
+ return self.operators[index]
+
+ def norm(self, **kwargs):
+ norm = [op.norm(**kwargs)**2 for op in self.operators]
+ return numpy.sqrt(sum(norm))
+
+ def direct(self, x, out=None):
+ '''Direct operation for the BlockOperator
+
+ BlockOperator work on BlockDataContainer, but they will work on DataContainers
+ and inherited classes by simple wrapping the input in a BlockDataContainer of shape (1,1)
+ '''
+
+ if not isinstance (x, BlockDataContainer):
+ x_b = BlockDataContainer(x)
+ else:
+ x_b = x
+ shape = self.get_output_shape(x_b.shape)
+ res = []
+
+ if out is None:
+
+ for row in range(self.shape[0]):
+ for col in range(self.shape[1]):
+ if col == 0:
+ prod = self.get_item(row,col).direct(x_b.get_item(col))
+ else:
+ prod += self.get_item(row,col).direct(x_b.get_item(col))
+ res.append(prod)
+ return BlockDataContainer(*res, shape=shape)
+
+ else:
+
+ tmp = self.range_geometry().allocate()
+ for row in range(self.shape[0]):
+ for col in range(self.shape[1]):
+ if col == 0:
+ self.get_item(row,col).direct(
+ x_b.get_item(col),
+ out=out.get_item(row))
+ else:
+ a = out.get_item(row)
+ self.get_item(row,col).direct(
+ x_b.get_item(col),
+ out=tmp.get_item(row))
+ a += tmp.get_item(row)
+
+ def adjoint(self, x, out=None):
+ '''Adjoint operation for the BlockOperator
+
+ BlockOperator may contain both LinearOperator and Operator
+ This method exists in BlockOperator as it is not known what type of
+ Operator it will contain.
+
+ BlockOperator work on BlockDataContainer, but they will work on DataContainers
+ and inherited classes by simple wrapping the input in a BlockDataContainer of shape (1,1)
+
+ Raises: ValueError if the contained Operators are not linear
+ '''
+ if not self.is_linear():
+ raise ValueError('Not all operators in Block are linear.')
+ if not isinstance (x, BlockDataContainer):
+ x_b = BlockDataContainer(x)
+ else:
+ x_b = x
+ shape = self.get_output_shape(x_b.shape, adjoint=True)
+ if out is None:
+ res = []
+ for col in range(self.shape[1]):
+ for row in range(self.shape[0]):
+ if row == 0:
+ prod = self.get_item(row, col).adjoint(x_b.get_item(row))
+ else:
+ prod += self.get_item(row, col).adjoint(x_b.get_item(row))
+ res.append(prod)
+ if self.shape[1]==1:
+ return ImageData(*res)
+ else:
+ return BlockDataContainer(*res, shape=shape)
+ else:
+ #tmp = self.domain_geometry().allocate()
+
+ for col in range(self.shape[1]):
+ for row in range(self.shape[0]):
+ if row == 0:
+ if issubclass(out.__class__, DataContainer):
+ self.get_item(row, col).adjoint(
+ x_b.get_item(row),
+ out=out)
+ else:
+ op = self.get_item(row,col)
+ self.get_item(row, col).adjoint(
+ x_b.get_item(row),
+ out=out.get_item(col))
+ else:
+ if issubclass(out.__class__, DataContainer):
+ out += self.get_item(row,col).adjoint(
+ x_b.get_item(row))
+ else:
+ a = out.get_item(col)
+ a += self.get_item(row,col).adjoint(
+ x_b.get_item(row),
+ )
+ def is_linear(self):
+ '''returns whether all the elements of the BlockOperator are linear'''
+ return functools.reduce(lambda x, y: x and y.is_linear(), self.operators, True)
+
+ def get_output_shape(self, xshape, adjoint=False):
+ '''returns the shape of the output BlockDataContainer
+
+ A(N,M) direct u(M,1) -> N,1
+ A(N,M)^T adjoint u(N,1) -> M,1
+ '''
+ rows , cols = self.shape
+ xrows, xcols = xshape
+ if xcols != 1:
+ raise ValueError('BlockDataContainer cannot have more than 1 column')
+ if adjoint:
+ if rows != xrows:
+ raise ValueError('Incompatible shapes {} {}'.format(self.shape, xshape))
+ return (cols,xcols)
+ if cols != xrows:
+ raise ValueError('Incompatible shapes {} {}'.format((rows,cols), xshape))
+ return (rows,xcols)
+
+ def __rmul__(self, scalar):
+ '''Defines the left multiplication with a scalar
+
+ Args: scalar (number or iterable containing numbers):
+
+ Returns: a block operator with Scaled Operators inside'''
+ if isinstance (scalar, list) or isinstance(scalar, tuple) or \
+ isinstance(scalar, numpy.ndarray):
+ if len(scalar) != len(self.operators):
+ raise ValueError('dimensions of scalars and operators do not match')
+ scalars = scalar
+ else:
+ scalars = [scalar for _ in self.operators]
+ # create a list of ScaledOperator-s
+ ops = [ v * op for v,op in zip(scalars, self.operators)]
+ #return BlockScaledOperator(self, scalars ,shape=self.shape)
+ return type(self)(*ops, shape=self.shape)
+ @property
+ def T(self):
+ '''Return the transposed of self
+
+ input in a row-by-row'''
+ newshape = (self.shape[1], self.shape[0])
+ oplist = []
+ for col in range(newshape[1]):
+ for row in range(newshape[0]):
+ oplist.append(self.get_item(col,row))
+ return type(self)(*oplist, shape=newshape)
+
+ def domain_geometry(self):
+ '''returns the domain of the BlockOperator
+
+ If the shape of the BlockOperator is (N,1) the domain is a ImageGeometry or AcquisitionGeometry.
+ Otherwise it is a BlockGeometry.
+ '''
+ if self.shape[1] == 1:
+ # column BlockOperator
+ return self.get_item(0,0).domain_geometry()
+ else:
+ # get the geometries column wise
+ # we need only the geometries from the first row
+ # since it is compatible from __init__
+ tmp = []
+ for i in range(self.shape[1]):
+ tmp.append(self.get_item(0,i).domain_geometry())
+ return BlockGeometry(*tmp)
+
+ #shape = (self.shape[0], 1)
+ #return BlockGeometry(*[el.domain_geometry() for el in self.operators],
+ # shape=self.shape)
+
+ def range_geometry(self):
+ '''returns the range of the BlockOperator'''
+
+ tmp = []
+ for i in range(self.shape[0]):
+ tmp.append(self.get_item(i,0).range_geometry())
+ return BlockGeometry(*tmp)
+
+
+ #shape = (self.shape[1], 1)
+ #return BlockGeometry(*[el.range_geometry() for el in self.operators],
+ # shape=shape)
+
+ def sum_abs_row(self):
+
+ res = []
+ for row in range(self.shape[0]):
+ for col in range(self.shape[1]):
+ if col == 0:
+ prod = self.get_item(row,col).sum_abs_row()
+ else:
+ prod += self.get_item(row,col).sum_abs_row()
+ res.append(prod)
+
+ if self.shape[1]==1:
+ tmp = sum(res)
+ return ImageData(tmp)
+ else:
+
+ return BlockDataContainer(*res)
+
+ def sum_abs_col(self):
+
+ res = []
+ for row in range(self.shape[0]):
+ for col in range(self.shape[1]):
+ if col == 0:
+ prod = self.get_item(row, col).sum_abs_col()
+ else:
+ prod += self.get_item(row, col).sum_abs_col()
+ res.append(prod)
+
+ return BlockDataContainer(*res)
+
+
+
+if __name__ == '__main__':
+
+ from ccpi.framework import ImageGeometry
+ from ccpi.optimisation.operators import Gradient, Identity, \
+ SparseFiniteDiff, SymmetrizedGradient, ZeroOperator
+
+
+ M, N = 4, 3
+ ig = ImageGeometry(M, N)
+ arr = ig.allocate('random_int')
+
+ G = Gradient(ig)
+ Id = Identity(ig)
+
+ B = BlockOperator(G, Id)
+
+ print(B.sum_abs_row())
+#
+ Gx = SparseFiniteDiff(ig, direction=1, bnd_cond='Neumann')
+ Gy = SparseFiniteDiff(ig, direction=0, bnd_cond='Neumann')
+
+ d1 = abs(Gx.matrix()).toarray().sum(axis=0)
+ d2 = abs(Gy.matrix()).toarray().sum(axis=0)
+ d3 = abs(Id.matrix()).toarray().sum(axis=0)
+
+
+ d_res = numpy.reshape(d1 + d2 + d3, ig.shape, 'F')
+
+ print(d_res)
+#
+ z1 = abs(Gx.matrix()).toarray().sum(axis=1)
+ z2 = abs(Gy.matrix()).toarray().sum(axis=1)
+ z3 = abs(Id.matrix()).toarray().sum(axis=1)
+#
+ z_res = BlockDataContainer(BlockDataContainer(ImageData(numpy.reshape(z2, ig.shape, 'F')),\
+ ImageData(numpy.reshape(z1, ig.shape, 'F'))),\
+ ImageData(numpy.reshape(z3, ig.shape, 'F')))
+#
+ ttt = B.sum_abs_col()
+#
+ #TODO this is not working
+# numpy.testing.assert_array_almost_equal(z_res[0][0].as_array(), ttt[0][0].as_array(), decimal=4)
+# numpy.testing.assert_array_almost_equal(z_res[0][1].as_array(), ttt[0][1].as_array(), decimal=4)
+# numpy.testing.assert_array_almost_equal(z_res[1].as_array(), ttt[1].as_array(), decimal=4)
+
+
+ u = ig.allocate('random_int')
+
+ z1 = B.direct(u)
+ res = B.range_geometry().allocate()
+
+ B.direct(u, out = res)
+
+
+
+ ###########################################################################
+ # Block Operator for TGV reconstruction
+
+ M, N = 2,3
+ ig = ImageGeometry(M, N)
+ ag = ig
+
+ op11 = Gradient(ig)
+ op12 = Identity(op11.range_geometry())
+
+ op22 = SymmetrizedGradient(op11.domain_geometry())
+
+ op21 = ZeroOperator(ig, op22.range_geometry())
+
+
+ op31 = Identity(ig, ag)
+ op32 = ZeroOperator(op22.domain_geometry(), ag)
+
+ operator = BlockOperator(op11, -1*op12, op21, op22, op31, op32, shape=(3,2) )
+
+ z1 = operator.domain_geometry()
+ z2 = operator.range_geometry()
+
+ print(z1.shape)
+ print(z2.shape)
+
+
+
+
+
+
+
+
+
+
+ \ No newline at end of file
diff --git a/Wrappers/Python/ccpi/optimisation/operators/BlockScaledOperator.py b/Wrappers/Python/ccpi/optimisation/operators/BlockScaledOperator.py
new file mode 100644
index 0000000..74ba9e4
--- /dev/null
+++ b/Wrappers/Python/ccpi/optimisation/operators/BlockScaledOperator.py
@@ -0,0 +1,67 @@
+from numbers import Number
+import numpy
+from ccpi.optimisation.operators import ScaledOperator
+import functools
+
+class BlockScaledOperator(ScaledOperator):
+ '''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.
+
+ Args:
+ operator (Operator): a Operator or LinearOperator
+ scalar (Number): a scalar multiplier
+ Example:
+ The scaled operator behaves like the following:
+ 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, shape=None):
+ if shape is None:
+ shape = operator.shape
+
+ if isinstance(scalar, (list, tuple, numpy.ndarray)):
+ size = functools.reduce(lambda x,y:x*y, shape, 1)
+ if len(scalar) != size:
+ raise ValueError('Scalar and operators size do not match: {}!={}'
+ .format(len(scalar), len(operator)))
+ self.scalar = scalar[:]
+ print ("BlockScaledOperator ", self.scalar)
+ elif isinstance (scalar, Number):
+ self.scalar = scalar
+ else:
+ raise TypeError('expected scalar to be a number of an iterable: got {}'.format(type(scalar)))
+ self.operator = operator
+ self.shape = shape
+ def direct(self, x, out=None):
+ print ("BlockScaledOperator self.scalar", self.scalar)
+ #print ("self.scalar", self.scalar[0]* x.get_item(0).as_array())
+ return self.scalar * (self.operator.direct(x, out=out))
+ def adjoint(self, x, out=None):
+ if self.operator.is_linear():
+ return self.scalar * self.operator.adjoint(x, out=out)
+ else:
+ raise TypeError('Operator is not linear')
+ def norm(self, **kwargs):
+ return numpy.abs(self.scalar) * self.operator.norm(**kwargs)
+ def range_geometry(self):
+ return self.operator.range_geometry()
+ def domain_geometry(self):
+ return self.operator.domain_geometry()
+ @property
+ def T(self):
+ '''Return the transposed of self'''
+ #print ("transpose before" , self.shape)
+ #shape = (self.shape[1], self.shape[0])
+ ##self.shape = shape
+ ##self.operator.shape = shape
+ #print ("transpose" , shape)
+ #return self
+ return type(self)(self.operator.T, self.scalar) \ No newline at end of file
diff --git a/Wrappers/Python/ccpi/optimisation/operators/FiniteDifferenceOperator.py b/Wrappers/Python/ccpi/optimisation/operators/FiniteDifferenceOperator.py
new file mode 100644
index 0000000..876f45f
--- /dev/null
+++ b/Wrappers/Python/ccpi/optimisation/operators/FiniteDifferenceOperator.py
@@ -0,0 +1,367 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Created on Fri Mar 1 22:51:17 2019
+
+@author: evangelos
+"""
+
+from ccpi.optimisation.operators import LinearOperator
+from ccpi.framework import ImageData, BlockDataContainer
+import numpy as np
+
+class FiniteDiff(LinearOperator):
+
+ # Works for Neum/Symmetric & periodic boundary conditions
+ # TODO add central differences???
+ # TODO not very well optimised, too many conditions
+ # TODO add discretisation step, should get that from imageGeometry
+
+ # Grad_order = ['channels', 'direction_z', 'direction_y', 'direction_x']
+ # Grad_order = ['channels', 'direction_y', 'direction_x']
+ # Grad_order = ['direction_z', 'direction_y', 'direction_x']
+ # Grad_order = ['channels', 'direction_z', 'direction_y', 'direction_x']
+
+ def __init__(self, gm_domain, gm_range=None, direction=0, bnd_cond = 'Neumann'):
+ ''''''
+ super(FiniteDiff, self).__init__()
+ '''FIXME: domain and range should be geometries'''
+ self.gm_domain = gm_domain
+ self.gm_range = gm_range
+
+ self.direction = direction
+ self.bnd_cond = bnd_cond
+
+ # Domain Geometry = Range Geometry if not stated
+ if self.gm_range is None:
+ self.gm_range = self.gm_domain
+ # check direction and "length" of geometry
+ if self.direction + 1 > len(self.gm_domain.shape):
+ raise ValueError('Gradient directions more than geometry domain')
+
+ #self.voxel_size = kwargs.get('voxel_size',1)
+ # this wrongly assumes a homogeneous voxel size
+# self.voxel_size = self.gm_domain.voxel_size_x
+
+
+ def direct(self, x, out=None):
+
+ x_asarr = x.as_array()
+ x_sz = len(x.shape)
+
+ if out is None:
+ out = np.zeros_like(x_asarr)
+ else:
+ out = out.as_array()
+ out[:]=0
+
+
+
+ ######################## Direct for 2D ###############################
+ if x_sz == 2:
+
+ if self.direction == 1:
+
+ np.subtract( x_asarr[:,1:], x_asarr[:,0:-1], out = out[:,0:-1] )
+
+ if self.bnd_cond == 'Neumann':
+ pass
+ elif self.bnd_cond == 'Periodic':
+ np.subtract( x_asarr[:,0], x_asarr[:,-1], out = out[:,-1] )
+ else:
+ raise ValueError('No valid boundary conditions')
+
+ if self.direction == 0:
+
+ np.subtract( x_asarr[1:], x_asarr[0:-1], out = out[0:-1,:] )
+
+ if self.bnd_cond == 'Neumann':
+ pass
+ elif self.bnd_cond == 'Periodic':
+ np.subtract( x_asarr[0,:], x_asarr[-1,:], out = out[-1,:] )
+ else:
+ raise ValueError('No valid boundary conditions')
+
+ ######################## Direct for 3D ###############################
+ elif x_sz == 3:
+
+ if self.direction == 0:
+
+ np.subtract( x_asarr[1:,:,:], x_asarr[0:-1,:,:], out = out[0:-1,:,:] )
+
+ if self.bnd_cond == 'Neumann':
+ pass
+ elif self.bnd_cond == 'Periodic':
+ np.subtract( x_asarr[0,:,:], x_asarr[-1,:,:], out = out[-1,:,:] )
+ else:
+ raise ValueError('No valid boundary conditions')
+
+ if self.direction == 1:
+
+ np.subtract( x_asarr[:,1:,:], x_asarr[:,0:-1,:], out = out[:,0:-1,:] )
+
+ if self.bnd_cond == 'Neumann':
+ pass
+ elif self.bnd_cond == 'Periodic':
+ np.subtract( x_asarr[:,0,:], x_asarr[:,-1,:], out = out[:,-1,:] )
+ else:
+ raise ValueError('No valid boundary conditions')
+
+
+ if self.direction == 2:
+
+ np.subtract( x_asarr[:,:,1:], x_asarr[:,:,0:-1], out = out[:,:,0:-1] )
+
+ if self.bnd_cond == 'Neumann':
+ pass
+ elif self.bnd_cond == 'Periodic':
+ np.subtract( x_asarr[:,:,0], x_asarr[:,:,-1], out = out[:,:,-1] )
+ else:
+ raise ValueError('No valid boundary conditions')
+
+ ######################## Direct for 4D ###############################
+ elif x_sz == 4:
+
+ if self.direction == 0:
+ np.subtract( x_asarr[1:,:,:,:], x_asarr[0:-1,:,:,:], out = out[0:-1,:,:,:] )
+
+ if self.bnd_cond == 'Neumann':
+ pass
+ elif self.bnd_cond == 'Periodic':
+ np.subtract( x_asarr[0,:,:,:], x_asarr[-1,:,:,:], out = out[-1,:,:,:] )
+ else:
+ raise ValueError('No valid boundary conditions')
+
+ if self.direction == 1:
+ np.subtract( x_asarr[:,1:,:,:], x_asarr[:,0:-1,:,:], out = out[:,0:-1,:,:] )
+
+ if self.bnd_cond == 'Neumann':
+ pass
+ elif self.bnd_cond == 'Periodic':
+ np.subtract( x_asarr[:,0,:,:], x_asarr[:,-1,:,:], out = out[:,-1,:,:] )
+ else:
+ raise ValueError('No valid boundary conditions')
+
+ if self.direction == 2:
+ np.subtract( x_asarr[:,:,1:,:], x_asarr[:,:,0:-1,:], out = out[:,:,0:-1,:] )
+
+ if self.bnd_cond == 'Neumann':
+ pass
+ elif self.bnd_cond == 'Periodic':
+ np.subtract( x_asarr[:,:,0,:], x_asarr[:,:,-1,:], out = out[:,:,-1,:] )
+ else:
+ raise ValueError('No valid boundary conditions')
+
+ if self.direction == 3:
+ np.subtract( x_asarr[:,:,:,1:], x_asarr[:,:,:,0:-1], out = out[:,:,:,0:-1] )
+
+ if self.bnd_cond == 'Neumann':
+ pass
+ elif self.bnd_cond == 'Periodic':
+ np.subtract( x_asarr[:,:,:,0], x_asarr[:,:,:,-1], out = out[:,:,:,-1] )
+ else:
+ raise ValueError('No valid boundary conditions')
+
+ else:
+ raise NotImplementedError
+
+# res = out #/self.voxel_size
+ return type(x)(out)
+
+
+ def adjoint(self, x, out=None):
+
+ x_asarr = x.as_array()
+ x_sz = len(x.shape)
+
+ if out is None:
+ out = np.zeros_like(x_asarr)
+ else:
+ out = out.as_array()
+ out[:]=0
+
+
+ ######################## Adjoint for 2D ###############################
+ if x_sz == 2:
+
+ if self.direction == 1:
+
+ np.subtract( x_asarr[:,1:], x_asarr[:,0:-1], out = out[:,1:] )
+
+ if self.bnd_cond == 'Neumann':
+ np.subtract( x_asarr[:,0], 0, out = out[:,0] )
+ np.subtract( -x_asarr[:,-2], 0, out = out[:,-1] )
+
+ elif self.bnd_cond == 'Periodic':
+ np.subtract( x_asarr[:,0], x_asarr[:,-1], out = out[:,0] )
+
+ else:
+ raise ValueError('No valid boundary conditions')
+
+ if self.direction == 0:
+
+ np.subtract( x_asarr[1:,:], x_asarr[0:-1,:], out = out[1:,:] )
+
+ if self.bnd_cond == 'Neumann':
+ np.subtract( x_asarr[0,:], 0, out = out[0,:] )
+ np.subtract( -x_asarr[-2,:], 0, out = out[-1,:] )
+
+ elif self.bnd_cond == 'Periodic':
+ np.subtract( x_asarr[0,:], x_asarr[-1,:], out = out[0,:] )
+
+ else:
+ raise ValueError('No valid boundary conditions')
+
+ ######################## Adjoint for 3D ###############################
+ elif x_sz == 3:
+
+ if self.direction == 0:
+
+ np.subtract( x_asarr[1:,:,:], x_asarr[0:-1,:,:], out = out[1:,:,:] )
+
+ if self.bnd_cond == 'Neumann':
+ np.subtract( x_asarr[0,:,:], 0, out = out[0,:,:] )
+ np.subtract( -x_asarr[-2,:,:], 0, out = out[-1,:,:] )
+ elif self.bnd_cond == 'Periodic':
+ np.subtract( x_asarr[0,:,:], x_asarr[-1,:,:], out = out[0,:,:] )
+ else:
+ raise ValueError('No valid boundary conditions')
+
+ if self.direction == 1:
+ np.subtract( x_asarr[:,1:,:], x_asarr[:,0:-1,:], out = out[:,1:,:] )
+
+ if self.bnd_cond == 'Neumann':
+ np.subtract( x_asarr[:,0,:], 0, out = out[:,0,:] )
+ np.subtract( -x_asarr[:,-2,:], 0, out = out[:,-1,:] )
+ elif self.bnd_cond == 'Periodic':
+ np.subtract( x_asarr[:,0,:], x_asarr[:,-1,:], out = out[:,0,:] )
+ else:
+ raise ValueError('No valid boundary conditions')
+
+ if self.direction == 2:
+ np.subtract( x_asarr[:,:,1:], x_asarr[:,:,0:-1], out = out[:,:,1:] )
+
+ if self.bnd_cond == 'Neumann':
+ np.subtract( x_asarr[:,:,0], 0, out = out[:,:,0] )
+ np.subtract( -x_asarr[:,:,-2], 0, out = out[:,:,-1] )
+ elif self.bnd_cond == 'Periodic':
+ np.subtract( x_asarr[:,:,0], x_asarr[:,:,-1], out = out[:,:,0] )
+ else:
+ raise ValueError('No valid boundary conditions')
+
+ ######################## Adjoint for 4D ###############################
+ elif x_sz == 4:
+
+ if self.direction == 0:
+ np.subtract( x_asarr[1:,:,:,:], x_asarr[0:-1,:,:,:], out = out[1:,:,:,:] )
+
+ if self.bnd_cond == 'Neumann':
+ np.subtract( x_asarr[0,:,:,:], 0, out = out[0,:,:,:] )
+ np.subtract( -x_asarr[-2,:,:,:], 0, out = out[-1,:,:,:] )
+
+ elif self.bnd_cond == 'Periodic':
+ np.subtract( x_asarr[0,:,:,:], x_asarr[-1,:,:,:], out = out[0,:,:,:] )
+ else:
+ raise ValueError('No valid boundary conditions')
+
+ if self.direction == 1:
+ np.subtract( x_asarr[:,1:,:,:], x_asarr[:,0:-1,:,:], out = out[:,1:,:,:] )
+
+ if self.bnd_cond == 'Neumann':
+ np.subtract( x_asarr[:,0,:,:], 0, out = out[:,0,:,:] )
+ np.subtract( -x_asarr[:,-2,:,:], 0, out = out[:,-1,:,:] )
+
+ elif self.bnd_cond == 'Periodic':
+ np.subtract( x_asarr[:,0,:,:], x_asarr[:,-1,:,:], out = out[:,0,:,:] )
+ else:
+ raise ValueError('No valid boundary conditions')
+
+
+ if self.direction == 2:
+ np.subtract( x_asarr[:,:,1:,:], x_asarr[:,:,0:-1,:], out = out[:,:,1:,:] )
+
+ if self.bnd_cond == 'Neumann':
+ np.subtract( x_asarr[:,:,0,:], 0, out = out[:,:,0,:] )
+ np.subtract( -x_asarr[:,:,-2,:], 0, out = out[:,:,-1,:] )
+
+ elif self.bnd_cond == 'Periodic':
+ np.subtract( x_asarr[:,:,0,:], x_asarr[:,:,-1,:], out = out[:,:,0,:] )
+ else:
+ raise ValueError('No valid boundary conditions')
+
+ if self.direction == 3:
+ np.subtract( x_asarr[:,:,:,1:], x_asarr[:,:,:,0:-1], out = out[:,:,:,1:] )
+
+ if self.bnd_cond == 'Neumann':
+ np.subtract( x_asarr[:,:,:,0], 0, out = out[:,:,:,0] )
+ np.subtract( -x_asarr[:,:,:,-2], 0, out = out[:,:,:,-1] )
+
+ elif self.bnd_cond == 'Periodic':
+ np.subtract( x_asarr[:,:,:,0], x_asarr[:,:,:,-1], out = out[:,:,:,0] )
+ else:
+ raise ValueError('No valid boundary conditions')
+
+ else:
+ raise NotImplementedError
+
+ out *= -1 #/self.voxel_size
+ return type(x)(out)
+
+ def range_geometry(self):
+ '''Returns the range geometry'''
+ return self.gm_range
+
+ def domain_geometry(self):
+ '''Returns the domain geometry'''
+ return self.gm_domain
+
+
+if __name__ == '__main__':
+
+ from ccpi.framework import ImageGeometry
+ import numpy
+
+ N, M = 2, 3
+
+ ig = ImageGeometry(N, M)
+
+
+ FD = FiniteDiff(ig, direction = 1, bnd_cond = 'Neumann')
+ u = FD.domain_geometry().allocate('random_int')
+
+ res = FD.domain_geometry().allocate()
+ res1 = FD.range_geometry().allocate()
+ FD.direct(u, out=res)
+
+ z = FD.direct(u)
+# print(z.as_array(), res.as_array())
+
+ for i in range(10):
+#
+ z1 = FD.direct(u)
+ FD.direct(u, out=res)
+
+ u = ig.allocate('random_int')
+ res = u
+ z1 = u
+ numpy.testing.assert_array_almost_equal(z1.as_array(), \
+ res.as_array(), decimal=4)
+
+# print(z1.as_array(), res.as_array())
+ z2 = FD.adjoint(z1)
+ FD.adjoint(z1, out=res1)
+ numpy.testing.assert_array_almost_equal(z2.as_array(), \
+ res1.as_array(), decimal=4)
+
+
+
+
+
+
+
+
+# w = G.range_geometry().allocate('random_int')
+
+
+
+ \ No newline at end of file
diff --git a/Wrappers/Python/ccpi/optimisation/operators/GradientOperator.py b/Wrappers/Python/ccpi/optimisation/operators/GradientOperator.py
new file mode 100644
index 0000000..cd58b7d
--- /dev/null
+++ b/Wrappers/Python/ccpi/optimisation/operators/GradientOperator.py
@@ -0,0 +1,267 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Created on Fri Mar 1 22:50:04 2019
+
+@author: evangelos
+"""
+
+from ccpi.optimisation.operators import Operator, LinearOperator, ScaledOperator
+from ccpi.framework import ImageData, ImageGeometry, BlockGeometry, BlockDataContainer
+import numpy
+from ccpi.optimisation.operators import FiniteDiff, SparseFiniteDiff
+
+#%%
+
+class Gradient(LinearOperator):
+ CORRELATION_SPACE = "Space"
+ CORRELATION_SPACECHANNEL = "SpaceChannels"
+ # Grad_order = ['channels', 'direction_z', 'direction_y', 'direction_x']
+ # Grad_order = ['channels', 'direction_y', 'direction_x']
+ # Grad_order = ['direction_z', 'direction_y', 'direction_x']
+ # Grad_order = ['channels', 'direction_z', 'direction_y', 'direction_x']
+ def __init__(self, gm_domain, bnd_cond = 'Neumann', **kwargs):
+
+ super(Gradient, self).__init__()
+
+ self.gm_domain = gm_domain # Domain of Grad Operator
+
+ self.correlation = kwargs.get('correlation',Gradient.CORRELATION_SPACE)
+
+ if self.correlation==Gradient.CORRELATION_SPACE:
+ if self.gm_domain.channels>1:
+ self.gm_range = BlockGeometry(*[self.gm_domain for _ in range(self.gm_domain.length-1)] )
+ if self.gm_domain.length == 4:
+ # 3D + Channel
+ # expected Grad_order = ['channels', 'direction_z', 'direction_y', 'direction_x']
+ expected_order = [ImageGeometry.CHANNEL, ImageGeometry.VERTICAL, ImageGeometry.HORIZONTAL_Y, ImageGeometry.HORIZONTAL_X]
+ else:
+ # 2D + Channel
+ # expected Grad_order = ['channels', 'direction_y', 'direction_x']
+ expected_order = [ImageGeometry.CHANNEL, ImageGeometry.HORIZONTAL_Y, ImageGeometry.HORIZONTAL_X]
+ order = self.gm_domain.get_order_by_label(self.gm_domain.dimension_labels, expected_order)
+ self.ind = order[1:]
+ #self.ind = numpy.arange(1,self.gm_domain.length)
+ else:
+ # no channel info
+ self.gm_range = BlockGeometry(*[self.gm_domain for _ in range(self.gm_domain.length) ] )
+ if self.gm_domain.length == 3:
+ # 3D
+ # expected Grad_order = ['direction_z', 'direction_y', 'direction_x']
+ expected_order = [ImageGeometry.VERTICAL, ImageGeometry.HORIZONTAL_Y, ImageGeometry.HORIZONTAL_X]
+ else:
+ # 2D
+ expected_order = [ImageGeometry.HORIZONTAL_Y, ImageGeometry.HORIZONTAL_X]
+ self.ind = self.gm_domain.get_order_by_label(self.gm_domain.dimension_labels, expected_order)
+ # self.ind = numpy.arange(self.gm_domain.length)
+ elif self.correlation==Gradient.CORRELATION_SPACECHANNEL:
+ if self.gm_domain.channels>1:
+ self.gm_range = BlockGeometry(*[self.gm_domain for _ in range(self.gm_domain.length)])
+ self.ind = range(self.gm_domain.length)
+ else:
+ raise ValueError('No channels to correlate')
+
+ self.bnd_cond = bnd_cond
+
+ # Call FiniteDiff operator
+
+ self.FD = FiniteDiff(self.gm_domain, direction = 0, bnd_cond = self.bnd_cond)
+
+
+ def direct(self, x, out=None):
+
+
+ if out is not None:
+
+ for i in range(self.gm_range.shape[0]):
+ self.FD.direction = self.ind[i]
+ self.FD.direct(x, out = out[i])
+ else:
+ tmp = self.gm_range.allocate()
+ for i in range(tmp.shape[0]):
+ self.FD.direction=self.ind[i]
+ tmp.get_item(i).fill(self.FD.direct(x))
+ return tmp
+
+ def adjoint(self, x, out=None):
+
+ if out is not None:
+
+ tmp = self.gm_domain.allocate()
+ for i in range(x.shape[0]):
+ self.FD.direction=self.ind[i]
+ self.FD.adjoint(x.get_item(i), out = tmp)
+ if i == 0:
+ out.fill(tmp)
+ else:
+ out += tmp
+ else:
+ tmp = self.gm_domain.allocate()
+ for i in range(x.shape[0]):
+ self.FD.direction=self.ind[i]
+
+ tmp += self.FD.adjoint(x.get_item(i))
+ return tmp
+
+
+ def domain_geometry(self):
+ return self.gm_domain
+
+ def range_geometry(self):
+ return self.gm_range
+
+ def __rmul__(self, scalar):
+ return ScaledOperator(self, scalar)
+
+ ###########################################################################
+ ############### For preconditioning ######################################
+ ###########################################################################
+ def matrix(self):
+
+ tmp = self.gm_range.allocate()
+
+ mat = []
+ for i in range(tmp.shape[0]):
+
+ spMat = SparseFiniteDiff(self.gm_domain, direction=self.ind[i], bnd_cond=self.bnd_cond)
+ mat.append(spMat.matrix())
+
+ return BlockDataContainer(*mat)
+
+
+ def sum_abs_col(self):
+
+ tmp = self.gm_range.allocate()
+ res = self.gm_domain.allocate()
+ for i in range(tmp.shape[0]):
+ spMat = SparseFiniteDiff(self.gm_domain, direction=self.ind[i], bnd_cond=self.bnd_cond)
+ res += spMat.sum_abs_row()
+ return res
+
+ def sum_abs_row(self):
+
+ tmp = self.gm_range.allocate()
+ res = []
+ for i in range(tmp.shape[0]):
+ spMat = SparseFiniteDiff(self.gm_domain, direction=self.ind[i], bnd_cond=self.bnd_cond)
+ res.append(spMat.sum_abs_col())
+ return BlockDataContainer(*res)
+
+
+if __name__ == '__main__':
+
+
+ from ccpi.optimisation.operators import Identity, BlockOperator
+
+
+ M, N = 20, 30
+ ig = ImageGeometry(M, N)
+ arr = ig.allocate('random_int' )
+
+ # check direct of Gradient and sparse matrix
+ G = Gradient(ig)
+ norm1 = G.norm(iterations=300)
+ print ("should be sqrt(8) {} {}".format(numpy.sqrt(8), norm1))
+ G_sp = G.matrix()
+ ig4 = ImageGeometry(M,N, channels=3)
+ G4 = Gradient(ig4, correlation=Gradient.CORRELATION_SPACECHANNEL)
+ norm4 = G4.norm(iterations=300)
+ print ("should be sqrt(12) {} {}".format(numpy.sqrt(12), norm4))
+
+
+ res1 = G.direct(arr)
+ res1y = numpy.reshape(G_sp[0].toarray().dot(arr.as_array().flatten('F')), ig.shape, 'F')
+
+ print(res1[0].as_array())
+ print(res1y)
+
+ res1x = numpy.reshape(G_sp[1].toarray().dot(arr.as_array().flatten('F')), ig.shape, 'F')
+
+ print(res1[1].as_array())
+ print(res1x)
+
+ #check sum abs row
+ conc_spmat = numpy.abs(numpy.concatenate( (G_sp[0].toarray(), G_sp[1].toarray() )))
+ print(numpy.reshape(conc_spmat.sum(axis=0), ig.shape, 'F'))
+ print(G.sum_abs_row().as_array())
+
+ print(numpy.reshape(conc_spmat.sum(axis=1), ((2,) + ig.shape), 'F'))
+
+ print(G.sum_abs_col()[0].as_array())
+ print(G.sum_abs_col()[1].as_array())
+
+ # Check Blockoperator sum abs col and row
+
+ op1 = Gradient(ig)
+ op2 = Identity(ig)
+
+ B = BlockOperator( op1, op2)
+
+ Brow = B.sum_abs_row()
+ Bcol = B.sum_abs_col()
+
+ concB = numpy.concatenate( (numpy.abs(numpy.concatenate( (G_sp[0].toarray(), G_sp[1].toarray() ))), op2.matrix().toarray()))
+
+ print(numpy.reshape(concB.sum(axis=0), ig.shape, 'F'))
+ print(Brow.as_array())
+
+ print(numpy.reshape(concB.sum(axis=1)[0:12], ((2,) + ig.shape), 'F'))
+ print(Bcol[1].as_array())
+
+
+# print(numpy.concatene(G_sp[0].toarray()+ ))
+# print(G_sp[1].toarray())
+#
+# d1 = G.sum_abs_row()
+# print(d1.as_array())
+#
+# d2 = G_neum.sum_abs_col()
+## print(d2)
+#
+#
+# ###########################################################
+ a = BlockDataContainer( BlockDataContainer(arr, arr), arr)
+ b = BlockDataContainer( BlockDataContainer(arr+5, arr+3), arr+2)
+ c = a/b
+
+ print(c[0][0].as_array(), (arr/(arr+5)).as_array())
+ print(c[0][1].as_array(), (arr/(arr+3)).as_array())
+ print(c[1].as_array(), (arr/(arr+2)).as_array())
+
+
+ a1 = BlockDataContainer( arr, BlockDataContainer(arr, arr))
+#
+# c1 = arr + a
+# c2 = arr + a
+# c2 = a1 + arr
+
+ from ccpi.framework import ImageGeometry
+# from ccpi.optimisation.operators import Gradient
+#
+ N, M = 2, 3
+#
+ ig = ImageGeometry(N, M)
+#
+ G = Gradient(ig)
+#
+ u = G.domain_geometry().allocate('random_int')
+ w = G.range_geometry().allocate('random_int')
+
+
+ print( "################ without out #############")
+
+ print( (G.direct(u)*w).sum(), (u*G.adjoint(w)).sum() )
+
+
+ print( "################ with out #############")
+
+ res = G.range_geometry().allocate()
+ res1 = G.domain_geometry().allocate()
+ G.direct(u, out = res)
+ G.adjoint(w, out = res1)
+
+ print( (res*w).sum(), (u*res1).sum() )
+
+
+
+
diff --git a/Wrappers/Python/ccpi/optimisation/operators/IdentityOperator.py b/Wrappers/Python/ccpi/optimisation/operators/IdentityOperator.py
new file mode 100644
index 0000000..8f35373
--- /dev/null
+++ b/Wrappers/Python/ccpi/optimisation/operators/IdentityOperator.py
@@ -0,0 +1,79 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Created on Wed Mar 6 19:30:51 2019
+
+@author: evangelos
+"""
+
+from ccpi.optimisation.operators import LinearOperator
+import scipy.sparse as sp
+import numpy as np
+from ccpi.framework import ImageData
+
+
+class Identity(LinearOperator):
+
+ def __init__(self, gm_domain, gm_range=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__()
+
+ def direct(self,x,out=None):
+ if out is None:
+ return x.copy()
+ else:
+ out.fill(x)
+
+ def adjoint(self,x, out=None):
+ if out is None:
+ return x.copy()
+ else:
+ out.fill(x)
+
+ def calculate_norm(self, **kwargs):
+ return 1.0
+
+ def domain_geometry(self):
+ return self.gm_domain
+
+ def range_geometry(self):
+ return self.gm_range
+
+ def matrix(self):
+
+ return sp.eye(np.prod(self.gm_domain.shape))
+
+ def sum_abs_row(self):
+
+ return self.gm_range.allocate(1)#ImageData(np.array(np.reshape(abs(self.matrix()).sum(axis=0), self.gm_domain.shape, 'F')))
+
+ def sum_abs_col(self):
+
+ return self.gm_domain.allocate(1)#ImageData(np.array(np.reshape(abs(self.matrix()).sum(axis=1), self.gm_domain.shape, 'F')))
+
+
+if __name__ == '__main__':
+
+ from ccpi.framework import ImageGeometry
+
+ M, N = 2, 3
+ ig = ImageGeometry(M, N)
+ arr = ig.allocate('random_int')
+
+ Id = Identity(ig)
+ d = Id.matrix()
+ print(d.toarray())
+
+ d1 = Id.sum_abs_col()
+ print(d1.as_array())
+
+
+
+
+
+ \ No newline at end of file
diff --git a/Wrappers/Python/ccpi/optimisation/operators/LinearOperator.py b/Wrappers/Python/ccpi/optimisation/operators/LinearOperator.py
new file mode 100755
index 0000000..55eb692
--- /dev/null
+++ b/Wrappers/Python/ccpi/optimisation/operators/LinearOperator.py
@@ -0,0 +1,55 @@
+# -*- coding: utf-8 -*-
+"""
+Created on Tue Mar 5 15:57:52 2019
+
+@author: ofn77899
+"""
+
+from ccpi.optimisation.operators import Operator
+from ccpi.framework import ImageGeometry
+import numpy
+
+
+class LinearOperator(Operator):
+ '''A Linear Operator that maps from a space X <-> Y'''
+ def __init__(self):
+ super(LinearOperator, self).__init__()
+ 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'''
+
+ # Initialise random
+ if x_init is None:
+ x0 = operator.domain_geometry().allocate(type(operator.domain_geometry()).RANDOM_INT)
+ 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()
+ s[it] = x1.dot(x0) / x0.squared_norm()
+ 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'''
+ x0 = kwargs.get('x0', None)
+ iterations = kwargs.get('iterations', 25)
+ s1, sall, svec = LinearOperator.PowerMethod(self, iterations, x_init=x0)
+ return s1
+
+
diff --git a/Wrappers/Python/ccpi/optimisation/operators/LinearOperatorMatrix.py b/Wrappers/Python/ccpi/optimisation/operators/LinearOperatorMatrix.py
new file mode 100644
index 0000000..90ef938
--- /dev/null
+++ b/Wrappers/Python/ccpi/optimisation/operators/LinearOperatorMatrix.py
@@ -0,0 +1,41 @@
+import numpy
+from scipy.sparse.linalg import svds
+from ccpi.framework import DataContainer
+from ccpi.framework import AcquisitionData
+from ccpi.framework import VectorData
+from ccpi.framework import VectorGeometry
+from ccpi.framework import AcquisitionGeometry
+from numbers import Number
+from ccpi.optimisation.operators import LinearOperator
+class LinearOperatorMatrix(LinearOperator):
+ def __init__(self,A):
+ self.A = A
+ M_A, N_A = self.A.shape
+ self.gm_domain = VectorGeometry(N_A)
+ self.gm_range = VectorGeometry(M_A)
+ self.s1 = None # Largest singular value, initially unknown
+ super(LinearOperatorMatrix, self).__init__()
+
+ def direct(self,x, out=None):
+ if out is None:
+ return type(x)(numpy.dot(self.A,x.as_array()))
+ else:
+ numpy.dot(self.A, x.as_array(), out=out.as_array())
+
+ def adjoint(self,x, out=None):
+ if out is None:
+ return type(x)(numpy.dot(self.A.transpose(),x.as_array()))
+ else:
+ numpy.dot(self.A.transpose(),x.as_array(), out=out.as_array())
+
+ def size(self):
+ return self.A.shape
+
+ 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
new file mode 100755
index 0000000..c1adf62
--- /dev/null
+++ b/Wrappers/Python/ccpi/optimisation/operators/Operator.py
@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*-
+"""
+Created on Tue Mar 5 15:55:56 2019
+
+@author: ofn77899
+"""
+from ccpi.optimisation.operators.ScaledOperator import ScaledOperator
+
+class Operator(object):
+ '''Operator that maps from a space X -> Y'''
+ def __init__(self, **kwargs):
+ self.__norm = None
+
+ def is_linear(self):
+ '''Returns if the operator is linear'''
+ return False
+ def direct(self,x, out=None):
+ '''Returns the application of the Operator on x'''
+ raise NotImplementedError
+ def norm(self, **kwargs):
+ '''Returns the norm of the Operator'''
+ if self.__norm is None:
+ 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
+ def domain_geometry(self):
+ '''Returns the domain of the Operator: X space'''
+ raise NotImplementedError
+ def __rmul__(self, scalar):
+ '''Defines the multiplication by a scalar on the left
+
+ returns a ScaledOperator'''
+ return ScaledOperator(self, scalar)
diff --git a/Wrappers/Python/ccpi/optimisation/operators/ScaledOperator.py b/Wrappers/Python/ccpi/optimisation/operators/ScaledOperator.py
new file mode 100644
index 0000000..f7be5de
--- /dev/null
+++ b/Wrappers/Python/ccpi/optimisation/operators/ScaledOperator.py
@@ -0,0 +1,51 @@
+from numbers import Number
+import numpy
+
+class ScaledOperator(object):
+ '''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.
+ Args:
+ operator (Operator): a Operator or LinearOperator
+ scalar (Number): a scalar multiplier
+ Example:
+ The scaled operator behaves like the following:
+ 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):
+ super(ScaledOperator, self).__init__()
+ 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):
+ 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):
+ 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):
+ return numpy.abs(self.scalar) * self.operator.norm(**kwargs)
+ def range_geometry(self):
+ return self.operator.range_geometry()
+ def domain_geometry(self):
+ return self.operator.domain_geometry()
+ def is_linear(self):
+ return self.operator.is_linear()
+
diff --git a/Wrappers/Python/ccpi/optimisation/operators/ShrinkageOperator.py b/Wrappers/Python/ccpi/optimisation/operators/ShrinkageOperator.py
new file mode 100644
index 0000000..f47c655
--- /dev/null
+++ b/Wrappers/Python/ccpi/optimisation/operators/ShrinkageOperator.py
@@ -0,0 +1,19 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Created on Wed Mar 6 19:30:51 2019
+
+@author: evangelos
+"""
+
+from ccpi.framework import DataContainer
+
+class ShrinkageOperator():
+
+ def __init__(self):
+ pass
+
+ def __call__(self, x, tau, out=None):
+
+ return x.sign() * (x.abs() - tau).maximum(0)
+ \ No newline at end of file
diff --git a/Wrappers/Python/ccpi/optimisation/operators/SparseFiniteDiff.py b/Wrappers/Python/ccpi/optimisation/operators/SparseFiniteDiff.py
new file mode 100644
index 0000000..cb76dce
--- /dev/null
+++ b/Wrappers/Python/ccpi/optimisation/operators/SparseFiniteDiff.py
@@ -0,0 +1,144 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Created on Tue Apr 2 14:06:15 2019
+
+@author: vaggelis
+"""
+
+import scipy.sparse as sp
+import numpy as np
+from ccpi.framework import ImageData
+
+class SparseFiniteDiff(object):
+
+ def __init__(self, gm_domain, gm_range=None, direction=0, bnd_cond = 'Neumann'):
+
+ super(SparseFiniteDiff, self).__init__()
+ self.gm_domain = gm_domain
+ self.gm_range = gm_range
+ self.direction = direction
+ self.bnd_cond = bnd_cond
+
+ if self.gm_range is None:
+ self.gm_range = self.gm_domain
+
+ self.get_dims = [i for i in gm_domain.shape]
+
+ if self.direction + 1 > len(self.gm_domain.shape):
+ raise ValueError('Gradient directions more than geometry domain')
+
+ def matrix(self):
+
+ i = self.direction
+
+ mat = sp.spdiags(np.vstack([-np.ones((1,self.get_dims[i])),np.ones((1,self.get_dims[i]))]), [0,1], self.get_dims[i], self.get_dims[i], format = 'lil')
+
+ if self.bnd_cond == 'Neumann':
+ mat[-1,:] = 0
+ elif self.bnd_cond == 'Periodic':
+ mat[-1,0] = 1
+
+ tmpGrad = mat if i == 0 else sp.eye(self.get_dims[0])
+
+ for j in range(1, self.gm_domain.length):
+
+ tmpGrad = sp.kron(mat, tmpGrad ) if j == i else sp.kron(sp.eye(self.get_dims[j]), tmpGrad )
+
+ return tmpGrad
+
+ def T(self):
+ return self.matrix().T
+
+ def direct(self, x):
+
+ x_asarr = x.as_array()
+ res = np.reshape( self.matrix() * x_asarr.flatten('F'), self.gm_domain.shape, 'F')
+ return type(x)(res)
+
+ def adjoint(self, x):
+
+ x_asarr = x.as_array()
+ res = np.reshape( self.matrix().T * x_asarr.flatten('F'), self.gm_domain.shape, 'F')
+ return type(x)(res)
+
+ def sum_abs_row(self):
+
+ res = np.array(np.reshape(abs(self.matrix()).sum(axis=0), self.gm_domain.shape, 'F'))
+ #res[res==0]=0
+ return ImageData(res)
+
+ def sum_abs_col(self):
+
+ res = np.array(np.reshape(abs(self.matrix()).sum(axis=1), self.gm_domain.shape, 'F') )
+ #res[res==0]=0
+ return ImageData(res)
+
+if __name__ == '__main__':
+
+ from ccpi.framework import ImageGeometry
+ from ccpi.optimisation.operators import FiniteDiff
+
+ # 2D
+ M, N= 2, 3
+ ig = ImageGeometry(M, N)
+ arr = ig.allocate('random_int')
+
+ for i in [0,1]:
+
+ # Neumann
+ sFD_neum = SparseFiniteDiff(ig, direction=i, bnd_cond='Neumann')
+ G_neum = FiniteDiff(ig, direction=i, bnd_cond='Neumann')
+
+ # Periodic
+ sFD_per = SparseFiniteDiff(ig, direction=i, bnd_cond='Periodic')
+ G_per = FiniteDiff(ig, direction=i, bnd_cond='Periodic')
+
+ u_neum_direct = G_neum.direct(arr)
+ u_neum_sp_direct = sFD_neum.direct(arr)
+ np.testing.assert_array_almost_equal(u_neum_direct.as_array(), u_neum_sp_direct.as_array(), decimal=4)
+
+ u_neum_adjoint = G_neum.adjoint(arr)
+ u_neum_sp_adjoint = sFD_neum.adjoint(arr)
+ np.testing.assert_array_almost_equal(u_neum_adjoint.as_array(), u_neum_sp_adjoint.as_array(), decimal=4)
+
+ u_per_direct = G_neum.direct(arr)
+ u_per_sp_direct = sFD_neum.direct(arr)
+ np.testing.assert_array_almost_equal(u_per_direct.as_array(), u_per_sp_direct.as_array(), decimal=4)
+
+ u_per_adjoint = G_per.adjoint(arr)
+ u_per_sp_adjoint = sFD_per.adjoint(arr)
+ np.testing.assert_array_almost_equal(u_per_adjoint.as_array(), u_per_sp_adjoint.as_array(), decimal=4)
+
+ # 3D
+ M, N, K = 2, 3, 4
+ ig3D = ImageGeometry(M, N, K)
+ arr3D = ig3D.allocate('random_int')
+
+ for i in [0,1,2]:
+
+ # Neumann
+ sFD_neum3D = SparseFiniteDiff(ig3D, direction=i, bnd_cond='Neumann')
+ G_neum3D = FiniteDiff(ig3D, direction=i, bnd_cond='Neumann')
+
+ # Periodic
+ sFD_per3D = SparseFiniteDiff(ig3D, direction=i, bnd_cond='Periodic')
+ G_per3D = FiniteDiff(ig3D, direction=i, bnd_cond='Periodic')
+
+ u_neum_direct3D = G_neum3D.direct(arr3D)
+ u_neum_sp_direct3D = sFD_neum3D.direct(arr3D)
+ np.testing.assert_array_almost_equal(u_neum_direct3D.as_array(), u_neum_sp_direct3D.as_array(), decimal=4)
+
+ u_neum_adjoint3D = G_neum3D.adjoint(arr3D)
+ u_neum_sp_adjoint3D = sFD_neum3D.adjoint(arr3D)
+ np.testing.assert_array_almost_equal(u_neum_adjoint3D.as_array(), u_neum_sp_adjoint3D.as_array(), decimal=4)
+
+ u_per_direct3D = G_neum3D.direct(arr3D)
+ u_per_sp_direct3D = sFD_neum3D.direct(arr3D)
+ np.testing.assert_array_almost_equal(u_per_direct3D.as_array(), u_per_sp_direct3D.as_array(), decimal=4)
+
+ u_per_adjoint3D = G_per3D.adjoint(arr3D)
+ u_per_sp_adjoint3D = sFD_per3D.adjoint(arr3D)
+ np.testing.assert_array_almost_equal(u_per_adjoint3D.as_array(), u_per_sp_adjoint3D.as_array(), decimal=4)
+
+ \ No newline at end of file
diff --git a/Wrappers/Python/ccpi/optimisation/operators/SymmetrizedGradientOperator.py b/Wrappers/Python/ccpi/optimisation/operators/SymmetrizedGradientOperator.py
new file mode 100644
index 0000000..205f7e1
--- /dev/null
+++ b/Wrappers/Python/ccpi/optimisation/operators/SymmetrizedGradientOperator.py
@@ -0,0 +1,233 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Created on Fri Mar 1 22:53:55 2019
+
+@author: evangelos
+"""
+
+from ccpi.optimisation.operators import Gradient, Operator, LinearOperator, ScaledOperator
+from ccpi.framework import ImageData, ImageGeometry, BlockGeometry, BlockDataContainer
+import numpy
+from ccpi.optimisation.operators import FiniteDiff, SparseFiniteDiff
+
+
+class SymmetrizedGradient(Gradient):
+
+ ''' Symmetrized Gradient, denoted by E: V --> W
+ where V is the Range of the Gradient Operator
+ and W is the Range of the Symmetrized Gradient.
+ '''
+
+
+ def __init__(self, gm_domain, bnd_cond = 'Neumann', **kwargs):
+
+ super(SymmetrizedGradient, self).__init__(gm_domain, bnd_cond, **kwargs)
+
+ '''
+ Domain of SymGrad is the Range of Gradient
+ '''
+
+ self.gm_domain = self.gm_range
+ self.bnd_cond = bnd_cond
+
+ self.channels = self.gm_range.get_item(0).channels
+
+ tmp_gm = len(self.gm_domain.geometries)*self.gm_domain.geometries
+
+ self.gm_range = BlockGeometry(*tmp_gm)
+
+ self.FD = FiniteDiff(self.gm_domain, direction = 0, bnd_cond = self.bnd_cond)
+
+ if self.gm_domain.shape[0]==2:
+ self.order_ind = [0,2,1,3]
+ else:
+ self.order_ind = [0,3,6,1,4,7,2,5,8]
+
+
+ def direct(self, x, out=None):
+
+ if out is None:
+
+ tmp = []
+ for i in range(self.gm_domain.shape[0]):
+ for j in range(x.shape[0]):
+ self.FD.direction = i
+ tmp.append(self.FD.adjoint(x.get_item(j)))
+
+ tmp1 = [tmp[i] for i in self.order_ind]
+
+ res = [0.5 * sum(x) for x in zip(tmp, tmp1)]
+
+ return BlockDataContainer(*res)
+
+ else:
+
+ ind = 0
+ for i in range(self.gm_domain.shape[0]):
+ for j in range(x.shape[0]):
+ self.FD.direction = i
+ self.FD.adjoint(x.get_item(j), out=out[ind])
+ ind+=1
+ out1 = BlockDataContainer(*[out[i] for i in self.order_ind])
+ out.fill( 0.5 * (out + out1) )
+
+
+ def adjoint(self, x, out=None):
+
+ if out is None:
+
+ tmp = [None]*self.gm_domain.shape[0]
+ i = 0
+
+ for k in range(self.gm_domain.shape[0]):
+ tmp1 = 0
+ for j in range(self.gm_domain.shape[0]):
+ self.FD.direction = j
+ tmp1 += self.FD.direct(x[i])
+ i+=1
+ tmp[k] = tmp1
+ return BlockDataContainer(*tmp)
+
+
+ else:
+
+ tmp = self.gm_domain.allocate()
+ i = 0
+ for k in range(self.gm_domain.shape[0]):
+ tmp1 = 0
+ for j in range(self.gm_domain.shape[0]):
+ self.FD.direction = j
+ self.FD.direct(x[i], out=tmp[j])
+ i+=1
+ tmp1+=tmp[j]
+ out[k].fill(tmp1)
+# tmp = self.adjoint(x)
+# out.fill(tmp)
+
+
+ def domain_geometry(self):
+ return self.gm_domain
+
+ def range_geometry(self):
+ return self.gm_range
+
+if __name__ == '__main__':
+
+ ###########################################################################
+ ## Symmetrized Gradient Tests
+ from ccpi.framework import DataContainer
+ from ccpi.optimisation.operators import Gradient, BlockOperator, FiniteDiff
+ import numpy as np
+
+ N, M = 2, 3
+ K = 2
+ C = 2
+
+ ig1 = ImageGeometry(N, M)
+ ig2 = ImageGeometry(N, M, channels=C)
+
+ E1 = SymmetrizedGradient(ig1, correlation = 'Space', bnd_cond='Neumann')
+
+ try:
+ E1 = SymmetrizedGradient(ig1, correlation = 'SpaceChannels', bnd_cond='Neumann')
+ except:
+ print("No Channels to correlate")
+
+ E2 = SymmetrizedGradient(ig2, correlation = 'SpaceChannels', bnd_cond='Neumann')
+
+ print(E1.domain_geometry().shape, E1.range_geometry().shape)
+ print(E2.domain_geometry().shape, E2.range_geometry().shape)
+
+ #check Linear operator property
+
+ u1 = E1.domain_geometry().allocate('random_int')
+ u2 = E2.domain_geometry().allocate('random_int')
+
+ # Need to allocate random_int at the Range of SymGradient
+
+ #a1 = ig1.allocate('random_int')
+ #a2 = ig1.allocate('random_int')
+ #a3 = ig1.allocate('random_int')
+
+ #a4 = ig1.allocate('random_int')
+ #a5 = ig1.allocate('random_int')
+ #a6 = ig1.allocate('random_int')
+
+ # TODO allocate has to create this symmetry by default!!!!!
+ #w1 = BlockDataContainer(*[a1, a2, \
+ # a2, a3])
+ w1 = E1.range_geometry().allocate('random_int',symmetry=True)
+
+ LHS = (E1.direct(u1) * w1).sum()
+ RHS = (u1 * E1.adjoint(w1)).sum()
+
+ numpy.testing.assert_equal(LHS, RHS)
+
+ u2 = E2.gm_domain.allocate('random_int')
+
+ #aa1 = ig2.allocate('random_int')
+ #aa2 = ig2.allocate('random_int')
+ #aa3 = ig2.allocate('random_int')
+ #aa4 = ig2.allocate('random_int')
+ #aa5 = ig2.allocate('random_int')
+ #aa6 = ig2.allocate('random_int')
+
+ #w2 = BlockDataContainer(*[aa1, aa2, aa3, \
+ # aa2, aa4, aa5, \
+ # aa3, aa5, aa6])
+ w2 = E2.range_geometry().allocate('random_int',symmetry=True)
+
+
+ LHS1 = (E2.direct(u2) * w2).sum()
+ RHS1 = (u2 * E2.adjoint(w2)).sum()
+
+ numpy.testing.assert_equal(LHS1, RHS1)
+
+ out = E1.range_geometry().allocate()
+ E1.direct(u1, out=out)
+ a1 = E1.direct(u1)
+ numpy.testing.assert_array_equal(a1[0].as_array(), out[0].as_array())
+ numpy.testing.assert_array_equal(a1[1].as_array(), out[1].as_array())
+ numpy.testing.assert_array_equal(a1[2].as_array(), out[2].as_array())
+ numpy.testing.assert_array_equal(a1[3].as_array(), out[3].as_array())
+
+
+ out1 = E1.domain_geometry().allocate()
+ E1.adjoint(w1, out=out1)
+ b1 = E1.adjoint(w1)
+
+ LHS_out = (out * w1).sum()
+ RHS_out = (u1 * out1).sum()
+ print(LHS_out, RHS_out)
+
+
+ out2 = E2.range_geometry().allocate()
+ E2.direct(u2, out=out2)
+ a2 = E2.direct(u2)
+
+ out21 = E2.domain_geometry().allocate()
+ E2.adjoint(w2, out=out21)
+ b2 = E2.adjoint(w2)
+
+ LHS_out = (out2 * w2).sum()
+ RHS_out = (u2 * out21).sum()
+ print(LHS_out, RHS_out)
+
+
+ out = E1.range_geometry().allocate()
+ E1.direct(u1, out=out)
+ E1.adjoint(out, out=out1)
+
+ print(E1.norm())
+ print(E2.norm())
+
+
+
+
+
+
+#
+#
+#
+#
diff --git a/Wrappers/Python/ccpi/optimisation/operators/ZeroOperator.py b/Wrappers/Python/ccpi/optimisation/operators/ZeroOperator.py
new file mode 100644
index 0000000..67808de
--- /dev/null
+++ b/Wrappers/Python/ccpi/optimisation/operators/ZeroOperator.py
@@ -0,0 +1,44 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Created on Wed Mar 6 19:25:53 2019
+
+@author: evangelos
+"""
+
+import numpy as np
+from ccpi.framework import ImageData
+from ccpi.optimisation.operators import LinearOperator
+
+class ZeroOperator(LinearOperator):
+
+ def __init__(self, gm_domain, gm_range=None):
+
+ super(ZeroOperator, self).__init__()
+
+ 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):
+ if out is None:
+ return self.gm_range.allocate()
+ else:
+ out.fill(self.gm_range.allocate())
+
+ def adjoint(self,x, out=None):
+ if out is None:
+ return self.gm_domain.allocate()
+ else:
+ out.fill(self.gm_domain.allocate())
+
+ def calculate_norm(self, **kwargs):
+ return 0.
+
+ def domain_geometry(self):
+ return self.gm_domain
+
+ def range_geometry(self):
+ 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
new file mode 100755
index 0000000..23222d4
--- /dev/null
+++ b/Wrappers/Python/ccpi/optimisation/operators/__init__.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+"""
+Created on Tue Mar 5 15:56:27 2019
+
+@author: ofn77899
+"""
+
+from .Operator import Operator
+from .LinearOperator import LinearOperator
+from .ScaledOperator import ScaledOperator
+from .BlockOperator import BlockOperator
+from .BlockScaledOperator import BlockScaledOperator
+
+from .SparseFiniteDiff import SparseFiniteDiff
+from .ShrinkageOperator import ShrinkageOperator
+
+from .FiniteDifferenceOperator import FiniteDiff
+from .GradientOperator import Gradient
+from .SymmetrizedGradientOperator import SymmetrizedGradient
+from .IdentityOperator import Identity
+from .ZeroOperator import ZeroOperator
+from .LinearOperatorMatrix import LinearOperatorMatrix
+
diff --git a/Wrappers/Python/ccpi/optimisation/ops.py b/Wrappers/Python/ccpi/optimisation/ops.py
deleted file mode 100755
index e9e7f44..0000000
--- a/Wrappers/Python/ccpi/optimisation/ops.py
+++ /dev/null
@@ -1,289 +0,0 @@
-# -*- coding: utf-8 -*-
-# This work is part of the Core Imaging Library developed by
-# Visual Analytics and Imaging System Group of the Science Technology
-# Facilities Council, STFC
-
-# Copyright 2018 Jakob Jorgensen, Daniil Kazantsev and Edoardo Pasca
-
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-
-# http://www.apache.org/licenses/LICENSE-2.0
-
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-import numpy
-from scipy.sparse.linalg import svds
-from ccpi.framework import DataContainer
-from ccpi.framework import AcquisitionData
-from ccpi.framework import ImageData
-from ccpi.framework import ImageGeometry
-from ccpi.framework import AcquisitionGeometry
-from numbers import Number
-# Maybe operators need to know what types they take as inputs/outputs
-# to not just use generic DataContainer
-
-
-class Operator(object):
- '''Operator that maps from a space X -> Y'''
- def __init__(self, **kwargs):
- self.scalar = 1
- def is_linear(self):
- '''Returns if the operator is linear'''
- return False
- def direct(self,x, out=None):
- raise NotImplementedError
- def size(self):
- # To be defined for specific class
- raise NotImplementedError
- def norm(self):
- raise NotImplementedError
- def allocate_direct(self):
- '''Allocates memory on the Y space'''
- raise NotImplementedError
- def allocate_adjoint(self):
- '''Allocates memory on the X space'''
- raise NotImplementedError
- def range_dim(self):
- raise NotImplementedError
- def domain_dim(self):
- raise NotImplementedError
- def __rmul__(self, other):
- '''reverse multiplication of Operator with number sets the variable scalar in the Operator'''
- assert isinstance(other, Number)
- self.scalar = other
- return self
-
-class LinearOperator(Operator):
- '''Operator that maps from a space X -> Y'''
- def is_linear(self):
- '''Returns if the operator is linear'''
- return True
- def adjoint(self,x, out=None):
- raise NotImplementedError
-
-class Identity(Operator):
- def __init__(self):
- self.s1 = 1.0
- self.L = 1
- super(Identity, self).__init__()
-
- def direct(self,x,out=None):
- if out is None:
- return x.copy()
- else:
- out.fill(x)
-
- def adjoint(self,x, out=None):
- if out is None:
- return x.copy()
- else:
- out.fill(x)
-
- def size(self):
- return NotImplemented
-
- def get_max_sing_val(self):
- return self.s1
-
-class TomoIdentity(Operator):
- def __init__(self, geometry, **kwargs):
- super(TomoIdentity, self).__init__()
- self.s1 = 1.0
- self.geometry = geometry
-
-
- def direct(self,x,out=None):
-
- if out is None:
- if self.scalar != 1:
- return x * self.scalar
- return x.copy()
- else:
- if self.scalar != 1:
- out.fill(x * self.scalar)
- return
- out.fill(x)
- return
-
- def adjoint(self,x, out=None):
- return self.direct(x, out)
-
- def size(self):
- return NotImplemented
-
- def get_max_sing_val(self):
- return self.s1
- def allocate_direct(self):
- if issubclass(type(self.geometry), ImageGeometry):
- return ImageData(geometry=self.geometry)
- elif issubclass(type(self.geometry), AcquisitionGeometry):
- return AcquisitionData(geometry=self.geometry)
- else:
- raise ValueError("Wrong geometry type: expected ImageGeometry of AcquisitionGeometry, got ", type(self.geometry))
- def allocate_adjoint(self):
- return self.allocate_direct()
-
-
-
-class FiniteDiff2D(Operator):
- def __init__(self):
- self.s1 = 8.0
- super(FiniteDiff2D, self).__init__()
-
- def direct(self,x, out=None):
- '''Forward differences with Neumann BC.'''
- # FIXME this seems to be working only with numpy arrays
-
- d1 = numpy.zeros_like(x.as_array())
- d1[:,:-1] = x.as_array()[:,1:] - x.as_array()[:,:-1]
- d2 = numpy.zeros_like(x.as_array())
- d2[:-1,:] = x.as_array()[1:,:] - x.as_array()[:-1,:]
- d = numpy.stack((d1,d2),0)
- #x.geometry.voxel_num_z = 2
- return type(x)(d,False,geometry=x.geometry)
-
- def adjoint(self,x, out=None):
- '''Backward differences, Neumann BC.'''
- Nrows = x.get_dimension_size('horizontal_x')
- Ncols = x.get_dimension_size('horizontal_y')
- Nchannels = 1
- if len(x.shape) == 4:
- Nchannels = x.get_dimension_size('channel')
- zer = numpy.zeros((Nrows,1))
- xxx = x.as_array()[0,:,:-1]
- #
- h = numpy.concatenate((zer,xxx), 1)
- h -= numpy.concatenate((xxx,zer), 1)
-
- zer = numpy.zeros((1,Ncols))
- xxx = x.as_array()[1,:-1,:]
- #
- v = numpy.concatenate((zer,xxx), 0)
- v -= numpy.concatenate((xxx,zer), 0)
- return type(x)(h + v, False, geometry=x.geometry)
-
- def size(self):
- return NotImplemented
-
- def get_max_sing_val(self):
- return self.s1
-
-def PowerMethodNonsquareOld(op,numiters):
- # Initialise random
- # Jakob's
- #inputsize = op.size()[1]
- #x0 = ImageContainer(numpy.random.randn(*inputsize)
- # Edo's
- #vg = ImageGeometry(voxel_num_x=inputsize[0],
- # voxel_num_y=inputsize[1],
- # voxel_num_z=inputsize[2])
- #
- #x0 = ImageData(geometry = vg, dimension_labels=['vertical','horizontal_y','horizontal_x'])
- #print (x0)
- #x0.fill(numpy.random.randn(*x0.shape))
-
- x0 = op.create_image_data()
-
- s = numpy.zeros(numiters)
- # Loop
- for it in numpy.arange(numiters):
- x1 = op.adjoint(op.direct(x0))
- x1norm = numpy.sqrt((x1**2).sum())
- #print ("x0 **********" ,x0)
- #print ("x1 **********" ,x1)
- s[it] = (x1*x0).sum() / (x0*x0).sum()
- x0 = (1.0/x1norm)*x1
- return numpy.sqrt(s[-1]), numpy.sqrt(s), x0
-
-#def PowerMethod(op,numiters):
-# # Initialise random
-# x0 = np.random.randn(400)
-# s = np.zeros(numiters)
-# # Loop
-# for it in np.arange(numiters):
-# x1 = np.dot(op.transpose(),np.dot(op,x0))
-# x1norm = np.sqrt(np.sum(np.dot(x1,x1)))
-# s[it] = np.dot(x1,x0) / np.dot(x1,x0)
-# x0 = (1.0/x1norm)*x1
-# return s, x0
-
-
-def PowerMethodNonsquare(op,numiters , x0=None):
- # Initialise random
- # Jakob's
- # inputsize , outputsize = op.size()
- #x0 = ImageContainer(numpy.random.randn(*inputsize)
- # Edo's
- #vg = ImageGeometry(voxel_num_x=inputsize[0],
- # voxel_num_y=inputsize[1],
- # voxel_num_z=inputsize[2])
- #
- #x0 = ImageData(geometry = vg, dimension_labels=['vertical','horizontal_y','horizontal_x'])
- #print (x0)
- #x0.fill(numpy.random.randn(*x0.shape))
-
- if x0 is None:
- #x0 = op.create_image_data()
- x0 = op.allocate_direct()
- x0.fill(numpy.random.randn(*x0.shape))
-
- s = numpy.zeros(numiters)
- # Loop
- for it in numpy.arange(numiters):
- x1 = op.adjoint(op.direct(x0))
- #x1norm = numpy.sqrt((x1*x1).sum())
- x1norm = x1.norm()
- #print ("x0 **********" ,x0)
- #print ("x1 **********" ,x1)
- s[it] = (x1*x0).sum() / (x0.squared_norm())
- x0 = (1.0/x1norm)*x1
- return numpy.sqrt(s[-1]), numpy.sqrt(s), x0
-
-class LinearOperatorMatrix(Operator):
- def __init__(self,A):
- self.A = A
- self.s1 = None # Largest singular value, initially unknown
- super(LinearOperatorMatrix, self).__init__()
-
- def direct(self,x, out=None):
- if out is None:
- return type(x)(numpy.dot(self.A,x.as_array()))
- else:
- numpy.dot(self.A, x.as_array(), out=out.as_array())
-
-
- def adjoint(self,x, out=None):
- if out is None:
- return type(x)(numpy.dot(self.A.transpose(),x.as_array()))
- else:
- numpy.dot(self.A.transpose(),x.as_array(), out=out.as_array())
-
-
- def size(self):
- return self.A.shape
-
- def get_max_sing_val(self):
- # If unknown, compute and store. If known, simply return it.
- if self.s1 is None:
- self.s1 = svds(self.A,1,return_singular_vectors=False)[0]
- return self.s1
- else:
- return self.s1
- def allocate_direct(self):
- '''allocates the memory to hold the result of adjoint'''
- #numpy.dot(self.A.transpose(),x.as_array())
- M_A, N_A = self.A.shape
- out = numpy.zeros((N_A,1))
- return DataContainer(out)
- def allocate_adjoint(self):
- '''allocate the memory to hold the result of direct'''
- #numpy.dot(self.A.transpose(),x.as_array())
- M_A, N_A = self.A.shape
- out = numpy.zeros((M_A,1))
- return DataContainer(out)
diff --git a/Wrappers/Python/ccpi/processors.py b/Wrappers/Python/ccpi/processors/CenterOfRotationFinder.py
index 3a3671a..936dc05 100755
--- a/Wrappers/Python/ccpi/processors.py
+++ b/Wrappers/Python/ccpi/processors/CenterOfRotationFinder.py
@@ -1,514 +1,408 @@
-# -*- coding: utf-8 -*-
-# This work is part of the Core Imaging Library developed by
-# Visual Analytics and Imaging System Group of the Science Technology
-# Facilities Council, STFC
-
-# Copyright 2018 Edoardo Pasca
-
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-
-# http://www.apache.org/licenses/LICENSE-2.0
-
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License
-
-from ccpi.framework import DataProcessor, DataContainer, AcquisitionData,\
- AcquisitionGeometry, ImageGeometry, ImageData
-from ccpi.reconstruction.parallelbeam import alg as pbalg
-import numpy
-from scipy import ndimage
-
-import matplotlib.pyplot as plt
-
-
-class Normalizer(DataProcessor):
- '''Normalization based on flat and dark
-
- This processor read in a AcquisitionData and normalises it based on
- the instrument reading with and without incident photons or neutrons.
-
- Input: AcquisitionData
- Parameter: 2D projection with flat field (or stack)
- 2D projection with dark field (or stack)
- Output: AcquisitionDataSetn
- '''
-
- def __init__(self, flat_field = None, dark_field = None, tolerance = 1e-5):
- kwargs = {
- 'flat_field' : flat_field,
- 'dark_field' : dark_field,
- # very small number. Used when there is a division by zero
- 'tolerance' : tolerance
- }
-
- #DataProcessor.__init__(self, **kwargs)
- super(Normalizer, self).__init__(**kwargs)
- if not flat_field is None:
- self.set_flat_field(flat_field)
- if not dark_field is None:
- self.set_dark_field(dark_field)
-
- def check_input(self, dataset):
- if dataset.number_of_dimensions == 3 or\
- dataset.number_of_dimensions == 2:
- return True
- else:
- raise ValueError("Expected input dimensions is 2 or 3, got {0}"\
- .format(dataset.number_of_dimensions))
-
- def set_dark_field(self, df):
- if type(df) is numpy.ndarray:
- if len(numpy.shape(df)) == 3:
- raise ValueError('Dark Field should be 2D')
- elif len(numpy.shape(df)) == 2:
- self.dark_field = df
- elif issubclass(type(df), DataContainer):
- self.dark_field = self.set_dark_field(df.as_array())
-
- def set_flat_field(self, df):
- if type(df) is numpy.ndarray:
- if len(numpy.shape(df)) == 3:
- raise ValueError('Flat Field should be 2D')
- elif len(numpy.shape(df)) == 2:
- self.flat_field = df
- elif issubclass(type(df), DataContainer):
- self.flat_field = self.set_flat_field(df.as_array())
-
- @staticmethod
- def normalize_projection(projection, flat, dark, tolerance):
- a = (projection - dark)
- b = (flat-dark)
- with numpy.errstate(divide='ignore', invalid='ignore'):
- c = numpy.true_divide( a, b )
- c[ ~ numpy.isfinite( c )] = tolerance # set to not zero if 0/0
- return c
-
- @staticmethod
- def estimate_normalised_error(projection, flat, dark, delta_flat, delta_dark):
- '''returns the estimated relative error of the normalised projection
-
- n = (projection - dark) / (flat - dark)
- Dn/n = (flat-dark + projection-dark)/((flat-dark)*(projection-dark))*(Df/f + Dd/d)
- '''
- a = (projection - dark)
- b = (flat-dark)
- df = delta_flat / flat
- dd = delta_dark / dark
- rel_norm_error = (b + a) / (b * a) * (df + dd)
- return rel_norm_error
-
- def process(self, out=None):
-
- projections = self.get_input()
- dark = self.dark_field
- flat = self.flat_field
-
- if projections.number_of_dimensions == 3:
- if not (projections.shape[1:] == dark.shape and \
- projections.shape[1:] == flat.shape):
- raise ValueError('Flats/Dark and projections size do not match.')
-
-
- a = numpy.asarray(
- [ Normalizer.normalize_projection(
- projection, flat, dark, self.tolerance) \
- for projection in projections.as_array() ]
- )
- elif projections.number_of_dimensions == 2:
- a = Normalizer.normalize_projection(projections.as_array(),
- flat, dark, self.tolerance)
- y = type(projections)( a , True,
- dimension_labels=projections.dimension_labels,
- geometry=projections.geometry)
- return y
-
-
-class CenterOfRotationFinder(DataProcessor):
- '''Processor to find the center of rotation in a parallel beam experiment
-
- This processor read in a AcquisitionDataSet and finds the center of rotation
- based on Nghia Vo's method. https://doi.org/10.1364/OE.22.019078
-
- Input: AcquisitionDataSet
-
- Output: float. center of rotation in pixel coordinate
- '''
-
- def __init__(self):
- kwargs = {
-
- }
-
- #DataProcessor.__init__(self, **kwargs)
- super(CenterOfRotationFinder, self).__init__(**kwargs)
-
- def check_input(self, dataset):
- if dataset.number_of_dimensions == 3:
- if dataset.geometry.geom_type == 'parallel':
- return True
- else:
- raise ValueError('{0} is suitable only for parallel beam geometry'\
- .format(self.__class__.__name__))
- else:
- raise ValueError("Expected input dimensions is 3, got {0}"\
- .format(dataset.number_of_dimensions))
-
-
- # #########################################################################
- # Copyright (c) 2015, UChicago Argonne, LLC. All rights reserved. #
- # #
- # Copyright 2015. UChicago Argonne, LLC. This software was produced #
- # under U.S. Government contract DE-AC02-06CH11357 for Argonne National #
- # Laboratory (ANL), which is operated by UChicago Argonne, LLC for the #
- # U.S. Department of Energy. The U.S. Government has rights to use, #
- # reproduce, and distribute this software. NEITHER THE GOVERNMENT NOR #
- # UChicago Argonne, LLC MAKES ANY WARRANTY, EXPRESS OR IMPLIED, OR #
- # ASSUMES ANY LIABILITY FOR THE USE OF THIS SOFTWARE. If software is #
- # modified to produce derivative works, such modified software should #
- # be clearly marked, so as not to confuse it with the version available #
- # from ANL. #
- # #
- # Additionally, redistribution and use in source and binary forms, with #
- # or without modification, are permitted provided that the following #
- # conditions are met: #
- # #
- # * Redistributions of source code must retain the above copyright #
- # notice, this list of conditions and the following disclaimer. #
- # #
- # * Redistributions in binary form must reproduce the above copyright #
- # notice, this list of conditions and the following disclaimer in #
- # the documentation and/or other materials provided with the #
- # distribution. #
- # #
- # * Neither the name of UChicago Argonne, LLC, Argonne National #
- # Laboratory, ANL, the U.S. Government, nor the names of its #
- # contributors may be used to endorse or promote products derived #
- # from this software without specific prior written permission. #
- # #
- # THIS SOFTWARE IS PROVIDED BY UChicago Argonne, LLC AND CONTRIBUTORS #
- # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT #
- # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS #
- # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL UChicago #
- # Argonne, LLC OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, #
- # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, #
- # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; #
- # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER #
- # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT #
- # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN #
- # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE #
- # POSSIBILITY OF SUCH DAMAGE. #
- # #########################################################################
-
- @staticmethod
- def as_ndarray(arr, dtype=None, copy=False):
- if not isinstance(arr, numpy.ndarray):
- arr = numpy.array(arr, dtype=dtype, copy=copy)
- return arr
-
- @staticmethod
- def as_dtype(arr, dtype, copy=False):
- if not arr.dtype == dtype:
- arr = numpy.array(arr, dtype=dtype, copy=copy)
- return arr
-
- @staticmethod
- def as_float32(arr):
- arr = CenterOfRotationFinder.as_ndarray(arr, numpy.float32)
- return CenterOfRotationFinder.as_dtype(arr, numpy.float32)
-
-
-
-
- @staticmethod
- def find_center_vo(tomo, ind=None, smin=-40, smax=40, srad=10, step=0.5,
- ratio=2., drop=20):
- """
- Find rotation axis location using Nghia Vo's method. :cite:`Vo:14`.
-
- Parameters
- ----------
- tomo : ndarray
- 3D tomographic data.
- ind : int, optional
- Index of the slice to be used for reconstruction.
- smin, smax : int, optional
- Reference to the horizontal center of the sinogram.
- srad : float, optional
- Fine search radius.
- step : float, optional
- Step of fine searching.
- ratio : float, optional
- The ratio between the FOV of the camera and the size of object.
- It's used to generate the mask.
- drop : int, optional
- Drop lines around vertical center of the mask.
-
- Returns
- -------
- float
- Rotation axis location.
-
- Notes
- -----
- The function may not yield a correct estimate, if:
-
- - the sample size is bigger than the field of view of the camera.
- In this case the ``ratio`` argument need to be set larger
- than the default of 2.0.
-
- - there is distortion in the imaging hardware. If there's
- no correction applied, the center of the projection image may
- yield a better estimate.
-
- - the sample contrast is weak. Paganin's filter need to be applied
- to overcome this.
-
- - the sample was changed during the scan.
- """
- tomo = CenterOfRotationFinder.as_float32(tomo)
-
- if ind is None:
- ind = tomo.shape[1] // 2
- _tomo = tomo[:, ind, :]
-
-
-
- # Reduce noise by smooth filters. Use different filters for coarse and fine search
- _tomo_cs = ndimage.filters.gaussian_filter(_tomo, (3, 1))
- _tomo_fs = ndimage.filters.median_filter(_tomo, (2, 2))
-
- # Coarse and fine searches for finding the rotation center.
- if _tomo.shape[0] * _tomo.shape[1] > 4e6: # If data is large (>2kx2k)
- #_tomo_coarse = downsample(numpy.expand_dims(_tomo_cs,1), level=2)[:, 0, :]
- #init_cen = _search_coarse(_tomo_coarse, smin, smax, ratio, drop)
- #fine_cen = _search_fine(_tomo_fs, srad, step, init_cen*4, ratio, drop)
- init_cen = CenterOfRotationFinder._search_coarse(_tomo_cs, smin,
- smax, ratio, drop)
- fine_cen = CenterOfRotationFinder._search_fine(_tomo_fs, srad,
- step, init_cen,
- ratio, drop)
- else:
- init_cen = CenterOfRotationFinder._search_coarse(_tomo_cs,
- smin, smax,
- ratio, drop)
- fine_cen = CenterOfRotationFinder._search_fine(_tomo_fs, srad,
- step, init_cen,
- ratio, drop)
-
- #logger.debug('Rotation center search finished: %i', fine_cen)
- return fine_cen
-
-
- @staticmethod
- def _search_coarse(sino, smin, smax, ratio, drop):
- """
- Coarse search for finding the rotation center.
- """
- (Nrow, Ncol) = sino.shape
- centerfliplr = (Ncol - 1.0) / 2.0
-
- # Copy the sinogram and flip left right, the purpose is to
- # make a full [0;2Pi] sinogram
- _copy_sino = numpy.fliplr(sino[1:])
-
- # This image is used for compensating the shift of sinogram 2
- temp_img = numpy.zeros((Nrow - 1, Ncol), dtype='float32')
- temp_img[:] = sino[-1]
-
- # Start coarse search in which the shift step is 1
- listshift = numpy.arange(smin, smax + 1)
- listmetric = numpy.zeros(len(listshift), dtype='float32')
- mask = CenterOfRotationFinder._create_mask(2 * Nrow - 1, Ncol,
- 0.5 * ratio * Ncol, drop)
- for i in listshift:
- _sino = numpy.roll(_copy_sino, i, axis=1)
- if i >= 0:
- _sino[:, 0:i] = temp_img[:, 0:i]
- else:
- _sino[:, i:] = temp_img[:, i:]
- listmetric[i - smin] = numpy.sum(numpy.abs(numpy.fft.fftshift(
- #pyfftw.interfaces.numpy_fft.fft2(
- # numpy.vstack((sino, _sino)))
- numpy.fft.fft2(numpy.vstack((sino, _sino)))
- )) * mask)
- minpos = numpy.argmin(listmetric)
- return centerfliplr + listshift[minpos] / 2.0
-
- @staticmethod
- def _search_fine(sino, srad, step, init_cen, ratio, drop):
- """
- Fine search for finding the rotation center.
- """
- Nrow, Ncol = sino.shape
- centerfliplr = (Ncol + 1.0) / 2.0 - 1.0
- # Use to shift the sinogram 2 to the raw CoR.
- shiftsino = numpy.int16(2 * (init_cen - centerfliplr))
- _copy_sino = numpy.roll(numpy.fliplr(sino[1:]), shiftsino, axis=1)
- if init_cen <= centerfliplr:
- lefttake = numpy.int16(numpy.ceil(srad + 1))
- righttake = numpy.int16(numpy.floor(2 * init_cen - srad - 1))
- else:
- lefttake = numpy.int16(numpy.ceil(
- init_cen - (Ncol - 1 - init_cen) + srad + 1))
- righttake = numpy.int16(numpy.floor(Ncol - 1 - srad - 1))
- Ncol1 = righttake - lefttake + 1
- mask = CenterOfRotationFinder._create_mask(2 * Nrow - 1, Ncol1,
- 0.5 * ratio * Ncol, drop)
- numshift = numpy.int16((2 * srad) / step) + 1
- listshift = numpy.linspace(-srad, srad, num=numshift)
- listmetric = numpy.zeros(len(listshift), dtype='float32')
- factor1 = numpy.mean(sino[-1, lefttake:righttake])
- num1 = 0
- for i in listshift:
- _sino = ndimage.interpolation.shift(
- _copy_sino, (0, i), prefilter=False)
- factor2 = numpy.mean(_sino[0,lefttake:righttake])
- _sino = _sino * factor1 / factor2
- sinojoin = numpy.vstack((sino, _sino))
- listmetric[num1] = numpy.sum(numpy.abs(numpy.fft.fftshift(
- #pyfftw.interfaces.numpy_fft.fft2(
- # sinojoin[:, lefttake:righttake + 1])
- numpy.fft.fft2(sinojoin[:, lefttake:righttake + 1])
- )) * mask)
- num1 = num1 + 1
- minpos = numpy.argmin(listmetric)
- return init_cen + listshift[minpos] / 2.0
-
- @staticmethod
- def _create_mask(nrow, ncol, radius, drop):
- du = 1.0 / ncol
- dv = (nrow - 1.0) / (nrow * 2.0 * numpy.pi)
- centerrow = numpy.ceil(nrow / 2) - 1
- centercol = numpy.ceil(ncol / 2) - 1
- # added by Edoardo Pasca
- centerrow = int(centerrow)
- centercol = int(centercol)
- mask = numpy.zeros((nrow, ncol), dtype='float32')
- for i in range(nrow):
- num1 = numpy.round(((i - centerrow) * dv / radius) / du)
- (p1, p2) = numpy.int16(numpy.clip(numpy.sort(
- (-num1 + centercol, num1 + centercol)), 0, ncol - 1))
- mask[i, p1:p2 + 1] = numpy.ones(p2 - p1 + 1, dtype='float32')
- if drop < centerrow:
- mask[centerrow - drop:centerrow + drop + 1,
- :] = numpy.zeros((2 * drop + 1, ncol), dtype='float32')
- mask[:,centercol-1:centercol+2] = numpy.zeros((nrow, 3), dtype='float32')
- return mask
-
- def process(self, out=None):
-
- projections = self.get_input()
-
- cor = CenterOfRotationFinder.find_center_vo(projections.as_array())
-
- return cor
-
-
-class AcquisitionDataPadder(DataProcessor):
- '''Normalization based on flat and dark
-
- This processor read in a AcquisitionData and normalises it based on
- the instrument reading with and without incident photons or neutrons.
-
- Input: AcquisitionData
- Parameter: 2D projection with flat field (or stack)
- 2D projection with dark field (or stack)
- Output: AcquisitionDataSetn
- '''
-
- def __init__(self,
- center_of_rotation = None,
- acquisition_geometry = None,
- pad_value = 1e-5):
- kwargs = {
- 'acquisition_geometry' : acquisition_geometry,
- 'center_of_rotation' : center_of_rotation,
- 'pad_value' : pad_value
- }
-
- super(AcquisitionDataPadder, self).__init__(**kwargs)
-
- def check_input(self, dataset):
- if self.acquisition_geometry is None:
- self.acquisition_geometry = dataset.geometry
- if dataset.number_of_dimensions == 3:
- return True
- else:
- raise ValueError("Expected input dimensions is 2 or 3, got {0}"\
- .format(dataset.number_of_dimensions))
-
- def process(self, out=None):
- projections = self.get_input()
- w = projections.get_dimension_size('horizontal')
- delta = w - 2 * self.center_of_rotation
-
- padded_width = int (
- numpy.ceil(abs(delta)) + w
- )
- delta_pix = padded_width - w
-
- voxel_per_pixel = 1
- geom = pbalg.pb_setup_geometry_from_acquisition(projections.as_array(),
- self.acquisition_geometry.angles,
- self.center_of_rotation,
- voxel_per_pixel )
-
- padded_geometry = self.acquisition_geometry.clone()
-
- padded_geometry.pixel_num_h = geom['n_h']
- padded_geometry.pixel_num_v = geom['n_v']
-
- delta_pix_h = padded_geometry.pixel_num_h - self.acquisition_geometry.pixel_num_h
- delta_pix_v = padded_geometry.pixel_num_v - self.acquisition_geometry.pixel_num_v
-
- if delta_pix_h == 0:
- delta_pix_h = delta_pix
- padded_geometry.pixel_num_h = padded_width
- #initialize a new AcquisitionData with values close to 0
- out = AcquisitionData(geometry=padded_geometry)
- out = out + self.pad_value
-
-
- #pad in the horizontal-vertical plane -> slice on angles
- if delta > 0:
- #pad left of middle
- command = "out.array["
- for i in range(out.number_of_dimensions):
- if out.dimension_labels[i] == 'horizontal':
- value = '{0}:{1}'.format(delta_pix_h, delta_pix_h+w)
- command = command + str(value)
- else:
- if out.dimension_labels[i] == 'vertical' :
- value = '{0}:'.format(delta_pix_v)
- command = command + str(value)
- else:
- command = command + ":"
- if i < out.number_of_dimensions -1:
- command = command + ','
- command = command + '] = projections.array'
- #print (command)
- else:
- #pad right of middle
- command = "out.array["
- for i in range(out.number_of_dimensions):
- if out.dimension_labels[i] == 'horizontal':
- value = '{0}:{1}'.format(0, w)
- command = command + str(value)
- else:
- if out.dimension_labels[i] == 'vertical' :
- value = '{0}:'.format(delta_pix_v)
- command = command + str(value)
- else:
- command = command + ":"
- if i < out.number_of_dimensions -1:
- command = command + ','
- command = command + '] = projections.array'
- #print (command)
- #cleaned = eval(command)
- exec(command)
+# -*- coding: utf-8 -*-
+# This work is part of the Core Imaging Library developed by
+# Visual Analytics and Imaging System Group of the Science Technology
+# Facilities Council, STFC
+
+# Copyright 2018 Edoardo Pasca
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License
+
+from ccpi.framework import DataProcessor, DataContainer, AcquisitionData,\
+ AcquisitionGeometry, ImageGeometry, ImageData
+import numpy
+from scipy import ndimage
+
+class CenterOfRotationFinder(DataProcessor):
+ '''Processor to find the center of rotation in a parallel beam experiment
+
+ This processor read in a AcquisitionDataSet and finds the center of rotation
+ based on Nghia Vo's method. https://doi.org/10.1364/OE.22.019078
+
+ Input: AcquisitionDataSet
+
+ Output: float. center of rotation in pixel coordinate
+ '''
+
+ def __init__(self):
+ kwargs = {
+
+ }
+
+ #DataProcessor.__init__(self, **kwargs)
+ super(CenterOfRotationFinder, self).__init__(**kwargs)
+
+ def check_input(self, dataset):
+ if dataset.number_of_dimensions == 3:
+ if dataset.geometry.geom_type == 'parallel':
+ return True
+ else:
+ raise ValueError('{0} is suitable only for parallel beam geometry'\
+ .format(self.__class__.__name__))
+ else:
+ raise ValueError("Expected input dimensions is 3, got {0}"\
+ .format(dataset.number_of_dimensions))
+
+
+ # #########################################################################
+ # Copyright (c) 2015, UChicago Argonne, LLC. All rights reserved. #
+ # #
+ # Copyright 2015. UChicago Argonne, LLC. This software was produced #
+ # under U.S. Government contract DE-AC02-06CH11357 for Argonne National #
+ # Laboratory (ANL), which is operated by UChicago Argonne, LLC for the #
+ # U.S. Department of Energy. The U.S. Government has rights to use, #
+ # reproduce, and distribute this software. NEITHER THE GOVERNMENT NOR #
+ # UChicago Argonne, LLC MAKES ANY WARRANTY, EXPRESS OR IMPLIED, OR #
+ # ASSUMES ANY LIABILITY FOR THE USE OF THIS SOFTWARE. If software is #
+ # modified to produce derivative works, such modified software should #
+ # be clearly marked, so as not to confuse it with the version available #
+ # from ANL. #
+ # #
+ # Additionally, redistribution and use in source and binary forms, with #
+ # or without modification, are permitted provided that the following #
+ # conditions are met: #
+ # #
+ # * Redistributions of source code must retain the above copyright #
+ # notice, this list of conditions and the following disclaimer. #
+ # #
+ # * Redistributions in binary form must reproduce the above copyright #
+ # notice, this list of conditions and the following disclaimer in #
+ # the documentation and/or other materials provided with the #
+ # distribution. #
+ # #
+ # * Neither the name of UChicago Argonne, LLC, Argonne National #
+ # Laboratory, ANL, the U.S. Government, nor the names of its #
+ # contributors may be used to endorse or promote products derived #
+ # from this software without specific prior written permission. #
+ # #
+ # THIS SOFTWARE IS PROVIDED BY UChicago Argonne, LLC AND CONTRIBUTORS #
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT #
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS #
+ # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL UChicago #
+ # Argonne, LLC OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, #
+ # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, #
+ # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; #
+ # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER #
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT #
+ # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN #
+ # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE #
+ # POSSIBILITY OF SUCH DAMAGE. #
+ # #########################################################################
+
+ @staticmethod
+ def as_ndarray(arr, dtype=None, copy=False):
+ if not isinstance(arr, numpy.ndarray):
+ arr = numpy.array(arr, dtype=dtype, copy=copy)
+ return arr
+
+ @staticmethod
+ def as_dtype(arr, dtype, copy=False):
+ if not arr.dtype == dtype:
+ arr = numpy.array(arr, dtype=dtype, copy=copy)
+ return arr
+
+ @staticmethod
+ def as_float32(arr):
+ arr = CenterOfRotationFinder.as_ndarray(arr, numpy.float32)
+ return CenterOfRotationFinder.as_dtype(arr, numpy.float32)
+
+
+
+
+ @staticmethod
+ def find_center_vo(tomo, ind=None, smin=-40, smax=40, srad=10, step=0.5,
+ ratio=2., drop=20):
+ """
+ Find rotation axis location using Nghia Vo's method. :cite:`Vo:14`.
+
+ Parameters
+ ----------
+ tomo : ndarray
+ 3D tomographic data.
+ ind : int, optional
+ Index of the slice to be used for reconstruction.
+ smin, smax : int, optional
+ Reference to the horizontal center of the sinogram.
+ srad : float, optional
+ Fine search radius.
+ step : float, optional
+ Step of fine searching.
+ ratio : float, optional
+ The ratio between the FOV of the camera and the size of object.
+ It's used to generate the mask.
+ drop : int, optional
+ Drop lines around vertical center of the mask.
+
+ Returns
+ -------
+ float
+ Rotation axis location.
+
+ Notes
+ -----
+ The function may not yield a correct estimate, if:
+
+ - the sample size is bigger than the field of view of the camera.
+ In this case the ``ratio`` argument need to be set larger
+ than the default of 2.0.
+
+ - there is distortion in the imaging hardware. If there's
+ no correction applied, the center of the projection image may
+ yield a better estimate.
+
+ - the sample contrast is weak. Paganin's filter need to be applied
+ to overcome this.
+
+ - the sample was changed during the scan.
+ """
+ tomo = CenterOfRotationFinder.as_float32(tomo)
+
+ if ind is None:
+ ind = tomo.shape[1] // 2
+ _tomo = tomo[:, ind, :]
+
+
+
+ # Reduce noise by smooth filters. Use different filters for coarse and fine search
+ _tomo_cs = ndimage.filters.gaussian_filter(_tomo, (3, 1))
+ _tomo_fs = ndimage.filters.median_filter(_tomo, (2, 2))
+
+ # Coarse and fine searches for finding the rotation center.
+ if _tomo.shape[0] * _tomo.shape[1] > 4e6: # If data is large (>2kx2k)
+ #_tomo_coarse = downsample(numpy.expand_dims(_tomo_cs,1), level=2)[:, 0, :]
+ #init_cen = _search_coarse(_tomo_coarse, smin, smax, ratio, drop)
+ #fine_cen = _search_fine(_tomo_fs, srad, step, init_cen*4, ratio, drop)
+ init_cen = CenterOfRotationFinder._search_coarse(_tomo_cs, smin,
+ smax, ratio, drop)
+ fine_cen = CenterOfRotationFinder._search_fine(_tomo_fs, srad,
+ step, init_cen,
+ ratio, drop)
+ else:
+ init_cen = CenterOfRotationFinder._search_coarse(_tomo_cs,
+ smin, smax,
+ ratio, drop)
+ fine_cen = CenterOfRotationFinder._search_fine(_tomo_fs, srad,
+ step, init_cen,
+ ratio, drop)
+
+ #logger.debug('Rotation center search finished: %i', fine_cen)
+ return fine_cen
+
+
+ @staticmethod
+ def _search_coarse(sino, smin, smax, ratio, drop):
+ """
+ Coarse search for finding the rotation center.
+ """
+ (Nrow, Ncol) = sino.shape
+ centerfliplr = (Ncol - 1.0) / 2.0
+
+ # Copy the sinogram and flip left right, the purpose is to
+ # make a full [0;2Pi] sinogram
+ _copy_sino = numpy.fliplr(sino[1:])
+
+ # This image is used for compensating the shift of sinogram 2
+ temp_img = numpy.zeros((Nrow - 1, Ncol), dtype='float32')
+ temp_img[:] = sino[-1]
+
+ # Start coarse search in which the shift step is 1
+ listshift = numpy.arange(smin, smax + 1)
+ listmetric = numpy.zeros(len(listshift), dtype='float32')
+ mask = CenterOfRotationFinder._create_mask(2 * Nrow - 1, Ncol,
+ 0.5 * ratio * Ncol, drop)
+ for i in listshift:
+ _sino = numpy.roll(_copy_sino, i, axis=1)
+ if i >= 0:
+ _sino[:, 0:i] = temp_img[:, 0:i]
+ else:
+ _sino[:, i:] = temp_img[:, i:]
+ listmetric[i - smin] = numpy.sum(numpy.abs(numpy.fft.fftshift(
+ #pyfftw.interfaces.numpy_fft.fft2(
+ # numpy.vstack((sino, _sino)))
+ numpy.fft.fft2(numpy.vstack((sino, _sino)))
+ )) * mask)
+ minpos = numpy.argmin(listmetric)
+ return centerfliplr + listshift[minpos] / 2.0
+
+ @staticmethod
+ def _search_fine(sino, srad, step, init_cen, ratio, drop):
+ """
+ Fine search for finding the rotation center.
+ """
+ Nrow, Ncol = sino.shape
+ centerfliplr = (Ncol + 1.0) / 2.0 - 1.0
+ # Use to shift the sinogram 2 to the raw CoR.
+ shiftsino = numpy.int16(2 * (init_cen - centerfliplr))
+ _copy_sino = numpy.roll(numpy.fliplr(sino[1:]), shiftsino, axis=1)
+ if init_cen <= centerfliplr:
+ lefttake = numpy.int16(numpy.ceil(srad + 1))
+ righttake = numpy.int16(numpy.floor(2 * init_cen - srad - 1))
+ else:
+ lefttake = numpy.int16(numpy.ceil(
+ init_cen - (Ncol - 1 - init_cen) + srad + 1))
+ righttake = numpy.int16(numpy.floor(Ncol - 1 - srad - 1))
+ Ncol1 = righttake - lefttake + 1
+ mask = CenterOfRotationFinder._create_mask(2 * Nrow - 1, Ncol1,
+ 0.5 * ratio * Ncol, drop)
+ numshift = numpy.int16((2 * srad) / step) + 1
+ listshift = numpy.linspace(-srad, srad, num=numshift)
+ listmetric = numpy.zeros(len(listshift), dtype='float32')
+ factor1 = numpy.mean(sino[-1, lefttake:righttake])
+ num1 = 0
+ for i in listshift:
+ _sino = ndimage.interpolation.shift(
+ _copy_sino, (0, i), prefilter=False)
+ factor2 = numpy.mean(_sino[0,lefttake:righttake])
+ _sino = _sino * factor1 / factor2
+ sinojoin = numpy.vstack((sino, _sino))
+ listmetric[num1] = numpy.sum(numpy.abs(numpy.fft.fftshift(
+ #pyfftw.interfaces.numpy_fft.fft2(
+ # sinojoin[:, lefttake:righttake + 1])
+ numpy.fft.fft2(sinojoin[:, lefttake:righttake + 1])
+ )) * mask)
+ num1 = num1 + 1
+ minpos = numpy.argmin(listmetric)
+ return init_cen + listshift[minpos] / 2.0
+
+ @staticmethod
+ def _create_mask(nrow, ncol, radius, drop):
+ du = 1.0 / ncol
+ dv = (nrow - 1.0) / (nrow * 2.0 * numpy.pi)
+ centerrow = numpy.ceil(nrow / 2) - 1
+ centercol = numpy.ceil(ncol / 2) - 1
+ # added by Edoardo Pasca
+ centerrow = int(centerrow)
+ centercol = int(centercol)
+ mask = numpy.zeros((nrow, ncol), dtype='float32')
+ for i in range(nrow):
+ num1 = numpy.round(((i - centerrow) * dv / radius) / du)
+ (p1, p2) = numpy.int16(numpy.clip(numpy.sort(
+ (-num1 + centercol, num1 + centercol)), 0, ncol - 1))
+ mask[i, p1:p2 + 1] = numpy.ones(p2 - p1 + 1, dtype='float32')
+ if drop < centerrow:
+ mask[centerrow - drop:centerrow + drop + 1,
+ :] = numpy.zeros((2 * drop + 1, ncol), dtype='float32')
+ mask[:,centercol-1:centercol+2] = numpy.zeros((nrow, 3), dtype='float32')
+ return mask
+
+ def process(self, out=None):
+
+ projections = self.get_input()
+
+ cor = CenterOfRotationFinder.find_center_vo(projections.as_array())
+
+ return cor
+
+
+class AcquisitionDataPadder(DataProcessor):
+ '''Normalization based on flat and dark
+
+ This processor read in a AcquisitionData and normalises it based on
+ the instrument reading with and without incident photons or neutrons.
+
+ Input: AcquisitionData
+ Parameter: 2D projection with flat field (or stack)
+ 2D projection with dark field (or stack)
+ Output: AcquisitionDataSetn
+ '''
+
+ def __init__(self,
+ center_of_rotation = None,
+ acquisition_geometry = None,
+ pad_value = 1e-5):
+ kwargs = {
+ 'acquisition_geometry' : acquisition_geometry,
+ 'center_of_rotation' : center_of_rotation,
+ 'pad_value' : pad_value
+ }
+
+ super(AcquisitionDataPadder, self).__init__(**kwargs)
+
+ def check_input(self, dataset):
+ if self.acquisition_geometry is None:
+ self.acquisition_geometry = dataset.geometry
+ if dataset.number_of_dimensions == 3:
+ return True
+ else:
+ raise ValueError("Expected input dimensions is 2 or 3, got {0}"\
+ .format(dataset.number_of_dimensions))
+
+ def process(self, out=None):
+ projections = self.get_input()
+ w = projections.get_dimension_size('horizontal')
+ delta = w - 2 * self.center_of_rotation
+
+ padded_width = int (
+ numpy.ceil(abs(delta)) + w
+ )
+ delta_pix = padded_width - w
+
+ voxel_per_pixel = 1
+ geom = pbalg.pb_setup_geometry_from_acquisition(projections.as_array(),
+ self.acquisition_geometry.angles,
+ self.center_of_rotation,
+ voxel_per_pixel )
+
+ padded_geometry = self.acquisition_geometry.clone()
+
+ padded_geometry.pixel_num_h = geom['n_h']
+ padded_geometry.pixel_num_v = geom['n_v']
+
+ delta_pix_h = padded_geometry.pixel_num_h - self.acquisition_geometry.pixel_num_h
+ delta_pix_v = padded_geometry.pixel_num_v - self.acquisition_geometry.pixel_num_v
+
+ if delta_pix_h == 0:
+ delta_pix_h = delta_pix
+ padded_geometry.pixel_num_h = padded_width
+ #initialize a new AcquisitionData with values close to 0
+ out = AcquisitionData(geometry=padded_geometry)
+ out = out + self.pad_value
+
+
+ #pad in the horizontal-vertical plane -> slice on angles
+ if delta > 0:
+ #pad left of middle
+ command = "out.array["
+ for i in range(out.number_of_dimensions):
+ if out.dimension_labels[i] == 'horizontal':
+ value = '{0}:{1}'.format(delta_pix_h, delta_pix_h+w)
+ command = command + str(value)
+ else:
+ if out.dimension_labels[i] == 'vertical' :
+ value = '{0}:'.format(delta_pix_v)
+ command = command + str(value)
+ else:
+ command = command + ":"
+ if i < out.number_of_dimensions -1:
+ command = command + ','
+ command = command + '] = projections.array'
+ #print (command)
+ else:
+ #pad right of middle
+ command = "out.array["
+ for i in range(out.number_of_dimensions):
+ if out.dimension_labels[i] == 'horizontal':
+ value = '{0}:{1}'.format(0, w)
+ command = command + str(value)
+ else:
+ if out.dimension_labels[i] == 'vertical' :
+ value = '{0}:'.format(delta_pix_v)
+ command = command + str(value)
+ else:
+ command = command + ":"
+ if i < out.number_of_dimensions -1:
+ command = command + ','
+ command = command + '] = projections.array'
+ #print (command)
+ #cleaned = eval(command)
+ exec(command)
return out \ No newline at end of file
diff --git a/Wrappers/Python/ccpi/processors/Normalizer.py b/Wrappers/Python/ccpi/processors/Normalizer.py
new file mode 100755
index 0000000..da65e5c
--- /dev/null
+++ b/Wrappers/Python/ccpi/processors/Normalizer.py
@@ -0,0 +1,124 @@
+# -*- coding: utf-8 -*-
+# This work is part of the Core Imaging Library developed by
+# Visual Analytics and Imaging System Group of the Science Technology
+# Facilities Council, STFC
+
+# Copyright 2018 Edoardo Pasca
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License
+
+from ccpi.framework import DataProcessor, DataContainer, AcquisitionData,\
+ AcquisitionGeometry, ImageGeometry, ImageData
+import numpy
+
+class Normalizer(DataProcessor):
+ '''Normalization based on flat and dark
+
+ This processor read in a AcquisitionData and normalises it based on
+ the instrument reading with and without incident photons or neutrons.
+
+ Input: AcquisitionData
+ Parameter: 2D projection with flat field (or stack)
+ 2D projection with dark field (or stack)
+ Output: AcquisitionDataSetn
+ '''
+
+ def __init__(self, flat_field = None, dark_field = None, tolerance = 1e-5):
+ kwargs = {
+ 'flat_field' : flat_field,
+ 'dark_field' : dark_field,
+ # very small number. Used when there is a division by zero
+ 'tolerance' : tolerance
+ }
+
+ #DataProcessor.__init__(self, **kwargs)
+ super(Normalizer, self).__init__(**kwargs)
+ if not flat_field is None:
+ self.set_flat_field(flat_field)
+ if not dark_field is None:
+ self.set_dark_field(dark_field)
+
+ def check_input(self, dataset):
+ if dataset.number_of_dimensions == 3 or\
+ dataset.number_of_dimensions == 2:
+ return True
+ else:
+ raise ValueError("Expected input dimensions is 2 or 3, got {0}"\
+ .format(dataset.number_of_dimensions))
+
+ def set_dark_field(self, df):
+ if type(df) is numpy.ndarray:
+ if len(numpy.shape(df)) == 3:
+ raise ValueError('Dark Field should be 2D')
+ elif len(numpy.shape(df)) == 2:
+ self.dark_field = df
+ elif issubclass(type(df), DataContainer):
+ self.dark_field = self.set_dark_field(df.as_array())
+
+ def set_flat_field(self, df):
+ if type(df) is numpy.ndarray:
+ if len(numpy.shape(df)) == 3:
+ raise ValueError('Flat Field should be 2D')
+ elif len(numpy.shape(df)) == 2:
+ self.flat_field = df
+ elif issubclass(type(df), DataContainer):
+ self.flat_field = self.set_flat_field(df.as_array())
+
+ @staticmethod
+ def normalize_projection(projection, flat, dark, tolerance):
+ a = (projection - dark)
+ b = (flat-dark)
+ with numpy.errstate(divide='ignore', invalid='ignore'):
+ c = numpy.true_divide( a, b )
+ c[ ~ numpy.isfinite( c )] = tolerance # set to not zero if 0/0
+ return c
+
+ @staticmethod
+ def estimate_normalised_error(projection, flat, dark, delta_flat, delta_dark):
+ '''returns the estimated relative error of the normalised projection
+
+ n = (projection - dark) / (flat - dark)
+ Dn/n = (flat-dark + projection-dark)/((flat-dark)*(projection-dark))*(Df/f + Dd/d)
+ '''
+ a = (projection - dark)
+ b = (flat-dark)
+ df = delta_flat / flat
+ dd = delta_dark / dark
+ rel_norm_error = (b + a) / (b * a) * (df + dd)
+ return rel_norm_error
+
+ def process(self, out=None):
+
+ projections = self.get_input()
+ dark = self.dark_field
+ flat = self.flat_field
+
+ if projections.number_of_dimensions == 3:
+ if not (projections.shape[1:] == dark.shape and \
+ projections.shape[1:] == flat.shape):
+ raise ValueError('Flats/Dark and projections size do not match.')
+
+
+ a = numpy.asarray(
+ [ Normalizer.normalize_projection(
+ projection, flat, dark, self.tolerance) \
+ for projection in projections.as_array() ]
+ )
+ elif projections.number_of_dimensions == 2:
+ a = Normalizer.normalize_projection(projections.as_array(),
+ flat, dark, self.tolerance)
+ y = type(projections)( a , True,
+ dimension_labels=projections.dimension_labels,
+ geometry=projections.geometry)
+ return y
+ \ No newline at end of file
diff --git a/Wrappers/Python/ccpi/processors/Resizer.py b/Wrappers/Python/ccpi/processors/Resizer.py
new file mode 100755
index 0000000..7509a90
--- /dev/null
+++ b/Wrappers/Python/ccpi/processors/Resizer.py
@@ -0,0 +1,262 @@
+# -*- coding: utf-8 -*-
+# This work is part of the Core Imaging Library developed by
+# Visual Analytics and Imaging System Group of the Science Technology
+# Facilities Council, STFC
+
+# Copyright 2018 Edoardo Pasca
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License
+
+from ccpi.framework import DataProcessor, AcquisitionData, ImageData
+import warnings
+
+
+class Resizer(DataProcessor):
+
+ def __init__(self,
+ roi = -1,
+ binning = 1):
+
+ '''
+ Constructor
+
+ Input:
+
+ roi region-of-interest to crop. If roi = -1 (default), then no crop.
+ Otherwise roi is given by a list with ndim elements,
+ where each element is either -1 if no crop along this
+ dimension or a tuple with beginning and end coodinates to crop to.
+ Example:
+ to crop 4D array along 2nd dimension:
+ roi = [-1, -1, (100, 900), -1]
+
+ binning number of pixels to bin (combine) along each dimension.
+ If binning = 1, then projections in original resolution are loaded.
+ Otherwise, binning is given by a list with ndim integers.
+ Example:
+ to rebin 3D array along 1st direction:
+ binning = [1, 5, 1]
+ '''
+
+ kwargs = {'roi': roi,
+ 'binning': binning}
+
+ super(Resizer, self).__init__(**kwargs)
+
+ def check_input(self, data):
+ if not ((isinstance(data, ImageData)) or
+ (isinstance(data, AcquisitionData))):
+ raise Exception('Processor supports only following data types:\n' +
+ ' - ImageData\n - AcquisitionData')
+ elif (data.geometry == None):
+ raise Exception('Geometry is not defined.')
+ else:
+ return True
+
+ def process(self):
+
+ data = self.get_input()
+ ndim = len(data.dimension_labels)
+
+ geometry_0 = data.geometry
+ geometry = geometry_0.clone()
+
+ if (self.roi == -1):
+ roi_par = [-1] * ndim
+ else:
+ roi_par = self.roi.copy()
+ if (len(roi_par) != ndim):
+ raise Exception('Number of dimensions and number of elements in roi parameter do not match')
+
+ if (self.binning == 1):
+ binning = [1] * ndim
+ else:
+ binning = self.binning.copy()
+ if (len(binning) != ndim):
+ raise Exception('Number of dimensions and number of elements in binning parameter do not match')
+
+ if (isinstance(data, ImageData)):
+ if ((all(x == -1 for x in roi_par)) and (all(x == 1 for x in binning))):
+ for key in data.dimension_labels:
+ if data.dimension_labels[key] == 'channel':
+ geometry.channels = geometry_0.channels
+ roi_par[key] = (0, geometry.channels)
+ elif data.dimension_labels[key] == 'horizontal_y':
+ geometry.voxel_size_y = geometry_0.voxel_size_y
+ geometry.voxel_num_y = geometry_0.voxel_num_y
+ roi_par[key] = (0, geometry.voxel_num_y)
+ elif data.dimension_labels[key] == 'vertical':
+ geometry.voxel_size_z = geometry_0.voxel_size_z
+ geometry.voxel_num_z = geometry_0.voxel_num_z
+ roi_par[key] = (0, geometry.voxel_num_z)
+ elif data.dimension_labels[key] == 'horizontal_x':
+ geometry.voxel_size_x = geometry_0.voxel_size_x
+ geometry.voxel_num_x = geometry_0.voxel_num_x
+ roi_par[key] = (0, geometry.voxel_num_x)
+ else:
+ for key in data.dimension_labels:
+ if data.dimension_labels[key] == 'channel':
+ if (roi_par[key] != -1):
+ geometry.channels = (roi_par[key][1] - roi_par[key][0]) // binning[key]
+ roi_par[key] = (roi_par[key][0], roi_par[key][0] + ((roi_par[key][1] - roi_par[key][0]) // binning[key]) * binning[key])
+ else:
+ geometry.channels = geometry_0.channels // binning[key]
+ roi_par[key] = (0, geometry.channels * binning[key])
+ elif data.dimension_labels[key] == 'horizontal_y':
+ if (roi_par[key] != -1):
+ geometry.voxel_num_y = (roi_par[key][1] - roi_par[key][0]) // binning[key]
+ geometry.voxel_size_y = geometry_0.voxel_size_y * binning[key]
+ roi_par[key] = (roi_par[key][0], roi_par[key][0] + ((roi_par[key][1] - roi_par[key][0]) // binning[key]) * binning[key])
+ else:
+ geometry.voxel_num_y = geometry_0.voxel_num_y // binning[key]
+ geometry.voxel_size_y = geometry_0.voxel_size_y * binning[key]
+ roi_par[key] = (0, geometry.voxel_num_y * binning[key])
+ elif data.dimension_labels[key] == 'vertical':
+ if (roi_par[key] != -1):
+ geometry.voxel_num_z = (roi_par[key][1] - roi_par[key][0]) // binning[key]
+ geometry.voxel_size_z = geometry_0.voxel_size_z * binning[key]
+ roi_par[key] = (roi_par[key][0], roi_par[key][0] + ((roi_par[key][1] - roi_par[key][0]) // binning[key]) * binning[key])
+ else:
+ geometry.voxel_num_z = geometry_0.voxel_num_z // binning[key]
+ geometry.voxel_size_z = geometry_0.voxel_size_z * binning[key]
+ roi_par[key] = (0, geometry.voxel_num_z * binning[key])
+ elif data.dimension_labels[key] == 'horizontal_x':
+ if (roi_par[key] != -1):
+ geometry.voxel_num_x = (roi_par[key][1] - roi_par[key][0]) // binning[key]
+ geometry.voxel_size_x = geometry_0.voxel_size_x * binning[key]
+ roi_par[key] = (roi_par[key][0], roi_par[key][0]+ ((roi_par[key][1] - roi_par[key][0]) // binning[key]) * binning[key])
+ else:
+ geometry.voxel_num_x = geometry_0.voxel_num_x // binning[key]
+ geometry.voxel_size_x = geometry_0.voxel_size_x * binning[key]
+ roi_par[key] = (0, geometry.voxel_num_x * binning[key])
+
+ else: # AcquisitionData
+ if ((all(x == -1 for x in roi_par)) and (all(x == 1 for x in binning))):
+ for key in data.dimension_labels:
+ if data.dimension_labels[key] == 'channel':
+ geometry.channels = geometry_0.channels
+ roi_par[key] = (0, geometry.channels)
+ elif data.dimension_labels[key] == 'angle':
+ geometry.angles = geometry_0.angles
+ roi_par[key] = (0, len(geometry.angles))
+ elif data.dimension_labels[key] == 'vertical':
+ geometry.pixel_size_v = geometry_0.pixel_size_v
+ geometry.pixel_num_v = geometry_0.pixel_num_v
+ roi_par[key] = (0, geometry.pixel_num_v)
+ elif data.dimension_labels[key] == 'horizontal':
+ geometry.pixel_size_h = geometry_0.pixel_size_h
+ geometry.pixel_num_h = geometry_0.pixel_num_h
+ roi_par[key] = (0, geometry.pixel_num_h)
+ else:
+ for key in data.dimension_labels:
+ if data.dimension_labels[key] == 'channel':
+ if (roi_par[key] != -1):
+ geometry.channels = (roi_par[key][1] - roi_par[key][0]) // binning[key]
+ roi_par[key] = (roi_par[key][0], roi_par[key][0] + ((roi_par[key][1] - roi_par[key][0]) // binning[key]) * binning[key])
+ else:
+ geometry.channels = geometry_0.channels // binning[key]
+ roi_par[key] = (0, geometry.channels * binning[key])
+ elif data.dimension_labels[key] == 'angle':
+ if (roi_par[key] != -1):
+ geometry.angles = geometry_0.angles[roi_par[key][0]:roi_par[key][1]]
+ else:
+ geometry.angles = geometry_0.angles
+ roi_par[key] = (0, len(geometry.angles))
+ if (binning[key] != 1):
+ binning[key] = 1
+ warnings.warn('Rebinning in angular dimensions is not supported: \n binning[{}] is set to 1.'.format(key))
+ elif data.dimension_labels[key] == 'vertical':
+ if (roi_par[key] != -1):
+ geometry.pixel_num_v = (roi_par[key][1] - roi_par[key][0]) // binning[key]
+ geometry.pixel_size_v = geometry_0.pixel_size_v * binning[key]
+ roi_par[key] = (roi_par[key][0], roi_par[key][0] + ((roi_par[key][1] - roi_par[key][0]) // binning[key]) * binning[key])
+ else:
+ geometry.pixel_num_v = geometry_0.pixel_num_v // binning[key]
+ geometry.pixel_size_v = geometry_0.pixel_size_v * binning[key]
+ roi_par[key] = (0, geometry.pixel_num_v * binning[key])
+ elif data.dimension_labels[key] == 'horizontal':
+ if (roi_par[key] != -1):
+ geometry.pixel_num_h = (roi_par[key][1] - roi_par[key][0]) // binning[key]
+ geometry.pixel_size_h = geometry_0.pixel_size_h * binning[key]
+ roi_par[key] = (roi_par[key][0], roi_par[key][0] + ((roi_par[key][1] - roi_par[key][0]) // binning[key]) * binning[key])
+ else:
+ geometry.pixel_num_h = geometry_0.pixel_num_h // binning[key]
+ geometry.pixel_size_h = geometry_0.pixel_size_h * binning[key]
+ roi_par[key] = (0, geometry.pixel_num_h * binning[key])
+
+ if ndim == 2:
+ n_pix_0 = (roi_par[0][1] - roi_par[0][0]) // binning[0]
+ n_pix_1 = (roi_par[1][1] - roi_par[1][0]) // binning[1]
+ shape = (n_pix_0, binning[0],
+ n_pix_1, binning[1])
+ data_resized = data.as_array()[roi_par[0][0]:(roi_par[0][0] + n_pix_0 * binning[0]),
+ roi_par[1][0]:(roi_par[1][0] + n_pix_1 * binning[1])].reshape(shape).mean(-1).mean(1)
+ if ndim == 3:
+ n_pix_0 = (roi_par[0][1] - roi_par[0][0]) // binning[0]
+ n_pix_1 = (roi_par[1][1] - roi_par[1][0]) // binning[1]
+ n_pix_2 = (roi_par[2][1] - roi_par[2][0]) // binning[2]
+ shape = (n_pix_0, binning[0],
+ n_pix_1, binning[1],
+ n_pix_2, binning[2])
+ data_resized = data.as_array()[roi_par[0][0]:(roi_par[0][0] + n_pix_0 * binning[0]),
+ roi_par[1][0]:(roi_par[1][0] + n_pix_1 * binning[1]),
+ roi_par[2][0]:(roi_par[2][0] + n_pix_2 * binning[2])].reshape(shape).mean(-1).mean(1).mean(2)
+ if ndim == 4:
+ n_pix_0 = (roi_par[0][1] - roi_par[0][0]) // binning[0]
+ n_pix_1 = (roi_par[1][1] - roi_par[1][0]) // binning[1]
+ n_pix_2 = (roi_par[2][1] - roi_par[2][0]) // binning[2]
+ n_pix_3 = (roi_par[3][1] - roi_par[3][0]) // binning[3]
+ shape = (n_pix_0, binning[0],
+ n_pix_1, binning[1],
+ n_pix_2, binning[2],
+ n_pix_3, binning[3])
+ data_resized = data.as_array()[roi_par[0][0]:(roi_par[0][0] + n_pix_0 * binning[0]),
+ roi_par[1][0]:(roi_par[1][0] + n_pix_1 * binning[1]),
+ roi_par[2][0]:(roi_par[2][0] + n_pix_2 * binning[2]),
+ roi_par[3][0]:(roi_par[3][0] + n_pix_3 * binning[3])].reshape(shape).mean(-1).mean(1).mean(2).mean(3)
+
+ out = type(data)(array = data_resized,
+ deep_copy = False,
+ dimension_labels = data.dimension_labels,
+ geometry = geometry)
+
+ return out
+
+
+'''
+#usage exaample
+ig = ImageGeometry(voxel_num_x = 200,
+ voxel_num_y = 200,
+ voxel_num_z = 200,
+ voxel_size_x = 1,
+ voxel_size_y = 1,
+ voxel_size_z = 1,
+ center_x = 0,
+ center_y = 0,
+ center_z = 0,
+ channels = 200)
+
+im = ImageData(array = numpy.zeros((200, 200, 200, 200)),
+ geometry = ig,
+ deep_copy = False,
+ dimension_labels = ['channel',\
+ 'vertical',\
+ 'horizontal_y',\
+ 'horizontal_x'])
+
+
+resizer = Resizer(binning = [1, 1, 7, 1], roi = -1)
+resizer.input = im
+data_resized = resizer.process()
+print(data_resized)
+'''
diff --git a/Wrappers/Python/ccpi/processors/__init__.py b/Wrappers/Python/ccpi/processors/__init__.py
new file mode 100755
index 0000000..cba5897
--- /dev/null
+++ b/Wrappers/Python/ccpi/processors/__init__.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+"""
+Created on Tue Apr 30 13:51:09 2019
+
+@author: ofn77899
+"""
+
+from .CenterOfRotationFinder import CenterOfRotationFinder
+from .Normalizer import Normalizer
+from .Resizer import Resizer
diff --git a/Wrappers/Python/conda-recipe/conda_build_config.yaml b/Wrappers/Python/conda-recipe/conda_build_config.yaml
index 96a211f..393ae18 100644
--- a/Wrappers/Python/conda-recipe/conda_build_config.yaml
+++ b/Wrappers/Python/conda-recipe/conda_build_config.yaml
@@ -4,5 +4,5 @@ python:
- 3.6
numpy:
# TODO investigage, as it doesn't currently build with cvxp, requires >1.14
- #- 1.12
- - 1.15
+ - 1.11
+ - 1.12
diff --git a/Wrappers/Python/conda-recipe/meta.yaml b/Wrappers/Python/conda-recipe/meta.yaml
index 8ded429..9d03220 100644
--- a/Wrappers/Python/conda-recipe/meta.yaml
+++ b/Wrappers/Python/conda-recipe/meta.yaml
@@ -11,7 +11,7 @@ build:
test:
requires:
- python-wget
- - cvxpy # [not win]
+ - cvxpy # [ unix and py36 and np115 ]
source_files:
- ./test # [win]
@@ -24,8 +24,8 @@ test:
requirements:
build:
+ - {{ pin_compatible('numpy', max_pin='x.x') }}
- python
- - numpy {{ numpy }}
- setuptools
run:
@@ -35,6 +35,7 @@ requirements:
- scipy
- matplotlib
- h5py
+ - pillow
about:
home: http://www.ccpi.ac.uk
diff --git a/Wrappers/Python/data/24737_fd_normalised.nxs b/Wrappers/Python/data/24737_fd_normalised.nxs
new file mode 100644
index 0000000..c1fe83d
--- /dev/null
+++ b/Wrappers/Python/data/24737_fd_normalised.nxs
Binary files differ
diff --git a/Wrappers/Python/data/boat.tiff b/Wrappers/Python/data/boat.tiff
new file mode 100644
index 0000000..fc1205a
--- /dev/null
+++ b/Wrappers/Python/data/boat.tiff
Binary files differ
diff --git a/Wrappers/Python/data/camera.png b/Wrappers/Python/data/camera.png
new file mode 100644
index 0000000..49be869
--- /dev/null
+++ b/Wrappers/Python/data/camera.png
Binary files differ
diff --git a/Wrappers/Python/data/peppers.tiff b/Wrappers/Python/data/peppers.tiff
new file mode 100644
index 0000000..8c956f8
--- /dev/null
+++ b/Wrappers/Python/data/peppers.tiff
Binary files differ
diff --git a/Wrappers/Python/data/resolution_chart.tiff b/Wrappers/Python/data/resolution_chart.tiff
new file mode 100755
index 0000000..d09cef3
--- /dev/null
+++ b/Wrappers/Python/data/resolution_chart.tiff
Binary files differ
diff --git a/Wrappers/Python/data/shapes.png b/Wrappers/Python/data/shapes.png
new file mode 100644
index 0000000..dd4f680
--- /dev/null
+++ b/Wrappers/Python/data/shapes.png
Binary files differ
diff --git a/Wrappers/Python/data/test_show_data.py b/Wrappers/Python/data/test_show_data.py
new file mode 100644
index 0000000..7325c27
--- /dev/null
+++ b/Wrappers/Python/data/test_show_data.py
@@ -0,0 +1,30 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Created on Tue May 7 20:43:48 2019
+
+@author: evangelos
+"""
+
+from ccpi.data import camera, boat, peppers
+import matplotlib.pyplot as plt
+
+
+d = camera(size=(256,256))
+
+plt.imshow(d.as_array())
+plt.colorbar()
+plt.show()
+
+d1 = boat(size=(256,256))
+
+plt.imshow(d1.as_array())
+plt.colorbar()
+plt.show()
+
+
+d2 = peppers(size=(256,256))
+
+plt.imshow(d2.as_array())
+plt.colorbar()
+plt.show() \ No newline at end of file
diff --git a/Wrappers/Python/demos/CGLS_examples/CGLS_Tikhonov.py b/Wrappers/Python/demos/CGLS_examples/CGLS_Tikhonov.py
new file mode 100644
index 0000000..653e191
--- /dev/null
+++ b/Wrappers/Python/demos/CGLS_examples/CGLS_Tikhonov.py
@@ -0,0 +1,106 @@
+#========================================================================
+# Copyright 2019 Science Technology Facilities Council
+# Copyright 2019 University of Manchester
+#
+# This work is part of the Core Imaging Library developed by Science Technology
+# Facilities Council and University of Manchester
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0.txt
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+#=========================================================================
+
+"""
+Compare solutions of PDHG & "Block CGLS" algorithms for
+
+
+Problem: min_x alpha * ||\grad x ||^{2}_{2} + || A x - g ||_{2}^{2}
+
+
+ A: Projection operator
+ g: Sinogram
+
+"""
+
+
+from ccpi.framework import AcquisitionGeometry, BlockDataContainer, AcquisitionData
+
+import numpy as np
+import numpy
+import matplotlib.pyplot as plt
+
+from ccpi.optimisation.algorithms import CGLS
+from ccpi.optimisation.operators import BlockOperator, Gradient
+
+from ccpi.framework import TestData
+import os, sys
+from ccpi.astra.ops import AstraProjectorSimple
+
+# Load Data
+loader = TestData(data_dir=os.path.join(sys.prefix, 'share','ccpi'))
+N = 150
+M = 150
+data = loader.load(TestData.SIMPLE_PHANTOM_2D, size=(N,M), scale=(0,1))
+
+ig = data.geometry
+
+detectors = N
+angles = np.linspace(0, np.pi, N, dtype=np.float32)
+
+ag = AcquisitionGeometry('parallel','2D', angles, detectors)
+
+device = input('Available device: GPU==1 / CPU==0 ')
+if device=='1':
+ dev = 'gpu'
+else:
+ dev = 'cpu'
+
+Aop = AstraProjectorSimple(ig, ag, dev)
+sin = Aop.direct(data)
+
+noisy_data = AcquisitionData( sin.as_array() + np.random.normal(0,3,ig.shape))
+
+# Show Ground Truth and Noisy Data
+plt.figure(figsize=(10,10))
+plt.subplot(2,1,1)
+plt.imshow(data.as_array())
+plt.title('Ground Truth')
+plt.colorbar()
+plt.subplot(2,1,2)
+plt.imshow(noisy_data.as_array())
+plt.title('Noisy Data')
+plt.colorbar()
+plt.show()
+
+# Setup and run the CGLS algorithm
+#alpha = 50
+#Grad = Gradient(ig)
+#
+## Form Tikhonov as a Block CGLS structure
+#op_CGLS = BlockOperator( Aop, alpha * Grad, shape=(2,1))
+#block_data = BlockDataContainer(noisy_data, Grad.range_geometry().allocate())
+#
+#x_init = ig.allocate()
+#cgls = CGLS(x_init=x_init, operator=op_CGLS, data=block_data)
+#cgls.max_iteration = 1000
+#cgls.update_objective_interval = 200
+#cgls.run(1000,verbose=False)
+
+#%%
+# Show results
+plt.figure(figsize=(5,5))
+plt.imshow(cgls.get_output().as_array())
+plt.title('CGLS reconstruction')
+plt.colorbar()
+plt.show()
+
+
diff --git a/Wrappers/Python/demos/CompareAlgorithms/CGLS_FISTA_PDHG_LeastSquares.py b/Wrappers/Python/demos/CompareAlgorithms/CGLS_FISTA_PDHG_LeastSquares.py
new file mode 100644
index 0000000..672d4bc
--- /dev/null
+++ b/Wrappers/Python/demos/CompareAlgorithms/CGLS_FISTA_PDHG_LeastSquares.py
@@ -0,0 +1,189 @@
+#========================================================================
+# Copyright 2019 Science Technology Facilities Council
+# Copyright 2019 University of Manchester
+#
+# This work is part of the Core Imaging Library developed by Science Technology
+# Facilities Council and University of Manchester
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0.txt
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+#=========================================================================
+
+"""
+Compare solutions of FISTA & PDHG
+ & CGLS & Astra Built-in algorithms for Least Squares
+
+
+Problem: min_x || A x - g ||_{2}^{2}
+
+ A: Projection operator
+ g: Sinogram
+
+"""
+
+
+from ccpi.framework import ImageData, TestData, AcquisitionGeometry
+
+import numpy as np
+import numpy
+import matplotlib.pyplot as plt
+
+from ccpi.optimisation.algorithms import PDHG, CGLS, FISTA
+
+from ccpi.optimisation.functions import ZeroFunction, L2NormSquared, FunctionOperatorComposition
+from ccpi.astra.ops import AstraProjectorSimple
+import astra
+import os, sys
+
+
+# Load Data
+loader = TestData(data_dir=os.path.join(sys.prefix, 'share','ccpi'))
+
+N = 50
+M = 50
+data = loader.load(TestData.SIMPLE_PHANTOM_2D, size=(N,M), scale=(0,1))
+ig = data.geometry
+
+detectors = N
+angles = np.linspace(0, np.pi, N, dtype=np.float32)
+
+device = input('Available device: GPU==1 / CPU==0 ')
+ag = AcquisitionGeometry('parallel','2D', angles, detectors)
+if device=='1':
+ dev = 'gpu'
+else:
+ dev = 'cpu'
+
+Aop = AstraProjectorSimple(ig, ag, dev)
+sin = Aop.direct(data)
+
+noisy_data = sin
+
+###############################################################################
+# Setup and run Astra CGLS algorithm
+vol_geom = astra.create_vol_geom(N, N)
+proj_geom = astra.create_proj_geom('parallel', 1.0, detectors, angles)
+proj_id = astra.create_projector('linear', proj_geom, vol_geom)
+
+# Create a sinogram from a phantom
+sinogram_id, sinogram = astra.create_sino(data.as_array(), proj_id)
+
+# Create a data object for the reconstruction
+rec_id = astra.data2d.create('-vol', vol_geom)
+
+cgls_astra = astra.astra_dict('CGLS')
+cgls_astra['ReconstructionDataId'] = rec_id
+cgls_astra['ProjectionDataId'] = sinogram_id
+cgls_astra['ProjectorId'] = proj_id
+
+# Create the algorithm object from the configuration structure
+alg_id = astra.algorithm.create(cgls_astra)
+
+astra.algorithm.run(alg_id, 1000)
+
+recon_cgls_astra = astra.data2d.get(rec_id)
+
+###############################################################################
+# Setup and run the CGLS algorithm
+x_init = ig.allocate()
+cgls = CGLS(x_init=x_init, operator=Aop, data=noisy_data)
+cgls.max_iteration = 1000
+cgls.update_objective_interval = 200
+cgls.run(1000, verbose=False)
+
+###############################################################################
+# Setup and run the PDHG algorithm
+operator = Aop
+f = L2NormSquared(b = noisy_data)
+g = ZeroFunction()
+
+## Compute operator Norm
+normK = operator.norm()
+
+## Primal & dual stepsizes
+sigma = 1
+tau = 1/(sigma*normK**2)
+
+pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma, memopt=True)
+pdhg.max_iteration = 1000
+pdhg.update_objective_interval = 200
+pdhg.run(1000, verbose=True)
+
+###############################################################################
+# Setup and run the FISTA algorithm
+fidelity = FunctionOperatorComposition(L2NormSquared(b=noisy_data), Aop)
+regularizer = ZeroFunction()
+
+fista = FISTA(x_init=x_init , f=fidelity, g=regularizer)
+fista.max_iteration = 1000
+fista.update_objective_interval = 200
+fista.run(1000, verbose=True)
+
+#%% Show results
+
+plt.figure(figsize=(10,10))
+plt.suptitle('Reconstructions ', fontsize=16)
+
+plt.subplot(2,2,1)
+plt.imshow(cgls.get_output().as_array())
+plt.colorbar()
+plt.title('CGLS reconstruction')
+
+plt.subplot(2,2,2)
+plt.imshow(fista.get_output().as_array())
+plt.colorbar()
+plt.title('FISTA reconstruction')
+
+plt.subplot(2,2,3)
+plt.imshow(pdhg.get_output().as_array())
+plt.colorbar()
+plt.title('PDHG reconstruction')
+
+plt.subplot(2,2,4)
+plt.imshow(recon_cgls_astra)
+plt.colorbar()
+plt.title('CGLS astra')
+
+diff1 = pdhg.get_output() - cgls.get_output()
+diff2 = fista.get_output() - cgls.get_output()
+diff3 = ImageData(recon_cgls_astra) - cgls.get_output()
+
+plt.figure(figsize=(15,15))
+
+plt.subplot(3,1,1)
+plt.imshow(diff1.abs().as_array())
+plt.title('Diff PDHG vs CGLS')
+plt.colorbar()
+
+plt.subplot(3,1,2)
+plt.imshow(diff2.abs().as_array())
+plt.title('Diff FISTA vs CGLS')
+plt.colorbar()
+
+plt.subplot(3,1,3)
+plt.imshow(diff3.abs().as_array())
+plt.title('Diff CLGS astra vs CGLS')
+plt.colorbar()
+
+
+#%%
+
+print('Primal Objective (FISTA) {} '.format(fista.objective[-1]))
+print('Primal Objective (CGLS) {} '.format(cgls.objective[-1]))
+print('Primal Objective (PDHG) {} '.format(pdhg.objective[-1][0]))
+
+
+true_obj = (Aop.direct(cglsd.get_output())-noisy_data).squared_norm()
+print('True objective {}'.format(true_obj))
+
+
diff --git a/Wrappers/Python/demos/CompareAlgorithms/CGLS_PDHG_Tikhonov.py b/Wrappers/Python/demos/CompareAlgorithms/CGLS_PDHG_Tikhonov.py
new file mode 100644
index 0000000..9b6d10f
--- /dev/null
+++ b/Wrappers/Python/demos/CompareAlgorithms/CGLS_PDHG_Tikhonov.py
@@ -0,0 +1,133 @@
+#========================================================================
+# Copyright 2019 Science Technology Facilities Council
+# Copyright 2019 University of Manchester
+#
+# This work is part of the Core Imaging Library developed by Science Technology
+# Facilities Council and University of Manchester
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0.txt
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+#=========================================================================
+
+"""
+Compare solutions of PDHG & "Block CGLS" algorithms for
+
+
+Problem: min_x alpha * ||\grad x ||^{2}_{2} + || A x - g ||_{2}^{2}
+
+
+ A: Projection operator
+ g: Sinogram
+
+"""
+
+
+from ccpi.framework import AcquisitionGeometry, BlockDataContainer, AcquisitionData
+
+import numpy as np
+import numpy
+import matplotlib.pyplot as plt
+
+from ccpi.optimisation.algorithms import PDHG, CGLS
+from ccpi.optimisation.operators import BlockOperator, Gradient
+
+from ccpi.optimisation.functions import ZeroFunction, BlockFunction, L2NormSquared
+from ccpi.astra.ops import AstraProjectorSimple
+from ccpi.framework import TestData
+import os, sys
+
+loader = TestData(data_dir=os.path.join(sys.prefix, 'share','ccpi'))
+
+# Create Ground truth phantom and Sinogram
+N = 150
+M = 150
+data = loader.load(TestData.SIMPLE_PHANTOM_2D, size=(N,M), scale=(0,1))
+ig = data.geometry
+
+detectors = N
+angles = np.linspace(0, np.pi, N, dtype=np.float32)
+ag = AcquisitionGeometry('parallel','2D', angles, detectors)
+
+device = input('Available device: GPU==1 / CPU==0 ')
+if device=='1':
+ dev = 'gpu'
+else:
+ dev = 'cpu'
+
+Aop = AstraProjectorSimple(ig, ag, dev)
+sin = Aop.direct(data)
+
+noisy_data = AcquisitionData( sin.as_array() + np.random.normal(0,3,ig.shape))
+
+# Setup and run the CGLS algorithm
+alpha = 50
+Grad = Gradient(ig)
+
+# Form Tikhonov as a Block CGLS structure
+op_CGLS = BlockOperator( Aop, alpha * Grad, shape=(2,1))
+block_data = BlockDataContainer(noisy_data, Grad.range_geometry().allocate())
+
+x_init = ig.allocate()
+cgls = CGLS(x_init=x_init, operator=op_CGLS, data=block_data)
+cgls.max_iteration = 1000
+cgls.update_objective_interval = 200
+cgls.run(1000,verbose=False)
+
+
+#Setup and run the PDHG algorithm
+
+# Create BlockOperator
+op_PDHG = BlockOperator(Grad, Aop, shape=(2,1) )
+# Create functions
+f1 = 0.5 * alpha**2 * L2NormSquared()
+f2 = 0.5 * L2NormSquared(b = noisy_data)
+f = BlockFunction(f1, f2)
+g = ZeroFunction()
+
+## Compute operator Norm
+normK = op_PDHG.norm()
+
+## Primal & dual stepsizes
+sigma = 10
+tau = 1/(sigma*normK**2)
+
+pdhg = PDHG(f=f,g=g,operator=op_PDHG, tau=tau, sigma=sigma)
+pdhg.max_iteration = 1000
+pdhg.update_objective_interval = 200
+pdhg.run(1000, verbose=False)
+
+# Show results
+plt.figure(figsize=(10,10))
+
+plt.subplot(2,1,1)
+plt.imshow(cgls.get_output().as_array())
+plt.title('CGLS reconstruction')
+
+plt.subplot(2,1,2)
+plt.imshow(pdhg.get_output().as_array())
+plt.title('PDHG reconstruction')
+
+plt.show()
+
+diff1 = pdhg.get_output() - cgls.get_output()
+
+plt.imshow(diff1.abs().as_array())
+plt.title('Diff PDHG vs CGLS')
+plt.colorbar()
+plt.show()
+
+plt.plot(np.linspace(0,N,M), pdhg.get_output().as_array()[int(N/2),:], label = 'PDHG')
+plt.plot(np.linspace(0,N,M), cgls.get_output().as_array()[int(N/2),:], label = 'CGLS')
+plt.legend()
+plt.title('Middle Line Profiles')
+plt.show()
diff --git a/Wrappers/Python/demos/CompareAlgorithms/FISTA_vs_PDHG.py b/Wrappers/Python/demos/CompareAlgorithms/FISTA_vs_PDHG.py
new file mode 100644
index 0000000..c24ebac
--- /dev/null
+++ b/Wrappers/Python/demos/CompareAlgorithms/FISTA_vs_PDHG.py
@@ -0,0 +1,124 @@
+#========================================================================
+# Copyright 2019 Science Technology Facilities Council
+# Copyright 2019 University of Manchester
+#
+# This work is part of the Core Imaging Library developed by Science Technology
+# Facilities Council and University of Manchester
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0.txt
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+#=========================================================================
+
+
+"""
+Compare solutions of FISTA & PDHG algorithms
+
+
+Problem: min_x alpha * ||\grad x ||^{2}_{2} + || x - g ||_{1}
+
+ \alpha: Regularization parameter
+
+ \nabla: Gradient operator
+
+ g: Noisy Data with Salt & Pepper Noise
+
+"""
+
+from ccpi.framework import ImageData, ImageGeometry
+
+import numpy as np
+import numpy
+import matplotlib.pyplot as plt
+
+from ccpi.optimisation.algorithms import FISTA, PDHG
+
+from ccpi.optimisation.operators import BlockOperator, Gradient, Identity
+from ccpi.optimisation.functions import L2NormSquared, L1Norm, \
+ FunctionOperatorComposition, BlockFunction, ZeroFunction
+
+from skimage.util import random_noise
+
+
+# Create Ground truth and Noisy data
+N = 100
+
+data = np.zeros((N,N))
+data[round(N/4):round(3*N/4),round(N/4):round(3*N/4)] = 0.5
+data[round(N/8):round(7*N/8),round(3*N/8):round(5*N/8)] = 1
+data = ImageData(data)
+ig = ImageGeometry(voxel_num_x = N, voxel_num_y = N)
+ag = ig
+
+n1 = random_noise(data.as_array(), mode = 's&p', salt_vs_pepper = 0.9, amount=0.2)
+noisy_data = ImageData(n1)
+
+# Regularisation Parameter
+alpha = 5
+
+###############################################################################
+# Setup and run the FISTA algorithm
+operator = Gradient(ig)
+fidelity = L1Norm(b=noisy_data)
+regulariser = FunctionOperatorComposition(alpha * L2NormSquared(), operator)
+
+x_init = ig.allocate()
+opt = {'memopt':True}
+fista = FISTA(x_init=x_init , f=regulariser, g=fidelity, opt=opt)
+fista.max_iteration = 2000
+fista.update_objective_interval = 50
+fista.run(2000, verbose=False)
+###############################################################################
+
+
+###############################################################################
+# Setup and run the PDHG algorithm
+op1 = Gradient(ig)
+op2 = Identity(ig, ag)
+
+operator = BlockOperator(op1, op2, shape=(2,1) )
+f = BlockFunction(alpha * L2NormSquared(), fidelity)
+g = ZeroFunction()
+
+normK = operator.norm()
+
+sigma = 1
+tau = 1/(sigma*normK**2)
+
+pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma, memopt=True)
+pdhg.max_iteration = 2000
+pdhg.update_objective_interval = 200
+pdhg.run(2000, verbose=False)
+###############################################################################
+
+# Show results
+
+plt.figure(figsize=(10,10))
+
+plt.subplot(2,1,1)
+plt.imshow(pdhg.get_output().as_array())
+plt.title('PDHG reconstruction')
+
+plt.subplot(2,1,2)
+plt.imshow(fista.get_output().as_array())
+plt.title('FISTA reconstruction')
+
+plt.show()
+
+diff1 = pdhg.get_output() - fista.get_output()
+
+plt.imshow(diff1.abs().as_array())
+plt.title('Diff PDHG vs FISTA')
+plt.colorbar()
+plt.show()
+
+
diff --git a/Wrappers/Python/demos/FISTA_examples/FISTA_CGLS.py b/Wrappers/Python/demos/FISTA_examples/FISTA_CGLS.py
new file mode 100644
index 0000000..1a96a2d
--- /dev/null
+++ b/Wrappers/Python/demos/FISTA_examples/FISTA_CGLS.py
@@ -0,0 +1,215 @@
+#========================================================================
+# Copyright 2019 Science Technology Facilities Council
+# Copyright 2019 University of Manchester
+#
+# This work is part of the Core Imaging Library developed by Science Technology
+# Facilities Council and University of Manchester
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0.txt
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+#=========================================================================
+
+from ccpi.framework import AcquisitionGeometry
+from ccpi.optimisation.algorithms import FISTA
+from ccpi.optimisation.functions import IndicatorBox, ZeroFunction, \
+ L2NormSquared, FunctionOperatorComposition
+from ccpi.astra.operators import AstraProjectorSimple
+
+import numpy as np
+import matplotlib.pyplot as plt
+from ccpi.framework import TestData
+import os, sys
+from ccpi.optimisation.funcs import Norm2sq
+
+# Load Data
+loader = TestData(data_dir=os.path.join(sys.prefix, 'share','ccpi'))
+
+N = 50
+M = 50
+data = loader.load(TestData.SIMPLE_PHANTOM_2D, size=(N,M), scale=(0,1))
+
+ig = data.geometry
+
+# Show Ground Truth
+plt.figure(figsize=(5,5))
+plt.imshow(data.as_array())
+plt.title('Ground Truth')
+plt.colorbar()
+plt.show()
+
+#%%
+# Set up AcquisitionGeometry object to hold the parameters of the measurement
+# setup geometry: # Number of angles, the actual angles from 0 to
+# pi for parallel beam and 0 to 2pi for fanbeam, set the width of a detector
+# pixel relative to an object pixel, the number of detector pixels, and the
+# source-origin and origin-detector distance (here the origin-detector distance
+# set to 0 to simulate a "virtual detector" with same detector pixel size as
+# object pixel size).
+
+#device = input('Available device: GPU==1 / CPU==0 ')
+
+#if device=='1':
+# dev = 'gpu'
+#else:
+# dev = 'cpu'
+
+test_case = 1
+dev = 'cpu'
+
+if test_case==1:
+
+ detectors = N
+ angles_num = N
+ det_w = 1.0
+
+ angles = np.linspace(0, np.pi, angles_num, endpoint=False)
+ ag = AcquisitionGeometry('parallel',
+ '2D',
+ angles,
+ detectors,det_w)
+
+elif test_case==2:
+
+ SourceOrig = 200
+ OrigDetec = 0
+ angles = np.linspace(0,2*np.pi,angles_num)
+ ag = AcquisitionGeometry('cone',
+ '2D',
+ angles,
+ detectors,
+ det_w,
+ dist_source_center=SourceOrig,
+ dist_center_detector=OrigDetec)
+else:
+ NotImplemented
+
+# Set up Operator object combining the ImageGeometry and AcquisitionGeometry
+# wrapping calls to ASTRA as well as specifying whether to use CPU or GPU.
+Aop = AstraProjectorSimple(ig, ag, dev)
+
+# Forward and backprojection are available as methods direct and adjoint. Here
+# generate test data b and do simple backprojection to obtain z.
+sin = Aop.direct(data)
+back_proj = Aop.adjoint(sin)
+
+plt.figure(figsize=(5,5))
+plt.imshow(sin.array)
+plt.title('Simulated data')
+plt.show()
+
+plt.figure(figsize=(5,5))
+plt.imshow(back_proj.array)
+plt.title('Backprojected data')
+plt.show()
+
+#%%
+
+# Using the test data b, different reconstruction methods can now be set up as
+# demonstrated in the rest of this file. In general all methods need an initial
+# guess and some algorithm options to be set:
+
+f = FunctionOperatorComposition(L2NormSquared(b=sin), Aop)
+#f = Norm2sq(Aop, sin, c=0.5, memopt=True)
+g = ZeroFunction()
+
+x_init = ig.allocate()
+fista = FISTA(x_init=x_init, f=f, g=g)
+fista.max_iteration = 1000
+fista.update_objective_interval = 100
+fista.run(1000, verbose=True)
+
+plt.figure()
+plt.imshow(fista.get_output().as_array())
+plt.title('FISTA unconstrained')
+plt.colorbar()
+plt.show()
+
+
+# Run FISTA for least squares with lower/upper bound
+fista0 = FISTA(x_init=x_init, f=f, g=IndicatorBox(lower=0,upper=1))
+fista0.max_iteration = 1000
+fista0.update_objective_interval = 100
+fista0.run(1000, verbose=True)
+
+plt.figure()
+plt.imshow(fista0.get_output().as_array())
+plt.title('FISTA constrained in [0,1]')
+plt.colorbar()
+plt.show()
+
+#plt.figure()
+#plt.semilogy(fista0.objective)
+#plt.title('FISTA constrained in [0,1]')
+#plt.show()
+
+#%% Check with CVX solution
+
+import astra
+import numpy
+
+try:
+ from cvxpy import *
+ cvx_not_installable = True
+except ImportError:
+ cvx_not_installable = False
+
+if cvx_not_installable:
+
+ ##Construct problem
+ u = Variable(N*N)
+
+ # create matrix representation for Astra operator
+ vol_geom = astra.create_vol_geom(N, N)
+ proj_geom = astra.create_proj_geom('parallel', 1.0, detectors, angles)
+
+ proj_id = astra.create_projector('linear', proj_geom, vol_geom)
+
+ matrix_id = astra.projector.matrix(proj_id)
+
+ ProjMat = astra.matrix.get(matrix_id)
+
+ tmp = sin.as_array().ravel()
+
+ fidelity = 0.5 * sum_squares(ProjMat * u - tmp)
+
+ solver = MOSEK
+ obj = Minimize(fidelity)
+ constraints = [u>=0, u<=1]
+ prob = Problem(obj, constraints=constraints)
+ result = prob.solve(verbose = True, solver = solver)
+
+ diff_cvx = numpy.abs( fista0.get_output().as_array() - np.reshape(u.value, (N,M) ))
+
+ plt.figure(figsize=(15,15))
+ plt.subplot(3,1,1)
+ plt.imshow(fista0.get_output().as_array())
+ plt.title('FISTA solution')
+ plt.colorbar()
+ plt.subplot(3,1,2)
+ plt.imshow(np.reshape(u.value, (N, M)))
+ plt.title('CVX solution')
+ plt.colorbar()
+ plt.subplot(3,1,3)
+ plt.imshow(diff_cvx)
+ plt.title('Difference')
+ plt.colorbar()
+ plt.show()
+
+ plt.plot(np.linspace(0,N,M), fista0.get_output().as_array()[int(N/2),:], label = 'FISTA')
+ plt.plot(np.linspace(0,N,M), np.reshape(u.value, (N,M) )[int(N/2),:], label = 'CVX')
+ plt.legend()
+ plt.title('Middle Line Profiles')
+ plt.show()
+
+ print('Primal Objective (CVX) {} '.format(obj.value))
+ print('Primal Objective (FISTA) {} '.format(fista0.loss[1])) \ No newline at end of file
diff --git a/Wrappers/Python/demos/FISTA_examples/FISTA_Tikhonov_Poisson_Denoising.py b/Wrappers/Python/demos/FISTA_examples/FISTA_Tikhonov_Poisson_Denoising.py
new file mode 100644
index 0000000..6007990
--- /dev/null
+++ b/Wrappers/Python/demos/FISTA_examples/FISTA_Tikhonov_Poisson_Denoising.py
@@ -0,0 +1,201 @@
+#========================================================================
+# Copyright 2019 Science Technology Facilities Council
+# Copyright 2019 University of Manchester
+#
+# This work is part of the Core Imaging Library developed by Science Technology
+# Facilities Council and University of Manchester
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0.txt
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+#=========================================================================
+
+"""
+
+"Tikhonov regularization" for Poisson denoising using FISTA algorithm:
+
+Problem: min_x, x>0 \alpha * ||\nabla x||_{2}^{2} + \int x - g * log(x)
+
+ \alpha: Regularization parameter
+
+ \nabla: Gradient operator
+
+ g: Noisy Data with Poisson Noise
+
+
+"""
+
+from ccpi.framework import ImageData
+
+import numpy as np
+import numpy
+import matplotlib.pyplot as plt
+
+from ccpi.optimisation.algorithms import FISTA
+
+from ccpi.optimisation.operators import Gradient
+from ccpi.optimisation.functions import KullbackLeibler, L2NormSquared, FunctionOperatorComposition
+
+from ccpi.framework import TestData
+import os, sys
+from skimage.util import random_noise
+
+loader = TestData(data_dir=os.path.join(sys.prefix, 'share','ccpi'))
+
+# Load Data
+N = 100
+M = 100
+data = loader.load(TestData.SIMPLE_PHANTOM_2D, size=(N,M), scale=(0,1))
+
+ig = data.geometry
+ag = ig
+
+# Create Noisy data with Poisson noise
+n1 = random_noise(data.as_array(), mode = 'poisson', seed = 10)
+noisy_data = ImageData(n1)
+
+# Show Ground Truth and Noisy Data
+plt.figure(figsize=(10,10))
+plt.subplot(2,1,1)
+plt.imshow(data.as_array())
+plt.title('Ground Truth')
+plt.colorbar()
+plt.subplot(2,1,2)
+plt.imshow(noisy_data.as_array())
+plt.title('Noisy Data')
+plt.colorbar()
+plt.show()
+
+# Regularisation Parameter
+alpha = 10
+
+# Setup and run the FISTA algorithm
+operator = Gradient(ig)
+fid = KullbackLeibler(noisy_data)
+
+def KL_Prox_PosCone(x, tau, out=None):
+
+ if out is None:
+ tmp = 0.5 *( (x - fid.bnoise - tau) + ( (x + fid.bnoise - tau)**2 + 4*tau*fid.b ) .sqrt() )
+ return tmp.maximum(0)
+ else:
+ tmp = 0.5 *( (x - fid.bnoise - tau) +
+ ( (x + fid.bnoise - tau)**2 + 4*tau*fid.b ) .sqrt()
+ )
+ x.add(fid.bnoise, out=out)
+ out -= tau
+ out *= out
+ tmp = fid.b * (4 * tau)
+ out.add(tmp, out=out)
+ out.sqrt(out=out)
+
+ x.subtract(fid.bnoise, out=tmp)
+ tmp -= tau
+
+ out += tmp
+
+ out *= 0.5
+
+ # ADD the constraint here
+ out.maximum(0, out=out)
+
+fid.proximal = KL_Prox_PosCone
+
+reg = FunctionOperatorComposition(alpha * L2NormSquared(), operator)
+
+x_init = ig.allocate()
+fista = FISTA(x_init=x_init , f=reg, g=fid)
+fista.max_iteration = 2000
+fista.update_objective_interval = 500
+fista.run(2000, verbose=True)
+
+# Show results
+plt.figure(figsize=(15,15))
+plt.subplot(3,1,1)
+plt.imshow(data.as_array())
+plt.title('Ground Truth')
+plt.colorbar()
+plt.subplot(3,1,2)
+plt.imshow(noisy_data.as_array())
+plt.title('Noisy Data')
+plt.colorbar()
+plt.subplot(3,1,3)
+plt.imshow(fista.get_output().as_array())
+plt.title('Reconstruction')
+plt.colorbar()
+plt.show()
+
+plt.plot(np.linspace(0,N,M), data.as_array()[int(N/2),:], label = 'GTruth')
+plt.plot(np.linspace(0,N,M), fista.get_output().as_array()[int(N/2),:], label = 'Reconstruction')
+plt.legend()
+plt.title('Middle Line Profiles')
+plt.show()
+
+
+#%% Check with CVX solution
+
+from ccpi.optimisation.operators import SparseFiniteDiff
+
+try:
+ from cvxpy import *
+ cvx_not_installable = True
+except ImportError:
+ cvx_not_installable = False
+
+
+if cvx_not_installable:
+
+ ##Construct problem
+ u1 = Variable(ig.shape)
+ q = Variable()
+
+ DY = SparseFiniteDiff(ig, direction=0, bnd_cond='Neumann')
+ DX = SparseFiniteDiff(ig, direction=1, bnd_cond='Neumann')
+
+ regulariser = alpha * sum_squares(norm(vstack([DX.matrix() * vec(u1), DY.matrix() * vec(u1)]), 2, axis = 0))
+ fidelity = sum(kl_div(noisy_data.as_array(), u1))
+
+ constraints = [q>=fidelity, u1>=0]
+
+ solver = SCS
+ obj = Minimize( regulariser + q)
+ prob = Problem(obj, constraints)
+ result = prob.solve(verbose = True, solver = solver)
+
+ diff_cvx = numpy.abs( fista.get_output().as_array() - u1.value )
+
+ plt.figure(figsize=(15,15))
+ plt.subplot(3,1,1)
+ plt.imshow(fista.get_output().as_array())
+ plt.title('FISTA solution')
+ plt.colorbar()
+ plt.subplot(3,1,2)
+ plt.imshow(u1.value)
+ plt.title('CVX solution')
+ plt.colorbar()
+ plt.subplot(3,1,3)
+ plt.imshow(diff_cvx)
+ plt.title('Difference')
+ plt.colorbar()
+ plt.show()
+
+ plt.plot(np.linspace(0,N,M), fista.get_output().as_array()[int(N/2),:], label = 'FISTA')
+ plt.plot(np.linspace(0,N,M), u1.value[int(N/2),:], label = 'CVX')
+ plt.legend()
+ plt.title('Middle Line Profiles')
+ plt.show()
+
+ print('Primal Objective (CVX) {} '.format(obj.value))
+ print('Primal Objective (FISTA) {} '.format(fista.loss[1]))
+
+
+
diff --git a/Wrappers/Python/demos/PDHG_examples/ColorbayDemo.py b/Wrappers/Python/demos/PDHG_examples/ColorbayDemo.py
new file mode 100644
index 0000000..e69060f
--- /dev/null
+++ b/Wrappers/Python/demos/PDHG_examples/ColorbayDemo.py
@@ -0,0 +1,273 @@
+#========================================================================
+# Copyright 2019 Science Technology Facilities Council
+# Copyright 2019 University of Manchester
+#
+# This work is part of the Core Imaging Library developed by Science Technology
+# Facilities Council and University of Manchester
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0.txt
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+#=========================================================================
+
+
+from ccpi.framework import ImageGeometry, ImageData, AcquisitionGeometry, AcquisitionData, BlockDataContainer
+
+import numpy as numpy
+import matplotlib.pyplot as plt
+
+from ccpi.optimisation.algorithms import PDHG, CGLS
+from ccpi.optimisation.algs import CGLS as CGLS_old
+
+from ccpi.optimisation.operators import BlockOperator, Gradient
+from ccpi.optimisation.functions import ZeroFunction, L2NormSquared, \
+ MixedL21Norm, BlockFunction
+
+from ccpi.astra.operators import AstraProjectorMC
+from scipy.io import loadmat
+import h5py
+
+#%%
+
+phantom = 'powder'
+
+if phantom == 'carbon':
+ pathname = '/media/newhd/shared/Data/ColourBay/spectral_data_sets/CarbonPd/'
+ filename = 'carbonPd_full_sinogram_stripes_removed.mat'
+ X = loadmat(pathname + filename)
+ X = numpy.transpose(X['SS'],(3,1,2,0))
+ X = X[80:100] # delete this to take all channels
+
+elif phantom == 'powder':
+ pathname = '/media/newhd/shared/DataProcessed/'
+ filename = 'S_180.mat'
+ path = pathname + filename
+ arrays = {}
+ f = h5py.File(path)
+ for k, v in f.items():
+ arrays[k] = numpy.array(v)
+ XX = arrays['S']
+ X = numpy.transpose(XX,(0,2,1,3))
+ X = X[100:120]
+
+
+#%% Setup Geometry of Colorbay
+
+num_channels = X.shape[0]
+num_pixels_h = X.shape[3]
+num_pixels_v = X.shape[2]
+num_angles = X.shape[1]
+
+# Display a single projection in a single channel
+plt.imshow(X[5,5,:,:])
+plt.title('Example of a projection image in one channel' )
+plt.show()
+
+# Set angles to use
+angles = numpy.linspace(-numpy.pi,numpy.pi,num_angles,endpoint=False)
+
+# Define full 3D acquisition geometry and data container.
+# Geometric info is taken from the txt-file in the same dir as the mat-file
+ag = AcquisitionGeometry('cone',
+ '3D',
+ angles,
+ pixel_num_h=num_pixels_h,
+ pixel_size_h=0.25,
+ pixel_num_v=num_pixels_v,
+ pixel_size_v=0.25,
+ dist_source_center=233.0,
+ dist_center_detector=245.0,
+ channels=num_channels)
+data = AcquisitionData(X, geometry=ag)
+
+# Reduce to central slice by extracting relevant parameters from data and its
+# geometry. Perhaps create function to extract central slice automatically?
+data2d = data.subset(vertical=40)
+ag2d = AcquisitionGeometry('cone',
+ '2D',
+ ag.angles,
+ pixel_num_h=ag.pixel_num_h,
+ pixel_size_h=ag.pixel_size_h,
+ pixel_num_v=1,
+ pixel_size_v=ag.pixel_size_h,
+ dist_source_center=ag.dist_source_center,
+ dist_center_detector=ag.dist_center_detector,
+ channels=ag.channels)
+data2d.geometry = ag2d
+
+# Set up 2D Image Geometry.
+# First need the geometric magnification to scale the voxel size relative
+# to the detector pixel size.
+mag = (ag.dist_source_center + ag.dist_center_detector)/ag.dist_source_center
+ig2d = ImageGeometry(voxel_num_x=ag2d.pixel_num_h,
+ voxel_num_y=ag2d.pixel_num_h,
+ voxel_size_x=ag2d.pixel_size_h/mag,
+ voxel_size_y=ag2d.pixel_size_h/mag,
+ channels=X.shape[0])
+
+# Create GPU multichannel projector/backprojector operator with ASTRA.
+Aall = AstraProjectorMC(ig2d,ag2d,'gpu')
+
+# Compute and simple backprojction and display one channel as image.
+Xbp = Aall.adjoint(data2d)
+plt.imshow(Xbp.subset(channel=5).array)
+plt.show()
+
+#%% CGLS
+
+def callback(iteration, objective, x):
+ plt.imshow(x.as_array()[5])
+ plt.colorbar()
+ plt.show()
+
+x_init = ig2d.allocate()
+cgls1 = CGLS(x_init=x_init, operator=Aall, data=data2d)
+cgls1.max_iteration = 100
+cgls1.update_objective_interval = 1
+cgls1.run(5,verbose=True, callback = callback)
+
+plt.imshow(cgls1.get_output().subset(channel=5).array)
+plt.title('CGLS')
+plt.show()
+
+#%% Tikhonov
+
+alpha = 2.5
+Grad = Gradient(ig2d, correlation=Gradient.CORRELATION_SPACE) # use also CORRELATION_SPACECHANNEL
+
+# Form Tikhonov as a Block CGLS structure
+op_CGLS = BlockOperator( Aall, alpha * Grad, shape=(2,1))
+block_data = BlockDataContainer(data2d, Grad.range_geometry().allocate())
+
+cgls2 = CGLS(x_init=x_init, operator=op_CGLS, data=block_data)
+cgls2.max_iteration = 100
+cgls2.update_objective_interval = 1
+
+cgls2.run(10,verbose=True, callback=callback)
+
+plt.imshow(cgls2.get_output().subset(channel=5).array)
+plt.title('Tikhonov')
+plt.show()
+
+#%% Total Variation
+
+# Regularisation Parameter
+#alpha_TV = 0.08 # for carbon
+alpha_TV = 0.08 # for powder
+
+# Create operators
+op1 = Gradient(ig2d, correlation=Gradient.CORRELATION_SPACE)
+op2 = Aall
+
+# Create BlockOperator
+operator = BlockOperator(op1, op2, shape=(2,1) )
+
+# Create functions
+f1 = alpha_TV * MixedL21Norm()
+f2 = 0.5 * L2NormSquared(b=data2d)
+f = BlockFunction(f1, f2)
+g = ZeroFunction()
+
+# Compute operator Norm
+#normK = 8.70320267279591 # For powder Run one time no need to compute again takes time
+normK = 14.60320657253632 # for carbon
+
+# Primal & dual stepsizes
+sigma = 1
+tau = 1/(sigma*normK**2)
+
+# Setup and run the PDHG algorithm
+pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma)
+pdhg.max_iteration = 2000
+pdhg.update_objective_interval = 100
+pdhg.run(1000, verbose =True, callback=callback)
+
+
+#%% Show sinograms
+channel_ind = [10,15,19]
+
+plt.figure(figsize=(15,15))
+
+plt.subplot(4,3,1)
+plt.imshow(data2d.subset(channel = channel_ind[0]).as_array())
+plt.title('Channel {}'.format(channel_ind[0]))
+plt.colorbar()
+
+plt.subplot(4,3,2)
+plt.imshow(data2d.subset(channel = channel_ind[1]).as_array())
+plt.title('Channel {}'.format(channel_ind[1]))
+plt.colorbar()
+
+plt.subplot(4,3,3)
+plt.imshow(data2d.subset(channel = channel_ind[2]).as_array())
+plt.title('Channel {}'.format(channel_ind[2]))
+plt.colorbar()
+
+###############################################################################
+# Show CGLS
+plt.subplot(4,3,4)
+plt.imshow(cgls1.get_output().subset(channel = channel_ind[0]).as_array())
+plt.colorbar()
+
+plt.subplot(4,3,5)
+plt.imshow(cgls1.get_output().subset(channel = channel_ind[1]).as_array())
+plt.colorbar()
+
+plt.subplot(4,3,6)
+plt.imshow(cgls1.get_output().subset(channel = channel_ind[2]).as_array())
+plt.colorbar()
+
+###############################################################################
+# Show Tikhonov
+
+plt.subplot(4,3,7)
+plt.imshow(cgls2.get_output().subset(channel = channel_ind[0]).as_array())
+plt.colorbar()
+
+plt.subplot(4,3,8)
+plt.imshow(cgls2.get_output().subset(channel = channel_ind[1]).as_array())
+plt.colorbar()
+
+plt.subplot(4,3,9)
+plt.imshow(cgls2.get_output().subset(channel = channel_ind[2]).as_array())
+plt.colorbar()
+
+
+###############################################################################
+# Show Total variation
+
+plt.subplot(4,3,10)
+plt.imshow(pdhg.get_output().subset(channel = channel_ind[0]).as_array())
+plt.colorbar()
+
+plt.subplot(4,3,11)
+plt.imshow(pdhg.get_output().subset(channel = channel_ind[1]).as_array())
+plt.colorbar()
+
+plt.subplot(4,3,12)
+plt.imshow(pdhg.get_output().subset(channel = channel_ind[2]).as_array())
+plt.colorbar()
+
+
+###############################################################################
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Wrappers/Python/demos/PDHG_examples/GatherAll/PDHG_TGV_Denoising.py b/Wrappers/Python/demos/PDHG_examples/GatherAll/PDHG_TGV_Denoising.py
new file mode 100755
index 0000000..9dbcf3e
--- /dev/null
+++ b/Wrappers/Python/demos/PDHG_examples/GatherAll/PDHG_TGV_Denoising.py
@@ -0,0 +1,282 @@
+#========================================================================
+# Copyright 2019 Science Technology Facilities Council
+# Copyright 2019 University of Manchester
+#
+# This work is part of the Core Imaging Library developed by Science Technology
+# Facilities Council and University of Manchester
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0.txt
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+#=========================================================================
+"""
+
+Total Generalised Variation (TGV) Denoising using PDHG algorithm:
+
+
+Problem: min_{u} \alpha * ||\nabla u - w||_{2,1} +
+ \beta * || E u ||_{2,1} +
+ Fidelity(u, g)
+
+ \nabla: Gradient operator
+ E: Symmetrized Gradient operator
+ \alpha: Regularization parameter
+ \beta: Regularization parameter
+
+ g: Noisy Data
+
+ Fidelity = 1) L2NormSquarred ( \frac{1}{2} * || u - g ||_{2}^{2} ) if Noise is Gaussian
+ 2) L1Norm ( ||u - g||_{1} )if Noise is Salt & Pepper
+ 3) Kullback Leibler (\int u - g * log(u) + Id_{u>0}) if Noise is Poisson
+
+
+ Method = 0 ( PDHG - split ) : K = [ \nabla, - Identity
+ ZeroOperator, E
+ Identity, ZeroOperator]
+
+
+ Method = 1 (PDHG - explicit ): K = [ \nabla, - Identity
+ ZeroOperator, E ]
+
+ Default: TGV denoising
+ noise = Gaussian
+ Fidelity = L2NormSquarred
+ method = 0
+
+"""
+
+from ccpi.framework import ImageData
+
+import numpy as np
+import numpy
+import matplotlib.pyplot as plt
+
+from ccpi.optimisation.algorithms import PDHG
+
+from ccpi.optimisation.operators import BlockOperator, Identity, \
+ Gradient, SymmetrizedGradient, ZeroOperator
+from ccpi.optimisation.functions import ZeroFunction, L1Norm, \
+ MixedL21Norm, BlockFunction, KullbackLeibler, L2NormSquared
+
+from ccpi.framework import TestData
+import os, sys
+if int(numpy.version.version.split('.')[1]) > 12:
+ from skimage.util import random_noise
+else:
+ from demoutil import random_noise
+
+# user supplied input
+if len(sys.argv) > 1:
+ which_noise = int(sys.argv[1])
+else:
+ which_noise = 0
+print ("Applying {} noise")
+
+if len(sys.argv) > 2:
+ method = sys.argv[2]
+else:
+ method = '0'
+print ("method ", method)
+
+
+loader = TestData(data_dir=os.path.join(sys.prefix, 'share','ccpi'))
+data = loader.load(TestData.SHAPES)
+ig = data.geometry
+ag = ig
+
+# Create noisy data.
+noises = ['gaussian', 'poisson', 's&p']
+noise = noises[which_noise]
+if noise == 's&p':
+ n1 = random_noise(data.as_array(), mode = noise, salt_vs_pepper = 0.9, amount=0.2, seed=10)
+elif noise == 'poisson':
+ scale = 5
+ n1 = random_noise( data.as_array()/scale, mode = noise, seed = 10)*scale
+elif noise == 'gaussian':
+ n1 = random_noise(data.as_array(), mode = noise, seed = 10)
+else:
+ raise ValueError('Unsupported Noise ', noise)
+noisy_data = ImageData(n1)
+
+# Show Ground Truth and Noisy Data
+plt.figure(figsize=(10,5))
+plt.subplot(1,2,1)
+plt.imshow(data.as_array())
+plt.title('Ground Truth')
+plt.colorbar()
+plt.subplot(1,2,2)
+plt.imshow(noisy_data.as_array())
+plt.title('Noisy Data')
+plt.colorbar()
+plt.show()
+
+# Regularisation Parameter depending on the noise distribution
+if noise == 's&p':
+ alpha = 0.8
+elif noise == 'poisson':
+ alpha = .3
+elif noise == 'gaussian':
+ alpha = .2
+
+# TODO add ref why this choice
+beta = 2 * alpha
+
+# Fidelity
+if noise == 's&p':
+ f3 = L1Norm(b=noisy_data)
+elif noise == 'poisson':
+ f3 = KullbackLeibler(noisy_data)
+elif noise == 'gaussian':
+ f3 = 0.5 * L2NormSquared(b=noisy_data)
+
+if method == '0':
+
+ # Create operators
+ op11 = Gradient(ig)
+ op12 = Identity(op11.range_geometry())
+
+ op22 = SymmetrizedGradient(op11.domain_geometry())
+ op21 = ZeroOperator(ig, op22.range_geometry())
+
+ op31 = Identity(ig, ag)
+ op32 = ZeroOperator(op22.domain_geometry(), ag)
+
+ operator = BlockOperator(op11, -1*op12, op21, op22, op31, op32, shape=(3,2) )
+
+ f1 = alpha * MixedL21Norm()
+ f2 = beta * MixedL21Norm()
+
+ f = BlockFunction(f1, f2, f3)
+ g = ZeroFunction()
+
+else:
+
+ # Create operators
+ op11 = Gradient(ig)
+ op12 = Identity(op11.range_geometry())
+ op22 = SymmetrizedGradient(op11.domain_geometry())
+ op21 = ZeroOperator(ig, op22.range_geometry())
+
+ operator = BlockOperator(op11, -1*op12, op21, op22, shape=(2,2) )
+
+ f1 = alpha * MixedL21Norm()
+ f2 = beta * MixedL21Norm()
+
+ f = BlockFunction(f1, f2)
+ g = BlockFunction(f3, ZeroFunction())
+
+# Compute operator Norm
+normK = operator.norm()
+
+# Primal & dual stepsizes
+sigma = 1
+tau = 1/(sigma*normK**2)
+
+# Setup and run the PDHG algorithm
+pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma)
+pdhg.max_iteration = 2000
+pdhg.update_objective_interval = 100
+pdhg.run(2000)
+
+# Show results
+plt.figure(figsize=(20,5))
+plt.subplot(1,4,1)
+plt.imshow(data.subset(channel=0).as_array())
+plt.title('Ground Truth')
+plt.colorbar()
+plt.subplot(1,4,2)
+plt.imshow(noisy_data.subset(channel=0).as_array())
+plt.title('Noisy Data')
+plt.colorbar()
+plt.subplot(1,4,3)
+plt.imshow(pdhg.get_output()[0].as_array())
+plt.title('TGV Reconstruction')
+plt.colorbar()
+plt.subplot(1,4,4)
+plt.plot(np.linspace(0,ig.shape[1],ig.shape[1]), data.as_array()[int(ig.shape[0]/2),:], label = 'GTruth')
+plt.plot(np.linspace(0,ig.shape[1],ig.shape[1]), pdhg.get_output()[0].as_array()[int(ig.shape[0]/2),:], label = 'TGV reconstruction')
+plt.legend()
+plt.title('Middle Line Profiles')
+plt.show()
+
+#%% Check with CVX solution
+
+from ccpi.optimisation.operators import SparseFiniteDiff
+
+try:
+ from cvxpy import *
+ cvx_not_installable = True
+except ImportError:
+ cvx_not_installable = False
+
+if cvx_not_installable:
+
+ u = Variable(ig.shape)
+ w1 = Variable(ig.shape)
+ w2 = Variable(ig.shape)
+
+ # create TGV regulariser
+ DY = SparseFiniteDiff(ig, direction=0, bnd_cond='Neumann')
+ DX = SparseFiniteDiff(ig, direction=1, bnd_cond='Neumann')
+
+ regulariser = alpha * sum(norm(vstack([DX.matrix() * vec(u) - vec(w1), \
+ DY.matrix() * vec(u) - vec(w2)]), 2, axis = 0)) + \
+ beta * sum(norm(vstack([ DX.matrix().transpose() * vec(w1), DY.matrix().transpose() * vec(w2), \
+ 0.5 * ( DX.matrix().transpose() * vec(w2) + DY.matrix().transpose() * vec(w1) ), \
+ 0.5 * ( DX.matrix().transpose() * vec(w2) + DY.matrix().transpose() * vec(w1) ) ]), 2, axis = 0 ) )
+
+ constraints = []
+
+ # choose solver
+ if 'MOSEK' in installed_solvers():
+ solver = MOSEK
+ else:
+ solver = SCS
+
+ # fidelity
+ if noise == 's&p':
+ fidelity = pnorm( u - noisy_data.as_array(),1)
+ elif noise == 'poisson':
+ fidelity = sum(kl_div(noisy_data.as_array(), u))
+ solver = SCS
+ elif noise == 'gaussian':
+ fidelity = 0.5 * sum_squares(noisy_data.as_array() - u)
+
+ obj = Minimize( regulariser + fidelity)
+ prob = Problem(obj)
+ result = prob.solve(verbose = True, solver = solver)
+
+ diff_cvx = numpy.abs( pdhg.get_output()[0].as_array() - u.value )
+
+ plt.figure(figsize=(15,15))
+ plt.subplot(3,1,1)
+ plt.imshow(pdhg.get_output()[0].as_array())
+ plt.title('PDHG solution')
+ plt.colorbar()
+ plt.subplot(3,1,2)
+ plt.imshow(u.value)
+ plt.title('CVX solution')
+ plt.colorbar()
+ plt.subplot(3,1,3)
+ plt.imshow(diff_cvx)
+ plt.title('Difference')
+ plt.colorbar()
+ plt.show()
+
+ plt.plot(np.linspace(0,ig.shape[1],ig.shape[1]), pdhg.get_output()[0].as_array()[int(ig.shape[0]/2),:], label = 'PDHG')
+ plt.plot(np.linspace(0,ig.shape[1],ig.shape[1]), u.value[int(ig.shape[0]/2),:], label = 'CVX')
+ plt.legend()
+ plt.title('Middle Line Profiles')
+ plt.show()
+
+ print('Primal Objective (CVX) {} '.format(obj.value))
+ print('Primal Objective (PDHG) {} '.format(pdhg.objective[-1][0])) \ No newline at end of file
diff --git a/Wrappers/Python/demos/PDHG_examples/GatherAll/PDHG_TV_Denoising.py b/Wrappers/Python/demos/PDHG_examples/GatherAll/PDHG_TV_Denoising.py
new file mode 100755
index 0000000..6937fa0
--- /dev/null
+++ b/Wrappers/Python/demos/PDHG_examples/GatherAll/PDHG_TV_Denoising.py
@@ -0,0 +1,280 @@
+#========================================================================
+# Copyright 2019 Science Technology Facilities Council
+# Copyright 2019 University of Manchester
+#
+# This work is part of the Core Imaging Library developed by Science Technology
+# Facilities Council and University of Manchester
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0.txt
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+#=========================================================================
+
+"""
+
+Total Variation Denoising using PDHG algorithm:
+
+
+Problem: min_{u}, \alpha * ||\nabla u||_{2,1} + Fidelity(u, g)
+
+ \alpha: Regularization parameter
+
+ \nabla: Gradient operator
+
+ g: Noisy Data
+
+ Fidelity = 1) L2NormSquarred ( \frac{1}{2} * || u - g ||_{2}^{2} ) if Noise is Gaussian
+ 2) L1Norm ( ||u - g||_{1} )if Noise is Salt & Pepper
+ 3) Kullback Leibler (\int u - g * log(u) + Id_{u>0}) if Noise is Poisson
+
+ Method = 0 ( PDHG - split ) : K = [ \nabla,
+ Identity]
+
+
+ Method = 1 (PDHG - explicit ): K = \nabla
+
+
+ Default: ROF denoising
+ noise = Gaussian
+ Fidelity = L2NormSquarred
+ method = 0
+
+
+"""
+
+import numpy as np
+import numpy
+import matplotlib.pyplot as plt
+
+from ccpi.optimisation.algorithms import PDHG
+
+from ccpi.optimisation.operators import BlockOperator, Identity, Gradient
+from ccpi.optimisation.functions import ZeroFunction, L1Norm, \
+ MixedL21Norm, BlockFunction, L2NormSquared,\
+ KullbackLeibler
+from ccpi.framework import TestData
+import os, sys
+#import scipy.io
+if int(numpy.version.version.split('.')[1]) > 12:
+ from skimage.util import random_noise
+else:
+ from demoutil import random_noise
+
+# user supplied input
+if len(sys.argv) > 1:
+ which_noise = int(sys.argv[1])
+else:
+ which_noise = 0
+print ("Applying {} noise")
+
+if len(sys.argv) > 2:
+ method = sys.argv[2]
+else:
+ method = '0'
+print ("method ", method)
+
+
+loader = TestData(data_dir=os.path.join(sys.prefix, 'share','ccpi'))
+data = loader.load(TestData.SHAPES, size=(50,50))
+ig = data.geometry
+ag = ig
+
+# Create noisy data.
+noises = ['gaussian', 'poisson', 's&p']
+noise = noises[which_noise]
+if noise == 's&p':
+ n1 = random_noise(data.as_array(), mode = noise, salt_vs_pepper = 0.9, amount=0.2)
+elif noise == 'poisson':
+ scale = 5
+ n1 = random_noise( data.as_array()/scale, mode = noise, seed = 10)*scale
+elif noise == 'gaussian':
+ n1 = random_noise(data.as_array(), mode = noise, seed = 10)
+else:
+ raise ValueError('Unsupported Noise ', noise)
+noisy_data = ig.allocate()
+noisy_data.fill(n1)
+
+# Show Ground Truth and Noisy Data
+plt.figure(figsize=(10,5))
+plt.subplot(1,2,1)
+plt.imshow(data.as_array())
+plt.title('Ground Truth')
+plt.colorbar()
+plt.subplot(1,2,2)
+plt.imshow(noisy_data.as_array())
+plt.title('Noisy Data')
+plt.colorbar()
+plt.show()
+
+
+# Regularisation Parameter depending on the noise distribution
+if noise == 's&p':
+ alpha = 0.8
+elif noise == 'poisson':
+ alpha = 1
+elif noise == 'gaussian':
+ alpha = .3
+
+# fidelity
+if noise == 's&p':
+ f2 = L1Norm(b=noisy_data)
+elif noise == 'poisson':
+ f2 = KullbackLeibler(noisy_data)
+elif noise == 'gaussian':
+ f2 = 0.5 * L2NormSquared(b=noisy_data)
+
+if method == '0':
+
+ # Create operators
+ op1 = Gradient(ig, correlation=Gradient.CORRELATION_SPACE)
+ op2 = Identity(ig, ag)
+
+ # Create BlockOperator
+ operator = BlockOperator(op1, op2, shape=(2,1) )
+
+ # Create functions
+ f = BlockFunction(alpha * MixedL21Norm(), f2)
+ g = ZeroFunction()
+
+else:
+
+ operator = Gradient(ig)
+ f = alpha * MixedL21Norm()
+ g = f2
+
+
+# Compute operator Norm
+normK = operator.norm()
+
+# Primal & dual stepsizes
+sigma = 1
+tau = 1/(sigma*normK**2)
+
+
+# Setup and run the PDHG algorithm
+pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma)
+pdhg.max_iteration = 2000
+pdhg.update_objective_interval = 100
+pdhg.run(2000)
+
+
+if data.geometry.channels > 1:
+ plt.figure(figsize=(20,15))
+ for row in range(data.geometry.channels):
+
+ plt.subplot(3,4,1+row*4)
+ plt.imshow(data.subset(channel=row).as_array())
+ plt.title('Ground Truth')
+ plt.colorbar()
+ plt.subplot(3,4,2+row*4)
+ plt.imshow(noisy_data.subset(channel=row).as_array())
+ plt.title('Noisy Data')
+ plt.colorbar()
+ plt.subplot(3,4,3+row*4)
+ plt.imshow(pdhg.get_output().subset(channel=row).as_array())
+ plt.title('TV Reconstruction')
+ plt.colorbar()
+ plt.subplot(3,4,4+row*4)
+ plt.plot(np.linspace(0,ig.shape[1],ig.shape[1]), data.subset(channel=row).as_array()[int(N/2),:], label = 'GTruth')
+ plt.plot(np.linspace(0,ig.shape[1],ig.shape[1]), pdhg.get_output().subset(channel=row).as_array()[int(N/2),:], label = 'TV reconstruction')
+ plt.legend()
+ plt.title('Middle Line Profiles')
+ plt.show()
+
+else:
+ plt.figure(figsize=(20,5))
+ plt.subplot(1,4,1)
+ plt.imshow(data.subset(channel=0).as_array())
+ plt.title('Ground Truth')
+ plt.colorbar()
+ plt.subplot(1,4,2)
+ plt.imshow(noisy_data.subset(channel=0).as_array())
+ plt.title('Noisy Data')
+ plt.colorbar()
+ plt.subplot(1,4,3)
+ plt.imshow(pdhg.get_output().subset(channel=0).as_array())
+ plt.title('TV Reconstruction')
+ plt.colorbar()
+ plt.subplot(1,4,4)
+ plt.plot(np.linspace(0,ig.shape[1],ig.shape[1]), data.as_array()[int(ig.shape[0]/2),:], label = 'GTruth')
+ plt.plot(np.linspace(0,ig.shape[1],ig.shape[1]), pdhg.get_output().as_array()[int(ig.shape[0]/2),:], label = 'TV reconstruction')
+ plt.legend()
+ plt.title('Middle Line Profiles')
+ plt.show()
+
+
+#%% Check with CVX solution
+
+from ccpi.optimisation.operators import SparseFiniteDiff
+
+try:
+ from cvxpy import *
+ cvx_not_installable = True
+except ImportError:
+ cvx_not_installable = False
+
+
+if cvx_not_installable:
+
+ ##Construct problem
+ u = Variable(ig.shape)
+
+ DY = SparseFiniteDiff(ig, direction=0, bnd_cond='Neumann')
+ DX = SparseFiniteDiff(ig, direction=1, bnd_cond='Neumann')
+
+ # Define Total Variation as a regulariser
+ regulariser = alpha * sum(norm(vstack([Constant(DX.matrix()) * vec(u), Constant(DY.matrix()) * vec(u)]), 2, axis = 0))
+
+ # choose solver
+ if 'MOSEK' in installed_solvers():
+ solver = MOSEK
+ else:
+ solver = SCS
+
+ # fidelity
+ if noise == 's&p':
+ fidelity = pnorm( u - noisy_data.as_array(),1)
+ elif noise == 'poisson':
+ fidelity = sum(kl_div(noisy_data.as_array(), u))
+ solver = SCS
+ elif noise == 'gaussian':
+ fidelity = 0.5 * sum_squares(noisy_data.as_array() - u)
+
+ obj = Minimize( regulariser + fidelity)
+ prob = Problem(obj)
+ result = prob.solve(verbose = True, solver = solver)
+
+ diff_cvx = numpy.abs( pdhg.get_output().as_array() - u.value )
+
+ plt.figure(figsize=(15,15))
+ plt.subplot(3,1,1)
+ plt.imshow(pdhg.get_output().as_array())
+ plt.title('PDHG solution')
+ plt.colorbar()
+ plt.subplot(3,1,2)
+ plt.imshow(u.value)
+ plt.title('CVX solution')
+ plt.colorbar()
+ plt.subplot(3,1,3)
+ plt.imshow(diff_cvx)
+ plt.title('Difference')
+ plt.colorbar()
+ plt.show()
+
+ plt.plot(np.linspace(0,ig.shape[1],ig.shape[1]), pdhg.get_output().as_array()[int(ig.shape[0]/2),:], label = 'PDHG')
+ plt.plot(np.linspace(0,ig.shape[1],ig.shape[1]), u.value[int(ig.shape[0]/2),:], label = 'CVX')
+ plt.legend()
+ plt.title('Middle Line Profiles')
+ plt.show()
+
+ print('Primal Objective (CVX) {} '.format(obj.value))
+ print('Primal Objective (PDHG) {} '.format(pdhg.objective[-1][0]))
diff --git a/Wrappers/Python/demos/PDHG_examples/GatherAll/PDHG_TV_Denoising_2D_time.py b/Wrappers/Python/demos/PDHG_examples/GatherAll/PDHG_TV_Denoising_2D_time.py
new file mode 100644
index 0000000..febe76d
--- /dev/null
+++ b/Wrappers/Python/demos/PDHG_examples/GatherAll/PDHG_TV_Denoising_2D_time.py
@@ -0,0 +1,243 @@
+#========================================================================
+# Copyright 2019 Science Technology Facilities Council
+# Copyright 2019 University of Manchester
+#
+# This work is part of the Core Imaging Library developed by Science Technology
+# Facilities Council and University of Manchester
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0.txt
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+#=========================================================================
+"""
+
+Total Variation (Dynamic) Denoising using PDHG algorithm and Tomophantom:
+
+
+Problem: min_{x} \alpha * ||\nabla x||_{2,1} + \frac{1}{2} * || x - g ||_{2}^{2}
+
+ \alpha: Regularization parameter
+
+ \nabla: Gradient operator
+
+ g: 2D Dynamic noisy data with Gaussian Noise
+
+ K = \nabla
+
+"""
+
+from ccpi.framework import ImageData, ImageGeometry
+
+import numpy as np
+import numpy
+import matplotlib.pyplot as plt
+
+from ccpi.optimisation.algorithms import PDHG
+
+from ccpi.optimisation.operators import BlockOperator, Gradient, Identity
+from ccpi.optimisation.functions import ZeroFunction, L2NormSquared, \
+ MixedL21Norm, BlockFunction
+
+from ccpi.astra.ops import AstraProjectorMC
+
+import os
+import tomophantom
+from tomophantom import TomoP2D
+
+# Create phantom for TV 2D dynamic tomography
+
+model = 102 # note that the selected model is temporal (2D + time)
+N = 128 # set dimension of the phantom
+
+path = os.path.dirname(tomophantom.__file__)
+path_library2D = os.path.join(path, "Phantom2DLibrary.dat")
+phantom_2Dt = TomoP2D.ModelTemporal(model, N, path_library2D)
+
+plt.close('all')
+plt.figure(1)
+plt.rcParams.update({'font.size': 21})
+plt.title('{}''{}'.format('2D+t phantom using model no.',model))
+for sl in range(0,np.shape(phantom_2Dt)[0]):
+ im = phantom_2Dt[sl,:,:]
+ plt.imshow(im, vmin=0, vmax=1)
+# plt.pause(.1)
+# plt.draw
+
+# Setup geometries
+ig = ImageGeometry(voxel_num_x = N, voxel_num_y = N, channels = np.shape(phantom_2Dt)[0])
+data = ImageData(phantom_2Dt, geometry=ig)
+ag = ig
+
+# Create noisy data. Apply Gaussian noise
+np.random.seed(10)
+noisy_data = ImageData( data.as_array() + np.random.normal(0, 0.25, size=ig.shape) )
+
+# time-frames index
+tindex = [8, 16, 24]
+
+fig, axes = plt.subplots(nrows=1, ncols=3, figsize=(10, 10))
+plt.subplot(1,3,1)
+plt.imshow(noisy_data.as_array()[tindex[0],:,:])
+plt.axis('off')
+plt.title('Time {}'.format(tindex[0]))
+plt.subplot(1,3,2)
+plt.imshow(noisy_data.as_array()[tindex[1],:,:])
+plt.axis('off')
+plt.title('Time {}'.format(tindex[1]))
+plt.subplot(1,3,3)
+plt.imshow(noisy_data.as_array()[tindex[2],:,:])
+plt.axis('off')
+plt.title('Time {}'.format(tindex[2]))
+
+fig.subplots_adjust(bottom=0.1, top=0.9, left=0.1, right=0.8,
+ wspace=0.02, hspace=0.02)
+
+plt.show()
+
+# Regularisation Parameter
+alpha = 0.3
+
+# Create operators
+op1 = Gradient(ig, correlation='Space')
+op2 = Gradient(ig, correlation='SpaceChannels')
+op3 = Identity(ig, ag)
+
+# Create BlockOperator
+operator1 = BlockOperator(op1, op3, shape=(2,1) )
+operator2 = BlockOperator(op2, op3, shape=(2,1) )
+
+# Create functions
+
+f1 = alpha * MixedL21Norm()
+f2 = 0.5 * L2NormSquared(b = noisy_data)
+f = BlockFunction(f1, f2)
+
+g = ZeroFunction()
+
+# Compute operator Norm
+normK1 = operator1.norm()
+normK2 = operator2.norm()
+
+#%%
+# Primal & dual stepsizes
+sigma1 = 1
+tau1 = 1/(sigma1*normK1**2)
+
+sigma2 = 1
+tau2 = 1/(sigma2*normK2**2)
+
+# Setup and run the PDHG algorithm
+pdhg1 = PDHG(f=f,g=g,operator=operator1, tau=tau1, sigma=sigma1)
+pdhg1.max_iteration = 2000
+pdhg1.update_objective_interval = 200
+pdhg1.run(2000)
+
+# Setup and run the PDHG algorithm
+pdhg2 = PDHG(f=f,g=g,operator=operator2, tau=tau2, sigma=sigma2)
+pdhg2.max_iteration = 2000
+pdhg2.update_objective_interval = 200
+pdhg2.run(2000)
+
+
+#%%
+
+tindex = [8, 16, 24]
+fig, axes = plt.subplots(nrows=2, ncols=3, figsize=(10, 8))
+
+plt.subplot(3,3,1)
+plt.imshow(phantom_2Dt[tindex[0],:,:])
+plt.axis('off')
+plt.title('Time {}'.format(tindex[0]))
+
+plt.subplot(3,3,2)
+plt.imshow(phantom_2Dt[tindex[1],:,:])
+plt.axis('off')
+plt.title('Time {}'.format(tindex[1]))
+
+plt.subplot(3,3,3)
+plt.imshow(phantom_2Dt[tindex[2],:,:])
+plt.axis('off')
+plt.title('Time {}'.format(tindex[2]))
+
+plt.subplot(3,3,4)
+plt.imshow(pdhg1.get_output().as_array()[tindex[0],:,:])
+plt.axis('off')
+plt.subplot(3,3,5)
+plt.imshow(pdhg1.get_output().as_array()[tindex[1],:,:])
+plt.axis('off')
+plt.subplot(3,3,6)
+plt.imshow(pdhg1.get_output().as_array()[tindex[2],:,:])
+plt.axis('off')
+
+
+plt.subplot(3,3,7)
+plt.imshow(pdhg2.get_output().as_array()[tindex[0],:,:])
+plt.axis('off')
+plt.subplot(3,3,8)
+plt.imshow(pdhg2.get_output().as_array()[tindex[1],:,:])
+plt.axis('off')
+plt.subplot(3,3,9)
+plt.imshow(pdhg2.get_output().as_array()[tindex[2],:,:])
+plt.axis('off')
+
+
+im = plt.imshow(pdhg1.get_output().as_array()[tindex[0],:,:])
+
+
+fig.subplots_adjust(bottom=0.1, top=0.9, left=0.1, right=0.8,
+ wspace=0.02, hspace=0.02)
+
+cb_ax = fig.add_axes([0.83, 0.1, 0.02, 0.8])
+cbar = fig.colorbar(im, cax=cb_ax)
+plt.show()
+
+#%%
+import matplotlib.animation as animation
+fig, axes = plt.subplots(nrows=1, ncols=3, figsize=(10, 30))
+ims1 = []
+ims2 = []
+ims3 = []
+for sl in range(0,np.shape(phantom_2Dt)[0]):
+
+ plt.subplot(1,3,1)
+ im1 = plt.imshow(phantom_2Dt[sl,:,:], animated=True)
+
+ plt.subplot(1,3,2)
+ im2 = plt.imshow(pdhg1.get_output().as_array()[sl,:,:])
+
+ plt.subplot(1,3,3)
+ im3 = plt.imshow(pdhg2.get_output().as_array()[sl,:,:])
+
+ ims1.append([im1])
+ ims2.append([im2])
+ ims3.append([im3])
+
+
+ani1 = animation.ArtistAnimation(fig, ims1, interval=500,
+ repeat_delay=10)
+
+ani2 = animation.ArtistAnimation(fig, ims2, interval=500,
+ repeat_delay=10)
+
+ani3 = animation.ArtistAnimation(fig, ims3, interval=500,
+ repeat_delay=10)
+plt.show()
+# plt.pause(0.25)
+# plt.show()
+
+
+
+
+
+
+
+
diff --git a/Wrappers/Python/demos/PDHG_examples/GatherAll/PDHG_TV_Denoising_Gaussian_3D.py b/Wrappers/Python/demos/PDHG_examples/GatherAll/PDHG_TV_Denoising_Gaussian_3D.py
new file mode 100644
index 0000000..15709cd
--- /dev/null
+++ b/Wrappers/Python/demos/PDHG_examples/GatherAll/PDHG_TV_Denoising_Gaussian_3D.py
@@ -0,0 +1,147 @@
+#========================================================================
+# Copyright 2019 Science Technology Facilities Council
+# Copyright 2019 University of Manchester
+#
+# This work is part of the Core Imaging Library developed by Science Technology
+# Facilities Council and University of Manchester
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0.txt
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+#=========================================================================
+"""
+
+Total Variation (3D) Denoising using PDHG algorithm and Tomophantom:
+
+
+Problem: min_{x} \alpha * ||\nabla x||_{2,1} + \frac{1}{2} * || x - g ||_{2}^{2}
+
+ \alpha: Regularization parameter
+
+ \nabla: Gradient operator
+
+ g: 3D Noisy Data with Gaussian Noise
+
+ Method = 0 ( PDHG - split ) : K = [ \nabla,
+ Identity]
+
+
+ Method = 1 (PDHG - explicit ): K = \nabla
+
+"""
+
+from ccpi.framework import ImageData, ImageGeometry
+import matplotlib.pyplot as plt
+from ccpi.optimisation.algorithms import PDHG
+from ccpi.optimisation.operators import Gradient
+from ccpi.optimisation.functions import L2NormSquared, MixedL21Norm
+
+from skimage.util import random_noise
+
+import timeit
+import os
+from tomophantom import TomoP3D
+import tomophantom
+
+# Create a phantom from Tomophantom
+print ("Building 3D phantom using TomoPhantom software")
+tic=timeit.default_timer()
+model = 13 # select a model number from the library
+N = 64 # Define phantom dimensions using a scalar value (cubic phantom)
+path = os.path.dirname(tomophantom.__file__)
+path_library3D = os.path.join(path, "Phantom3DLibrary.dat")
+
+#This will generate a N x N x N phantom (3D)
+phantom_tm = TomoP3D.Model(model, N, path_library3D)
+
+# Create noisy data. Apply Gaussian noise
+ig = ImageGeometry(voxel_num_x=N, voxel_num_y=N, voxel_num_z=N)
+ag = ig
+n1 = random_noise(phantom_tm, mode = 'gaussian', mean=0, var = 0.001, seed=10)
+noisy_data = ImageData(n1)
+
+# Show results
+sliceSel = int(0.5*N)
+plt.figure(figsize=(15,15))
+plt.subplot(3,1,1)
+plt.imshow(noisy_data.as_array()[sliceSel,:,:],vmin=0, vmax=1)
+plt.title('Axial View')
+plt.colorbar()
+plt.subplot(3,1,2)
+plt.imshow(noisy_data.as_array()[:,sliceSel,:],vmin=0, vmax=1)
+plt.title('Coronal View')
+plt.colorbar()
+plt.subplot(3,1,3)
+plt.imshow(noisy_data.as_array()[:,:,sliceSel],vmin=0, vmax=1)
+plt.title('Sagittal View')
+plt.colorbar()
+plt.show()
+
+# Regularisation Parameter
+alpha = 0.05
+
+# Setup and run the PDHG algorithm
+operator = Gradient(ig)
+f = alpha * MixedL21Norm()
+g = 0.5 * L2NormSquared(b = noisy_data)
+
+normK = operator.norm()
+
+sigma = 1
+tau = 1/(sigma*normK**2)
+
+pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma, memopt=True)
+pdhg.max_iteration = 1000
+pdhg.update_objective_interval = 200
+pdhg.run(1000, verbose = True)
+
+# Show results
+fig, axes = plt.subplots(nrows=2, ncols=3, figsize=(10, 8))
+fig.suptitle('TV Reconstruction',fontsize=20)
+
+plt.subplot(2,3,1)
+plt.imshow(noisy_data.as_array()[sliceSel,:,:],vmin=0, vmax=1)
+plt.axis('off')
+plt.title('Axial View')
+
+plt.subplot(2,3,2)
+plt.imshow(noisy_data.as_array()[:,sliceSel,:],vmin=0, vmax=1)
+plt.axis('off')
+plt.title('Coronal View')
+
+plt.subplot(2,3,3)
+plt.imshow(noisy_data.as_array()[:,:,sliceSel],vmin=0, vmax=1)
+plt.axis('off')
+plt.title('Sagittal View')
+
+
+plt.subplot(2,3,4)
+plt.imshow(pdhg.get_output().as_array()[sliceSel,:,:],vmin=0, vmax=1)
+plt.axis('off')
+plt.subplot(2,3,5)
+plt.imshow(pdhg.get_output().as_array()[:,sliceSel,:],vmin=0, vmax=1)
+plt.axis('off')
+plt.subplot(2,3,6)
+plt.imshow(pdhg.get_output().as_array()[:,:,sliceSel],vmin=0, vmax=1)
+plt.axis('off')
+im = plt.imshow(pdhg.get_output().as_array()[:,:,sliceSel],vmin=0, vmax=1)
+
+
+fig.subplots_adjust(bottom=0.1, top=0.9, left=0.1, right=0.8,
+ wspace=0.02, hspace=0.02)
+
+cb_ax = fig.add_axes([0.83, 0.1, 0.02, 0.8])
+cbar = fig.colorbar(im, cax=cb_ax)
+
+
+plt.show()
+
diff --git a/Wrappers/Python/demos/PDHG_examples/GatherAll/PDHG_TV_Tomo2D.py b/Wrappers/Python/demos/PDHG_examples/GatherAll/PDHG_TV_Tomo2D.py
new file mode 100644
index 0000000..4f7639e
--- /dev/null
+++ b/Wrappers/Python/demos/PDHG_examples/GatherAll/PDHG_TV_Tomo2D.py
@@ -0,0 +1,173 @@
+#========================================================================
+# Copyright 2019 Science Technology Facilities Council
+# Copyright 2019 University of Manchester
+#
+# This work is part of the Core Imaging Library developed by Science Technology
+# Facilities Council and University of Manchester
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0.txt
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+#=========================================================================
+
+"""
+
+Total Variation 2D Tomography Reconstruction using PDHG algorithm:
+
+
+Problem: min_u \alpha * ||\nabla u||_{2,1} + \frac{1}{2}||Au - g||^{2}
+ min_u, u>0 \alpha * ||\nabla u||_{2,1} + \int A u - g log (Au + \eta)
+
+ \nabla: Gradient operator
+ A: System Matrix
+ g: Noisy sinogram
+ \eta: Background noise
+
+ \alpha: Regularization parameter
+
+"""
+
+from ccpi.framework import ImageData, ImageGeometry, AcquisitionGeometry, AcquisitionData
+
+import numpy as np
+import numpy
+import matplotlib.pyplot as plt
+
+from ccpi.optimisation.algorithms import PDHG
+
+from ccpi.optimisation.operators import BlockOperator, Gradient
+from ccpi.optimisation.functions import ZeroFunction, L2NormSquared, \
+ MixedL21Norm, BlockFunction, KullbackLeibler, IndicatorBox
+
+from ccpi.astra.ops import AstraProjectorSimple
+from ccpi.framework import TestData
+from PIL import Image
+import os, sys
+#if int(numpy.version.version.split('.')[1]) > 12:
+from skimage.util import random_noise
+#else:
+# from demoutil import random_noise
+
+#import scipy.io
+
+# user supplied input
+if len(sys.argv) > 1:
+ which_noise = int(sys.argv[1])
+else:
+ which_noise = 1
+
+# Load 256 shepp-logan
+data256 = scipy.io.loadmat('phantom.mat')['phantom256']
+data = ImageData(numpy.array(Image.fromarray(data256).resize((256,256))))
+N, M = data.shape
+ig = ImageGeometry(voxel_num_x=N, voxel_num_y=M)
+
+# Add it to testdata or use tomophantom
+#loader = TestData(data_dir=os.path.join(sys.prefix, 'share','ccpi'))
+#data = loader.load(TestData.SIMPLE_PHANTOM_2D, size=(50, 50))
+#ig = data.geometry
+
+# Create acquisition data and geometry
+detectors = N
+angles = np.linspace(0, np.pi, 180)
+ag = AcquisitionGeometry('parallel','2D',angles, detectors)
+
+# Select device
+device = '0'
+#device = input('Available device: GPU==1 / CPU==0 ')
+if device=='1':
+ dev = 'gpu'
+else:
+ dev = 'cpu'
+
+Aop = AstraProjectorSimple(ig, ag, dev)
+sin = Aop.direct(data)
+
+# Create noisy data. Apply Gaussian noise
+noises = ['gaussian', 'poisson']
+noise = noises[which_noise]
+
+if noise == 'poisson':
+ scale = 5
+ eta = 0
+ noisy_data = AcquisitionData(np.random.poisson( scale * (eta + sin.as_array()))/scale, ag)
+elif noise == 'gaussian':
+ n1 = np.random.normal(0, 1, size = ag.shape)
+ noisy_data = AcquisitionData(n1 + sin.as_array(), ag)
+else:
+ raise ValueError('Unsupported Noise ', noise)
+
+# Show Ground Truth and Noisy Data
+plt.figure(figsize=(10,10))
+plt.subplot(1,2,2)
+plt.imshow(data.as_array())
+plt.title('Ground Truth')
+plt.colorbar()
+plt.subplot(1,2,1)
+plt.imshow(noisy_data.as_array())
+plt.title('Noisy Data')
+plt.colorbar()
+plt.show()
+
+# Create operators
+op1 = Gradient(ig)
+op2 = Aop
+
+# Create BlockOperator
+operator = BlockOperator(op1, op2, shape=(2,1) )
+
+# Compute operator Norm
+normK = operator.norm()
+
+# Create functions
+if noise == 'poisson':
+ alpha = 3
+ f2 = KullbackLeibler(noisy_data)
+ g = IndicatorBox(lower=0)
+ sigma = 1
+ tau = 1/(sigma*normK**2)
+
+elif noise == 'gaussian':
+ alpha = 20
+ f2 = 0.5 * L2NormSquared(b=noisy_data)
+ g = ZeroFunction()
+ sigma = 10
+ tau = 1/(sigma*normK**2)
+
+f1 = alpha * MixedL21Norm()
+f = BlockFunction(f1, f2)
+
+# Setup and run the PDHG algorithm
+pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma)
+pdhg.max_iteration = 2000
+pdhg.update_objective_interval = 200
+pdhg.run(2000)
+
+plt.figure(figsize=(15,15))
+plt.subplot(3,1,1)
+plt.imshow(data.as_array())
+plt.title('Ground Truth')
+plt.colorbar()
+plt.subplot(3,1,2)
+plt.imshow(noisy_data.as_array())
+plt.title('Noisy Data')
+plt.colorbar()
+plt.subplot(3,1,3)
+plt.imshow(pdhg.get_output().as_array())
+plt.title('TV Reconstruction')
+plt.colorbar()
+plt.show()
+plt.plot(np.linspace(0,ig.shape[1],ig.shape[1]), data.as_array()[int(N/2),:], label = 'GTruth')
+plt.plot(np.linspace(0,ig.shape[1],ig.shape[1]), pdhg.get_output().as_array()[int(N/2),:], label = 'TV reconstruction')
+plt.legend()
+plt.title('Middle Line Profiles')
+plt.show()
diff --git a/Wrappers/Python/demos/PDHG_examples/GatherAll/PDHG_Tikhonov_Denoising.py b/Wrappers/Python/demos/PDHG_examples/GatherAll/PDHG_Tikhonov_Denoising.py
new file mode 100644
index 0000000..78d4980
--- /dev/null
+++ b/Wrappers/Python/demos/PDHG_examples/GatherAll/PDHG_Tikhonov_Denoising.py
@@ -0,0 +1,258 @@
+#========================================================================
+# Copyright 2019 Science Technology Facilities Council
+# Copyright 2019 University of Manchester
+#
+# This work is part of the Core Imaging Library developed by Science Technology
+# Facilities Council and University of Manchester
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0.txt
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+#=========================================================================
+
+"""
+
+``Tikhonov`` Regularization Denoising using PDHG algorithm:
+
+
+Problem: min_{u}, \alpha * ||\nabla u||_{2,2} + Fidelity(u, g)
+
+ \alpha: Regularization parameter
+
+ \nabla: Gradient operator
+
+ g: Noisy Data
+
+ Fidelity = 1) L2NormSquarred ( \frac{1}{2} * || u - g ||_{2}^{2} ) if Noise is Gaussian
+ 2) L1Norm ( ||u - g||_{1} )if Noise is Salt & Pepper
+ 3) Kullback Leibler (\int u - g * log(u) + Id_{u>0}) if Noise is Poisson
+
+ Method = 0 ( PDHG - split ) : K = [ \nabla,
+ Identity]
+
+
+ Method = 1 (PDHG - explicit ): K = \nabla
+
+
+ Default: Tikhonov denoising
+ noise = Gaussian
+ Fidelity = L2NormSquarred
+ method = 0
+
+"""
+
+from ccpi.framework import ImageData, TestData
+
+import numpy as np
+import numpy
+import matplotlib.pyplot as plt
+
+from ccpi.optimisation.algorithms import PDHG
+
+from ccpi.optimisation.operators import BlockOperator, Identity, Gradient
+from ccpi.optimisation.functions import ZeroFunction, L2NormSquared,\
+ BlockFunction, KullbackLeibler, L1Norm
+
+import sys, os
+if int(numpy.version.version.split('.')[1]) > 12:
+ from skimage.util import random_noise
+else:
+ from demoutil import random_noise
+
+
+# user supplied input
+if len(sys.argv) > 1:
+ which_noise = int(sys.argv[1])
+else:
+ which_noise = 2
+print ("Applying {} noise")
+
+if len(sys.argv) > 2:
+ method = sys.argv[2]
+else:
+ method = '0'
+print ("method ", method)
+
+loader = TestData(data_dir=os.path.join(sys.prefix, 'share','ccpi'))
+data = loader.load(TestData.SHAPES)
+ig = data.geometry
+ag = ig
+
+# Create noisy data.
+noises = ['gaussian', 'poisson', 's&p']
+noise = noises[which_noise]
+if noise == 's&p':
+ n1 = random_noise(data.as_array(), mode = noise, salt_vs_pepper = 0.9, amount=0.2)
+elif noise == 'poisson':
+ scale = 5
+ n1 = random_noise( data.as_array()/scale, mode = noise, seed = 10)*scale
+elif noise == 'gaussian':
+ n1 = random_noise(data.as_array(), mode = noise, seed = 10)
+else:
+ raise ValueError('Unsupported Noise ', noise)
+noisy_data = ImageData(n1)
+
+# Show Ground Truth and Noisy Data
+plt.figure(figsize=(10,5))
+plt.subplot(1,2,1)
+plt.imshow(data.as_array())
+plt.title('Ground Truth')
+plt.colorbar()
+plt.subplot(1,2,2)
+plt.imshow(noisy_data.as_array())
+plt.title('Noisy Data')
+plt.colorbar()
+plt.show()
+
+# Regularisation Parameter depending on the noise distribution
+if noise == 's&p':
+ alpha = 20
+elif noise == 'poisson':
+ alpha = 10
+elif noise == 'gaussian':
+ alpha = 5
+
+# fidelity
+if noise == 's&p':
+ f2 = L1Norm(b=noisy_data)
+elif noise == 'poisson':
+ f2 = KullbackLeibler(noisy_data)
+elif noise == 'gaussian':
+ f2 = 0.5 * L2NormSquared(b=noisy_data)
+
+if method == '0':
+
+ # Create operators
+ op1 = Gradient(ig)
+ op2 = Identity(ig, ag)
+
+ # Create BlockOperator
+ operator = BlockOperator(op1, op2, shape=(2,1) )
+
+ # Create functions
+ f1 = alpha * L2NormSquared()
+ f = BlockFunction(f1, f2)
+ g = ZeroFunction()
+
+else:
+
+ operator = Gradient(ig)
+ g = f2
+
+
+# Compute operator Norm
+normK = operator.norm()
+
+# Primal & dual stepsizes
+sigma = 1
+tau = 1/(sigma*normK**2)
+
+# Setup and run the PDHG algorithm
+pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma)
+pdhg.max_iteration = 2000
+pdhg.update_objective_interval = 100
+pdhg.run(2000)
+
+
+plt.figure(figsize=(20,5))
+plt.subplot(1,4,1)
+plt.imshow(data.as_array())
+plt.title('Ground Truth')
+plt.colorbar()
+plt.subplot(1,4,2)
+plt.imshow(noisy_data.as_array())
+plt.title('Noisy Data')
+plt.colorbar()
+plt.subplot(1,4,3)
+plt.imshow(pdhg.get_output().as_array())
+plt.title('TV Reconstruction')
+plt.colorbar()
+plt.subplot(1,4,4)
+plt.plot(np.linspace(0,ig.shape[1],ig.shape[1]), data.as_array()[int(ig.shape[0]/2),:], label = 'GTruth')
+plt.plot(np.linspace(0,ig.shape[1],ig.shape[1]), pdhg.get_output().as_array()[int(ig.shape[0]/2),:], label = 'Tikhonov reconstruction')
+plt.legend()
+plt.title('Middle Line Profiles')
+plt.show()
+
+
+
+##%% Check with CVX solution
+
+from ccpi.optimisation.operators import SparseFiniteDiff
+
+try:
+ from cvxpy import *
+ cvx_not_installable = True
+except ImportError:
+ cvx_not_installable = False
+
+if cvx_not_installable:
+
+ ##Construct problem
+ u = Variable(ig.shape)
+
+ DY = SparseFiniteDiff(ig, direction=0, bnd_cond='Neumann')
+ DX = SparseFiniteDiff(ig, direction=1, bnd_cond='Neumann')
+
+ # Define Total Variation as a regulariser
+
+ regulariser = alpha * sum_squares(norm(vstack([DX.matrix() * vec(u), DY.matrix() * vec(u)]), 2, axis = 0))
+
+ # choose solver
+ if 'MOSEK' in installed_solvers():
+ solver = MOSEK
+ else:
+ solver = SCS
+
+ # fidelity
+ if noise == 's&p':
+ fidelity = pnorm( u - noisy_data.as_array(),1)
+ elif noise == 'poisson':
+ fidelity = sum(kl_div(noisy_data.as_array(), u))
+ solver = SCS
+ elif noise == 'gaussian':
+ fidelity = 0.5 * sum_squares(noisy_data.as_array() - u)
+
+ obj = Minimize( regulariser + fidelity)
+ prob = Problem(obj)
+ result = prob.solve(verbose = True, solver = solver)
+
+ diff_cvx = numpy.abs( pdhg.get_output().as_array() - u.value )
+
+ plt.figure(figsize=(15,15))
+ plt.subplot(3,1,1)
+ plt.imshow(pdhg.get_output().as_array())
+ plt.title('PDHG solution')
+ plt.colorbar()
+ plt.subplot(3,1,2)
+ plt.imshow(u.value)
+ plt.title('CVX solution')
+ plt.colorbar()
+ plt.subplot(3,1,3)
+ plt.imshow(diff_cvx)
+ plt.title('Difference')
+ plt.colorbar()
+ plt.show()
+
+ plt.plot(np.linspace(0,ig.shape[1],ig.shape[1]), pdhg.get_output().as_array()[int(ig.shape[0]/2),:], label = 'PDHG')
+ plt.plot(np.linspace(0,ig.shape[1],ig.shape[1]), u.value[int(ig.shape[0]/2),:], label = 'CVX')
+ plt.legend()
+ plt.title('Middle Line Profiles')
+ plt.show()
+
+ print('Primal Objective (CVX) {} '.format(obj.value))
+ print('Primal Objective (PDHG) {} '.format(pdhg.objective[-1][0]))
+#
+#
+#
+#
+#
diff --git a/Wrappers/Python/demos/PDHG_examples/GatherAll/phantom.mat b/Wrappers/Python/demos/PDHG_examples/GatherAll/phantom.mat
new file mode 100755
index 0000000..c465bbe
--- /dev/null
+++ b/Wrappers/Python/demos/PDHG_examples/GatherAll/phantom.mat
Binary files differ
diff --git a/Wrappers/Python/demos/PDHG_examples/IMATDemo.py b/Wrappers/Python/demos/PDHG_examples/IMATDemo.py
new file mode 100644
index 0000000..2051860
--- /dev/null
+++ b/Wrappers/Python/demos/PDHG_examples/IMATDemo.py
@@ -0,0 +1,339 @@
+
+
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Created on Mon Mar 25 12:50:27 2019
+
+@author: vaggelis
+"""
+
+from ccpi.framework import ImageData, ImageGeometry, BlockDataContainer, AcquisitionGeometry, AcquisitionData
+from astropy.io import fits
+import numpy
+import matplotlib.pyplot as plt
+
+from ccpi.optimisation.algorithms import CGLS, PDHG
+from ccpi.optimisation.functions import MixedL21Norm, L2NormSquared, BlockFunction, ZeroFunction, KullbackLeibler, IndicatorBox
+from ccpi.optimisation.operators import Gradient, BlockOperator
+
+from ccpi.astra.operators import AstraProjectorMC, AstraProjectorSimple
+
+import pickle
+
+
+# load file
+
+#filename_sino = '/media/newhd/shared/DataProcessed/IMAT_beamtime_Feb_2019/preprocessed_test_flat/sino/rebin_slice_350/sino_log_rebin_282.fits'
+#filename_sino = '/media/newhd/shared/DataProcessed/IMAT_beamtime_Feb_2019/preprocessed_test_flat/sino/rebin_slice_350/sino_log_rebin_564.fits'
+#filename_sino = '/media/newhd/shared/DataProcessed/IMAT_beamtime_Feb_2019/preprocessed_test_flat/sino/rebin_slice_350/sino_log_rebin_141.fits'
+filename_sino = '/media/newhd/shared/DataProcessed/IMAT_beamtime_Feb_2019/preprocessed_test_flat/sino/rebin_slice_350/sino_log_rebin_80_channels.fits'
+
+sino_handler = fits.open(filename_sino)
+sino = numpy.array(sino_handler[0].data, dtype=float)
+
+# change axis order: channels, angles, detectors
+sino_new = numpy.rollaxis(sino, 2)
+sino_handler.close()
+
+
+sino_shape = sino_new.shape
+
+num_channels = sino_shape[0] # channelss
+num_pixels_h = sino_shape[2] # detectors
+num_pixels_v = sino_shape[2] # detectors
+num_angles = sino_shape[1] # angles
+
+
+ig = ImageGeometry(voxel_num_x = num_pixels_h, voxel_num_y = num_pixels_v, channels = num_channels)
+
+with open("/media/newhd/vaggelis/CCPi/IMAT_reconstruction/CCPi-Framework/Wrappers/Python/ccpi/optimisation/IMAT_data/golden_angles_new.txt") as f:
+ angles_string = [line.rstrip() for line in f]
+ angles = numpy.array(angles_string).astype(float)
+
+
+ag = AcquisitionGeometry('parallel', '2D', angles * numpy.pi / 180, pixel_num_h = num_pixels_h, channels = num_channels)
+op_MC = AstraProjectorMC(ig, ag, 'gpu')
+
+sino_aqdata = AcquisitionData(sino_new, ag)
+result_bp = op_MC.adjoint(sino_aqdata)
+
+#%%
+
+channel = [40, 60]
+for j in range(2):
+ z4 = sino_aqdata.as_array()[channel[j]]
+ plt.figure(figsize=(10,6))
+ plt.imshow(z4, cmap='viridis')
+ plt.axis('off')
+ plt.savefig('Sino_141/Sinogram_ch_{}_.png'.format(channel[j]), bbox_inches='tight', transparent=True)
+ plt.show()
+
+#%%
+
+def callback(iteration, objective, x):
+ plt.imshow(x.as_array()[40])
+ plt.colorbar()
+ plt.show()
+
+#%%
+# CGLS
+
+x_init = ig.allocate()
+cgls1 = CGLS(x_init=x_init, operator=op_MC, data=sino_aqdata)
+cgls1.max_iteration = 100
+cgls1.update_objective_interval = 2
+cgls1.run(20,verbose=True, callback=callback)
+
+plt.imshow(cgls1.get_output().subset(channel=20).array)
+plt.title('CGLS')
+plt.colorbar()
+plt.show()
+
+#%%
+with open('Sino_141/CGLS/CGLS_{}_iter.pkl'.format(20), 'wb') as f:
+ z = cgls1.get_output()
+ pickle.dump(z, f)
+
+#%%
+#% Tikhonov Space
+
+x_init = ig.allocate()
+alpha = [1,3,5,10,20,50]
+
+for a in alpha:
+
+ Grad = Gradient(ig, correlation = Gradient.CORRELATION_SPACE)
+ operator = BlockOperator(op_MC, a * Grad, shape=(2,1))
+ blockData = BlockDataContainer(sino_aqdata, \
+ Grad.range_geometry().allocate())
+ cgls2 = CGLS()
+ cgls2.max_iteration = 500
+ cgls2.set_up(x_init, operator, blockData)
+ cgls2.update_objective_interval = 50
+ cgls2.run(100,verbose=True)
+
+ with open('Sino_141/CGLS_Space/CGLS_a_{}.pkl'.format(a), 'wb') as f:
+ z = cgls2.get_output()
+ pickle.dump(z, f)
+
+#% Tikhonov SpaceChannels
+
+for a1 in alpha:
+
+ Grad1 = Gradient(ig, correlation = Gradient.CORRELATION_SPACECHANNEL)
+ operator1 = BlockOperator(op_MC, a1 * Grad1, shape=(2,1))
+ blockData1 = BlockDataContainer(sino_aqdata, \
+ Grad1.range_geometry().allocate())
+ cgls3 = CGLS()
+ cgls3.max_iteration = 500
+ cgls3.set_up(x_init, operator1, blockData1)
+ cgls3.update_objective_interval = 10
+ cgls3.run(100, verbose=True)
+
+ with open('Sino_141/CGLS_SpaceChannels/CGLS_a_{}.pkl'.format(a1), 'wb') as f1:
+ z1 = cgls3.get_output()
+ pickle.dump(z1, f1)
+
+
+
+#%%
+#
+ig_tmp = ImageGeometry(voxel_num_x = num_pixels_h, voxel_num_y = num_pixels_v)
+ag_tmp = AcquisitionGeometry('parallel', '2D', angles * numpy.pi / 180, pixel_num_h = num_pixels_h)
+op_tmp = AstraProjectorSimple(ig_tmp, ag_tmp, 'gpu')
+normK1 = op_tmp.norm()
+
+alpha_TV = [2, 5, 10] # for powder
+
+# Create operators
+op1 = Gradient(ig, correlation=Gradient.CORRELATION_SPACECHANNEL)
+op2 = op_MC
+
+# Create BlockOperator
+operator = BlockOperator(op1, op2, shape=(2,1) )
+
+
+for alpha in alpha_TV:
+# Create functions
+ f1 = alpha * MixedL21Norm()
+
+ f2 = KullbackLeibler(sino_aqdata)
+ f = BlockFunction(f1, f2)
+ g = IndicatorBox(lower=0)
+
+ # Compute operator Norm
+ normK = numpy.sqrt(8 + normK1**2)
+
+ # Primal & dual stepsizes
+ sigma = 1
+ tau = 1/(sigma*normK**2)
+
+ # Setup and run the PDHG algorithm
+ pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma)
+ pdhg.max_iteration = 5000
+ pdhg.update_objective_interval = 500
+# pdhg.run(2000, verbose=True, callback=callback)
+ pdhg.run(5000, verbose=True, callback=callback)
+#
+ with open('Sino_141/TV_SpaceChannels/TV_a = {}.pkl'.format(alpha), 'wb') as f3:
+ z3 = pdhg.get_output()
+ pickle.dump(z3, f3)
+#
+#
+#
+#
+##%%
+#
+#ig_tmp = ImageGeometry(voxel_num_x = num_pixels_h, voxel_num_y = num_pixels_v)
+#ag_tmp = AcquisitionGeometry('parallel', '2D', angles * numpy.pi / 180, pixel_num_h = num_pixels_h)
+#op_tmp = AstraProjectorSimple(ig_tmp, ag_tmp, 'gpu')
+#normK1 = op_tmp.norm()
+#
+#alpha_TV = 10 # for powder
+#
+## Create operators
+#op1 = Gradient(ig, correlation=Gradient.CORRELATION_SPACECHANNEL)
+#op2 = op_MC
+#
+## Create BlockOperator
+#operator = BlockOperator(op1, op2, shape=(2,1) )
+#
+#
+## Create functions
+#f1 = alpha_TV * MixedL21Norm()
+#f2 = 0.5 * L2NormSquared(b=sino_aqdata)
+#f = BlockFunction(f1, f2)
+#g = ZeroFunction()
+#
+## Compute operator Norm
+##normK = 8.70320267279591 # For powder Run one time no need to compute again takes time
+#normK = numpy.sqrt(8 + normK1**2) # for carbon
+#
+## Primal & dual stepsizes
+#sigma = 0.1
+#tau = 1/(sigma*normK**2)
+#
+#def callback(iteration, objective, x):
+# plt.imshow(x.as_array()[100])
+# plt.colorbar()
+# plt.show()
+#
+## Setup and run the PDHG algorithm
+#pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma)
+#pdhg.max_iteration = 2000
+#pdhg.update_objective_interval = 100
+#pdhg.run(2000, verbose=True)
+#
+#
+#
+#
+#
+#
+
+
+
+
+
+
+
+
+
+
+#%%
+
+#with open('/media/newhd/vaggelis/CCPi/IMAT_reconstruction/CCPi-Framework/Wrappers/Python/ccpi/optimisation/CGLS_Tikhonov/CGLS_Space/CGLS_Space_a = 50.pkl', 'wb') as f:
+# z = cgls2.get_output()
+# pickle.dump(z, f)
+#
+
+ #%%
+with open('Sino_141/CGLS_Space/CGLS_Space_a_20.pkl', 'rb') as f1:
+ x = pickle.load(f1)
+
+with open('Sino_141/CGLS_SpaceChannels/CGLS_SpaceChannels_a_20.pkl', 'rb') as f1:
+ x1 = pickle.load(f1)
+
+
+
+#
+plt.imshow(x.as_array()[40]*mask)
+plt.colorbar()
+plt.show()
+
+plt.imshow(x1.as_array()[40]*mask)
+plt.colorbar()
+plt.show()
+
+plt.plot(x.as_array()[40,100,:])
+plt.plot(x1.as_array()[40,100,:])
+plt.show()
+
+#%%
+
+# Show results
+
+def circ_mask(h, w, center=None, radius=None):
+
+ if center is None: # use the middle of the image
+ center = [int(w/2), int(h/2)]
+ if radius is None: # use the smallest distance between the center and image walls
+ radius = min(center[0], center[1], w-center[0], h-center[1])
+
+ Y, X = numpy.ogrid[:h, :w]
+ dist_from_center = numpy.sqrt((X - center[0])**2 + (Y-center[1])**2)
+
+ mask = dist_from_center <= radius
+ return mask
+
+mask = circ_mask(141, 141, center=None, radius = 55)
+plt.imshow(numpy.multiply(x.as_array()[40],mask))
+plt.show()
+#%%
+#channel = [100, 200, 300]
+#
+#for i in range(3):
+# tmp = cgls1.get_output().as_array()[channel[i]]
+#
+# z = tmp * mask
+# plt.figure(figsize=(10,6))
+# plt.imshow(z, vmin=0, cmap='viridis')
+# plt.axis('off')
+## plt.clim(0, 0.02)
+## plt.colorbar()
+## del z
+# plt.savefig('CGLS_282/CGLS_Chan_{}.png'.format(channel[i]), bbox_inches='tight', transparent=True)
+# plt.show()
+#
+#
+##%% Line Profiles
+#
+#n1, n2, n3 = cgs.get_output().as_array().shape
+#mask = circ_mask(564, 564, center=None, radius = 220)
+#material = ['Cu', 'Fe', 'Ni']
+#ycoords = [200, 300, 380]
+#
+#for i in range(3):
+# z = cgs.get_output().as_array()[channel[i]] * mask
+#
+# for k1 in range(len(ycoords)):
+# plt.plot(numpy.arange(0,n2), z[ycoords[k1],:])
+# plt.title('Channel {}: {}'.format(channel[i], material[k1]))
+# plt.savefig('CGLS/line_profile_chan_{}_material_{}.png'.\
+# format(channel[i], material[k1]), bbox_inches='tight')
+# plt.show()
+#
+#
+#
+#
+#
+##%%
+#
+#%%
+
+
+
+#%%
+
+#plt.imshow(pdhg.get_output().subset(channel=100).as_array())
+#plt.show()
diff --git a/Wrappers/Python/demos/PDHG_examples/MultiChannel/PDHG_2D_time_denoising.py b/Wrappers/Python/demos/PDHG_examples/MultiChannel/PDHG_2D_time_denoising.py
new file mode 100644
index 0000000..045458a
--- /dev/null
+++ b/Wrappers/Python/demos/PDHG_examples/MultiChannel/PDHG_2D_time_denoising.py
@@ -0,0 +1,169 @@
+# -*- coding: utf-8 -*-
+# This work is part of the Core Imaging Library developed by
+# Visual Analytics and Imaging System Group of the Science Technology
+# Facilities Council, STFC
+
+# Copyright 2018-2019 Evangelos Papoutsellis and Edoardo Pasca
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from ccpi.framework import ImageData, ImageGeometry, AcquisitionGeometry, AcquisitionData
+
+import numpy as np
+import numpy
+import matplotlib.pyplot as plt
+
+from ccpi.optimisation.algorithms import PDHG
+
+from ccpi.optimisation.operators import BlockOperator, Gradient
+from ccpi.optimisation.functions import ZeroFunction, KullbackLeibler, \
+ MixedL21Norm, BlockFunction
+
+from ccpi.astra.ops import AstraProjectorMC
+
+import os
+import tomophantom
+from tomophantom import TomoP2D
+
+# Create phantom for TV 2D dynamic tomography
+
+model = 102 # note that the selected model is temporal (2D + time)
+N = 50 # set dimension of the phantom
+# one can specify an exact path to the parameters file
+# path_library2D = '../../../PhantomLibrary/models/Phantom2DLibrary.dat'
+path = os.path.dirname(tomophantom.__file__)
+path_library2D = os.path.join(path, "Phantom2DLibrary.dat")
+#This will generate a N_size x N_size x Time frames phantom (2D + time)
+phantom_2Dt = TomoP2D.ModelTemporal(model, N, path_library2D)
+
+plt.close('all')
+plt.figure(1)
+plt.rcParams.update({'font.size': 21})
+plt.title('{}''{}'.format('2D+t phantom using model no.',model))
+for sl in range(0,np.shape(phantom_2Dt)[0]):
+ im = phantom_2Dt[sl,:,:]
+ plt.imshow(im, vmin=0, vmax=1)
+ plt.pause(.1)
+ plt.draw
+
+
+ig = ImageGeometry(voxel_num_x = N, voxel_num_y = N, channels = np.shape(phantom_2Dt)[0])
+data = ImageData(phantom_2Dt, geometry=ig)
+
+detectors = N
+angles = np.linspace(0,np.pi,N)
+
+ag = AcquisitionGeometry('parallel','2D', angles, detectors, channels = np.shape(phantom_2Dt)[0])
+Aop = AstraProjectorMC(ig, ag, 'gpu')
+sin = Aop.direct(data)
+
+scale = 2
+n1 = scale * np.random.poisson(sin.as_array()/scale)
+noisy_data = AcquisitionData(n1, ag)
+
+tindex = [3, 6, 10]
+
+fig, axes = plt.subplots(nrows=1, ncols=3, figsize=(10, 10))
+plt.subplot(1,3,1)
+plt.imshow(noisy_data.as_array()[tindex[0],:,:])
+plt.axis('off')
+plt.title('Time {}'.format(tindex[0]))
+plt.subplot(1,3,2)
+plt.imshow(noisy_data.as_array()[tindex[1],:,:])
+plt.axis('off')
+plt.title('Time {}'.format(tindex[1]))
+plt.subplot(1,3,3)
+plt.imshow(noisy_data.as_array()[tindex[2],:,:])
+plt.axis('off')
+plt.title('Time {}'.format(tindex[2]))
+
+fig.subplots_adjust(bottom=0.1, top=0.9, left=0.1, right=0.8,
+ wspace=0.02, hspace=0.02)
+
+plt.show()
+
+#%%
+# Regularisation Parameter
+alpha = 5
+
+# Create operators
+#op1 = Gradient(ig)
+op1 = Gradient(ig, correlation='SpaceChannels')
+op2 = Aop
+
+# Create BlockOperator
+operator = BlockOperator(op1, op2, shape=(2,1) )
+
+# Create functions
+
+f1 = alpha * MixedL21Norm()
+f2 = KullbackLeibler(noisy_data)
+f = BlockFunction(f1, f2)
+
+g = ZeroFunction()
+
+# Compute operator Norm
+normK = operator.norm()
+
+# Primal & dual stepsizes
+sigma = 1
+tau = 1/(sigma*normK**2)
+
+
+# Setup and run the PDHG algorithm
+pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma, memopt=True)
+pdhg.max_iteration = 2000
+pdhg.update_objective_interval = 200
+pdhg.run(2000)
+
+
+#%%
+fig, axes = plt.subplots(nrows=2, ncols=3, figsize=(10, 8))
+
+plt.subplot(2,3,1)
+plt.imshow(phantom_2Dt[tindex[0],:,:],vmin=0, vmax=1)
+plt.axis('off')
+plt.title('Time {}'.format(tindex[0]))
+
+plt.subplot(2,3,2)
+plt.imshow(phantom_2Dt[tindex[1],:,:],vmin=0, vmax=1)
+plt.axis('off')
+plt.title('Time {}'.format(tindex[1]))
+
+plt.subplot(2,3,3)
+plt.imshow(phantom_2Dt[tindex[2],:,:],vmin=0, vmax=1)
+plt.axis('off')
+plt.title('Time {}'.format(tindex[2]))
+
+
+plt.subplot(2,3,4)
+plt.imshow(pdhg.get_output().as_array()[tindex[0],:,:])
+plt.axis('off')
+plt.subplot(2,3,5)
+plt.imshow(pdhg.get_output().as_array()[tindex[1],:,:])
+plt.axis('off')
+plt.subplot(2,3,6)
+plt.imshow(pdhg.get_output().as_array()[tindex[2],:,:])
+plt.axis('off')
+im = plt.imshow(pdhg.get_output().as_array()[tindex[0],:,:])
+
+
+fig.subplots_adjust(bottom=0.1, top=0.9, left=0.1, right=0.8,
+ wspace=0.02, hspace=0.02)
+
+cb_ax = fig.add_axes([0.83, 0.1, 0.02, 0.8])
+cbar = fig.colorbar(im, cax=cb_ax)
+
+
+plt.show()
+
diff --git a/Wrappers/Python/demos/PDHG_examples/MultiChannel/PDHG_TV_Tomo2D_time.py b/Wrappers/Python/demos/PDHG_examples/MultiChannel/PDHG_TV_Tomo2D_time.py
new file mode 100644
index 0000000..045458a
--- /dev/null
+++ b/Wrappers/Python/demos/PDHG_examples/MultiChannel/PDHG_TV_Tomo2D_time.py
@@ -0,0 +1,169 @@
+# -*- coding: utf-8 -*-
+# This work is part of the Core Imaging Library developed by
+# Visual Analytics and Imaging System Group of the Science Technology
+# Facilities Council, STFC
+
+# Copyright 2018-2019 Evangelos Papoutsellis and Edoardo Pasca
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from ccpi.framework import ImageData, ImageGeometry, AcquisitionGeometry, AcquisitionData
+
+import numpy as np
+import numpy
+import matplotlib.pyplot as plt
+
+from ccpi.optimisation.algorithms import PDHG
+
+from ccpi.optimisation.operators import BlockOperator, Gradient
+from ccpi.optimisation.functions import ZeroFunction, KullbackLeibler, \
+ MixedL21Norm, BlockFunction
+
+from ccpi.astra.ops import AstraProjectorMC
+
+import os
+import tomophantom
+from tomophantom import TomoP2D
+
+# Create phantom for TV 2D dynamic tomography
+
+model = 102 # note that the selected model is temporal (2D + time)
+N = 50 # set dimension of the phantom
+# one can specify an exact path to the parameters file
+# path_library2D = '../../../PhantomLibrary/models/Phantom2DLibrary.dat'
+path = os.path.dirname(tomophantom.__file__)
+path_library2D = os.path.join(path, "Phantom2DLibrary.dat")
+#This will generate a N_size x N_size x Time frames phantom (2D + time)
+phantom_2Dt = TomoP2D.ModelTemporal(model, N, path_library2D)
+
+plt.close('all')
+plt.figure(1)
+plt.rcParams.update({'font.size': 21})
+plt.title('{}''{}'.format('2D+t phantom using model no.',model))
+for sl in range(0,np.shape(phantom_2Dt)[0]):
+ im = phantom_2Dt[sl,:,:]
+ plt.imshow(im, vmin=0, vmax=1)
+ plt.pause(.1)
+ plt.draw
+
+
+ig = ImageGeometry(voxel_num_x = N, voxel_num_y = N, channels = np.shape(phantom_2Dt)[0])
+data = ImageData(phantom_2Dt, geometry=ig)
+
+detectors = N
+angles = np.linspace(0,np.pi,N)
+
+ag = AcquisitionGeometry('parallel','2D', angles, detectors, channels = np.shape(phantom_2Dt)[0])
+Aop = AstraProjectorMC(ig, ag, 'gpu')
+sin = Aop.direct(data)
+
+scale = 2
+n1 = scale * np.random.poisson(sin.as_array()/scale)
+noisy_data = AcquisitionData(n1, ag)
+
+tindex = [3, 6, 10]
+
+fig, axes = plt.subplots(nrows=1, ncols=3, figsize=(10, 10))
+plt.subplot(1,3,1)
+plt.imshow(noisy_data.as_array()[tindex[0],:,:])
+plt.axis('off')
+plt.title('Time {}'.format(tindex[0]))
+plt.subplot(1,3,2)
+plt.imshow(noisy_data.as_array()[tindex[1],:,:])
+plt.axis('off')
+plt.title('Time {}'.format(tindex[1]))
+plt.subplot(1,3,3)
+plt.imshow(noisy_data.as_array()[tindex[2],:,:])
+plt.axis('off')
+plt.title('Time {}'.format(tindex[2]))
+
+fig.subplots_adjust(bottom=0.1, top=0.9, left=0.1, right=0.8,
+ wspace=0.02, hspace=0.02)
+
+plt.show()
+
+#%%
+# Regularisation Parameter
+alpha = 5
+
+# Create operators
+#op1 = Gradient(ig)
+op1 = Gradient(ig, correlation='SpaceChannels')
+op2 = Aop
+
+# Create BlockOperator
+operator = BlockOperator(op1, op2, shape=(2,1) )
+
+# Create functions
+
+f1 = alpha * MixedL21Norm()
+f2 = KullbackLeibler(noisy_data)
+f = BlockFunction(f1, f2)
+
+g = ZeroFunction()
+
+# Compute operator Norm
+normK = operator.norm()
+
+# Primal & dual stepsizes
+sigma = 1
+tau = 1/(sigma*normK**2)
+
+
+# Setup and run the PDHG algorithm
+pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma, memopt=True)
+pdhg.max_iteration = 2000
+pdhg.update_objective_interval = 200
+pdhg.run(2000)
+
+
+#%%
+fig, axes = plt.subplots(nrows=2, ncols=3, figsize=(10, 8))
+
+plt.subplot(2,3,1)
+plt.imshow(phantom_2Dt[tindex[0],:,:],vmin=0, vmax=1)
+plt.axis('off')
+plt.title('Time {}'.format(tindex[0]))
+
+plt.subplot(2,3,2)
+plt.imshow(phantom_2Dt[tindex[1],:,:],vmin=0, vmax=1)
+plt.axis('off')
+plt.title('Time {}'.format(tindex[1]))
+
+plt.subplot(2,3,3)
+plt.imshow(phantom_2Dt[tindex[2],:,:],vmin=0, vmax=1)
+plt.axis('off')
+plt.title('Time {}'.format(tindex[2]))
+
+
+plt.subplot(2,3,4)
+plt.imshow(pdhg.get_output().as_array()[tindex[0],:,:])
+plt.axis('off')
+plt.subplot(2,3,5)
+plt.imshow(pdhg.get_output().as_array()[tindex[1],:,:])
+plt.axis('off')
+plt.subplot(2,3,6)
+plt.imshow(pdhg.get_output().as_array()[tindex[2],:,:])
+plt.axis('off')
+im = plt.imshow(pdhg.get_output().as_array()[tindex[0],:,:])
+
+
+fig.subplots_adjust(bottom=0.1, top=0.9, left=0.1, right=0.8,
+ wspace=0.02, hspace=0.02)
+
+cb_ax = fig.add_axes([0.83, 0.1, 0.02, 0.8])
+cbar = fig.colorbar(im, cax=cb_ax)
+
+
+plt.show()
+
diff --git a/Wrappers/Python/demos/PDHG_examples/PDHG_TV_Color_Denoising.py b/Wrappers/Python/demos/PDHG_examples/PDHG_TV_Color_Denoising.py
new file mode 100644
index 0000000..ddf5ace
--- /dev/null
+++ b/Wrappers/Python/demos/PDHG_examples/PDHG_TV_Color_Denoising.py
@@ -0,0 +1,115 @@
+#========================================================================
+# Copyright 2019 Science Technology Facilities Council
+# Copyright 2019 University of Manchester
+#
+# This work is part of the Core Imaging Library developed by Science Technology
+# Facilities Council and University of Manchester
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0.txt
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+#=========================================================================
+
+"""
+Total Variation Denoising using PDHG algorithm:
+Problem: min_x, x>0 \alpha * ||\nabla x||_{2,1} + ||x-g||_{1}
+ \alpha: Regularization parameter
+
+ \nabla: Gradient operator
+
+ g: Noisy Data with Salt & Pepper Noise
+
+
+ Method = 0 ( PDHG - split ) : K = [ \nabla,
+ Identity]
+
+
+ Method = 1 (PDHG - explicit ): K = \nabla
+
+
+"""
+
+import numpy
+import matplotlib.pyplot as plt
+
+from ccpi.optimisation.algorithms import PDHG
+
+from ccpi.optimisation.operators import Gradient, BlockOperator, FiniteDiff
+from ccpi.optimisation.functions import MixedL21Norm, MixedL11Norm, L2NormSquared, BlockFunction, L1Norm
+from ccpi.framework import TestData, ImageGeometry
+import os, sys
+if int(numpy.version.version.split('.')[1]) > 12:
+ from skimage.util import random_noise
+else:
+ from demoutil import random_noise
+
+loader = TestData(data_dir=os.path.join(sys.prefix, 'share','ccpi'))
+data = loader.load(TestData.PEPPERS, size=(256,256))
+ig = data.geometry
+ag = ig
+
+# Create noisy data.
+n1 = random_noise(data.as_array(), mode = 'gaussian', var = 0.15, seed = 50)
+noisy_data = ig.allocate()
+noisy_data.fill(n1)
+
+# Show Ground Truth and Noisy Data
+plt.figure(figsize=(10,5))
+plt.subplot(1,2,1)
+plt.imshow(data.as_array())
+plt.title('Ground Truth')
+plt.colorbar()
+plt.subplot(1,2,2)
+plt.imshow(noisy_data.as_array())
+plt.title('Noisy Data')
+plt.colorbar()
+plt.show()
+
+# Regularisation Parameter
+operator = Gradient(ig, correlation=Gradient.CORRELATION_SPACE)
+f1 = 5 * MixedL21Norm()
+g = 0.5 * L2NormSquared(b=noisy_data)
+
+# Compute operator Norm
+normK = operator.norm()
+
+# Primal & dual stepsizes
+sigma = 1
+tau = 1/(sigma*normK**2)
+
+# Setup and run the PDHG algorithm
+pdhg1 = PDHG(f=f1,g=g,operator=operator, tau=tau, sigma=sigma)
+pdhg1.max_iteration = 2000
+pdhg1.update_objective_interval = 200
+pdhg1.run(1000)
+
+
+# Show results
+plt.figure(figsize=(10,10))
+plt.subplot(2,2,1)
+plt.imshow(data.as_array())
+plt.title('Ground Truth')
+plt.colorbar()
+plt.subplot(2,2,2)
+plt.imshow(noisy_data.as_array())
+plt.title('Noisy Data')
+plt.colorbar()
+plt.subplot(2,2,3)
+plt.imshow(pdhg1.get_output().as_array())
+plt.title('TV Reconstruction')
+plt.colorbar()
+plt.subplot(2,2,4)
+plt.imshow(pdhg2.get_output().as_array())
+plt.title('TV Reconstruction')
+plt.colorbar()
+plt.show()
+
diff --git a/Wrappers/Python/demos/PDHG_examples/TV_Denoising/PDHG_TV_Denoising_2D_time.py b/Wrappers/Python/demos/PDHG_examples/TV_Denoising/PDHG_TV_Denoising_2D_time.py
new file mode 100644
index 0000000..14608db
--- /dev/null
+++ b/Wrappers/Python/demos/PDHG_examples/TV_Denoising/PDHG_TV_Denoising_2D_time.py
@@ -0,0 +1,192 @@
+#========================================================================
+# Copyright 2019 Science Technology Facilities Council
+# Copyright 2019 University of Manchester
+#
+# This work is part of the Core Imaging Library developed by Science Technology
+# Facilities Council and University of Manchester
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0.txt
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+#=========================================================================
+
+from ccpi.framework import ImageData, ImageGeometry, AcquisitionGeometry, AcquisitionData
+
+import numpy as np
+import numpy
+import matplotlib.pyplot as plt
+
+from ccpi.optimisation.algorithms import PDHG
+
+from ccpi.optimisation.operators import BlockOperator, Gradient, Identity
+from ccpi.optimisation.functions import ZeroFunction, L2NormSquared, \
+ MixedL21Norm, BlockFunction
+
+from ccpi.astra.ops import AstraProjectorMC
+
+import os
+import tomophantom
+from tomophantom import TomoP2D
+
+# Create phantom for TV 2D dynamic tomography
+
+model = 102 # note that the selected model is temporal (2D + time)
+N = 128 # set dimension of the phantom
+# one can specify an exact path to the parameters file
+# path_library2D = '../../../PhantomLibrary/models/Phantom2DLibrary.dat'
+path = os.path.dirname(tomophantom.__file__)
+path_library2D = os.path.join(path, "Phantom2DLibrary.dat")
+#This will generate a N_size x N_size x Time frames phantom (2D + time)
+phantom_2Dt = TomoP2D.ModelTemporal(model, N, path_library2D)
+
+plt.close('all')
+plt.figure(1)
+plt.rcParams.update({'font.size': 21})
+plt.title('{}''{}'.format('2D+t phantom using model no.',model))
+for sl in range(0,np.shape(phantom_2Dt)[0]):
+ im = phantom_2Dt[sl,:,:]
+ plt.imshow(im, vmin=0, vmax=1)
+# plt.pause(.1)
+# plt.draw
+
+
+ig = ImageGeometry(voxel_num_x = N, voxel_num_y = N, channels = np.shape(phantom_2Dt)[0])
+data = ImageData(phantom_2Dt, geometry=ig)
+ag = ig
+
+# Create Noisy data. Add Gaussian noise
+np.random.seed(10)
+noisy_data = ImageData( data.as_array() + np.random.normal(0, 0.25, size=ig.shape) )
+
+tindex = [3, 6, 10]
+
+fig, axes = plt.subplots(nrows=1, ncols=3, figsize=(10, 10))
+plt.subplot(1,3,1)
+plt.imshow(noisy_data.as_array()[tindex[0],:,:])
+plt.axis('off')
+plt.title('Time {}'.format(tindex[0]))
+plt.subplot(1,3,2)
+plt.imshow(noisy_data.as_array()[tindex[1],:,:])
+plt.axis('off')
+plt.title('Time {}'.format(tindex[1]))
+plt.subplot(1,3,3)
+plt.imshow(noisy_data.as_array()[tindex[2],:,:])
+plt.axis('off')
+plt.title('Time {}'.format(tindex[2]))
+
+fig.subplots_adjust(bottom=0.1, top=0.9, left=0.1, right=0.8,
+ wspace=0.02, hspace=0.02)
+
+plt.show()
+
+#%%
+# Regularisation Parameter
+alpha = 0.3
+
+# Create operators
+#op1 = Gradient(ig)
+op1 = Gradient(ig, correlation='Space')
+op2 = Gradient(ig, correlation='SpaceChannels')
+
+op3 = Identity(ig, ag)
+
+# Create BlockOperator
+operator1 = BlockOperator(op1, op3, shape=(2,1) )
+operator2 = BlockOperator(op2, op3, shape=(2,1) )
+
+# Create functions
+
+f1 = alpha * MixedL21Norm()
+f2 = 0.5 * L2NormSquared(b = noisy_data)
+f = BlockFunction(f1, f2)
+
+g = ZeroFunction()
+
+# Compute operator Norm
+normK1 = operator1.norm()
+normK2 = operator2.norm()
+
+#%%
+# Primal & dual stepsizes
+sigma1 = 1
+tau1 = 1/(sigma1*normK1**2)
+
+sigma2 = 1
+tau2 = 1/(sigma2*normK2**2)
+
+# Setup and run the PDHG algorithm
+pdhg1 = PDHG(f=f,g=g,operator=operator1, tau=tau1, sigma=sigma1)
+pdhg1.max_iteration = 2000
+pdhg1.update_objective_interval = 200
+pdhg1.run(2000)
+
+# Setup and run the PDHG algorithm
+pdhg2 = PDHG(f=f,g=g,operator=operator2, tau=tau2, sigma=sigma2)
+pdhg2.max_iteration = 2000
+pdhg2.update_objective_interval = 200
+pdhg2.run(2000)
+
+
+#%%
+
+tindex = [3, 6, 10]
+fig, axes = plt.subplots(nrows=2, ncols=3, figsize=(10, 8))
+
+plt.subplot(3,3,1)
+plt.imshow(phantom_2Dt[tindex[0],:,:])
+plt.axis('off')
+plt.title('Time {}'.format(tindex[0]))
+
+plt.subplot(3,3,2)
+plt.imshow(phantom_2Dt[tindex[1],:,:])
+plt.axis('off')
+plt.title('Time {}'.format(tindex[1]))
+
+plt.subplot(3,3,3)
+plt.imshow(phantom_2Dt[tindex[2],:,:])
+plt.axis('off')
+plt.title('Time {}'.format(tindex[2]))
+
+plt.subplot(3,3,4)
+plt.imshow(pdhg1.get_output().as_array()[tindex[0],:,:])
+plt.axis('off')
+plt.subplot(3,3,5)
+plt.imshow(pdhg1.get_output().as_array()[tindex[1],:,:])
+plt.axis('off')
+plt.subplot(3,3,6)
+plt.imshow(pdhg1.get_output().as_array()[tindex[2],:,:])
+plt.axis('off')
+
+
+plt.subplot(3,3,7)
+plt.imshow(pdhg2.get_output().as_array()[tindex[0],:,:])
+plt.axis('off')
+plt.subplot(3,3,8)
+plt.imshow(pdhg2.get_output().as_array()[tindex[1],:,:])
+plt.axis('off')
+plt.subplot(3,3,9)
+plt.imshow(pdhg2.get_output().as_array()[tindex[2],:,:])
+plt.axis('off')
+
+#%%
+im = plt.imshow(pdhg1.get_output().as_array()[tindex[0],:,:])
+
+
+fig.subplots_adjust(bottom=0.1, top=0.9, left=0.1, right=0.8,
+ wspace=0.02, hspace=0.02)
+
+cb_ax = fig.add_axes([0.83, 0.1, 0.02, 0.8])
+cbar = fig.colorbar(im, cax=cb_ax)
+
+
+plt.show()
+
diff --git a/Wrappers/Python/demos/PDHG_examples/TV_Denoising/PDHG_TV_Denoising_Gaussian_3D.py b/Wrappers/Python/demos/PDHG_examples/TV_Denoising/PDHG_TV_Denoising_Gaussian_3D.py
new file mode 100644
index 0000000..03dc2ef
--- /dev/null
+++ b/Wrappers/Python/demos/PDHG_examples/TV_Denoising/PDHG_TV_Denoising_Gaussian_3D.py
@@ -0,0 +1,181 @@
+#========================================================================
+# Copyright 2019 Science Technology Facilities Council
+# Copyright 2019 University of Manchester
+#
+# This work is part of the Core Imaging Library developed by Science Technology
+# Facilities Council and University of Manchester
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0.txt
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+#=========================================================================
+"""
+
+Total Variation (3D) Denoising using PDHG algorithm:
+
+
+Problem: min_{x} \alpha * ||\nabla x||_{2,1} + \frac{1}{2} * || x - g ||_{2}^{2}
+
+ \alpha: Regularization parameter
+
+ \nabla: Gradient operator
+
+ g: Noisy Data with Gaussian Noise
+
+ Method = 0 ( PDHG - split ) : K = [ \nabla,
+ Identity]
+
+
+ Method = 1 (PDHG - explicit ): K = \nabla
+
+"""
+
+from ccpi.framework import ImageData, ImageGeometry
+
+import matplotlib.pyplot as plt
+
+from ccpi.optimisation.algorithms import PDHG
+
+from ccpi.optimisation.operators import BlockOperator, Identity, Gradient
+from ccpi.optimisation.functions import ZeroFunction, L2NormSquared, \
+ MixedL21Norm, BlockFunction
+
+from skimage.util import random_noise
+
+# Create phantom for TV Gaussian denoising
+import timeit
+import os
+from tomophantom import TomoP3D
+import tomophantom
+
+print ("Building 3D phantom using TomoPhantom software")
+tic=timeit.default_timer()
+model = 13 # select a model number from the library
+N = 64 # Define phantom dimensions using a scalar value (cubic phantom)
+path = os.path.dirname(tomophantom.__file__)
+path_library3D = os.path.join(path, "Phantom3DLibrary.dat")
+
+#This will generate a N x N x N phantom (3D)
+phantom_tm = TomoP3D.Model(model, N, path_library3D)
+
+#%%
+
+# Create noisy data. Add Gaussian noise
+ig = ImageGeometry(voxel_num_x=N, voxel_num_y=N, voxel_num_z=N)
+ag = ig
+n1 = random_noise(phantom_tm, mode = 'gaussian', mean=0, var = 0.001, seed=10)
+noisy_data = ImageData(n1)
+
+sliceSel = int(0.5*N)
+plt.figure(figsize=(15,15))
+plt.subplot(3,1,1)
+plt.imshow(noisy_data.as_array()[sliceSel,:,:],vmin=0, vmax=1)
+plt.title('Axial View')
+plt.colorbar()
+plt.subplot(3,1,2)
+plt.imshow(noisy_data.as_array()[:,sliceSel,:],vmin=0, vmax=1)
+plt.title('Coronal View')
+plt.colorbar()
+plt.subplot(3,1,3)
+plt.imshow(noisy_data.as_array()[:,:,sliceSel],vmin=0, vmax=1)
+plt.title('Sagittal View')
+plt.colorbar()
+plt.show()
+
+#%%
+
+# Regularisation Parameter
+alpha = 0.05
+
+method = '0'
+
+if method == '0':
+
+ # Create operators
+ op1 = Gradient(ig)
+ op2 = Identity(ig, ag)
+
+ # Create BlockOperator
+ operator = BlockOperator(op1, op2, shape=(2,1) )
+
+ # Create functions
+
+ f1 = alpha * MixedL21Norm()
+ f2 = 0.5 * L2NormSquared(b = noisy_data)
+ f = BlockFunction(f1, f2)
+
+ g = ZeroFunction()
+
+else:
+
+ # Without the "Block Framework"
+ operator = Gradient(ig)
+ f = alpha * MixedL21Norm()
+ g = 0.5 * L2NormSquared(b = noisy_data)
+
+
+# Compute operator Norm
+normK = operator.norm()
+
+# Primal & dual stepsizes
+sigma = 1
+tau = 1/(sigma*normK**2)
+
+# Setup and run the PDHG algorithm
+pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma, memopt=True)
+pdhg.max_iteration = 2000
+pdhg.update_objective_interval = 200
+pdhg.run(2000, verbose = True)
+
+
+#%%
+fig, axes = plt.subplots(nrows=2, ncols=3, figsize=(10, 8))
+fig.suptitle('TV Reconstruction',fontsize=20)
+
+
+plt.subplot(2,3,1)
+plt.imshow(noisy_data.as_array()[sliceSel,:,:],vmin=0, vmax=1)
+plt.axis('off')
+plt.title('Axial View')
+
+plt.subplot(2,3,2)
+plt.imshow(noisy_data.as_array()[:,sliceSel,:],vmin=0, vmax=1)
+plt.axis('off')
+plt.title('Coronal View')
+
+plt.subplot(2,3,3)
+plt.imshow(noisy_data.as_array()[:,:,sliceSel],vmin=0, vmax=1)
+plt.axis('off')
+plt.title('Sagittal View')
+
+
+plt.subplot(2,3,4)
+plt.imshow(pdhg.get_output().as_array()[sliceSel,:,:],vmin=0, vmax=1)
+plt.axis('off')
+plt.subplot(2,3,5)
+plt.imshow(pdhg.get_output().as_array()[:,sliceSel,:],vmin=0, vmax=1)
+plt.axis('off')
+plt.subplot(2,3,6)
+plt.imshow(pdhg.get_output().as_array()[:,:,sliceSel],vmin=0, vmax=1)
+plt.axis('off')
+im = plt.imshow(pdhg.get_output().as_array()[:,:,sliceSel],vmin=0, vmax=1)
+
+
+fig.subplots_adjust(bottom=0.1, top=0.9, left=0.1, right=0.8,
+ wspace=0.02, hspace=0.02)
+
+cb_ax = fig.add_axes([0.83, 0.1, 0.02, 0.8])
+cbar = fig.colorbar(im, cax=cb_ax)
+
+
+plt.show()
+
diff --git a/Wrappers/Python/demos/PDHG_examples/Tomo/PDHG_Tikhonov_Tomo2D.py b/Wrappers/Python/demos/PDHG_examples/Tomo/PDHG_Tikhonov_Tomo2D.py
new file mode 100644
index 0000000..02cd053
--- /dev/null
+++ b/Wrappers/Python/demos/PDHG_examples/Tomo/PDHG_Tikhonov_Tomo2D.py
@@ -0,0 +1,156 @@
+#========================================================================
+# Copyright 2019 Science Technology Facilities Council
+# Copyright 2019 University of Manchester
+#
+# This work is part of the Core Imaging Library developed by Science Technology
+# Facilities Council and University of Manchester
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0.txt
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+#=========================================================================
+
+"""
+
+Total Variation Denoising using PDHG algorithm:
+
+Problem: min_x, x>0 \alpha * ||\nabla x||_{2}^{2} + int A x -g log(Ax + \eta)
+
+ \nabla: Gradient operator
+
+ A: Projection Matrix
+ g: Noisy sinogram corrupted with Poisson Noise
+
+ \eta: Background Noise
+ \alpha: Regularization parameter
+
+
+
+"""
+
+
+from ccpi.framework import ImageData, ImageGeometry, AcquisitionGeometry, AcquisitionData
+
+import numpy as np
+import numpy
+import matplotlib.pyplot as plt
+
+from ccpi.optimisation.algorithms import PDHG
+
+from ccpi.optimisation.operators import BlockOperator, Gradient
+from ccpi.optimisation.functions import ZeroFunction, L2NormSquared, BlockFunction
+
+from ccpi.astra.ops import AstraProjectorSimple
+from ccpi.framework import TestData
+import os, sys
+
+loader = TestData(data_dir=os.path.join(sys.prefix, 'share','ccpi'))
+
+# Load Data
+N = 100
+M = 100
+data = loader.load(TestData.SIMPLE_PHANTOM_2D, size=(N,M), scale=(0,1))
+
+ig = data.geometry
+ag = ig
+
+#Create Acquisition Data and apply poisson noise
+
+detectors = N
+angles = np.linspace(0, np.pi, N)
+
+ag = AcquisitionGeometry('parallel','2D',angles, detectors)
+
+device = input('Available device: GPU==1 / CPU==0 ')
+
+if device=='1':
+ dev = 'gpu'
+else:
+ dev = 'cpu'
+
+Aop = AstraProjectorSimple(ig, ag, 'cpu')
+sin = Aop.direct(data)
+
+# Create noisy data. Apply Poisson noise
+scale = 0.5
+eta = 0
+n1 = scale * np.random.poisson(eta + sin.as_array()/scale)
+
+noisy_data = AcquisitionData(n1, ag)
+
+# Show Ground Truth and Noisy Data
+plt.figure(figsize=(10,10))
+plt.subplot(2,1,1)
+plt.imshow(data.as_array())
+plt.title('Ground Truth')
+plt.colorbar()
+plt.subplot(2,1,2)
+plt.imshow(noisy_data.as_array())
+plt.title('Noisy Data')
+plt.colorbar()
+plt.show()
+
+
+# Regularisation Parameter
+alpha = 1000
+
+# Create operators
+op1 = Gradient(ig)
+op2 = Aop
+
+# Create BlockOperator
+operator = BlockOperator(op1, op2, shape=(2,1) )
+
+# Create functions
+
+f1 = alpha * L2NormSquared()
+f2 = 0.5 * L2NormSquared(b=noisy_data)
+f = BlockFunction(f1, f2)
+
+g = ZeroFunction()
+
+# Compute operator Norm
+normK = operator.norm()
+
+# Primal & dual stepsizes
+sigma = 1
+tau = 1/(sigma*normK**2)
+
+
+# Setup and run the PDHG algorithm
+pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma)
+pdhg.max_iteration = 2000
+pdhg.update_objective_interval = 500
+pdhg.run(2000)
+
+plt.figure(figsize=(15,15))
+plt.subplot(3,1,1)
+plt.imshow(data.as_array())
+plt.title('Ground Truth')
+plt.colorbar()
+plt.subplot(3,1,2)
+plt.imshow(noisy_data.as_array())
+plt.title('Noisy Data')
+plt.colorbar()
+plt.subplot(3,1,3)
+plt.imshow(pdhg.get_output().as_array())
+plt.title('Tikhonov Reconstruction')
+plt.colorbar()
+plt.show()
+##
+plt.plot(np.linspace(0,N,M), data.as_array()[int(N/2),:], label = 'GTruth')
+plt.plot(np.linspace(0,N,M), pdhg.get_output().as_array()[int(N/2),:], label = 'Tikhonov reconstruction')
+plt.legend()
+plt.title('Middle Line Profiles')
+plt.show()
+
+
diff --git a/Wrappers/Python/demos/pdhg_TV_tomography2Dccpi.py b/Wrappers/Python/demos/pdhg_TV_tomography2Dccpi.py
new file mode 100644
index 0000000..854f645
--- /dev/null
+++ b/Wrappers/Python/demos/pdhg_TV_tomography2Dccpi.py
@@ -0,0 +1,238 @@
+# -*- coding: utf-8 -*-
+
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Created on Fri Feb 22 14:53:03 2019
+
+@author: evangelos
+"""
+
+from ccpi.framework import ImageData, ImageGeometry, BlockDataContainer, \
+ AcquisitionGeometry, AcquisitionData
+
+import numpy as np
+import matplotlib.pyplot as plt
+
+from ccpi.optimisation.algorithms import PDHG, PDHG_old
+
+from ccpi.optimisation.operators import BlockOperator, Identity, Gradient
+from ccpi.optimisation.functions import ZeroFunction, L2NormSquared, \
+ MixedL21Norm, BlockFunction, ScaledFunction
+
+from ccpi.plugins.operators import CCPiProjectorSimple
+from timeit import default_timer as timer
+from ccpi.reconstruction.parallelbeam import alg as pbalg
+import os
+
+try:
+ import tomophantom
+ from tomophantom import TomoP3D
+ no_tomophantom = False
+except ImportError as ie:
+ no_tomophantom = True
+
+#%%
+
+#%%###############################################################################
+# Create phantom for TV tomography
+
+#import os
+#import tomophantom
+#from tomophantom import TomoP2D
+#from tomophantom.supp.qualitymetrics import QualityTools
+
+#model = 1 # select a model number from the library
+#N = 150 # set dimension of the phantom
+## one can specify an exact path to the parameters file
+## path_library2D = '../../../PhantomLibrary/models/Phantom2DLibrary.dat'
+#path = os.path.dirname(tomophantom.__file__)
+#path_library2D = os.path.join(path, "Phantom2DLibrary.dat")
+##This will generate a N_size x N_size phantom (2D)
+#phantom_2D = TomoP2D.Model(model, N, path_library2D)
+#ig = ImageGeometry(voxel_num_x = N, voxel_num_y = N)
+#data = ImageData(phantom_2D, geometry=ig)
+
+N = 75
+#x = np.zeros((N,N))
+
+vert = 4
+ig = ImageGeometry(voxel_num_x = N, voxel_num_y = N, voxel_num_z=vert)
+
+angles_num = 100
+det_w = 1.0
+det_num = N
+
+angles = np.linspace(-90.,90.,N, dtype=np.float32)
+# Inputs: Geometry, 2D or 3D, angles, horz detector pixel count,
+# horz detector pixel size, vert detector pixel count,
+# vert detector pixel size.
+ag = AcquisitionGeometry('parallel',
+ '3D',
+ angles,
+ N,
+ det_w,
+ vert,
+ det_w)
+
+#no_tomophantom = True
+if no_tomophantom:
+ data = ig.allocate()
+ Phantom = data
+ # Populate image data by looping over and filling slices
+ i = 0
+ while i < vert:
+ if vert > 1:
+ x = Phantom.subset(vertical=i).array
+ else:
+ x = Phantom.array
+ x[round(N/4):round(3*N/4),round(N/4):round(3*N/4)] = 0.5
+ x[round(N/8):round(7*N/8),round(3*N/8):round(5*N/8)] = 0.98
+ if vert > 1 :
+ Phantom.fill(x, vertical=i)
+ i += 1
+
+ Aop = CCPiProjectorSimple(ig, ag, 'cpu')
+ sin = Aop.direct(data)
+else:
+
+ model = 13 # select a model number from the library
+ N_size = N # Define phantom dimensions using a scalar value (cubic phantom)
+ path = os.path.dirname(tomophantom.__file__)
+ path_library3D = os.path.join(path, "Phantom3DLibrary.dat")
+ #This will generate a N_size x N_size x N_size phantom (3D)
+ phantom_tm = TomoP3D.Model(model, N_size, path_library3D)
+
+ #%%
+ Horiz_det = int(np.sqrt(2)*N_size) # detector column count (horizontal)
+ Vert_det = N_size # detector row count (vertical) (no reason for it to be > N)
+ #angles_num = int(0.5*np.pi*N_size); # angles number
+ #angles = np.linspace(0.0,179.9,angles_num,dtype='float32') # in degrees
+
+ print ("Building 3D analytical projection data with TomoPhantom")
+ projData3D_analyt = TomoP3D.ModelSino(model,
+ N_size,
+ Horiz_det,
+ Vert_det,
+ angles,
+ path_library3D)
+
+ # tomophantom outputs in [vert,angles,horiz]
+ # we want [angle,vert,horiz]
+ data = np.transpose(projData3D_analyt, [1,0,2])
+ ag.pixel_num_h = Horiz_det
+ ag.pixel_num_v = Vert_det
+ sin = ag.allocate()
+ sin.fill(data)
+ ig.voxel_num_y = Vert_det
+
+ Aop = CCPiProjectorSimple(ig, ag, 'cpu')
+
+
+plt.imshow(sin.subset(vertical=0).as_array())
+plt.title('Sinogram')
+plt.colorbar()
+plt.show()
+
+
+#%%
+# Add Gaussian noise to the sinogram data
+np.random.seed(10)
+n1 = np.random.random(sin.shape)
+
+noisy_data = sin + ImageData(5*n1)
+
+plt.imshow(noisy_data.subset(vertical=0).as_array())
+plt.title('Noisy Sinogram')
+plt.colorbar()
+plt.show()
+
+
+#%% Works only with Composite Operator Structure of PDHG
+
+#ig = ImageGeometry(voxel_num_x = N, voxel_num_y = N)
+
+# Create operators
+op1 = Gradient(ig)
+op2 = Aop
+
+# Form Composite Operator
+operator = BlockOperator(op1, op2, shape=(2,1) )
+
+alpha = 50
+f = BlockFunction( alpha * MixedL21Norm(), \
+ 0.5 * L2NormSquared(b = noisy_data) )
+g = ZeroFunction()
+
+normK = Aop.norm()
+
+# Compute operator Norm
+normK = operator.norm()
+
+## Primal & dual stepsizes
+diag_precon = False
+
+if diag_precon:
+
+ def tau_sigma_precond(operator):
+
+ tau = 1/operator.sum_abs_row()
+ sigma = 1/ operator.sum_abs_col()
+
+ return tau, sigma
+
+ tau, sigma = tau_sigma_precond(operator)
+
+else:
+ sigma = 1
+ tau = 1/(sigma*normK**2)
+
+# Compute operator Norm
+normK = operator.norm()
+
+# Primal & dual stepsizes
+sigma = 1
+tau = 1/(sigma*normK**2)
+niter = 50
+opt = {'niter':niter}
+opt1 = {'niter':niter, 'memopt': True}
+
+
+
+pdhg1 = PDHG(f=f,g=g, operator=operator, tau=tau, sigma=sigma, max_iteration=niter)
+#pdhg1.max_iteration = 2000
+pdhg1.update_objective_interval = 100
+
+t1_old = timer()
+resold, time, primal, dual, pdgap = PDHG_old(f, g, operator, tau = tau, sigma = sigma, opt = opt)
+t2_old = timer()
+
+pdhg1.run(niter)
+print (sum(pdhg1.timing))
+res = pdhg1.get_output().subset(vertical=0)
+
+#%%
+plt.figure()
+plt.subplot(1,4,1)
+plt.imshow(res.as_array())
+plt.title('Algorithm')
+plt.colorbar()
+plt.subplot(1,4,2)
+plt.imshow(resold.subset(vertical=0).as_array())
+plt.title('function')
+plt.colorbar()
+plt.subplot(1,4,3)
+plt.imshow((res - resold.subset(vertical=0)).abs().as_array())
+plt.title('diff')
+plt.colorbar()
+plt.subplot(1,4,4)
+plt.plot(np.linspace(0,N,N), res.as_array()[int(N/2),:], label = 'Algorithm')
+plt.plot(np.linspace(0,N,N), resold.subset(vertical=0).as_array()[int(N/2),:], label = 'function')
+plt.legend()
+plt.show()
+#
+print ("Time: No memopt in {}s, \n Time: Memopt in {}s ".format(sum(pdhg1.timing), t2_old -t1_old))
+diff = (res - resold.subset(vertical=0)).abs().as_array().max()
+#
+print(" Max of abs difference is {}".format(diff))
+
diff --git a/Wrappers/Python/environment.yml b/Wrappers/Python/environment.yml
new file mode 100644
index 0000000..5cdd6fe
--- /dev/null
+++ b/Wrappers/Python/environment.yml
@@ -0,0 +1,11 @@
+name: test_new
+dependencies:
+ - python=3.6.7=h8dc6b48_1004
+ - numpy=1.11.3=py36hdf140aa_1207
+ - spyder=3.3.4=py36_0
+ - scikit-image=0.15.0=py36h6de7cb9_0
+ - scipy=1.2.1=py36hbd7caa9_1
+ - astra-toolbox=1.8.3=py36h804c3c0_0
+
+
+
diff --git a/Wrappers/Python/setup.py b/Wrappers/Python/setup.py
index eaf124b..8bd33a6 100644
--- a/Wrappers/Python/setup.py
+++ b/Wrappers/Python/setup.py
@@ -31,8 +31,17 @@ if cil_version == '':
setup(
name="ccpi-framework",
version=cil_version,
- packages=['ccpi' , 'ccpi.io', 'ccpi.optimisation',
- 'ccpi.optimisation.algorithms'],
+ packages=['ccpi' , 'ccpi.io',
+ 'ccpi.framework', 'ccpi.optimisation',
+ 'ccpi.optimisation.operators',
+ 'ccpi.optimisation.algorithms',
+ 'ccpi.optimisation.functions',
+ 'ccpi.processors',
+ 'ccpi.contrib','ccpi.contrib.optimisation',
+ 'ccpi.contrib.optimisation.algorithms'],
+ data_files = [('share/ccpi', ['data/boat.tiff', 'data/peppers.tiff',
+ 'data/camera.png',
+ 'data/resolution_chart.tiff'])],
# Project uses reStructuredText, so ensure that the docutils get
# installed or upgraded on the target machine
@@ -47,8 +56,9 @@ setup(
# zip_safe = False,
# metadata for upload to PyPI
- author="Edoardo Pasca",
- author_email="edoardo.pasca@stfc.ac.uk",
+ author="CCPi developers",
+ maintainer="Edoardo Pasca",
+ maintainer_email="edoardo.pasca@stfc.ac.uk",
description='CCPi Core Imaging Library - Python Framework Module',
license="Apache v2.0",
keywords="Python Framework",
diff --git a/Wrappers/Python/test/test_BlockDataContainer.py b/Wrappers/Python/test/test_BlockDataContainer.py
new file mode 100755
index 0000000..aeb8454
--- /dev/null
+++ b/Wrappers/Python/test/test_BlockDataContainer.py
@@ -0,0 +1,468 @@
+# -*- coding: utf-8 -*-
+"""
+Created on Tue Mar 5 16:08:23 2019
+
+@author: ofn77899
+"""
+
+import unittest
+import numpy
+from ccpi.framework import ImageGeometry, AcquisitionGeometry
+from ccpi.framework import ImageData, AcquisitionData
+from ccpi.framework import BlockDataContainer, DataContainer
+import functools
+
+from ccpi.optimisation.operators import Gradient, Identity, BlockOperator
+
+class TestBlockDataContainer(unittest.TestCase):
+ def skiptest_BlockDataContainerShape(self):
+ print ("test block data container")
+ ig0 = ImageGeometry(12,42,55,32)
+ ig1 = ImageGeometry(12,42,55,32)
+
+ data0 = ImageData(geometry=ig0)
+ data1 = ImageData(geometry=ig1) + 1
+
+ data2 = ImageData(geometry=ig0) + 2
+ data3 = ImageData(geometry=ig1) + 3
+
+ cp0 = BlockDataContainer(data0,data1)
+ cp1 = BlockDataContainer(data2,data3)
+ transpose_shape = (cp0.shape[1], cp0.shape[0])
+ self.assertTrue(cp0.T.shape == transpose_shape)
+ def skiptest_BlockDataContainerShapeArithmetic(self):
+ print ("test block data container")
+ ig0 = ImageGeometry(2,3,4)
+ ig1 = ImageGeometry(2,3,4)
+
+ data0 = ImageData(geometry=ig0)
+ data1 = ImageData(geometry=ig1) + 1
+
+ data2 = ImageData(geometry=ig0) + 2
+ data3 = ImageData(geometry=ig1) + 3
+
+ cp0 = BlockDataContainer(data0,data1)
+ #cp1 = BlockDataContainer(data2,data3)
+ cp1 = cp0 + 1
+ self.assertTrue(cp1.shape == cp0.shape)
+ cp1 = cp0.T + 1
+
+ transpose_shape = (cp0.shape[1], cp0.shape[0])
+ self.assertTrue(cp1.shape == transpose_shape)
+
+ cp1 = cp0.T - 1
+ transpose_shape = (cp0.shape[1], cp0.shape[0])
+ self.assertTrue(cp1.shape == transpose_shape)
+
+ cp1 = (cp0.T + 1)*2
+ transpose_shape = (cp0.shape[1], cp0.shape[0])
+ self.assertTrue(cp1.shape == transpose_shape)
+
+ cp1 = (cp0.T + 1)/2
+ transpose_shape = (cp0.shape[1], cp0.shape[0])
+ self.assertTrue(cp1.shape == transpose_shape)
+
+ cp1 = cp0.T.power(2.2)
+ transpose_shape = (cp0.shape[1], cp0.shape[0])
+ self.assertTrue(cp1.shape == transpose_shape)
+
+ cp1 = cp0.T.maximum(3)
+ transpose_shape = (cp0.shape[1], cp0.shape[0])
+ self.assertTrue(cp1.shape == transpose_shape)
+
+ cp1 = cp0.T.abs()
+ transpose_shape = (cp0.shape[1], cp0.shape[0])
+ self.assertTrue(cp1.shape == transpose_shape)
+
+ cp1 = cp0.T.sign()
+ transpose_shape = (cp0.shape[1], cp0.shape[0])
+ self.assertTrue(cp1.shape == transpose_shape)
+
+ cp1 = cp0.T.sqrt()
+ transpose_shape = (cp0.shape[1], cp0.shape[0])
+ self.assertTrue(cp1.shape == transpose_shape)
+
+ cp1 = cp0.T.conjugate()
+ transpose_shape = (cp0.shape[1], cp0.shape[0])
+ self.assertTrue(cp1.shape == transpose_shape)
+
+ def test_BlockDataContainer(self):
+ print ("test block data container")
+ ig0 = ImageGeometry(2,3,4)
+ ig1 = ImageGeometry(2,3,5)
+
+ data0 = ImageData(geometry=ig0)
+ data1 = ImageData(geometry=ig1) + 1
+
+ data2 = ImageData(geometry=ig0) + 2
+ data3 = ImageData(geometry=ig1) + 3
+
+ cp0 = BlockDataContainer(data0,data1)
+ cp1 = BlockDataContainer(data2,data3)
+
+ cp2 = BlockDataContainer(data0+1, data2+1)
+ d = cp2 + data0
+ self.assertEqual(d.get_item(0).as_array()[0][0][0], 1)
+ try:
+ d = cp2 + data1
+ self.assertTrue(False)
+ except ValueError as ve:
+ print (ve)
+ self.assertTrue(True)
+ d = cp2 - data0
+ self.assertEqual(d.get_item(0).as_array()[0][0][0], 1)
+ try:
+ d = cp2 - data1
+ self.assertTrue(False)
+ except ValueError as ve:
+ print (ve)
+ self.assertTrue(True)
+ d = cp2 * data2
+ self.assertEqual(d.get_item(0).as_array()[0][0][0], 2)
+ try:
+ d = cp2 * data1
+ self.assertTrue(False)
+ except ValueError as ve:
+ print (ve)
+ self.assertTrue(True)
+
+ a = [ (el, ot) for el,ot in zip(cp0.containers,cp1.containers)]
+ print (a[0][0].shape)
+ #cp2 = BlockDataContainer(*a)
+ cp2 = cp0.add(cp1)
+ self.assertEqual (cp2.get_item(0).as_array()[0][0][0] , 2.)
+ self.assertEqual (cp2.get_item(1).as_array()[0][0][0] , 4.)
+
+ cp2 = cp0 + cp1
+ self.assertTrue (cp2.get_item(0).as_array()[0][0][0] == 2.)
+ self.assertTrue (cp2.get_item(1).as_array()[0][0][0] == 4.)
+ cp2 = cp0 + 1
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , 1. , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , 2., decimal = 5)
+ cp2 = cp0 + [1 ,2]
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , 1. , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , 3., decimal = 5)
+ cp2 += cp1
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , +3. , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , +6., decimal = 5)
+
+ cp2 += 1
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , +4. , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , +7., decimal = 5)
+
+ cp2 += [-2,-1]
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , 2. , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , 6., decimal = 5)
+
+
+ cp2 = cp0.subtract(cp1)
+ assert (cp2.get_item(0).as_array()[0][0][0] == -2.)
+ assert (cp2.get_item(1).as_array()[0][0][0] == -2.)
+ cp2 = cp0 - cp1
+ assert (cp2.get_item(0).as_array()[0][0][0] == -2.)
+ assert (cp2.get_item(1).as_array()[0][0][0] == -2.)
+
+ cp2 = cp0 - 1
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , -1. , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , 0, decimal = 5)
+ cp2 = cp0 - [1 ,2]
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , -1. , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , -1., decimal = 5)
+
+ cp2 -= cp1
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , -3. , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , -4., decimal = 5)
+
+ cp2 -= 1
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , -4. , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , -5., decimal = 5)
+
+ cp2 -= [-2,-1]
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , -2. , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , -4., decimal = 5)
+
+
+ cp2 = cp0.multiply(cp1)
+ assert (cp2.get_item(0).as_array()[0][0][0] == 0.)
+ assert (cp2.get_item(1).as_array()[0][0][0] == 3.)
+ cp2 = cp0 * cp1
+ assert (cp2.get_item(0).as_array()[0][0][0] == 0.)
+ assert (cp2.get_item(1).as_array()[0][0][0] == 3.)
+
+ cp2 = cp0 * 2
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , 0. , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , 2, decimal = 5)
+ cp2 = 2 * cp0
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , 0. , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , 2, decimal = 5)
+ cp2 = cp0 * [3 ,2]
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , 0. , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , 2., decimal = 5)
+ cp2 = cp0 * numpy.asarray([3 ,2])
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , 0. , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , 2., decimal = 5)
+
+ cp2 = [3,2] * cp0
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , 0. , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , 2., decimal = 5)
+ cp2 = numpy.asarray([3,2]) * cp0
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , 0. , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , 2., decimal = 5)
+
+ try:
+ cp2 = [3,2,3] * cp0
+ #numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , 0. , decimal=5)
+ #numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , 2., decimal = 5)
+ self.assertTrue(False)
+ except ValueError as ve:
+ print (ve)
+ self.assertTrue(True)
+ cp2 *= cp1
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , 0 , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , +6., decimal = 5)
+
+ cp2 *= 1
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , 0. , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , +6., decimal = 5)
+
+ cp2 *= [-2,-1]
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , 0. , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , -6., decimal = 5)
+
+ try:
+ cp2 *= [2,3,5]
+ self.assertTrue(False)
+ except ValueError as ve:
+ print (ve)
+ self.assertTrue(True)
+
+ cp2 = cp0.divide(cp1)
+ assert (cp2.get_item(0).as_array()[0][0][0] == 0.)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0], 1./3., decimal=4)
+ cp2 = cp0/cp1
+ assert (cp2.get_item(0).as_array()[0][0][0] == 0.)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0], 1./3., decimal=4)
+
+ cp2 = cp0 / 2
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , 0. , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , 0.5, decimal = 5)
+ cp2 = cp0 / [3 ,2]
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , 0. , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , 0.5, decimal = 5)
+ cp2 = cp0 / numpy.asarray([3 ,2])
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , 0. , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , 0.5, decimal = 5)
+ cp3 = numpy.asarray([3 ,2]) / (cp0+1)
+ numpy.testing.assert_almost_equal(cp3.get_item(0).as_array()[0][0][0] , 3. , decimal=5)
+ numpy.testing.assert_almost_equal(cp3.get_item(1).as_array()[0][0][0] , 1, decimal = 5)
+
+ cp2 += 1
+ cp2 /= cp1
+ # TODO fix inplace division
+
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , 1./2 , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , 1.5/3., decimal = 5)
+
+ cp2 /= 1
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , 0.5 , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , 0.5, decimal = 5)
+
+ cp2 /= [-2,-1]
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , -0.5/2. , decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , -0.5, decimal = 5)
+ ####
+
+ cp2 = cp0.power(cp1)
+ assert (cp2.get_item(0).as_array()[0][0][0] == 0.)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0], 1., decimal=4)
+ cp2 = cp0**cp1
+ assert (cp2.get_item(0).as_array()[0][0][0] == 0.)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0], 1., decimal=4)
+
+ cp2 = cp0 ** 2
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0] , 0., decimal=5)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0] , 1., decimal = 5)
+
+ cp2 = cp0.maximum(cp1)
+ assert (cp2.get_item(0).as_array()[0][0][0] == cp1.get_item(0).as_array()[0][0][0])
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0], cp2.get_item(1).as_array()[0][0][0], decimal=4)
+
+
+ cp2 = cp0.abs()
+ numpy.testing.assert_almost_equal(cp2.get_item(0).as_array()[0][0][0], 0., decimal=4)
+ numpy.testing.assert_almost_equal(cp2.get_item(1).as_array()[0][0][0], 1., decimal=4)
+
+ cp2 = cp0.subtract(cp1)
+ s = cp2.sign()
+ numpy.testing.assert_almost_equal(s.get_item(0).as_array()[0][0][0], -1., decimal=4)
+ numpy.testing.assert_almost_equal(s.get_item(1).as_array()[0][0][0], -1., decimal=4)
+
+ cp2 = cp0.add(cp1)
+ s = cp2.sqrt()
+ numpy.testing.assert_almost_equal(s.get_item(0).as_array()[0][0][0], numpy.sqrt(2), decimal=4)
+ numpy.testing.assert_almost_equal(s.get_item(1).as_array()[0][0][0], numpy.sqrt(4), decimal=4)
+
+ s = cp0.sum()
+ size = functools.reduce(lambda x,y: x*y, data1.shape, 1)
+ print ("size" , size)
+ numpy.testing.assert_almost_equal(s, 0 + size, decimal=4)
+ s0 = 1
+ s1 = 1
+ for i in cp0.get_item(0).shape:
+ s0 *= i
+ for i in cp0.get_item(1).shape:
+ s1 *= i
+
+ #numpy.testing.assert_almost_equal(s[1], cp0.get_item(0,0).as_array()[0][0][0]*s0 +cp0.get_item(1,0).as_array()[0][0][0]*s1, decimal=4)
+ def test_Nested_BlockDataContainer(self):
+ print ("test_Nested_BlockDataContainer")
+ ig0 = ImageGeometry(2,3,4)
+ ig1 = ImageGeometry(2,3,4)
+
+ data0 = ImageData(geometry=ig0)
+ data1 = ImageData(geometry=ig1) + 1
+
+ data2 = ImageData(geometry=ig0) + 2
+ data3 = ImageData(geometry=ig1) + 3
+
+ cp0 = BlockDataContainer(data0,data1)
+ cp1 = BlockDataContainer(data2,data3)
+
+ nbdc = BlockDataContainer(cp0, cp1)
+ nbdc2 = nbdc + 2
+ numpy.testing.assert_almost_equal(nbdc2.get_item(0).get_item(0).as_array()[0][0][0] , 2. , decimal=5)
+ numpy.testing.assert_almost_equal(nbdc2.get_item(0).get_item(1).as_array()[0][0][0] , 3. , decimal=5)
+ numpy.testing.assert_almost_equal(nbdc2.get_item(1).get_item(0).as_array()[0][0][0] , 4. , decimal=5)
+ numpy.testing.assert_almost_equal(nbdc2.get_item(1).get_item(1).as_array()[0][0][0] , 5. , decimal=5)
+
+ nbdc2 = 2 + nbdc
+ numpy.testing.assert_almost_equal(nbdc2.get_item(0).get_item(0).as_array()[0][0][0] , 2. , decimal=5)
+ numpy.testing.assert_almost_equal(nbdc2.get_item(0).get_item(1).as_array()[0][0][0] , 3. , decimal=5)
+ numpy.testing.assert_almost_equal(nbdc2.get_item(1).get_item(0).as_array()[0][0][0] , 4. , decimal=5)
+ numpy.testing.assert_almost_equal(nbdc2.get_item(1).get_item(1).as_array()[0][0][0] , 5. , decimal=5)
+
+
+ nbdc2 = nbdc * 2
+ numpy.testing.assert_almost_equal(nbdc2.get_item(0).get_item(0).as_array()[0][0][0] , 0. , decimal=5)
+ numpy.testing.assert_almost_equal(nbdc2.get_item(0).get_item(1).as_array()[0][0][0] , 2. , decimal=5)
+ numpy.testing.assert_almost_equal(nbdc2.get_item(1).get_item(0).as_array()[0][0][0] , 4. , decimal=5)
+ numpy.testing.assert_almost_equal(nbdc2.get_item(1).get_item(1).as_array()[0][0][0] , 6. , decimal=5)
+
+ nbdc2 = 2 * nbdc
+ numpy.testing.assert_almost_equal(nbdc2.get_item(0).get_item(0).as_array()[0][0][0] , 0. , decimal=5)
+ numpy.testing.assert_almost_equal(nbdc2.get_item(0).get_item(1).as_array()[0][0][0] , 2. , decimal=5)
+ numpy.testing.assert_almost_equal(nbdc2.get_item(1).get_item(0).as_array()[0][0][0] , 4. , decimal=5)
+ numpy.testing.assert_almost_equal(nbdc2.get_item(1).get_item(1).as_array()[0][0][0] , 6. , decimal=5)
+
+ nbdc2 = nbdc / 2
+ numpy.testing.assert_almost_equal(nbdc2.get_item(0).get_item(0).as_array()[0][0][0] , 0. , decimal=5)
+ numpy.testing.assert_almost_equal(nbdc2.get_item(0).get_item(1).as_array()[0][0][0] , .5 , decimal=5)
+ numpy.testing.assert_almost_equal(nbdc2.get_item(1).get_item(0).as_array()[0][0][0] , 1. , decimal=5)
+ numpy.testing.assert_almost_equal(nbdc2.get_item(1).get_item(1).as_array()[0][0][0] , 3./2 , decimal=5)
+
+ c5 = nbdc.get_item(0).power(2).sum()
+ c5a = nbdc.power(2).sum()
+ print ("sum", c5a, c5)
+
+ cp0 = BlockDataContainer(data0,data2)
+ a = cp0 * data2
+ b = data2 * cp0
+ self.assertBlockDataContainerEqual(a,b)
+
+
+ print ("test_Nested_BlockDataContainer OK")
+ def stest_NestedBlockDataContainer2(self):
+ M, N = 2, 3
+ ig = ImageGeometry(voxel_num_x = M, voxel_num_y = N)
+ ag = ig
+ u = ig.allocate(1)
+ op1 = Gradient(ig)
+ op2 = Identity(ig, ag)
+
+ operator = BlockOperator(op1, op2, shape=(2,1))
+
+ d1 = op1.direct(u)
+ d2 = op2.direct(u)
+
+ d = operator.direct(u)
+
+ dd = operator.domain_geometry()
+ ww = operator.range_geometry()
+
+ print(d.get_item(0).get_item(0).as_array())
+ print(d.get_item(0).get_item(1).as_array())
+ print(d.get_item(1).as_array())
+
+ c1 = d + d
+
+ c2 = 2*d
+
+ c3 = d / (d+0.0001)
+
+
+ c5 = d.get_item(0).power(2).sum()
+
+ def test_BlockDataContainer_fill(self):
+ print ("test block data container")
+ ig0 = ImageGeometry(2,3,4)
+ ig1 = ImageGeometry(2,3,5)
+
+ data0 = ImageData(geometry=ig0)
+ data1 = ImageData(geometry=ig1) + 1
+
+ data2 = ImageData(geometry=ig0) + 2
+ data3 = ImageData(geometry=ig1) + 3
+
+ cp0 = BlockDataContainer(data0,data1)
+ #cp1 = BlockDataContainer(data2,data3)
+
+ cp2 = BlockDataContainer(data0+1, data1+1)
+
+ data0.fill(data2)
+ self.assertNumpyArrayEqual(data0.as_array(), data2.as_array())
+ data0 = ImageData(geometry=ig0)
+
+ for el,ot in zip(cp0, cp2):
+ print (el.shape, ot.shape)
+ cp0.fill(cp2)
+ self.assertBlockDataContainerEqual(cp0, cp2)
+
+ def test_NestedBlockDataContainer(self):
+ ig0 = ImageGeometry(2,3,4)
+ ig1 = ImageGeometry(2,3,5)
+
+ data0 = ig0.allocate(0)
+ data2 = ig0.allocate(1)
+
+ cp0 = BlockDataContainer(data0,data2)
+ #cp1 = BlockDataContainer(data2,data3)
+
+ nested = BlockDataContainer(cp0, data2, data2)
+ out = BlockDataContainer(BlockDataContainer(data0 , data0), data0, data0)
+ nested.divide(data2,out=out)
+ self.assertBlockDataContainerEqual(out, nested)
+
+
+ def assertBlockDataContainerEqual(self, container1, container2):
+ print ("assert Block Data Container Equal")
+ self.assertTrue(issubclass(container1.__class__, container2.__class__))
+ for col in range(container1.shape[0]):
+ if issubclass(container1.get_item(col).__class__, DataContainer):
+ print ("Checking col ", col)
+ self.assertNumpyArrayEqual(
+ container1.get_item(col).as_array(),
+ container2.get_item(col).as_array()
+ )
+ else:
+ self.assertBlockDataContainerEqual(container1.get_item(col),container2.get_item(col))
+
+ def assertNumpyArrayEqual(self, first, second):
+ res = True
+ try:
+ numpy.testing.assert_array_equal(first, second)
+ except AssertionError as err:
+ res = False
+ print(err)
+ self.assertTrue(res)
+
+
diff --git a/Wrappers/Python/test/test_BlockOperator.py b/Wrappers/Python/test/test_BlockOperator.py
new file mode 100644
index 0000000..b82c849
--- /dev/null
+++ b/Wrappers/Python/test/test_BlockOperator.py
@@ -0,0 +1,359 @@
+import unittest
+from ccpi.optimisation.operators import BlockOperator
+from ccpi.framework import BlockDataContainer
+from ccpi.optimisation.operators import Identity
+from ccpi.framework import ImageGeometry, ImageData
+import numpy
+from ccpi.optimisation.operators import FiniteDiff
+
+
+class TestBlockOperator(unittest.TestCase):
+
+ def test_BlockOperator(self):
+ print ("test_BlockOperator")
+ ig = [ ImageGeometry(10,20,30) , \
+ ImageGeometry(10,20,30) , \
+ ImageGeometry(10,20,30) ]
+ x = [ g.allocate() for g in ig ]
+ ops = [ Identity(g) for g in ig ]
+
+ K = BlockOperator(*ops)
+ X = BlockDataContainer(x[0])
+ Y = K.direct(X)
+ self.assertTrue(Y.shape == K.shape)
+
+ numpy.testing.assert_array_equal(Y.get_item(0).as_array(),X.get_item(0).as_array())
+ numpy.testing.assert_array_equal(Y.get_item(1).as_array(),X.get_item(0).as_array())
+ #numpy.testing.assert_array_equal(Y.get_item(2).as_array(),X.get_item(2).as_array())
+
+ X = BlockDataContainer(*x) + 1
+ Y = K.T.direct(X)
+ # K.T (1,3) X (3,1) => output shape (1,1)
+ self.assertTrue(Y.shape == (1,1))
+ zero = numpy.zeros(X.get_item(0).shape)
+ numpy.testing.assert_array_equal(Y.get_item(0).as_array(),len(x)+zero)
+
+ K2 = BlockOperator(*(ops+ops), shape=(3,2))
+ Y = K2.T.direct(X)
+ # K.T (2,3) X (3,1) => output shape (2,1)
+ self.assertTrue(Y.shape == (2,1))
+
+ try:
+ # this should fail as the domain is not compatible
+ ig = [ ImageGeometry(10,20,31) , \
+ ImageGeometry(10,20,30) , \
+ ImageGeometry(10,20,30) ]
+ x = [ g.allocate() for g in ig ]
+ ops = [ Identity(g) for g in ig ]
+
+ K = BlockOperator(*ops)
+ self.assertTrue(False)
+ except ValueError as ve:
+ print (ve)
+ self.assertTrue(True)
+
+ try:
+ # this should fail as the range is not compatible
+ ig = [ ImageGeometry(10,20,30) , \
+ ImageGeometry(10,20,30) , \
+ ImageGeometry(10,20,30) ]
+ rg0 = [ ImageGeometry(10,20,31) , \
+ ImageGeometry(10,20,31) , \
+ ImageGeometry(10,20,31) ]
+ rg1 = [ ImageGeometry(10,22,31) , \
+ 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) ]
+
+ K = BlockOperator(*ops, shape=(2,3))
+ print ("K col comp? " , K.column_wise_compatible())
+ print ("K row comp? " , K.row_wise_compatible())
+ for op in ops:
+ print ("range" , op.range_geometry().shape)
+ for op in ops:
+ print ("domain" , op.domain_geometry().shape)
+ self.assertTrue(False)
+ except ValueError as ve:
+ print (ve)
+ self.assertTrue(True)
+
+ def test_ScaledBlockOperatorSingleScalar(self):
+ ig = [ ImageGeometry(10,20,30) , \
+ ImageGeometry(10,20,30) , \
+ ImageGeometry(10,20,30) ]
+ x = [ g.allocate() for g in ig ]
+ ops = [ Identity(g) for g in ig ]
+
+ val = 1
+ # test limit as non Scaled
+ scalar = 1
+ k = BlockOperator(*ops)
+ K = scalar * k
+ X = BlockDataContainer(*x) + val
+
+ Y = K.T.direct(X)
+ self.assertTrue(Y.shape == (1,1))
+ zero = numpy.zeros(X.get_item(0).shape)
+ xx = numpy.asarray([val for _ in x])
+ numpy.testing.assert_array_equal(Y.get_item(0).as_array(),((scalar*xx).sum()+zero))
+
+ scalar = 0.5
+ k = BlockOperator(*ops)
+ K = scalar * k
+ X = BlockDataContainer(*x) + 1
+
+ Y = K.T.direct(X)
+ self.assertTrue(Y.shape == (1,1))
+ zero = numpy.zeros(X.get_item(0).shape)
+ numpy.testing.assert_array_equal(Y.get_item(0).as_array(),scalar*(len(x)+zero))
+
+
+ def test_ScaledBlockOperatorScalarList(self):
+ ig = [ ImageGeometry(2,3) , \
+ #ImageGeometry(10,20,30) , \
+ ImageGeometry(2,3 ) ]
+ x = [ g.allocate() for g in ig ]
+ ops = [ Identity(g) for g in ig ]
+
+
+ # test limit as non Scaled
+ scalar = numpy.asarray([1 for _ in x])
+ k = BlockOperator(*ops)
+ K = scalar * k
+ val = 1
+ X = BlockDataContainer(*x) + val
+
+ Y = K.T.direct(X)
+ self.assertTrue(Y.shape == (1,1))
+ zero = numpy.zeros(X.get_item(0).shape)
+ xx = numpy.asarray([val for _ in x])
+ numpy.testing.assert_array_equal(Y.get_item(0).as_array(),(scalar*xx).sum()+zero)
+
+ scalar = numpy.asarray([i+1 for i,el in enumerate(x)])
+ #scalar = numpy.asarray([6,0])
+ k = BlockOperator(*ops)
+ K = scalar * k
+ X = BlockDataContainer(*x) + val
+ Y = K.T.direct(X)
+ self.assertTrue(Y.shape == (1,1))
+ zero = numpy.zeros(X.get_item(0).shape)
+ xx = numpy.asarray([val for _ in x])
+
+
+ numpy.testing.assert_array_equal(Y.get_item(0).as_array(),
+ (scalar*xx).sum()+zero)
+
+
+ def test_TomoIdentity(self):
+ ig = ImageGeometry(10,20,30)
+ img = ig.allocate()
+ print (img.shape, ig.shape)
+ self.assertTrue(img.shape == (30,20,10))
+ self.assertEqual(img.sum(), 0)
+ Id = Identity(ig)
+ y = Id.direct(img)
+ numpy.testing.assert_array_equal(y.as_array(), img.as_array())
+
+ def skiptest_CGLS_tikhonov(self):
+ from ccpi.optimisation.algorithms import CGLS
+
+ from ccpi.plugins.ops import CCPiProjectorSimple
+ from ccpi.optimisation.ops import PowerMethodNonsquare
+ from ccpi.optimisation.operators import Identity
+ from ccpi.optimisation.funcs import Norm2sq, Norm1
+ from ccpi.framework import ImageGeometry, AcquisitionGeometry
+ from ccpi.optimisation.Algorithms import GradientDescent
+ #from ccpi.optimisation.Algorithms import CGLS
+ import matplotlib.pyplot as plt
+
+
+ # Set up phantom size N x N x vert by creating ImageGeometry, initialising the
+ # ImageData object with this geometry and empty array and finally put some
+ # data into its array, and display one slice as image.
+
+ # Image parameters
+ N = 128
+ vert = 4
+
+ # Set up image geometry
+ ig = ImageGeometry(voxel_num_x=N,
+ voxel_num_y=N,
+ voxel_num_z=vert)
+
+ # Set up empty image data
+ Phantom = ImageData(geometry=ig,
+ dimension_labels=['horizontal_x',
+ 'horizontal_y',
+ 'vertical'])
+ Phantom += 0.05
+ # Populate image data by looping over and filling slices
+ i = 0
+ while i < vert:
+ if vert > 1:
+ x = Phantom.subset(vertical=i).array
+ else:
+ x = Phantom.array
+ x[round(N/4):round(3*N/4),round(N/4):round(3*N/4)] = 0.5
+ x[round(N/8):round(7*N/8),round(3*N/8):round(5*N/8)] = 0.94
+ if vert > 1 :
+ Phantom.fill(x, vertical=i)
+ i += 1
+
+
+ perc = 0.02
+ # Set up empty image data
+ noise = ImageData(numpy.random.normal(loc = 0.04 ,
+ scale = perc ,
+ size = Phantom.shape), geometry=ig,
+ dimension_labels=['horizontal_x',
+ 'horizontal_y',
+ 'vertical'])
+ Phantom += noise
+
+ # Set up AcquisitionGeometry object to hold the parameters of the measurement
+ # setup geometry: # Number of angles, the actual angles from 0 to
+ # pi for parallel beam, set the width of a detector
+ # pixel relative to an object pixe and the number of detector pixels.
+ angles_num = 20
+ det_w = 1.0
+ det_num = N
+
+ angles = numpy.linspace(0,numpy.pi,angles_num,endpoint=False,dtype=numpy.float32)*\
+ 180/numpy.pi
+
+ # Inputs: Geometry, 2D or 3D, angles, horz detector pixel count,
+ # horz detector pixel size, vert detector pixel count,
+ # vert detector pixel size.
+ ag = AcquisitionGeometry('parallel',
+ '3D',
+ angles,
+ N,
+ det_w,
+ vert,
+ det_w)
+
+ # Set up Operator object combining the ImageGeometry and AcquisitionGeometry
+ # wrapping calls to CCPi projector.
+ A = CCPiProjectorSimple(ig, ag)
+
+ # Forward and backprojection are available as methods direct and adjoint. Here
+ # generate test data b and some noise
+
+ b = A.direct(Phantom)
+
+
+ #z = A.adjoint(b)
+
+
+ # Using the test data b, different reconstruction methods can now be set up as
+ # demonstrated in the rest of this file. In general all methods need an initial
+ # guess and some algorithm options to be set. Note that 100 iterations for
+ # some of the methods is a very low number and 1000 or 10000 iterations may be
+ # needed if one wants to obtain a converged solution.
+ x_init = ImageData(geometry=ig,
+ dimension_labels=['horizontal_x','horizontal_y','vertical'])
+ X_init = BlockDataContainer(x_init)
+ B = BlockDataContainer(b,
+ ImageData(geometry=ig, dimension_labels=['horizontal_x','horizontal_y','vertical']))
+
+ # setup a tomo identity
+ Ibig = 1e5 * Identity(ig)
+ Ismall = 1e-5 * Identity(ig)
+
+ # composite operator
+ Kbig = BlockOperator(A, Ibig, shape=(2,1))
+ Ksmall = BlockOperator(A, Ismall, shape=(2,1))
+
+ #out = K.direct(X_init)
+
+ f = Norm2sq(Kbig,B)
+ f.L = 0.00003
+
+ fsmall = Norm2sq(Ksmall,B)
+ f.L = 0.00003
+
+ simplef = Norm2sq(A, b)
+ simplef.L = 0.00003
+
+ gd = GradientDescent( x_init=x_init, objective_function=simplef,
+ rate=simplef.L)
+ gd.max_iteration = 10
+
+ cg = CGLS()
+ cg.set_up(X_init, Kbig, B )
+ cg.max_iteration = 1
+
+ cgsmall = CGLS()
+ cgsmall.set_up(X_init, Ksmall, B )
+ cgsmall.max_iteration = 1
+
+
+ cgs = CGLS()
+ cgs.set_up(x_init, A, b )
+ cgs.max_iteration = 6
+ #
+ #out.__isub__(B)
+ #out2 = K.adjoint(out)
+
+ #(2.0*self.c)*self.A.adjoint( self.A.direct(x) - self.b )
+
+ #for _ in gd:
+ # print ("iteration {} {}".format(gd.iteration, gd.get_current_loss()))
+
+ #cg.run(10, lambda it,val: print ("iteration {} objective {}".format(it,val)) )
+
+ #cgs.run(10, lambda it,val: print ("iteration {} objective {}".format(it,val)))
+
+ #cgsmall.run(10, lambda it,val: print ("iteration {} objective {}".format(it,val)))
+ #cgsmall.run(10, lambda it,val: print ("iteration {} objective {}".format(it,val)))
+ # for _ in cg:
+ # print ("iteration {} {}".format(cg.iteration, cg.get_current_loss()))
+ #
+ # fig = plt.figure()
+ # plt.imshow(cg.get_output().get_item(0,0).subset(vertical=0).as_array())
+ # plt.title('Composite CGLS')
+ # plt.show()
+ #
+ # for _ in cgs:
+ # print ("iteration {} {}".format(cgs.iteration, cgs.get_current_loss()))
+ #
+ fig = plt.figure()
+ plt.subplot(1,5,1)
+ plt.imshow(Phantom.subset(vertical=0).as_array())
+ plt.title('Simulated Phantom')
+ plt.subplot(1,5,2)
+ plt.imshow(gd.get_output().subset(vertical=0).as_array())
+ plt.title('Simple Gradient Descent')
+ plt.subplot(1,5,3)
+ plt.imshow(cgs.get_output().subset(vertical=0).as_array())
+ plt.title('Simple CGLS')
+ plt.subplot(1,5,4)
+ plt.imshow(cg.get_output().get_item(0,0).subset(vertical=0).as_array())
+ plt.title('Composite CGLS\nbig lambda')
+ plt.subplot(1,5,5)
+ plt.imshow(cgsmall.get_output().get_item(0,0).subset(vertical=0).as_array())
+ plt.title('Composite CGLS\nsmall lambda')
+ plt.show()
+
+ def test_FiniteDiffOperator(self):
+ N, M = 200, 300
+
+
+ ig = ImageGeometry(voxel_num_x = M, voxel_num_y = N)
+ u = ig.allocate('random_int')
+ G = FiniteDiff(ig, direction=0, bnd_cond = 'Neumann')
+ print(type(u), u.as_array())
+ print(G.direct(u).as_array())
+
+ # Gradient Operator norm, for one direction should be close to 2
+ numpy.testing.assert_allclose(G.norm(), numpy.sqrt(4), atol=0.1)
+
+ M1, N1, K1 = 200, 300, 2
+ ig1 = ImageGeometry(voxel_num_x = M1, voxel_num_y = N1, channels = K1)
+ u1 = ig1.allocate('random_int')
+ G1 = FiniteDiff(ig1, direction=2, bnd_cond = 'Periodic')
+ print(ig1.shape==u1.shape)
+ print (G1.norm())
+ numpy.testing.assert_allclose(G1.norm(), numpy.sqrt(4), atol=0.1)
diff --git a/Wrappers/Python/test/test_DataContainer.py b/Wrappers/Python/test/test_DataContainer.py
index f23179c..16f7b86 100755
--- a/Wrappers/Python/test/test_DataContainer.py
+++ b/Wrappers/Python/test/test_DataContainer.py
@@ -174,7 +174,7 @@ class TestDataContainer(unittest.TestCase):
def binary_add(self):
print("Test binary add")
X, Y, Z = 512, 512, 512
- X, Y, Z = 256, 512, 512
+ #X, Y, Z = 1024, 512, 512
steps = [timer()]
a = numpy.ones((X, Y, Z), dtype='float32')
steps.append(timer())
@@ -183,9 +183,10 @@ class TestDataContainer(unittest.TestCase):
#print("a refcount " , sys.getrefcount(a))
ds = DataContainer(a, False, ['X', 'Y', 'Z'])
ds1 = ds.copy()
+ out = ds.copy()
steps.append(timer())
- ds.add(ds1, out=ds)
+ ds.add(ds1, out=out)
steps.append(timer())
t1 = dt(steps)
print("ds.add(ds1, out=ds)", dt(steps))
@@ -196,20 +197,29 @@ class TestDataContainer(unittest.TestCase):
print("ds2 = ds.add(ds1)", dt(steps))
self.assertLess(t1, t2)
- self.assertEqual(ds.as_array()[0][0][0], 2.)
-
+ self.assertEqual(out.as_array()[0][0][0], 2.)
+ self.assertNumpyArrayEqual(out.as_array(), ds2.as_array())
+
ds0 = ds
- ds0.add(2, out=ds0)
- steps.append(timer())
- print("ds0.add(2,out=ds0)", dt(steps), 3, ds0.as_array()[0][0][0])
- self.assertEqual(4., ds0.as_array()[0][0][0])
-
- dt1 = dt(steps)
- ds3 = ds0.add(2)
- steps.append(timer())
- print("ds3 = ds0.add(2)", dt(steps), 5, ds3.as_array()[0][0][0])
- dt2 = dt(steps)
+ dt1 = 0
+ dt2 = 0
+ for i in range(10):
+ steps.append(timer())
+ ds0.add(2, out=out)
+ steps.append(timer())
+ print("ds0.add(2,out=out)", dt(steps), 3, ds0.as_array()[0][0][0])
+ self.assertEqual(3., out.as_array()[0][0][0])
+
+ dt1 += dt(steps)/10
+ steps.append(timer())
+ ds3 = ds0.add(2)
+ steps.append(timer())
+ print("ds3 = ds0.add(2)", dt(steps), 5, ds3.as_array()[0][0][0])
+ dt2 += dt(steps)/10
+
+ self.assertNumpyArrayEqual(out.as_array(), ds3.as_array())
self.assertLess(dt1, dt2)
+
def binary_subtract(self):
print("Test binary subtract")
@@ -222,16 +232,17 @@ class TestDataContainer(unittest.TestCase):
#print("a refcount " , sys.getrefcount(a))
ds = DataContainer(a, False, ['X', 'Y', 'Z'])
ds1 = ds.copy()
+ out = ds.copy()
steps.append(timer())
- ds.subtract(ds1, out=ds)
+ ds.subtract(ds1, out=out)
steps.append(timer())
t1 = dt(steps)
print("ds.subtract(ds1, out=ds)", dt(steps))
- self.assertEqual(0., ds.as_array()[0][0][0])
+ self.assertEqual(0., out.as_array()[0][0][0])
steps.append(timer())
- ds2 = ds.subtract(ds1)
+ ds2 = out.subtract(ds1)
self.assertEqual(-1., ds2.as_array()[0][0][0])
steps.append(timer())
@@ -247,8 +258,8 @@ class TestDataContainer(unittest.TestCase):
#ds0.__isub__( 2 )
steps.append(timer())
print("ds0.subtract(2,out=ds0)", dt(
- steps), -2., ds0.as_array()[0][0][0])
- self.assertEqual(-2., ds0.as_array()[0][0][0])
+ steps), -1., ds0.as_array()[0][0][0])
+ self.assertEqual(-1., ds0.as_array()[0][0][0])
dt1 = dt(steps)
ds3 = ds0.subtract(2)
@@ -256,8 +267,8 @@ class TestDataContainer(unittest.TestCase):
print("ds3 = ds0.subtract(2)", dt(steps), 0., ds3.as_array()[0][0][0])
dt2 = dt(steps)
self.assertLess(dt1, dt2)
- self.assertEqual(-2., ds0.as_array()[0][0][0])
- self.assertEqual(-4., ds3.as_array()[0][0][0])
+ self.assertEqual(-1., ds0.as_array()[0][0][0])
+ self.assertEqual(-3., ds3.as_array()[0][0][0])
def binary_multiply(self):
print("Test binary multiply")
@@ -300,6 +311,9 @@ class TestDataContainer(unittest.TestCase):
self.assertLess(dt1, dt2)
self.assertEqual(4., ds3.as_array()[0][0][0])
self.assertEqual(2., ds.as_array()[0][0][0])
+
+ ds.multiply(2.5, out=ds0)
+ self.assertEqual(2.5*2., ds0.as_array()[0][0][0])
def binary_divide(self):
print("Test binary divide")
@@ -314,16 +328,19 @@ class TestDataContainer(unittest.TestCase):
ds = DataContainer(a, False, ['X', 'Y', 'Z'])
ds1 = ds.copy()
- steps.append(timer())
- ds.divide(ds1, out=ds)
- steps.append(timer())
- t1 = dt(steps)
- print("ds.divide(ds1, out=ds)", dt(steps))
- steps.append(timer())
- ds2 = ds.divide(ds1)
- steps.append(timer())
- t2 = dt(steps)
- print("ds2 = ds.divide(ds1)", dt(steps))
+ t1 = 0
+ t2 = 0
+ for i in range(10):
+ steps.append(timer())
+ ds.divide(ds1, out=ds)
+ steps.append(timer())
+ t1 += dt(steps)/10.
+ print("ds.divide(ds1, out=ds)", dt(steps))
+ steps.append(timer())
+ ds2 = ds.divide(ds1)
+ steps.append(timer())
+ t2 += dt(steps)/10.
+ print("ds2 = ds.divide(ds1)", dt(steps))
self.assertLess(t1, t2)
self.assertEqual(ds.as_array()[0][0][0], 1.)
@@ -445,6 +462,11 @@ class TestDataContainer(unittest.TestCase):
self.assertTrue(False)
except ValueError as ve:
self.assertTrue(True)
+
+ print ("test dot numpy")
+ n0 = (ds0 * ds1).sum()
+ n1 = ds0.as_array().ravel().dot(ds1.as_array().ravel())
+ self.assertEqual(n0, n1)
@@ -472,6 +494,13 @@ class TestDataContainer(unittest.TestCase):
self.assertNumpyArrayEqual(vol1.as_array(), numpy.ones(vol.shape) * 4)
self.assertEqual(vol.number_of_dimensions, 3)
+
+ ig2 = ImageGeometry (voxel_num_x=2,voxel_num_y=3,voxel_num_z=4,
+ dimension_labels=[ImageGeometry.HORIZONTAL_X, ImageGeometry.HORIZONTAL_Y,
+ ImageGeometry.VERTICAL])
+ data = ig2.allocate()
+ self.assertNumpyArrayEqual(numpy.asarray(data.shape), numpy.asarray(ig2.shape))
+ self.assertNumpyArrayEqual(numpy.asarray(data.shape), data.as_array().shape)
def test_AcquisitionData(self):
sgeometry = AcquisitionGeometry(dimension=2, angles=numpy.linspace(0, 180, num=10),
@@ -479,6 +508,29 @@ class TestDataContainer(unittest.TestCase):
pixel_num_h=5, channels=2)
sino = AcquisitionData(geometry=sgeometry)
self.assertEqual(sino.shape, (2, 10, 3, 5))
+
+ ag = AcquisitionGeometry (pixel_num_h=2,pixel_num_v=3,channels=4, dimension=2, angles=numpy.linspace(0, 180, num=10),
+ geom_type='parallel', )
+ print (ag.shape)
+ print (ag.dimension_labels)
+
+ data = ag.allocate()
+ self.assertNumpyArrayEqual(numpy.asarray(data.shape), numpy.asarray(ag.shape))
+ self.assertNumpyArrayEqual(numpy.asarray(data.shape), data.as_array().shape)
+
+ print (data.shape, ag.shape, data.as_array().shape)
+
+ ag2 = AcquisitionGeometry (pixel_num_h=2,pixel_num_v=3,channels=4, dimension=2, angles=numpy.linspace(0, 180, num=10),
+ geom_type='parallel',
+ dimension_labels=[AcquisitionGeometry.VERTICAL ,
+ AcquisitionGeometry.ANGLE, AcquisitionGeometry.HORIZONTAL, AcquisitionGeometry.CHANNEL])
+
+ data = ag2.allocate()
+ print (data.shape, ag2.shape, data.as_array().shape)
+ self.assertNumpyArrayEqual(numpy.asarray(data.shape), numpy.asarray(ag2.shape))
+ self.assertNumpyArrayEqual(numpy.asarray(data.shape), data.as_array().shape)
+
+
def test_ImageGeometry_allocate(self):
vgeometry = ImageGeometry(voxel_num_x=4, voxel_num_y=3, channels=2)
image = vgeometry.allocate()
@@ -494,10 +546,21 @@ class TestDataContainer(unittest.TestCase):
self.assertEqual(order[0], image.dimension_labels[0])
self.assertEqual(order[1], image.dimension_labels[1])
self.assertEqual(order[2], image.dimension_labels[2])
+
+ ig = ImageGeometry(2,3,2)
+ try:
+ z = ImageData(numpy.random.randint(10, size=(2,3)), geometry=ig)
+ self.assertTrue(False)
+ except ValueError as ve:
+ print (ve)
+ self.assertTrue(True)
+
+ #vgeometry.allocate('')
def test_AcquisitionGeometry_allocate(self):
- ageometry = AcquisitionGeometry(dimension=2, angles=numpy.linspace(0, 180, num=10),
- geom_type='parallel', pixel_num_v=3,
- pixel_num_h=5, channels=2)
+ ageometry = AcquisitionGeometry(dimension=2,
+ angles=numpy.linspace(0, 180, num=10),
+ geom_type='parallel', pixel_num_v=3,
+ pixel_num_h=5, channels=2)
sino = ageometry.allocate()
shape = sino.shape
print ("shape", shape)
@@ -509,8 +572,8 @@ class TestDataContainer(unittest.TestCase):
self.assertEqual(1,sino.as_array()[shape[0]-1][shape[1]-1][shape[2]-1][shape[3]-1])
print (sino.dimension_labels, sino.shape, ageometry)
- default_order = ['channel' , ' angle' ,
- 'vertical' , 'horizontal']
+ default_order = ['channel' , 'angle' ,
+ 'vertical' , 'horizontal']
self.assertEqual(default_order[0], sino.dimension_labels[0])
self.assertEqual(default_order[1], sino.dimension_labels[1])
self.assertEqual(default_order[2], sino.dimension_labels[2])
@@ -522,7 +585,15 @@ class TestDataContainer(unittest.TestCase):
self.assertEqual(order[1], sino.dimension_labels[1])
self.assertEqual(order[2], sino.dimension_labels[2])
self.assertEqual(order[2], sino.dimension_labels[2])
-
+
+
+ try:
+ z = AcquisitionData(numpy.random.randint(10, size=(2,3)), geometry=ageometry)
+ self.assertTrue(False)
+ except ValueError as ve:
+ print (ve)
+ self.assertTrue(True)
+
def assertNumpyArrayEqual(self, first, second):
res = True
try:
@@ -556,9 +627,35 @@ class TestDataContainer(unittest.TestCase):
norm = dc.norm()
self.assertEqual(sqnorm, 8.0)
numpy.testing.assert_almost_equal(norm, numpy.sqrt(8.0), decimal=7)
+
+ def test_multiply_out(self):
+ print ("test multiply_out")
+ import functools
+ ig = ImageGeometry(10,11,12)
+ u = ig.allocate()
+ a = numpy.ones(u.shape)
+
+ u.fill(a)
+
+ numpy.testing.assert_array_equal(a, u.as_array())
+
+ #u = ig.allocate(ImageGeometry.RANDOM_INT, seed=1)
+ l = functools.reduce(lambda x,y: x*y, (10,11,12), 1)
+
+ a = numpy.zeros((l, ), dtype=numpy.float32)
+ for i in range(l):
+ a[i] = numpy.sin(2 * i* 3.1415/l)
+ b = numpy.reshape(a, u.shape)
+ u.fill(b)
+ numpy.testing.assert_array_equal(b, u.as_array())
+
+ u.multiply(2, out=u)
+ c = b * 2
+ numpy.testing.assert_array_equal(u.as_array(), c)
+
if __name__ == '__main__':
unittest.main()
- \ No newline at end of file
+
diff --git a/Wrappers/Python/test/test_DataProcessor.py b/Wrappers/Python/test/test_DataProcessor.py
index 1c1de3a..3e6a83e 100755
--- a/Wrappers/Python/test/test_DataProcessor.py
+++ b/Wrappers/Python/test/test_DataProcessor.py
@@ -11,8 +11,32 @@ from timeit import default_timer as timer
from ccpi.framework import AX, CastDataContainer, PixelByPixelDataProcessor
+from ccpi.io.reader import NexusReader
+from ccpi.processors import CenterOfRotationFinder
+import wget
+import os
+
class TestDataProcessor(unittest.TestCase):
+ def setUp(self):
+ wget.download('https://github.com/DiamondLightSource/Savu/raw/master/test_data/data/24737_fd.nxs')
+ self.filename = '24737_fd.nxs'
+
+ def tearDown(self):
+ os.remove(self.filename)
+ def test_CenterOfRotation(self):
+ reader = NexusReader(self.filename)
+ ad = reader.get_acquisition_data_whole()
+ print (ad.geometry)
+ cf = CenterOfRotationFinder()
+ cf.set_input(ad)
+ print ("Center of rotation", cf.get_output())
+ self.assertAlmostEqual(86.25, cf.get_output())
+ def test_Normalizer(self):
+ pass
+
+
+
def test_DataProcessorChaining(self):
shape = (2,3,4,5)
size = shape[0]
diff --git a/Wrappers/Python/test/test_Gradient.py b/Wrappers/Python/test/test_Gradient.py
new file mode 100755
index 0000000..4b7a034
--- /dev/null
+++ b/Wrappers/Python/test/test_Gradient.py
@@ -0,0 +1,101 @@
+import unittest
+import numpy
+from ccpi.framework import ImageGeometry, AcquisitionGeometry
+from ccpi.framework import ImageData, AcquisitionData
+#from ccpi.optimisation.algorithms import GradientDescent
+from ccpi.framework import BlockDataContainer
+#from ccpi.optimisation.Algorithms import CGLS
+import functools
+
+from ccpi.optimisation.operators import Gradient, Identity, BlockOperator
+
+class TestGradient(unittest.TestCase):
+ def test_Gradient(self):
+ N, M, K = 20, 30, 40
+ channels = 10
+
+ # check range geometry, examples
+
+ ig1 = ImageGeometry(voxel_num_x = M, voxel_num_y = N)
+ ig2 = ImageGeometry(voxel_num_x = M, voxel_num_y = N, voxel_num_z = K)
+ ig3 = ImageGeometry(voxel_num_x = M, voxel_num_y = N, channels = channels)
+ ig4 = ImageGeometry(voxel_num_x = M, voxel_num_y = N, channels = channels, voxel_num_z= K)
+
+ G1 = Gradient(ig1, correlation = 'Space')
+ print(G1.range_geometry().shape, '2D no channels')
+
+ G4 = Gradient(ig3, correlation = 'SpaceChannels')
+ print(G4.range_geometry().shape, '2D with channels corr')
+ G5 = Gradient(ig3, correlation = 'Space')
+ print(G5.range_geometry().shape, '2D with channels no corr')
+
+ G6 = Gradient(ig4, correlation = 'Space')
+ print(G6.range_geometry().shape, '3D with channels no corr')
+ G7 = Gradient(ig4, correlation = 'SpaceChannels')
+ print(G7.range_geometry().shape, '3D with channels with corr')
+
+
+ u = ig1.allocate(ImageGeometry.RANDOM)
+ w = G1.range_geometry().allocate(ImageGeometry.RANDOM_INT)
+
+ LHS = (G1.direct(u)*w).sum()
+ RHS = (u * G1.adjoint(w)).sum()
+ numpy.testing.assert_approx_equal(LHS, RHS, significant = 1)
+ numpy.testing.assert_approx_equal(G1.norm(), numpy.sqrt(2*4), significant = 1)
+
+
+ u1 = ig3.allocate('random')
+ w1 = G4.range_geometry().allocate('random')
+ LHS1 = (G4.direct(u1) * w1).sum()
+ RHS1 = (u1 * G4.adjoint(w1)).sum()
+ numpy.testing.assert_approx_equal(LHS1, RHS1, significant=1)
+ numpy.testing.assert_almost_equal(G4.norm(), numpy.sqrt(3*4), decimal = 0)
+
+ u2 = ig4.allocate('random')
+ w2 = G7.range_geometry().allocate('random')
+ LHS2 = (G7.direct(u2) * w2).sum()
+ RHS2 = (u2 * G7.adjoint(w2)).sum()
+ numpy.testing.assert_approx_equal(LHS2, RHS2, significant = 3)
+ numpy.testing.assert_approx_equal(G7.norm(), numpy.sqrt(3*4), significant = 1)
+
+
+ #check direct/adjoint for space/channels correlation
+
+ ig_channel = ImageGeometry(voxel_num_x = 2, voxel_num_y = 3, channels = 2)
+ G_no_channel = Gradient(ig_channel, correlation = 'Space')
+ G_channel = Gradient(ig_channel, correlation = 'SpaceChannels')
+
+ u3 = ig_channel.allocate('random_int')
+ res_no_channel = G_no_channel.direct(u3)
+ res_channel = G_channel.direct(u3)
+
+ print(" Derivative for 3 directions, first is wrt Channel direction\n")
+ print(res_channel[0].as_array())
+ print(res_channel[1].as_array())
+ print(res_channel[2].as_array())
+
+ print(" Derivative for 2 directions, no Channel direction\n")
+ print(res_no_channel[0].as_array())
+ print(res_no_channel[1].as_array())
+
+ ig2D = ImageGeometry(voxel_num_x = 2, voxel_num_y = 3)
+ u4 = ig2D.allocate('random_int')
+ G2D = Gradient(ig2D)
+ res = G2D.direct(u4)
+ print(res[0].as_array())
+ print(res[1].as_array())
+
+ M, N = 20, 30
+ ig = ImageGeometry(M, N)
+ arr = ig.allocate('random_int' )
+
+ # check direct of Gradient and sparse matrix
+ G = Gradient(ig)
+ norm1 = G.norm(iterations=300)
+ print ("should be sqrt(8) {} {}".format(numpy.sqrt(8), norm1))
+ numpy.testing.assert_almost_equal(norm1, numpy.sqrt(8), decimal=1)
+ ig4 = ImageGeometry(M,N, channels=3)
+ G4 = Gradient(ig4, correlation=Gradient.CORRELATION_SPACECHANNEL)
+ norm4 = G4.norm(iterations=300)
+ print ("should be sqrt(12) {} {}".format(numpy.sqrt(12), norm4))
+ self.assertTrue((norm4 - numpy.sqrt(12))/norm4 < 0.2)
diff --git a/Wrappers/Python/test/test_NexusReader.py b/Wrappers/Python/test/test_NexusReader.py
index 55543ba..a498d71 100755
--- a/Wrappers/Python/test/test_NexusReader.py
+++ b/Wrappers/Python/test/test_NexusReader.py
@@ -21,67 +21,67 @@ class TestNexusReader(unittest.TestCase):
def tearDown(self):
os.remove(self.filename)
-
- def testGetDimensions(self):
+ def testAll(self):
+ # def testGetDimensions(self):
nr = NexusReader(self.filename)
self.assertEqual(nr.get_sinogram_dimensions(), (135, 91, 160), "Sinogram dimensions are not correct")
- def testGetProjectionDimensions(self):
+ # def testGetProjectionDimensions(self):
nr = NexusReader(self.filename)
self.assertEqual(nr.get_projection_dimensions(), (91,135,160), "Projection dimensions are not correct")
- def testLoadProjectionWithoutDimensions(self):
+ # def testLoadProjectionWithoutDimensions(self):
nr = NexusReader(self.filename)
projections = nr.load_projection()
self.assertEqual(projections.shape, (91,135,160), "Loaded projection data dimensions are not correct")
- def testLoadProjectionWithDimensions(self):
+ # def testLoadProjectionWithDimensions(self):
nr = NexusReader(self.filename)
projections = nr.load_projection((slice(0,1), slice(0,135), slice(0,160)))
self.assertEqual(projections.shape, (1,135,160), "Loaded projection data dimensions are not correct")
- def testLoadProjectionCompareSingle(self):
+ # def testLoadProjectionCompareSingle(self):
nr = NexusReader(self.filename)
projections_full = nr.load_projection()
projections_part = nr.load_projection((slice(0,1), slice(0,135), slice(0,160)))
numpy.testing.assert_array_equal(projections_part, projections_full[0:1,:,:])
- def testLoadProjectionCompareMulti(self):
+ # def testLoadProjectionCompareMulti(self):
nr = NexusReader(self.filename)
projections_full = nr.load_projection()
projections_part = nr.load_projection((slice(0,3), slice(0,135), slice(0,160)))
numpy.testing.assert_array_equal(projections_part, projections_full[0:3,:,:])
- def testLoadProjectionCompareRandom(self):
+ # def testLoadProjectionCompareRandom(self):
nr = NexusReader(self.filename)
projections_full = nr.load_projection()
projections_part = nr.load_projection((slice(1,8), slice(5,10), slice(8,20)))
numpy.testing.assert_array_equal(projections_part, projections_full[1:8,5:10,8:20])
- def testLoadProjectionCompareFull(self):
+ # def testLoadProjectionCompareFull(self):
nr = NexusReader(self.filename)
projections_full = nr.load_projection()
projections_part = nr.load_projection((slice(None,None), slice(None,None), slice(None,None)))
numpy.testing.assert_array_equal(projections_part, projections_full[:,:,:])
- def testLoadFlatCompareFull(self):
+ # def testLoadFlatCompareFull(self):
nr = NexusReader(self.filename)
flats_full = nr.load_flat()
flats_part = nr.load_flat((slice(None,None), slice(None,None), slice(None,None)))
numpy.testing.assert_array_equal(flats_part, flats_full[:,:,:])
- def testLoadDarkCompareFull(self):
+ # def testLoadDarkCompareFull(self):
nr = NexusReader(self.filename)
darks_full = nr.load_dark()
darks_part = nr.load_dark((slice(None,None), slice(None,None), slice(None,None)))
numpy.testing.assert_array_equal(darks_part, darks_full[:,:,:])
- def testProjectionAngles(self):
+ # def testProjectionAngles(self):
nr = NexusReader(self.filename)
angles = nr.get_projection_angles()
self.assertEqual(angles.shape, (91,), "Loaded projection number of angles are not correct")
- def test_get_acquisition_data_subset(self):
+ # def test_get_acquisition_data_subset(self):
nr = NexusReader(self.filename)
key = nr.get_image_keys()
sl = nr.get_acquisition_data_subset(0,10)
diff --git a/Wrappers/Python/test/test_Operator.py b/Wrappers/Python/test/test_Operator.py
new file mode 100644
index 0000000..d890e46
--- /dev/null
+++ b/Wrappers/Python/test/test_Operator.py
@@ -0,0 +1,466 @@
+import unittest
+#from ccpi.optimisation.operators import Operator
+#from ccpi.optimisation.ops import TomoIdentity
+from ccpi.framework import ImageGeometry, ImageData, BlockDataContainer, DataContainer
+from ccpi.optimisation.operators import BlockOperator, BlockScaledOperator,\
+ FiniteDiff
+import numpy
+from timeit import default_timer as timer
+from ccpi.framework import ImageGeometry
+from ccpi.optimisation.operators import Gradient, Identity, SparseFiniteDiff
+from ccpi.optimisation.operators import LinearOperator
+
+def dt(steps):
+ return steps[-1] - steps[-2]
+
+class CCPiTestClass(unittest.TestCase):
+ def assertBlockDataContainerEqual(self, container1, container2):
+ print ("assert Block Data Container Equal")
+ self.assertTrue(issubclass(container1.__class__, container2.__class__))
+ for col in range(container1.shape[0]):
+ if issubclass(container1.get_item(col).__class__, DataContainer):
+ print ("Checking col ", col)
+ self.assertNumpyArrayEqual(
+ container1.get_item(col).as_array(),
+ container2.get_item(col).as_array()
+ )
+ else:
+ self.assertBlockDataContainerEqual(container1.get_item(col),container2.get_item(col))
+
+ def assertNumpyArrayEqual(self, first, second):
+ res = True
+ try:
+ numpy.testing.assert_array_equal(first, second)
+ except AssertionError as err:
+ res = False
+ print(err)
+ self.assertTrue(res)
+
+ def assertNumpyArrayAlmostEqual(self, first, second, decimal=6):
+ res = True
+ try:
+ numpy.testing.assert_array_almost_equal(first, second, decimal)
+ except AssertionError as err:
+ res = False
+ print(err)
+ print("expected " , second)
+ print("actual " , first)
+
+ self.assertTrue(res)
+
+
+class TestOperator(CCPiTestClass):
+ def test_ScaledOperator(self):
+ print ("test_ScaledOperator")
+ ig = ImageGeometry(10,20,30)
+ img = ig.allocate()
+ scalar = 0.5
+ sid = scalar * Identity(ig)
+ numpy.testing.assert_array_equal(scalar * img.as_array(), sid.direct(img).as_array())
+
+
+ def test_Identity(self):
+ print ("test_Identity")
+ ig = ImageGeometry(10,20,30)
+ img = ig.allocate()
+ self.assertTrue(img.shape == (30,20,10))
+ self.assertEqual(img.sum(), 0)
+ Id = Identity(ig)
+ y = Id.direct(img)
+ numpy.testing.assert_array_equal(y.as_array(), img.as_array())
+
+ def test_FiniteDifference(self):
+ print ("test FiniteDifference")
+ ##
+ N, M = 2, 3
+
+ ig = ImageGeometry(N, M)
+ Id = Identity(ig)
+
+ FD = FiniteDiff(ig, direction = 0, bnd_cond = 'Neumann')
+ u = FD.domain_geometry().allocate('random_int')
+
+
+ res = FD.domain_geometry().allocate(ImageGeometry.RANDOM_INT)
+ FD.adjoint(u, out=res)
+ w = FD.adjoint(u)
+
+ self.assertNumpyArrayEqual(res.as_array(), w.as_array())
+
+ res = Id.domain_geometry().allocate(ImageGeometry.RANDOM_INT)
+ Id.adjoint(u, out=res)
+ w = Id.adjoint(u)
+
+ self.assertNumpyArrayEqual(res.as_array(), w.as_array())
+ self.assertNumpyArrayEqual(u.as_array(), w.as_array())
+
+ G = Gradient(ig)
+
+ u = G.range_geometry().allocate(ImageGeometry.RANDOM_INT)
+ res = G.domain_geometry().allocate()
+ G.adjoint(u, out=res)
+ w = G.adjoint(u)
+ self.assertNumpyArrayEqual(res.as_array(), w.as_array())
+
+ u = G.domain_geometry().allocate(ImageGeometry.RANDOM_INT)
+ res = G.range_geometry().allocate()
+ G.direct(u, out=res)
+ w = G.direct(u)
+ self.assertBlockDataContainerEqual(res, w)
+
+ def test_PowerMethod(self):
+ print ("test_BlockOperator")
+
+ N, M = 200, 300
+ niter = 10
+ ig = ImageGeometry(N, M)
+ Id = Identity(ig)
+
+ G = Gradient(ig)
+
+ uid = Id.domain_geometry().allocate(ImageGeometry.RANDOM_INT, seed=1)
+
+ a = LinearOperator.PowerMethod(Id, niter, uid)
+ #b = LinearOperator.PowerMethodNonsquare(Id, niter, uid)
+ b = LinearOperator.PowerMethod(Id, niter)
+ print ("Edo impl", a[0])
+ print ("None impl", b[0])
+
+ #self.assertAlmostEqual(a[0], b[0])
+ self.assertNumpyArrayAlmostEqual(a[0],b[0],decimal=6)
+
+ a = LinearOperator.PowerMethod(G, niter, uid)
+ b = LinearOperator.PowerMethod(G, niter)
+ #b = LinearOperator.PowerMethodNonsquare(G, niter, uid)
+
+ print ("Edo impl", a[0])
+ #print ("old impl", b[0])
+ self.assertNumpyArrayAlmostEqual(a[0],b[0],decimal=2)
+ #self.assertAlmostEqual(a[0], b[0])
+
+ def test_Norm(self):
+ print ("test_BlockOperator")
+ ##
+ N, M = 200, 300
+
+ ig = ImageGeometry(N, M)
+ G = Gradient(ig)
+ t0 = timer()
+ norm = G.norm()
+ t1 = timer()
+ norm2 = G.norm()
+ t2 = timer()
+ print ("Norm dT1 {} dT2 {}".format(t1-t0,t2-t1))
+ self.assertLess(t2-t1, t1-t0)
+
+
+
+
+class TestBlockOperator(unittest.TestCase):
+ def assertBlockDataContainerEqual(self, container1, container2):
+ print ("assert Block Data Container Equal")
+ self.assertTrue(issubclass(container1.__class__, container2.__class__))
+ for col in range(container1.shape[0]):
+ if issubclass(container1.get_item(col).__class__, DataContainer):
+ print ("Checking col ", col)
+ self.assertNumpyArrayEqual(
+ container1.get_item(col).as_array(),
+ container2.get_item(col).as_array()
+ )
+ else:
+ self.assertBlockDataContainerEqual(container1.get_item(col),container2.get_item(col))
+
+ def assertNumpyArrayEqual(self, first, second):
+ res = True
+ try:
+ numpy.testing.assert_array_equal(first, second)
+ except AssertionError as err:
+ res = False
+ print(err)
+ self.assertTrue(res)
+
+ def assertNumpyArrayAlmostEqual(self, first, second, decimal=6):
+ res = True
+ try:
+ numpy.testing.assert_array_almost_equal(first, second, decimal)
+ except AssertionError as err:
+ res = False
+ print(err)
+ print("expected " , second)
+ print("actual " , first)
+
+ self.assertTrue(res)
+
+ def test_BlockOperator(self):
+ print ("test_BlockOperator")
+
+ M, N = 3, 4
+ ig = ImageGeometry(M, N)
+ arr = ig.allocate('random_int')
+
+ G = Gradient(ig)
+ Id = Identity(ig)
+
+ B = BlockOperator(G, Id)
+ # Nx1 case
+ u = ig.allocate('random_int')
+ z1 = B.direct(u)
+
+ res = B.range_geometry().allocate()
+ #res = z1.copy()
+ B.direct(u, out=res)
+
+
+ print (type(z1), type(res))
+ print (z1.shape)
+ print(z1[0][0].as_array())
+ print(res[0][0].as_array())
+ self.assertBlockDataContainerEqual(z1, res)
+ # for col in range(z1.shape[0]):
+ # a = z1.get_item(col)
+ # b = res.get_item(col)
+ # if isinstance(a, BlockDataContainer):
+ # for col2 in range(a.shape[0]):
+ # self.assertNumpyArrayEqual(
+ # a.get_item(col2).as_array(),
+ # b.get_item(col2).as_array()
+ # )
+ # else:
+ # self.assertNumpyArrayEqual(
+ # a.as_array(),
+ # b.as_array()
+ # )
+ z1 = B.range_geometry().allocate(ImageGeometry.RANDOM_INT)
+
+ res1 = B.adjoint(z1)
+ res2 = B.domain_geometry().allocate()
+ B.adjoint(z1, out=res2)
+
+ self.assertNumpyArrayEqual(res1.as_array(), res2.as_array())
+
+ BB = BlockOperator( Id, 2 * Id)
+ B = BlockOperator( BB, Id )
+ v = B.domain_geometry().allocate()
+ B.adjoint(res,out=v)
+ vv = B.adjoint(res)
+ el1 = B.get_item(0,0).adjoint(z1.get_item(0)) +\
+ B.get_item(1,0).adjoint(z1.get_item(1))
+ print ("el1" , el1.as_array())
+ print ("vv" , vv.as_array())
+ print ("v" , v.as_array())
+
+ self.assertNumpyArrayEqual(v.as_array(),vv.as_array())
+ # test adjoint
+ print ("############ 2x1 #############")
+
+ BB = BlockOperator( Id, 2 * Id)
+ u = ig.allocate(1)
+ z1 = BB.direct(u)
+ print ("z1 shape {} one\n{} two\n{}".format(z1.shape,
+ z1.get_item(0).as_array(),
+ z1.get_item(1).as_array()))
+ res = BB.range_geometry().allocate(0)
+ BB.direct(u, out=res)
+ print ("res shape {} one\n{} two\n{}".format(res.shape,
+ res.get_item(0).as_array(),
+ res.get_item(1).as_array()))
+
+
+ self.assertNumpyArrayEqual(z1.get_item(0).as_array(),
+ u.as_array())
+ self.assertNumpyArrayEqual(z1.get_item(1).as_array(),
+ 2 * u.as_array())
+ self.assertNumpyArrayEqual(res.get_item(0).as_array(),
+ u.as_array())
+ self.assertNumpyArrayEqual(res.get_item(1).as_array(),
+ 2 * u.as_array())
+
+ x1 = BB.adjoint(z1)
+ print("adjoint x1\n",x1.as_array())
+
+ res1 = BB.domain_geometry().allocate()
+ BB.adjoint(z1, out=res1)
+ print("res1\n",res1.as_array())
+ self.assertNumpyArrayEqual(x1.as_array(),
+ res1.as_array())
+
+ self.assertNumpyArrayEqual(x1.as_array(),
+ 5 * u.as_array())
+ self.assertNumpyArrayEqual(res1.as_array(),
+ 5 * u.as_array())
+ #################################################
+
+ print ("############ 2x2 #############")
+ BB = BlockOperator( Id, 2 * Id, 3 * Id, Id, shape=(2,2))
+ B = BB
+ u = ig.allocate(1)
+ U = BlockDataContainer(u,u)
+ z1 = B.direct(U)
+
+
+ print ("z1 shape {} one\n{} two\n{}".format(z1.shape,
+ z1.get_item(0).as_array(),
+ z1.get_item(1).as_array()))
+ self.assertNumpyArrayEqual(z1.get_item(0).as_array(),
+ 3 * u.as_array())
+ self.assertNumpyArrayEqual(z1.get_item(1).as_array(),
+ 4 * u.as_array())
+ res = B.range_geometry().allocate()
+ B.direct(U, out=res)
+ self.assertNumpyArrayEqual(res.get_item(0).as_array(),
+ 3 * u.as_array())
+ self.assertNumpyArrayEqual(res.get_item(1).as_array(),
+ 4 * u.as_array())
+
+
+ x1 = B.adjoint(z1)
+ # this should be [15 u, 10 u]
+ el1 = B.get_item(0,0).adjoint(z1.get_item(0)) + B.get_item(1,0).adjoint(z1.get_item(1))
+ el2 = B.get_item(0,1).adjoint(z1.get_item(0)) + B.get_item(1,1).adjoint(z1.get_item(1))
+
+ shape = B.get_output_shape(z1.shape, adjoint=True)
+ print ("shape ", shape)
+ out = B.domain_geometry().allocate()
+
+ for col in range(B.shape[1]):
+ for row in range(B.shape[0]):
+ if row == 0:
+ el = B.get_item(row,col).adjoint(z1.get_item(row))
+ else:
+ el += B.get_item(row,col).adjoint(z1.get_item(row))
+ out.get_item(col).fill(el)
+
+ print ("el1 " , el1.as_array())
+ print ("el2 " , el2.as_array())
+ print ("out shape {} one\n{} two\n{}".format(out.shape,
+ out.get_item(0).as_array(),
+ out.get_item(1).as_array()))
+
+ self.assertNumpyArrayEqual(out.get_item(0).as_array(),
+ 15 * u.as_array())
+ self.assertNumpyArrayEqual(out.get_item(1).as_array(),
+ 10 * u.as_array())
+
+ res2 = B.domain_geometry().allocate()
+ #print (res2, res2.as_array())
+ B.adjoint(z1, out = res2)
+
+ #print ("adjoint",x1.as_array(),"\n",res2.as_array())
+ self.assertNumpyArrayEqual(
+ out.get_item(0).as_array(),
+ res2.get_item(0).as_array()
+ )
+ self.assertNumpyArrayEqual(
+ out.get_item(1).as_array(),
+ res2.get_item(1).as_array()
+ )
+
+ if True:
+ #B1 = BlockOperator(Id, Id, Id, Id, shape=(2,2))
+ B1 = BlockOperator(G, Id)
+ U = ig.allocate(ImageGeometry.RANDOM_INT)
+ #U = BlockDataContainer(u,u)
+ RES1 = B1.range_geometry().allocate()
+
+ Z1 = B1.direct(U)
+ B1.direct(U, out = RES1)
+
+ self.assertBlockDataContainerEqual(Z1,RES1)
+
+
+
+ print("U", U.as_array())
+ print("Z1", Z1[0][0].as_array())
+ print("RES1", RES1[0][0].as_array())
+ print("Z1", Z1[0][1].as_array())
+ print("RES1", RES1[0][1].as_array())
+ def test_timedifference(self):
+ print ("test_timedifference")
+ M, N ,W = 100, 512, 512
+ ig = ImageGeometry(M, N, W)
+ arr = ig.allocate('random_int')
+
+ G = Gradient(ig)
+ Id = Identity(ig)
+
+ B = BlockOperator(G, Id)
+
+
+ # Nx1 case
+ u = ig.allocate('random_int')
+ steps = [timer()]
+ i = 0
+ n = 2.
+ t1 = t2 = 0
+ res = B.range_geometry().allocate()
+
+ while (i < n):
+ print ("i ", i)
+ steps.append(timer())
+ z1 = B.direct(u)
+ steps.append(timer())
+ t = dt(steps)
+ #print ("B.direct(u) " ,t)
+ t1 += t/n
+
+ steps.append(timer())
+ B.direct(u, out = res)
+ steps.append(timer())
+ t = dt(steps)
+ #print ("B.direct(u, out=res) " ,t)
+ t2 += t/n
+ i += 1
+
+ print ("Time difference ", t1,t2)
+ self.assertGreater(t1,t2)
+
+ steps = [timer()]
+ i = 0
+ #n = 50.
+ t1 = t2 = 0
+ resd = B.domain_geometry().allocate()
+ z1 = B.direct(u)
+ #B.adjoint(z1, out=resd)
+
+ print (type(res))
+ while (i < n):
+ print ("i ", i)
+ steps.append(timer())
+ w1 = B.adjoint(z1)
+ steps.append(timer())
+ t = dt(steps)
+ #print ("B.adjoint(z1) " ,t)
+ t1 += t/n
+
+ steps.append(timer())
+ B.adjoint(z1, out=resd)
+ steps.append(timer())
+ t = dt(steps)
+ #print ("B.adjoint(z1, out=res) " ,t)
+ t2 += t/n
+ i += 1
+
+ print ("Time difference ", t1,t2)
+ self.assertGreater(t1,t2)
+
+ def test_BlockOperatorLinearValidity(self):
+ print ("test_BlockOperatorLinearValidity")
+
+ M, N = 3, 4
+ ig = ImageGeometry(M, N)
+ arr = ig.allocate('random_int')
+
+ G = Gradient(ig)
+ Id = Identity(ig)
+
+ B = BlockOperator(G, Id)
+ # Nx1 case
+ u = ig.allocate('random_int')
+ w = B.range_geometry().allocate(ImageGeometry.RANDOM_INT)
+ w1 = B.direct(u)
+ u1 = B.adjoint(w)
+ self.assertEqual((w * w1).sum() , (u1*u).sum())
+
+
+
+
diff --git a/Wrappers/Python/test/test_algorithms.py b/Wrappers/Python/test/test_algorithms.py
index b5959b5..3bb3d57 100755
--- a/Wrappers/Python/test/test_algorithms.py
+++ b/Wrappers/Python/test/test_algorithms.py
@@ -12,12 +12,12 @@ from ccpi.framework import ImageData
from ccpi.framework import AcquisitionData
from ccpi.framework import ImageGeometry
from ccpi.framework import AcquisitionGeometry
-from ccpi.optimisation.ops import TomoIdentity
-from ccpi.optimisation.funcs import Norm2sq
+from ccpi.optimisation.operators import Identity
+from ccpi.optimisation.functions import Norm2Sq, ZeroFunction, \
+ L2NormSquared, FunctionOperatorComposition
from ccpi.optimisation.algorithms import GradientDescent
from ccpi.optimisation.algorithms import CGLS
from ccpi.optimisation.algorithms import FISTA
-from ccpi.optimisation.algorithms import FBPD
@@ -26,7 +26,7 @@ class TestAlgorithms(unittest.TestCase):
def setUp(self):
#wget.download('https://github.com/DiamondLightSource/Savu/raw/master/test_data/data/24737_fd.nxs')
#self.filename = '24737_fd.nxs'
- # we use TomoIdentity as the operator and solve the simple least squares
+ # we use Identity as the operator and solve the simple least squares
# problem for a random-valued ImageData or AcquisitionData b?
# Then we know the minimiser is b itself
@@ -48,13 +48,15 @@ class TestAlgorithms(unittest.TestCase):
# fill with random numbers
b.fill(numpy.random.random(x_init.shape))
- identity = TomoIdentity(geometry=ig)
+ identity = Identity(ig)
- norm2sq = Norm2sq(identity, b)
+ norm2sq = Norm2Sq(identity, b)
+ rate = 0.3
+ rate = norm2sq.L / 3.
alg = GradientDescent(x_init=x_init,
objective_function=norm2sq,
- rate=0.3)
+ rate=rate)
alg.max_iteration = 20
alg.run(20, verbose=True)
self.assertNumpyArrayAlmostEqual(alg.x.as_array(), b.as_array())
@@ -62,11 +64,12 @@ class TestAlgorithms(unittest.TestCase):
print ("Test CGLS")
ig = ImageGeometry(124,153,154)
x_init = ImageData(geometry=ig)
+ x_init = ig.allocate()
b = x_init.copy()
# fill with random numbers
b.fill(numpy.random.random(x_init.shape))
-
- identity = TomoIdentity(geometry=ig)
+ b = ig.allocate('random')
+ identity = Identity(ig)
alg = CGLS(x_init=x_init, operator=identity, data=b)
alg.max_iteration = 1
@@ -80,19 +83,19 @@ class TestAlgorithms(unittest.TestCase):
b = x_init.copy()
# fill with random numbers
b.fill(numpy.random.random(x_init.shape))
- x_init = ImageData(geometry=ig)
- x_init.fill(numpy.random.random(x_init.shape))
-
- identity = TomoIdentity(geometry=ig)
+ x_init = ig.allocate(ImageGeometry.RANDOM)
+ identity = Identity(ig)
- norm2sq = Norm2sq(identity, b)
+ #### it seems FISTA does not work with Nowm2Sq
+ # norm2sq = Norm2Sq(identity, b)
+ # norm2sq.L = 2 * norm2sq.c * identity.norm()**2
+ norm2sq = FunctionOperatorComposition(L2NormSquared(b=b), identity)
opt = {'tol': 1e-4, 'memopt':False}
- alg = FISTA(x_init=x_init, f=norm2sq, g=None, opt=opt)
+ print ("initial objective", norm2sq(x_init))
+ alg = FISTA(x_init=x_init, f=norm2sq, g=ZeroFunction())
alg.max_iteration = 2
alg.run(20, verbose=True)
self.assertNumpyArrayAlmostEqual(alg.x.as_array(), b.as_array())
- alg.run(20, verbose=True)
- self.assertNumpyArrayAlmostEqual(alg.x.as_array(), b.as_array())
@@ -120,4 +123,4 @@ class TestAlgorithms(unittest.TestCase):
if __name__ == '__main__':
unittest.main()
- \ No newline at end of file
+
diff --git a/Wrappers/Python/test/test_functions.py b/Wrappers/Python/test/test_functions.py
new file mode 100644
index 0000000..021ad99
--- /dev/null
+++ b/Wrappers/Python/test/test_functions.py
@@ -0,0 +1,367 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Created on Sat Mar 2 19:24:37 2019
+
+@author: evangelos
+"""
+
+
+import numpy as np
+#from ccpi.optimisation.funcs import Function
+from ccpi.optimisation.functions import Function, KullbackLeibler
+from ccpi.framework import DataContainer, ImageData, ImageGeometry
+from ccpi.optimisation.operators import Identity
+from ccpi.optimisation.operators import BlockOperator
+from ccpi.framework import BlockDataContainer
+from numbers import Number
+from ccpi.optimisation.operators import Gradient
+
+from ccpi.optimisation.functions import L2NormSquared
+from ccpi.optimisation.functions import L1Norm, MixedL21Norm
+
+from ccpi.optimisation.functions import Norm2Sq
+from ccpi.optimisation.functions import ZeroFunction
+
+from ccpi.optimisation.functions import FunctionOperatorComposition
+import unittest
+import numpy
+
+#
+
+
+class TestFunction(unittest.TestCase):
+ def assertBlockDataContainerEqual(self, container1, container2):
+ print ("assert Block Data Container Equal")
+ self.assertTrue(issubclass(container1.__class__, container2.__class__))
+ for col in range(container1.shape[0]):
+ if issubclass(container1.get_item(col).__class__, DataContainer):
+ print ("Checking col ", col)
+ self.assertNumpyArrayEqual(
+ container1.get_item(col).as_array(),
+ container2.get_item(col).as_array()
+ )
+ else:
+ self.assertBlockDataContainerEqual(container1.get_item(col),container2.get_item(col))
+
+ def assertNumpyArrayEqual(self, first, second):
+ res = True
+ try:
+ numpy.testing.assert_array_equal(first, second)
+ except AssertionError as err:
+ res = False
+ print(err)
+ self.assertTrue(res)
+
+ def assertNumpyArrayAlmostEqual(self, first, second, decimal=6):
+ res = True
+ try:
+ numpy.testing.assert_array_almost_equal(first, second, decimal)
+ except AssertionError as err:
+ res = False
+ print(err)
+ print("expected " , second)
+ print("actual " , first)
+
+ self.assertTrue(res)
+ def test_Function(self):
+
+
+ N = 3
+ ig = ImageGeometry(N,N)
+ ag = ig
+ op1 = Gradient(ig)
+ op2 = Identity(ig, ag)
+
+ # Form Composite Operator
+ operator = BlockOperator(op1, op2 , shape=(2,1) )
+
+ # Create functions
+ noisy_data = ag.allocate(ImageGeometry.RANDOM_INT)
+
+ d = ag.allocate(ImageGeometry.RANDOM_INT)
+ alpha = 0.5
+ # scaled function
+ g = alpha * L2NormSquared(b=noisy_data)
+
+ # Compare call of g
+ a2 = alpha*(d - noisy_data).power(2).sum()
+ #print(a2, g(d))
+ self.assertEqual(a2, g(d))
+
+ # Compare convex conjugate of g
+ a3 = 0.5 * d.squared_norm() + d.dot(noisy_data)
+ self.assertEqual(a3, g.convex_conjugate(d))
+ #print( a3, g.convex_conjugate(d))
+
+ #test proximal conjugate
+
+
+ def test_L2NormSquared(self):
+ # TESTS for L2 and scalar * L2
+ print ("Test L2NormSquared")
+
+ M, N, K = 2,3,5
+ ig = ImageGeometry(voxel_num_x=M, voxel_num_y = N, voxel_num_z = K)
+ u = ig.allocate(ImageGeometry.RANDOM_INT)
+ b = ig.allocate(ImageGeometry.RANDOM_INT)
+
+ # check grad/call no data
+ f = L2NormSquared()
+ a1 = f.gradient(u)
+ a2 = 2 * u
+ numpy.testing.assert_array_almost_equal(a1.as_array(), a2.as_array(), decimal=4)
+ numpy.testing.assert_equal(f(u), u.squared_norm())
+
+ # check grad/call with data
+ f1 = L2NormSquared(b=b)
+ b1 = f1.gradient(u)
+ b2 = 2 * (u-b)
+
+ numpy.testing.assert_array_almost_equal(b1.as_array(), b2.as_array(), decimal=4)
+ numpy.testing.assert_equal(f1(u), (u-b).squared_norm())
+
+ #check convex conjuagate no data
+ c1 = f.convex_conjugate(u)
+ c2 = 1/4. * u.squared_norm()
+ numpy.testing.assert_equal(c1, c2)
+
+ #check convex conjugate with data
+ d1 = f1.convex_conjugate(u)
+ d2 = (1./4.) * u.squared_norm() + (u*b).sum()
+ numpy.testing.assert_equal(d1, d2)
+
+ # check proximal no data
+ tau = 5
+ e1 = f.proximal(u, tau)
+ e2 = u/(1+2*tau)
+ numpy.testing.assert_array_almost_equal(e1.as_array(), e2.as_array(), decimal=4)
+
+ # check proximal with data
+ tau = 5
+ h1 = f1.proximal(u, tau)
+ h2 = (u-b)/(1+2*tau) + b
+ numpy.testing.assert_array_almost_equal(h1.as_array(), h2.as_array(), decimal=4)
+
+ # check proximal conjugate no data
+ tau = 0.2
+ k1 = f.proximal_conjugate(u, tau)
+ k2 = u/(1 + tau/2 )
+ numpy.testing.assert_array_almost_equal(k1.as_array(), k2.as_array(), decimal=4)
+
+ # check proximal conjugate with data
+ l1 = f1.proximal_conjugate(u, tau)
+ l2 = (u - tau * b)/(1 + tau/2 )
+ numpy.testing.assert_array_almost_equal(l1.as_array(), l2.as_array(), decimal=4)
+
+ # check scaled function properties
+
+ # scalar
+ scalar = 100
+ f_scaled_no_data = scalar * L2NormSquared()
+ f_scaled_data = scalar * L2NormSquared(b=b)
+
+ # call
+ numpy.testing.assert_equal(f_scaled_no_data(u), scalar*f(u))
+ numpy.testing.assert_equal(f_scaled_data(u), scalar*f1(u))
+
+ # grad
+ numpy.testing.assert_array_almost_equal(f_scaled_no_data.gradient(u).as_array(), scalar*f.gradient(u).as_array(), decimal=4)
+ numpy.testing.assert_array_almost_equal(f_scaled_data.gradient(u).as_array(), scalar*f1.gradient(u).as_array(), decimal=4)
+
+ # conj
+ numpy.testing.assert_almost_equal(f_scaled_no_data.convex_conjugate(u), \
+ f.convex_conjugate(u/scalar) * scalar, decimal=4)
+
+ numpy.testing.assert_almost_equal(f_scaled_data.convex_conjugate(u), \
+ scalar * f1.convex_conjugate(u/scalar), decimal=4)
+
+ # proximal
+ numpy.testing.assert_array_almost_equal(f_scaled_no_data.proximal(u, tau).as_array(), \
+ f.proximal(u, tau*scalar).as_array())
+
+
+ numpy.testing.assert_array_almost_equal(f_scaled_data.proximal(u, tau).as_array(), \
+ f1.proximal(u, tau*scalar).as_array())
+
+
+ # proximal conjugate
+ numpy.testing.assert_array_almost_equal(f_scaled_no_data.proximal_conjugate(u, tau).as_array(), \
+ (u/(1 + tau/(2*scalar) )).as_array(), decimal=4)
+
+ numpy.testing.assert_array_almost_equal(f_scaled_data.proximal_conjugate(u, tau).as_array(), \
+ ((u - tau * b)/(1 + tau/(2*scalar) )).as_array(), decimal=4)
+
+ def test_L2NormSquaredOut(self):
+ # TESTS for L2 and scalar * L2
+
+ M, N, K = 2,3,5
+ ig = ImageGeometry(voxel_num_x=M, voxel_num_y = N, voxel_num_z = K)
+ u = ig.allocate(ImageGeometry.RANDOM_INT)
+ b = ig.allocate(ImageGeometry.RANDOM_INT)
+
+ # check grad/call no data
+ f = L2NormSquared()
+ a1 = f.gradient(u)
+ a2 = a1 * 0.
+ f.gradient(u, out=a2)
+ numpy.testing.assert_array_almost_equal(a1.as_array(), a2.as_array(), decimal=4)
+ #numpy.testing.assert_equal(f(u), u.squared_norm())
+
+ # check grad/call with data
+ f1 = L2NormSquared(b=b)
+ b1 = f1.gradient(u)
+ b2 = b1 * 0.
+ f1.gradient(u, out=b2)
+
+ numpy.testing.assert_array_almost_equal(b1.as_array(), b2.as_array(), decimal=4)
+ #numpy.testing.assert_equal(f1(u), (u-b).squared_norm())
+
+ # check proximal no data
+ tau = 5
+ e1 = f.proximal(u, tau)
+ e2 = e1 * 0.
+ f.proximal(u, tau, out=e2)
+ numpy.testing.assert_array_almost_equal(e1.as_array(), e2.as_array(), decimal=4)
+
+ # check proximal with data
+ tau = 5
+ h1 = f1.proximal(u, tau)
+ h2 = h1 * 0.
+ f1.proximal(u, tau, out=h2)
+ numpy.testing.assert_array_almost_equal(h1.as_array(), h2.as_array(), decimal=4)
+
+ # check proximal conjugate no data
+ tau = 0.2
+ k1 = f.proximal_conjugate(u, tau)
+ k2 = k1 * 0.
+ f.proximal_conjugate(u, tau, out=k2)
+
+ numpy.testing.assert_array_almost_equal(k1.as_array(), k2.as_array(), decimal=4)
+
+ # check proximal conjugate with data
+ l1 = f1.proximal_conjugate(u, tau)
+ l2 = l1 * 0.
+ f1.proximal_conjugate(u, tau, out=l2)
+ numpy.testing.assert_array_almost_equal(l1.as_array(), l2.as_array(), decimal=4)
+
+ # check scaled function properties
+
+ # scalar
+ scalar = 100
+ f_scaled_no_data = scalar * L2NormSquared()
+ f_scaled_data = scalar * L2NormSquared(b=b)
+
+ # grad
+ w = f_scaled_no_data.gradient(u)
+ ww = w * 0
+ f_scaled_no_data.gradient(u, out=ww)
+
+ numpy.testing.assert_array_almost_equal(w.as_array(),
+ ww.as_array(), decimal=4)
+
+ # numpy.testing.assert_array_almost_equal(f_scaled_data.gradient(u).as_array(), scalar*f1.gradient(u).as_array(), decimal=4)
+
+ # # conj
+ # numpy.testing.assert_almost_equal(f_scaled_no_data.convex_conjugate(u), \
+ # f.convex_conjugate(u/scalar) * scalar, decimal=4)
+
+ # numpy.testing.assert_almost_equal(f_scaled_data.convex_conjugate(u), \
+ # scalar * f1.convex_conjugate(u/scalar), decimal=4)
+
+ # # proximal
+ w = f_scaled_no_data.proximal(u, tau)
+ ww = w * 0
+ f_scaled_no_data.proximal(u, tau, out=ww)
+ numpy.testing.assert_array_almost_equal(w.as_array(), \
+ ww.as_array())
+
+
+ # numpy.testing.assert_array_almost_equal(f_scaled_data.proximal(u, tau).as_array(), \
+ # f1.proximal(u, tau*scalar).as_array())
+
+
+ # proximal conjugate
+ w = f_scaled_no_data.proximal_conjugate(u, tau)
+ ww = w * 0
+ f_scaled_no_data.proximal_conjugate(u, tau, out=ww)
+ numpy.testing.assert_array_almost_equal(w.as_array(), \
+ ww.as_array(), decimal=4)
+
+ # numpy.testing.assert_array_almost_equal(f_scaled_data.proximal_conjugate(u, tau).as_array(), \
+ # ((u - tau * b)/(1 + tau/(2*scalar) )).as_array(), decimal=4)
+
+ def test_Norm2sq_as_FunctionOperatorComposition(self):
+ M, N, K = 2,3,5
+ ig = ImageGeometry(voxel_num_x=M, voxel_num_y = N, voxel_num_z = K)
+ u = ig.allocate(ImageGeometry.RANDOM_INT)
+ b = ig.allocate(ImageGeometry.RANDOM_INT)
+
+ A = 0.5 * Identity(ig)
+ old_chisq = Norm2Sq(A, b, 1.0)
+ new_chisq = FunctionOperatorComposition(L2NormSquared(b=b),A)
+
+ yold = old_chisq(u)
+ ynew = new_chisq(u)
+ self.assertEqual(yold, ynew)
+
+ yold = old_chisq.gradient(u)
+ ynew = new_chisq.gradient(u)
+ numpy.testing.assert_array_equal(yold.as_array(), ynew.as_array())
+
+ def test_mixedL12Norm(self):
+ M, N, K = 2,3,5
+ ig = ImageGeometry(voxel_num_x=M, voxel_num_y = N)
+ u1 = ig.allocate('random_int')
+ u2 = ig.allocate('random_int')
+
+ U = BlockDataContainer(u1, u2, shape=(2,1))
+
+ # Define no scale and scaled
+ f_no_scaled = MixedL21Norm()
+ f_scaled = 1 * MixedL21Norm()
+
+ # call
+
+ a1 = f_no_scaled(U)
+ a2 = f_scaled(U)
+ self.assertNumpyArrayAlmostEqual(a1,a2)
+
+
+ tmp = [ el**2 for el in U.containers ]
+ self.assertBlockDataContainerEqual(BlockDataContainer(*tmp),
+ U.power(2))
+
+ z1 = f_no_scaled.proximal_conjugate(U, 1)
+ u3 = ig.allocate('random_int')
+ u4 = ig.allocate('random_int')
+
+ z3 = BlockDataContainer(u3, u4, shape=(2,1))
+
+
+ f_no_scaled.proximal_conjugate(U, 1, out=z3)
+ self.assertBlockDataContainerEqual(z3,z1)
+
+ def test_KullbackLeibler(self):
+ print ("test_KullbackLeibler")
+ N, M = 2,3
+ ig = ImageGeometry(N, M)
+ data = ig.allocate(ImageGeometry.RANDOM_INT)
+ x = ig.allocate(ImageGeometry.RANDOM_INT)
+ bnoise = ig.allocate(ImageGeometry.RANDOM_INT)
+
+ out = ig.allocate()
+
+ f = KullbackLeibler(data, bnoise=bnoise)
+
+ grad = f.gradient(x)
+ f.gradient(x, out=out)
+ numpy.testing.assert_array_equal(grad.as_array(), out.as_array())
+
+ prox = f.proximal(x,1.2)
+ f.proximal(x, 1.2, out=out)
+ numpy.testing.assert_array_equal(prox.as_array(), out.as_array())
+
+ proxc = f.proximal_conjugate(x,1.2)
+ f.proximal_conjugate(x, 1.2, out=out)
+ numpy.testing.assert_array_equal(proxc.as_array(), out.as_array())
diff --git a/Wrappers/Python/test/test_run_test.py b/Wrappers/Python/test/test_run_test.py
index 3c7d9ab..81ee738 100755
--- a/Wrappers/Python/test/test_run_test.py
+++ b/Wrappers/Python/test/test_run_test.py
@@ -6,18 +6,17 @@ from ccpi.framework import ImageData
from ccpi.framework import AcquisitionData
from ccpi.framework import ImageGeometry
from ccpi.framework import AcquisitionGeometry
-from ccpi.optimisation.algs import FISTA
-from ccpi.optimisation.algs import FBPD
-from ccpi.optimisation.funcs import Norm2sq
-from ccpi.optimisation.funcs import ZeroFun
-from ccpi.optimisation.funcs import Norm1
-from ccpi.optimisation.funcs import TV2D
-from ccpi.optimisation.funcs import Norm2
+from ccpi.optimisation.algorithms import FISTA
+#from ccpi.optimisation.algs import FBPD
+from ccpi.optimisation.functions import Norm2Sq
+from ccpi.optimisation.functions import ZeroFunction
+# from ccpi.optimisation.funcs import Norm1
+from ccpi.optimisation.functions import L1Norm
-from ccpi.optimisation.ops import LinearOperatorMatrix
-from ccpi.optimisation.ops import TomoIdentity
-from ccpi.optimisation.ops import Identity
-from ccpi.optimisation.ops import PowerMethodNonsquare
+from ccpi.optimisation.operators import LinearOperatorMatrix
+from ccpi.optimisation.operators import Identity
+#from ccpi.optimisation.ops import PowerMethodNonsquare
+from ccpi.optimisation.operators import LinearOperator
import numpy.testing
@@ -81,8 +80,8 @@ class TestAlgorithms(unittest.TestCase):
lam = 10
opt = {'memopt': True}
# Create object instances with the test data A and b.
- f = Norm2sq(A, b, c=0.5, memopt=True)
- g0 = ZeroFun()
+ f = Norm2Sq(A, b, c=0.5, memopt=True)
+ g0 = ZeroFunction()
# Initial guess
x_init = DataContainer(np.zeros((n, 1)))
@@ -90,12 +89,15 @@ class TestAlgorithms(unittest.TestCase):
f.grad(x_init)
# Run FISTA for least squares plus zero function.
- x_fista0, it0, timing0, criter0 = FISTA(x_init, f, g0, opt=opt)
-
+ #x_fista0, it0, timing0, criter0 = FISTA(x_init, f, g0, opt=opt)
+ fa = FISTA(x_init=x_init, f=f, g=g0)
+ fa.max_iteration = 10
+ fa.run(10)
+
# Print solution and final objective/criterion value for comparison
print("FISTA least squares plus zero function solution and objective value:")
- print(x_fista0.array)
- print(criter0[-1])
+ print(fa.get_output())
+ print(fa.get_last_objective())
# Compare to CVXPY
@@ -135,18 +137,22 @@ class TestAlgorithms(unittest.TestCase):
# A = Identity()
# Change n to equal to m.
-
- b = DataContainer(bmat)
+ vgb = VectorGeometry(m)
+ vgx = VectorGeometry(n)
+ b = vgb.allocate()
+ b.fill(bmat)
+ #b = DataContainer(bmat)
# Regularization parameter
lam = 10
opt = {'memopt': True}
# Create object instances with the test data A and b.
- f = Norm2sq(A, b, c=0.5, memopt=True)
- g0 = ZeroFun()
+ f = Norm2Sq(A, b, c=0.5, memopt=True)
+ g0 = ZeroFunction()
# Initial guess
- x_init = DataContainer(np.zeros((n, 1)))
+ #x_init = DataContainer(np.zeros((n, 1)))
+ x_init = vgx.allocate()
# Create 1-norm object instance
g1 = Norm1(lam)
@@ -155,12 +161,16 @@ class TestAlgorithms(unittest.TestCase):
g1.prox(x_init, 0.02)
# Combine with least squares and solve using generic FISTA implementation
- x_fista1, it1, timing1, criter1 = FISTA(x_init, f, g1, opt=opt)
+ #x_fista1, it1, timing1, criter1 = FISTA(x_init, f, g1, opt=opt)
+ fa = FISTA(x_init=x_init, f=f, g=g1)
+ fa.max_iteration = 10
+ fa.run(10)
+
# Print for comparison
print("FISTA least squares plus 1-norm solution and objective value:")
- print(x_fista1.as_array().squeeze())
- print(criter1[-1])
+ print(fa.get_output())
+ print(fa.get_last_objective())
# Compare to CVXPY
@@ -212,8 +222,8 @@ class TestAlgorithms(unittest.TestCase):
x_init = DataContainer(np.random.randn(n, 1))
# Create object instances with the test data A and b.
- f = Norm2sq(A, b, c=0.5, memopt=True)
- f.L = PowerMethodNonsquare(A, 25, x_init)[0]
+ f = Norm2Sq(A, b, c=0.5, memopt=True)
+ f.L = LinearOperator.PowerMethod(A, 25, x_init)[0]
print ("Lipschitz", f.L)
g0 = ZeroFun()
@@ -280,9 +290,9 @@ class TestAlgorithms(unittest.TestCase):
y.array = y.array + 0.1*np.random.randn(N, N)
# Data fidelity term
- f_denoise = Norm2sq(I, y, c=0.5, memopt=True)
+ f_denoise = Norm2Sq(I, y, c=0.5, memopt=True)
x_init = ImageData(geometry=ig)
- f_denoise.L = PowerMethodNonsquare(I, 25, x_init)[0]
+ f_denoise.L = LinearOperator.PowerMethod(I, 25, x_init)[0]
# 1-norm regulariser
lam1_denoise = 1.0
@@ -328,43 +338,6 @@ class TestAlgorithms(unittest.TestCase):
self.assertNumpyArrayAlmostEqual(
x_fbpd1_denoise.array.flatten(), x1_denoise.value, 5)
- x1_cvx = x1_denoise.value
- x1_cvx.shape = (N, N)
-
- # Now TV with FBPD
- lam_tv = 0.1
- gtv = TV2D(lam_tv)
- gtv(gtv.op.direct(x_init_denoise))
-
- opt_tv = {'tol': 1e-4, 'iter': 10000}
-
- x_fbpdtv_denoise, itfbpdtv_denoise, timingfbpdtv_denoise,\
- criterfbpdtv_denoise = \
- FBPD(x_init_denoise, gtv.op, None, f_denoise, gtv, opt=opt_tv)
- print(x_fbpdtv_denoise)
- print(criterfbpdtv_denoise[-1])
-
- # Compare to CVXPY
-
- # Construct the problem.
- xtv_denoise = Variable((N, N))
- objectivetv_denoise = Minimize(
- 0.5*sum_squares(xtv_denoise - y.array) + lam_tv*tv(xtv_denoise))
- probtv_denoise = Problem(objectivetv_denoise)
-
- # The optimal objective is returned by prob.solve().
- resulttv_denoise = probtv_denoise.solve(
- verbose=False, solver=SCS, eps=1e-12)
-
- # The optimal solution for x is stored in x.value and optimal objective value
- # is in result as well as in objective.value
- print("CVXPY least squares plus 1-norm solution and objective value:")
- print(xtv_denoise.value)
- print(objectivetv_denoise.value)
-
- self.assertNumpyArrayAlmostEqual(
- x_fbpdtv_denoise.as_array(), xtv_denoise.value, 1)
-
else:
self.assertTrue(cvx_not_installable)
diff --git a/Wrappers/Python/wip/CGLS_tikhonov.py b/Wrappers/Python/wip/CGLS_tikhonov.py
new file mode 100644
index 0000000..e9bbcd9
--- /dev/null
+++ b/Wrappers/Python/wip/CGLS_tikhonov.py
@@ -0,0 +1,196 @@
+from ccpi.optimisation.algorithms import CGLS
+
+from ccpi.plugins.ops import CCPiProjectorSimple
+from ccpi.optimisation.ops import PowerMethodNonsquare
+from ccpi.optimisation.ops import TomoIdentity
+from ccpi.optimisation.funcs import Norm2sq, Norm1
+from ccpi.framework import ImageGeometry, AcquisitionGeometry, ImageData, AcquisitionData
+from ccpi.optimisation.algorithms import GradientDescent
+#from ccpi.optimisation.algorithms import CGLS
+import matplotlib.pyplot as plt
+import numpy
+from ccpi.framework import BlockDataContainer
+from ccpi.optimisation.operators import BlockOperator
+
+# Set up phantom size N x N x vert by creating ImageGeometry, initialising the
+# ImageData object with this geometry and empty array and finally put some
+# data into its array, and display one slice as image.
+
+# Image parameters
+N = 128
+vert = 4
+
+# Set up image geometry
+ig = ImageGeometry(voxel_num_x=N,
+ voxel_num_y=N,
+ voxel_num_z=vert)
+
+# Set up empty image data
+Phantom = ImageData(geometry=ig,
+ dimension_labels=['horizontal_x',
+ 'horizontal_y',
+ 'vertical'])
+Phantom += 0.05
+# Populate image data by looping over and filling slices
+i = 0
+while i < vert:
+ if vert > 1:
+ x = Phantom.subset(vertical=i).array
+ else:
+ x = Phantom.array
+ x[round(N/4):round(3*N/4),round(N/4):round(3*N/4)] = 0.5
+ x[round(N/8):round(7*N/8),round(3*N/8):round(5*N/8)] = 0.94
+ if vert > 1 :
+ Phantom.fill(x, vertical=i)
+ i += 1
+
+
+perc = 0.02
+# Set up empty image data
+noise = ImageData(numpy.random.normal(loc = 0.04 ,
+ scale = perc ,
+ size = Phantom.shape), geometry=ig,
+ dimension_labels=['horizontal_x',
+ 'horizontal_y',
+ 'vertical'])
+Phantom += noise
+
+# Set up AcquisitionGeometry object to hold the parameters of the measurement
+# setup geometry: # Number of angles, the actual angles from 0 to
+# pi for parallel beam, set the width of a detector
+# pixel relative to an object pixe and the number of detector pixels.
+angles_num = 20
+det_w = 1.0
+det_num = N
+
+angles = numpy.linspace(0,numpy.pi,angles_num,endpoint=False,dtype=numpy.float32)*\
+ 180/numpy.pi
+
+# Inputs: Geometry, 2D or 3D, angles, horz detector pixel count,
+# horz detector pixel size, vert detector pixel count,
+# vert detector pixel size.
+ag = AcquisitionGeometry('parallel',
+ '3D',
+ angles,
+ N,
+ det_w,
+ vert,
+ det_w)
+
+# Set up Operator object combining the ImageGeometry and AcquisitionGeometry
+# wrapping calls to CCPi projector.
+A = CCPiProjectorSimple(ig, ag)
+
+# Forward and backprojection are available as methods direct and adjoint. Here
+# generate test data b and some noise
+
+b = A.direct(Phantom)
+
+
+#z = A.adjoint(b)
+
+
+# Using the test data b, different reconstruction methods can now be set up as
+# demonstrated in the rest of this file. In general all methods need an initial
+# guess and some algorithm options to be set. Note that 100 iterations for
+# some of the methods is a very low number and 1000 or 10000 iterations may be
+# needed if one wants to obtain a converged solution.
+x_init = ImageData(geometry=ig,
+ dimension_labels=['horizontal_x','horizontal_y','vertical'])
+X_init = BlockDataContainer(x_init)
+B = BlockDataContainer(b,
+ ImageData(geometry=ig, dimension_labels=['horizontal_x','horizontal_y','vertical']))
+
+# setup a tomo identity
+Ibig = 1e5 * TomoIdentity(geometry=ig)
+Ismall = 1e-5 * TomoIdentity(geometry=ig)
+Iok = 1e1 * TomoIdentity(geometry=ig)
+
+# composite operator
+Kbig = BlockOperator(A, Ibig, shape=(2,1))
+Ksmall = BlockOperator(A, Ismall, shape=(2,1))
+Kok = BlockOperator(A, Iok, shape=(2,1))
+
+#out = K.direct(X_init)
+
+f = Norm2sq(Kbig,B)
+f.L = 0.00003
+
+fsmall = Norm2sq(Ksmall,B)
+fsmall.L = 0.00003
+
+fok = Norm2sq(Kok,B)
+fok.L = 0.00003
+
+simplef = Norm2sq(A, b)
+simplef.L = 0.00003
+
+gd = GradientDescent( x_init=x_init, objective_function=simplef,
+ rate=simplef.L)
+gd.max_iteration = 50
+
+Kbig.direct(X_init)
+Kbig.adjoint(B)
+cg = CGLS()
+cg.set_up(X_init, Kbig, B )
+cg.max_iteration = 10
+
+cgsmall = CGLS()
+cgsmall.set_up(X_init, Ksmall, B )
+cgsmall.max_iteration = 10
+
+
+cgs = CGLS()
+cgs.set_up(x_init, A, b )
+cgs.max_iteration = 10
+
+cgok = CGLS()
+cgok.set_up(X_init, Kok, B )
+cgok.max_iteration = 10
+# #
+#out.__isub__(B)
+#out2 = K.adjoint(out)
+
+#(2.0*self.c)*self.A.adjoint( self.A.direct(x) - self.b )
+
+for _ in gd:
+ print ("iteration {} {}".format(gd.iteration, gd.get_last_loss()))
+
+cg.run(10, lambda it,val: print ("iteration {} objective {}".format(it,val)))
+
+cgs.run(10, lambda it,val: print ("iteration {} objective {}".format(it,val)))
+
+cgsmall.run(10, lambda it,val: print ("iteration {} objective {}".format(it,val)))
+cgsmall.run(10, lambda it,val: print ("iteration {} objective {}".format(it,val)))
+cgok.run(10, verbose=True)
+# # for _ in cg:
+# print ("iteration {} {}".format(cg.iteration, cg.get_current_loss()))
+# #
+# # fig = plt.figure()
+# # plt.imshow(cg.get_output().get_item(0,0).subset(vertical=0).as_array())
+# # plt.title('Composite CGLS')
+# # plt.show()
+# #
+# # for _ in cgs:
+# print ("iteration {} {}".format(cgs.iteration, cgs.get_current_loss()))
+# #
+fig = plt.figure()
+plt.subplot(2,3,1)
+plt.imshow(Phantom.subset(vertical=0).as_array())
+plt.title('Simulated Phantom')
+plt.subplot(2,3,2)
+plt.imshow(gd.get_output().subset(vertical=0).as_array())
+plt.title('Simple Gradient Descent')
+plt.subplot(2,3,3)
+plt.imshow(cgs.get_output().subset(vertical=0).as_array())
+plt.title('Simple CGLS')
+plt.subplot(2,3,5)
+plt.imshow(cg.get_output().get_item(0).subset(vertical=0).as_array())
+plt.title('Composite CGLS\nbig lambda')
+plt.subplot(2,3,6)
+plt.imshow(cgsmall.get_output().get_item(0).subset(vertical=0).as_array())
+plt.title('Composite CGLS\nsmall lambda')
+plt.subplot(2,3,4)
+plt.imshow(cgok.get_output().get_item(0).subset(vertical=0).as_array())
+plt.title('Composite CGLS\nok lambda')
+plt.show()
diff --git a/Wrappers/Python/wip/CreatePhantom.py b/Wrappers/Python/wip/CreatePhantom.py
new file mode 100644
index 0000000..4bf6ea4
--- /dev/null
+++ b/Wrappers/Python/wip/CreatePhantom.py
@@ -0,0 +1,242 @@
+import numpy
+import tomophantom
+from tomophantom import TomoP3D
+from tomophantom.supp.artifacts import ArtifactsClass as Artifact
+import os
+
+import matplotlib.pyplot as plt
+
+from ccpi.optimisation.algorithms import CGLS
+from ccpi.plugins.ops import CCPiProjectorSimple
+from ccpi.optimisation.ops import PowerMethodNonsquare
+from ccpi.optimisation.ops import TomoIdentity
+from ccpi.optimisation.funcs import Norm2sq, Norm1
+from ccpi.framework import ImageGeometry, AcquisitionGeometry, ImageData, AcquisitionData
+from ccpi.optimisation.algorithms import GradientDescent
+from ccpi.framework import BlockDataContainer
+from ccpi.optimisation.operators import BlockOperator
+
+
+model = 13 # select a model number from tomophantom library
+N_size = 64 # Define phantom dimensions using a scalar value (cubic phantom)
+path = os.path.dirname(tomophantom.__file__)
+path_library3D = os.path.join(path, "Phantom3DLibrary.dat")
+
+#This will generate a N_size x N_size x N_size phantom (3D)
+phantom_tm = TomoP3D.Model(model, N_size, path_library3D)
+
+# detector column count (horizontal)
+detector_horiz = int(numpy.sqrt(2)*N_size)
+# detector row count (vertical) (no reason for it to be > N)
+detector_vert = N_size
+# number of projection angles
+angles_num = int(0.5*numpy.pi*N_size)
+# angles are expressed in degrees
+angles = numpy.linspace(0.0, 179.9, angles_num, dtype='float32')
+
+
+acquisition_data_array = TomoP3D.ModelSino(model, N_size,
+ detector_horiz, detector_vert,
+ angles,
+ path_library3D)
+
+tomophantom_acquisition_axes_order = ['vertical', 'angle', 'horizontal']
+
+artifacts = Artifact(acquisition_data_array)
+
+
+tp_acq_data = AcquisitionData(artifacts.noise(0.2, 'Gaussian'),
+ dimension_labels=tomophantom_acquisition_axes_order)
+#print ("size", acquisition_data.shape)
+print ("horiz", detector_horiz)
+print ("vert", detector_vert)
+print ("angles", angles_num)
+
+tp_acq_geometry = AcquisitionGeometry(geom_type='parallel', dimension='3D',
+ angles=angles,
+ pixel_num_h=detector_horiz,
+ pixel_num_v=detector_vert,
+ channels=1,
+ )
+
+acq_data = tp_acq_geometry.allocate()
+#print (tp_acq_geometry)
+print ("AcquisitionData", acq_data.shape)
+print ("TomoPhantom", tp_acq_data.shape, tp_acq_data.dimension_labels)
+
+default_acquisition_axes_order = ['angle', 'vertical', 'horizontal']
+
+acq_data2 = tp_acq_data.subset(dimensions=default_acquisition_axes_order)
+print ("AcquisitionData", acq_data2.shape, acq_data2.dimension_labels)
+print ("AcquisitionData {} TomoPhantom {}".format(id(acq_data2.as_array()),
+ id(acquisition_data_array)))
+
+fig = plt.figure()
+plt.subplot(1,2,1)
+plt.imshow(acquisition_data_array[20])
+plt.title('Sinogram')
+plt.subplot(1,2,2)
+plt.imshow(tp_acq_data.as_array()[20])
+plt.title('Sinogram + noise')
+plt.show()
+
+# Set up Operator object combining the ImageGeometry and AcquisitionGeometry
+# wrapping calls to CCPi projector.
+
+ig = ImageGeometry(voxel_num_x=detector_horiz,
+ voxel_num_y=detector_horiz,
+ voxel_num_z=detector_vert)
+A = CCPiProjectorSimple(ig, tp_acq_geometry)
+# Forward and backprojection are available as methods direct and adjoint. Here
+# generate test data b and some noise
+
+#b = A.direct(Phantom)
+b = acq_data2
+
+#z = A.adjoint(b)
+
+
+# Using the test data b, different reconstruction methods can now be set up as
+# demonstrated in the rest of this file. In general all methods need an initial
+# guess and some algorithm options to be set. Note that 100 iterations for
+# some of the methods is a very low number and 1000 or 10000 iterations may be
+# needed if one wants to obtain a converged solution.
+x_init = ImageData(geometry=ig,
+ dimension_labels=['horizontal_x','horizontal_y','vertical'])
+X_init = BlockDataContainer(x_init)
+B = BlockDataContainer(b,
+ ImageData(geometry=ig, dimension_labels=['horizontal_x','horizontal_y','vertical']))
+
+# setup a tomo identity
+Ibig = 4e1 * TomoIdentity(geometry=ig)
+Ismall = 1e-3 * TomoIdentity(geometry=ig)
+Iok = 7.6e0 * TomoIdentity(geometry=ig)
+
+# composite operator
+Kbig = BlockOperator(A, Ibig, shape=(2,1))
+Ksmall = BlockOperator(A, Ismall, shape=(2,1))
+Kok = BlockOperator(A, Iok, shape=(2,1))
+
+#out = K.direct(X_init)
+#x0 = x_init.copy()
+#x0.fill(numpy.random.randn(*x0.shape))
+#lipschitz = PowerMethodNonsquare(A, 5, x0)
+#print("lipschitz", lipschitz)
+
+#%%
+
+simplef = Norm2sq(A, b, memopt=False)
+#simplef.L = lipschitz[0]/3000.
+simplef.L = 0.00003
+
+f = Norm2sq(Kbig,B)
+f.L = 0.00003
+
+fsmall = Norm2sq(Ksmall,B)
+fsmall.L = 0.00003
+
+fok = Norm2sq(Kok,B)
+fok.L = 0.00003
+
+print("setup gradient descent")
+gd = GradientDescent( x_init=x_init, objective_function=simplef,
+ rate=simplef.L)
+gd.max_iteration = 5
+simplef2 = Norm2sq(A, b, memopt=True)
+#simplef.L = lipschitz[0]/3000.
+simplef2.L = 0.00003
+print("setup gradient descent")
+gd2 = GradientDescent( x_init=x_init, objective_function=simplef2,
+ rate=simplef2.L)
+gd2.max_iteration = 5
+
+Kbig.direct(X_init)
+Kbig.adjoint(B)
+print("setup CGLS")
+cg = CGLS()
+cg.set_up(X_init, Kbig, B )
+cg.max_iteration = 10
+
+print("setup CGLS")
+cgsmall = CGLS()
+cgsmall.set_up(X_init, Ksmall, B )
+cgsmall.max_iteration = 10
+
+
+print("setup CGLS")
+cgs = CGLS()
+cgs.set_up(x_init, A, b )
+cgs.max_iteration = 10
+
+print("setup CGLS")
+cgok = CGLS()
+cgok.set_up(X_init, Kok, B )
+cgok.max_iteration = 10
+# #
+#out.__isub__(B)
+#out2 = K.adjoint(out)
+
+#(2.0*self.c)*self.A.adjoint( self.A.direct(x) - self.b )
+
+
+for _ in gd:
+ print ("GradientDescent iteration {} {}".format(gd.iteration, gd.get_last_loss()))
+#gd2.run(5,verbose=True)
+print("CGLS block lambda big")
+cg.run(10, lambda it,val: print ("CGLS big iteration {} objective {}".format(it,val)))
+
+print("CGLS standard")
+cgs.run(10, lambda it,val: print ("CGLS standard iteration {} objective {}".format(it,val)))
+
+print("CGLS block lambda small")
+cgsmall.run(10, lambda it,val: print ("CGLS small iteration {} objective {}".format(it,val)))
+print("CGLS block lambdaok")
+cgok.run(10, verbose=True)
+# # for _ in cg:
+# print ("iteration {} {}".format(cg.iteration, cg.get_current_loss()))
+# #
+# # fig = plt.figure()
+# # plt.imshow(cg.get_output().get_item(0,0).subset(vertical=0).as_array())
+# # plt.title('Composite CGLS')
+# # plt.show()
+# #
+# # for _ in cgs:
+# print ("iteration {} {}".format(cgs.iteration, cgs.get_current_loss()))
+# #
+Phantom = ImageData(phantom_tm)
+
+theslice=40
+
+fig = plt.figure()
+plt.subplot(2,3,1)
+plt.imshow(numpy.flip(Phantom.subset(vertical=theslice).as_array(),axis=0), cmap='gray')
+plt.clim(0,0.7)
+plt.title('Simulated Phantom')
+plt.subplot(2,3,2)
+plt.imshow(gd.get_output().subset(vertical=theslice).as_array(), cmap='gray')
+plt.clim(0,0.7)
+plt.title('Simple Gradient Descent')
+plt.subplot(2,3,3)
+plt.imshow(cgs.get_output().subset(vertical=theslice).as_array(), cmap='gray')
+plt.clim(0,0.7)
+plt.title('Simple CGLS')
+plt.subplot(2,3,5)
+plt.imshow(cg.get_output().get_item(0).subset(vertical=theslice).as_array(), cmap='gray')
+plt.clim(0,0.7)
+plt.title('Composite CGLS\nbig lambda')
+plt.subplot(2,3,6)
+plt.imshow(cgsmall.get_output().get_item(0).subset(vertical=theslice).as_array(), cmap='gray')
+plt.clim(0,0.7)
+plt.title('Composite CGLS\nsmall lambda')
+plt.subplot(2,3,4)
+plt.imshow(cgok.get_output().get_item(0).subset(vertical=theslice).as_array(), cmap='gray')
+plt.clim(0,0.7)
+plt.title('Composite CGLS\nok lambda')
+plt.show()
+
+
+#Ibig = 7e1 * TomoIdentity(geometry=ig)
+#Kbig = BlockOperator(A, Ibig, shape=(2,1))
+#cg2 = CGLS(x_init=X_init, operator=Kbig, data=B)
+#cg2.max_iteration = 10
+#cg2.run(10, verbose=True)
diff --git a/Wrappers/Python/wip/Demos/FISTA_vs_CGLS.py b/Wrappers/Python/wip/Demos/FISTA_vs_CGLS.py
new file mode 100644
index 0000000..2dcaa89
--- /dev/null
+++ b/Wrappers/Python/wip/Demos/FISTA_vs_CGLS.py
@@ -0,0 +1,119 @@
+# -*- coding: utf-8 -*-
+# This work is part of the Core Imaging Library developed by
+# Visual Analytics and Imaging System Group of the Science Technology
+# Facilities Council, STFC
+
+# Copyright 2018-2019 Evangelos Papoutsellis and Edoardo Pasca
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from ccpi.framework import ImageData, ImageGeometry, AcquisitionGeometry, AcquisitionData
+
+import numpy as np
+import numpy
+import matplotlib.pyplot as plt
+
+from ccpi.optimisation.algorithms import FISTA, CGLS
+
+from ccpi.optimisation.operators import Gradient
+from ccpi.optimisation.functions import ZeroFunction, L2NormSquared, FunctionOperatorComposition
+from skimage.util import random_noise
+from ccpi.astra.ops import AstraProjectorSimple
+
+#%%
+
+N = 75
+x = np.zeros((N,N))
+x[round(N/4):round(3*N/4),round(N/4):round(3*N/4)] = 0.5
+x[round(N/8):round(7*N/8),round(3*N/8):round(5*N/8)] = 1
+
+data = ImageData(x)
+ig = ImageGeometry(voxel_num_x = N, voxel_num_y = N)
+
+detectors = N
+angles = np.linspace(0, np.pi, N, dtype=np.float32)
+
+ag = AcquisitionGeometry('parallel','2D',angles, detectors)
+Aop = AstraProjectorSimple(ig, ag, 'cpu')
+sin = Aop.direct(data)
+
+noisy_data = sin
+
+fidelity = FunctionOperatorComposition(L2NormSquared(b=noisy_data), Aop)
+regularizer = ZeroFunction()
+
+x_init = ig.allocate()
+
+## Setup and run the FISTA algorithm
+opt = {'tol': 1e-4, 'memopt':True}
+fista = FISTA(x_init=x_init , f=fidelity, g=regularizer, opt=opt)
+fista.max_iteration = 500
+fista.update_objective_interval = 50
+fista.run(500, verbose=True)
+
+## Setup and run the CGLS algorithm
+cgls = CGLS(x_init=x_init, operator=Aop, data=noisy_data)
+cgls.max_iteration = 500
+cgls.update_objective_interval = 50
+cgls.run(500, verbose=True)
+
+diff = fista.get_output() - cgls.get_output()
+
+
+#%%
+print( diff.norm())
+
+plt.figure(figsize=(15,15))
+plt.subplot(3,1,1)
+plt.imshow(fista.get_output().as_array())
+plt.title('FISTA reconstruction')
+plt.colorbar()
+plt.subplot(3,1,2)
+plt.imshow(cgls.get_output().as_array())
+plt.title('CGLS reconstruction')
+plt.colorbar()
+plt.subplot(3,1,3)
+plt.imshow(diff.abs().as_array())
+plt.title('Difference reconstruction')
+plt.colorbar()
+plt.show()
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Wrappers/Python/wip/Demos/FISTA_vs_PDHG.py b/Wrappers/Python/wip/Demos/FISTA_vs_PDHG.py
new file mode 100644
index 0000000..b7777ef
--- /dev/null
+++ b/Wrappers/Python/wip/Demos/FISTA_vs_PDHG.py
@@ -0,0 +1,120 @@
+# -*- coding: utf-8 -*-
+# This work is part of the Core Imaging Library developed by
+# Visual Analytics and Imaging System Group of the Science Technology
+# Facilities Council, STFC
+
+# Copyright 2018-2019 Evangelos Papoutsellis and Edoardo Pasca
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from ccpi.framework import ImageData, ImageGeometry
+
+import numpy as np
+import numpy
+import matplotlib.pyplot as plt
+
+from ccpi.optimisation.algorithms import FISTA, PDHG
+
+from ccpi.optimisation.operators import BlockOperator, Gradient, Identity
+from ccpi.optimisation.functions import L2NormSquared, L1Norm, \
+ MixedL21Norm, FunctionOperatorComposition, BlockFunction, ZeroFunction
+
+from skimage.util import random_noise
+
+# Create phantom for TV Gaussian denoising
+N = 100
+
+data = np.zeros((N,N))
+data[round(N/4):round(3*N/4),round(N/4):round(3*N/4)] = 0.5
+data[round(N/8):round(7*N/8),round(3*N/8):round(5*N/8)] = 1
+data = ImageData(data)
+ig = ImageGeometry(voxel_num_x = N, voxel_num_y = N)
+ag = ig
+
+# Create noisy data. Add Gaussian noise
+n1 = random_noise(data.as_array(), mode = 's&p', salt_vs_pepper = 0.9, amount=0.2)
+noisy_data = ImageData(n1)
+
+# Regularisation Parameter
+alpha = 5
+
+operator = Gradient(ig)
+
+fidelity = L1Norm(b=noisy_data)
+regulariser = FunctionOperatorComposition(alpha * L2NormSquared(), operator)
+
+x_init = ig.allocate()
+
+## Setup and run the PDHG algorithm
+opt = {'tol': 1e-4, 'memopt':True}
+fista = FISTA(x_init=x_init , f=regulariser, g=fidelity, opt=opt)
+fista.max_iteration = 2000
+fista.update_objective_interval = 50
+fista.run(2000, verbose=True)
+
+plt.figure(figsize=(15,15))
+plt.subplot(3,1,1)
+plt.imshow(data.as_array())
+plt.title('Ground Truth')
+plt.colorbar()
+plt.subplot(3,1,2)
+plt.imshow(noisy_data.as_array())
+plt.title('Noisy Data')
+plt.colorbar()
+plt.subplot(3,1,3)
+plt.imshow(fista.get_output().as_array())
+plt.title('TV Reconstruction')
+plt.colorbar()
+plt.show()
+
+# Compare with PDHG
+# Create operators
+op1 = Gradient(ig)
+op2 = Identity(ig, ag)
+
+# Create BlockOperator
+operator = BlockOperator(op1, op2, shape=(2,1) )
+f = BlockFunction(alpha * L2NormSquared(), fidelity)
+g = ZeroFunction()
+
+## Compute operator Norm
+normK = operator.norm()
+#
+## Primal & dual stepsizes
+sigma = 1
+tau = 1/(sigma*normK**2)
+#
+#
+## Setup and run the PDHG algorithm
+pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma, memopt=True)
+pdhg.max_iteration = 2000
+pdhg.update_objective_interval = 50
+pdhg.run(2000)
+#
+#%%
+plt.figure(figsize=(15,15))
+plt.subplot(3,1,1)
+plt.imshow(fista.get_output().as_array())
+plt.title('FISTA')
+plt.colorbar()
+plt.subplot(3,1,2)
+plt.imshow(pdhg.get_output().as_array())
+plt.title('PDHG')
+plt.colorbar()
+plt.subplot(3,1,3)
+plt.imshow(np.abs(pdhg.get_output().as_array()-fista.get_output().as_array()))
+plt.title('Diff FISTA-PDHG')
+plt.colorbar()
+plt.show()
+
+
diff --git a/Wrappers/Python/wip/Demos/IMAT_Reconstruction/TV_WhiteBeam_reconstruction.py b/Wrappers/Python/wip/Demos/IMAT_Reconstruction/TV_WhiteBeam_reconstruction.py
new file mode 100644
index 0000000..e67bdb1
--- /dev/null
+++ b/Wrappers/Python/wip/Demos/IMAT_Reconstruction/TV_WhiteBeam_reconstruction.py
@@ -0,0 +1,164 @@
+# -*- coding: utf-8 -*-
+# This work is part of the Core Imaging Library developed by
+# Visual Analytics and Imaging System Group of the Science Technology
+# Facilities Council, STFC
+
+# Copyright 2018-2019 Evangelos Papoutsellis and Edoardo Pasca
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from ccpi.framework import ImageGeometry, AcquisitionGeometry, AcquisitionData
+from astropy.io import fits
+import numpy as np
+import numpy
+import matplotlib.pyplot as plt
+
+from ccpi.optimisation.algorithms import PDHG
+
+from ccpi.optimisation.operators import BlockOperator, Gradient
+from ccpi.optimisation.functions import ZeroFunction, KullbackLeibler, L2NormSquared,\
+ MixedL21Norm, BlockFunction
+
+from ccpi.astra.ops import AstraProjectorSimple
+
+
+# load IMAT file
+#filename_sino = '/media/newhd/shared/DataProcessed/IMAT_beamtime_Feb_2019/preprocessed_test_flat/sino/rebin_slice_350/sino_log_rebin_141.fits'
+filename_sino = '/media/newhd/shared/DataProcessed/IMAT_beamtime_Feb_2019/preprocessed_test_flat/sino/rebin_slice_350/sino_log_rebin_564.fits'
+
+sino_handler = fits.open(filename_sino)
+sino_tmp = numpy.array(sino_handler[0].data, dtype=float)
+# reorder sino coordinate: channels, angles, detectors
+sinogram = numpy.rollaxis(sino_tmp, 2)
+sino_handler.close()
+#%%
+# white beam data
+sinogram_wb = sinogram.sum(axis=0)
+
+pixh = sinogram_wb.shape[1] # detectors
+pixv = sinogram_wb.shape[1] # detectors
+
+# WhiteBeam Geometry
+igWB = ImageGeometry(voxel_num_x = pixh, voxel_num_y = pixv)
+
+# Load Golden angles
+with open("golden_angles.txt") as f:
+ angles_string = [line.rstrip() for line in f]
+ angles = numpy.array(angles_string).astype(float)
+agWB = AcquisitionGeometry('parallel', '2D', angles * numpy.pi / 180, pixh)
+op_WB = AstraProjectorSimple(igWB, agWB, 'gpu')
+sinogram_aqdata = AcquisitionData(sinogram_wb, agWB)
+
+# BackProjection
+result_bp = op_WB.adjoint(sinogram_aqdata)
+
+plt.imshow(result_bp.subset(channel=50).array)
+plt.title('BackProjection')
+plt.show()
+
+
+
+#%%
+
+# Regularisation Parameter
+alpha = 2000
+
+# Create operators
+op1 = Gradient(igWB)
+op2 = op_WB
+
+# Create BlockOperator
+operator = BlockOperator(op1, op2, shape=(2,1) )
+
+# Create functions
+
+f1 = alpha * MixedL21Norm()
+f2 = KullbackLeibler(sinogram_aqdata)
+#f2 = L2NormSquared(b = sinogram_aqdata)
+f = BlockFunction(f1, f2)
+
+g = ZeroFunction()
+
+diag_precon = False
+
+if diag_precon:
+
+ def tau_sigma_precond(operator):
+
+ tau = 1/operator.sum_abs_row()
+ sigma = 1/ operator.sum_abs_col()
+
+ return tau, sigma
+
+ tau, sigma = tau_sigma_precond(operator)
+
+else:
+ # Compute operator Norm
+ normK = operator.norm()
+ print ("normK", normK)
+ # Primal & dual stepsizes
+ sigma = 0.1
+ tau = 1/(sigma*normK**2)
+
+#%%
+
+
+## Primal & dual stepsizes
+#sigma = 0.1
+#tau = 1/(sigma*normK**2)
+#
+#
+## Setup and run the PDHG algorithm
+pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma, memopt=True)
+pdhg.max_iteration = 10000
+pdhg.update_objective_interval = 500
+
+def circ_mask(h, w, center=None, radius=None):
+
+ if center is None: # use the middle of the image
+ center = [int(w/2), int(h/2)]
+ if radius is None: # use the smallest distance between the center and image walls
+ radius = min(center[0], center[1], w-center[0], h-center[1])
+
+ Y, X = numpy.ogrid[:h, :w]
+ dist_from_center = numpy.sqrt((X - center[0])**2 + (Y-center[1])**2)
+
+ mask = dist_from_center <= radius
+ return mask
+
+def show_result(niter, objective, solution):
+
+ mask = circ_mask(pixh, pixv, center=None, radius = 220) # 55 with 141,
+ plt.imshow(solution.as_array() * mask)
+ plt.colorbar()
+ plt.title("Iter: {}".format(niter))
+ plt.show()
+
+
+ print( "{:04}/{:04} {:<5} {:.4f} {:<5} {:.4f} {:<5} {:.4f}".\
+ format(niter, pdhg.max_iteration,'', \
+ objective[0],'',\
+ objective[1],'',\
+ objective[2]))
+
+pdhg.run(10000, callback = show_result)
+
+#%%
+
+mask = circ_mask(pixh, pixv, center=None, radius = 210) # 55 with 141,
+plt.figure(figsize=(15,15))
+plt.subplot(3,1,1)
+plt.imshow(pdhg.get_output().as_array() * mask)
+plt.title('Ground Truth')
+plt.colorbar()
+plt.show()
diff --git a/Wrappers/Python/wip/Demos/IMAT_Reconstruction/golden_angles.txt b/Wrappers/Python/wip/Demos/IMAT_Reconstruction/golden_angles.txt
new file mode 100644
index 0000000..95ce73a
--- /dev/null
+++ b/Wrappers/Python/wip/Demos/IMAT_Reconstruction/golden_angles.txt
@@ -0,0 +1,186 @@
+0
+0.9045
+1.809
+2.368
+3.2725
+4.736
+5.6405
+6.1995
+7.104
+8.5675
+9.472
+10.9356
+11.8401
+12.3991
+13.3036
+14.7671
+15.6716
+16.2306
+17.1351
+18.0396
+18.5986
+19.5031
+20.9666
+21.8711
+22.4301
+23.3346
+24.7981
+25.7026
+27.1661
+28.0706
+28.6297
+29.5342
+30.9977
+31.9022
+32.4612
+33.3657
+34.8292
+35.7337
+37.1972
+38.1017
+38.6607
+39.5652
+41.0287
+41.9332
+42.4922
+43.3967
+44.3012
+44.8602
+45.7647
+47.2283
+48.1328
+48.6918
+49.5963
+51.0598
+51.9643
+53.4278
+54.3323
+54.8913
+55.7958
+57.2593
+58.1638
+58.7228
+59.6273
+60.5318
+61.0908
+61.9953
+63.4588
+64.3633
+64.9224
+65.8269
+67.2904
+68.1949
+69.6584
+70.5629
+71.1219
+72.0264
+73.4899
+74.3944
+74.9534
+75.8579
+77.3214
+78.2259
+79.6894
+80.5939
+81.1529
+82.0574
+83.521
+84.4255
+84.9845
+85.889
+86.7935
+87.3525
+88.257
+89.7205
+90.625
+91.184
+92.0885
+93.552
+94.4565
+95.92
+96.8245
+97.3835
+98.288
+99.7516
+100.656
+101.215
+102.12
+103.583
+104.488
+105.951
+106.856
+107.415
+108.319
+109.783
+110.687
+111.246
+112.151
+113.055
+113.614
+114.519
+115.982
+116.887
+117.446
+118.35
+119.814
+120.718
+122.182
+123.086
+123.645
+124.55
+126.013
+126.918
+127.477
+128.381
+129.286
+129.845
+130.749
+132.213
+133.117
+133.676
+134.581
+136.044
+136.949
+138.412
+139.317
+139.876
+140.78
+142.244
+143.148
+143.707
+144.612
+146.075
+146.98
+148.443
+149.348
+149.907
+150.811
+152.275
+153.179
+153.738
+154.643
+155.547
+156.106
+157.011
+158.474
+159.379
+159.938
+160.842
+162.306
+163.21
+164.674
+165.578
+166.137
+167.042
+168.505
+169.41
+169.969
+170.873
+172.337
+173.242
+174.705
+175.609
+176.168
+177.073
+178.536
+179.441
diff --git a/Wrappers/Python/wip/Demos/LeastSq_CGLS_FISTA_PDHG.py b/Wrappers/Python/wip/Demos/LeastSq_CGLS_FISTA_PDHG.py
new file mode 100644
index 0000000..97c71ba
--- /dev/null
+++ b/Wrappers/Python/wip/Demos/LeastSq_CGLS_FISTA_PDHG.py
@@ -0,0 +1,154 @@
+# -*- coding: utf-8 -*-
+# This work is part of the Core Imaging Library developed by
+# Visual Analytics and Imaging System Group of the Science Technology
+# Facilities Council, STFC
+
+# Copyright 2018-2019 Evangelos Papoutsellis and Edoardo Pasca
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from ccpi.framework import ImageData, ImageGeometry, AcquisitionGeometry
+
+import numpy as np
+import numpy
+import matplotlib.pyplot as plt
+
+from ccpi.optimisation.algorithms import PDHG, CGLS, FISTA
+
+from ccpi.optimisation.functions import ZeroFunction, L2NormSquared, FunctionOperatorComposition
+from ccpi.astra.ops import AstraProjectorSimple
+
+#%%
+
+N = 68
+x = np.zeros((N,N))
+x[round(N/4):round(3*N/4),round(N/4):round(3*N/4)] = 0.5
+x[round(N/8):round(7*N/8),round(3*N/8):round(5*N/8)] = 1
+
+data = ImageData(x)
+ig = ImageGeometry(voxel_num_x = N, voxel_num_y = N)
+
+detectors = N
+angles = np.linspace(0, np.pi, N, dtype=np.float32)
+
+ag = AcquisitionGeometry('parallel','2D',angles, detectors)
+Aop = AstraProjectorSimple(ig, ag, 'cpu')
+sin = Aop.direct(data)
+
+noisy_data = sin
+
+
+#%%
+###############################################################################
+## Setup and run the CGLS algorithm
+
+x_init = ig.allocate()
+cgls = CGLS(x_init=x_init, operator=Aop, data=noisy_data)
+cgls.max_iteration = 500
+cgls.update_objective_interval = 50
+cgls.run(500, verbose=True)
+
+#%%
+plt.imshow(cgls.get_output().as_array())
+#%%
+###############################################################################
+## Setup and run the PDHG algorithm
+
+operator = Aop
+f = L2NormSquared(b = noisy_data)
+g = ZeroFunction()
+
+## Compute operator Norm
+normK = operator.norm()
+
+## Primal & dual stepsizes
+sigma = 0.1
+tau = 1/(sigma*normK**2)
+
+pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma, memopt=True)
+pdhg.max_iteration = 2000
+pdhg.update_objective_interval = 50
+pdhg.run(2000)
+
+
+#%%
+###############################################################################
+## Setup and run the FISTA algorithm
+
+fidelity = FunctionOperatorComposition(L2NormSquared(b=noisy_data), Aop)
+regularizer = ZeroFunction()
+
+## Setup and run the FISTA algorithm
+opt = {'memopt':True}
+fista = FISTA(x_init=x_init , f=fidelity, g=regularizer, opt=opt)
+fista.max_iteration = 2000
+fista.update_objective_interval = 200
+fista.run(2000, verbose=True)
+
+#%% Show results
+
+diff1 = pdhg.get_output() - cgls.get_output()
+diff2 = fista.get_output() - cgls.get_output()
+
+print( diff1.norm())
+print( diff2.norm())
+
+plt.figure(figsize=(10,10))
+plt.subplot(2,3,1)
+plt.imshow(cgls.get_output().as_array())
+plt.title('CGLS reconstruction')
+plt.subplot(2,3,2)
+plt.imshow(pdhg.get_output().as_array())
+plt.title('PDHG reconstruction')
+plt.subplot(2,3,3)
+plt.imshow(fista.get_output().as_array())
+plt.title('FISTA reconstruction')
+plt.subplot(2,3,4)
+plt.imshow(diff1.abs().as_array())
+plt.title('Diff PDHG vs CGLS')
+plt.colorbar()
+plt.subplot(2,3,5)
+plt.imshow(diff2.abs().as_array())
+plt.title('Diff FISTA vs CGLS')
+plt.colorbar()
+plt.show()
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+#
+#
+#
+#
+#
+#
+#
+#
diff --git a/Wrappers/Python/wip/Demos/PDHG_TGV_Denoising_SaltPepper.py b/Wrappers/Python/wip/Demos/PDHG_TGV_Denoising_SaltPepper.py
new file mode 100644
index 0000000..7b65c31
--- /dev/null
+++ b/Wrappers/Python/wip/Demos/PDHG_TGV_Denoising_SaltPepper.py
@@ -0,0 +1,194 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Created on Fri Feb 22 14:53:03 2019
+
+@author: evangelos
+"""
+
+from ccpi.framework import ImageData, ImageGeometry
+
+import numpy as np
+import numpy
+import matplotlib.pyplot as plt
+
+from ccpi.optimisation.algorithms import PDHG
+
+from ccpi.optimisation.operators import BlockOperator, Identity, \
+ Gradient, SymmetrizedGradient, ZeroOperator
+from ccpi.optimisation.functions import ZeroFunction, L1Norm, \
+ MixedL21Norm, BlockFunction
+
+from skimage.util import random_noise
+
+# Create phantom for TGV SaltPepper denoising
+
+N = 100
+
+data = np.zeros((N,N))
+
+x1 = np.linspace(0, int(N/2), N)
+x2 = np.linspace(int(N/2), 0., N)
+xv, yv = np.meshgrid(x1, x2)
+
+xv[int(N/4):int(3*N/4)-1, int(N/4):int(3*N/4)-1] = yv[int(N/4):int(3*N/4)-1, int(N/4):int(3*N/4)-1].T
+
+data = xv
+data = ImageData(data/data.max())
+
+ig = ImageGeometry(voxel_num_x = N, voxel_num_y = N)
+ag = ig
+
+# Create noisy data. Add Gaussian noise
+n1 = random_noise(data.as_array(), mode = 's&p', salt_vs_pepper = 0.9, amount=0.2)
+noisy_data = ImageData(n1)
+
+# Regularisation Parameters
+alpha = 0.8
+beta = numpy.sqrt(2)* alpha
+
+method = '1'
+
+if method == '0':
+
+ # Create operators
+ op11 = Gradient(ig)
+ op12 = Identity(op11.range_geometry())
+
+ op22 = SymmetrizedGradient(op11.domain_geometry())
+ op21 = ZeroOperator(ig, op22.range_geometry())
+
+ op31 = Identity(ig, ag)
+ op32 = ZeroOperator(op22.domain_geometry(), ag)
+
+ operator = BlockOperator(op11, -1*op12, op21, op22, op31, op32, shape=(3,2) )
+
+ f1 = alpha * MixedL21Norm()
+ f2 = beta * MixedL21Norm()
+ f3 = L1Norm(b=noisy_data)
+ f = BlockFunction(f1, f2, f3)
+ g = ZeroFunction()
+
+else:
+
+ # Create operators
+ op11 = Gradient(ig)
+ op12 = Identity(op11.range_geometry())
+ op22 = SymmetrizedGradient(op11.domain_geometry())
+ op21 = ZeroOperator(ig, op22.range_geometry())
+
+ operator = BlockOperator(op11, -1*op12, op21, op22, shape=(2,2) )
+
+ f1 = alpha * MixedL21Norm()
+ f2 = beta * MixedL21Norm()
+
+ f = BlockFunction(f1, f2)
+ g = BlockFunction(L1Norm(b=noisy_data), ZeroFunction())
+
+## Compute operator Norm
+normK = operator.norm()
+#
+# Primal & dual stepsizes
+sigma = 1
+tau = 1/(sigma*normK**2)
+
+
+# Setup and run the PDHG algorithm
+pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma, memopt=True)
+pdhg.max_iteration = 2000
+pdhg.update_objective_interval = 50
+pdhg.run(2000)
+
+#%%
+plt.figure(figsize=(15,15))
+plt.subplot(3,1,1)
+plt.imshow(data.as_array())
+plt.title('Ground Truth')
+plt.colorbar()
+plt.subplot(3,1,2)
+plt.imshow(noisy_data.as_array())
+plt.title('Noisy Data')
+plt.colorbar()
+plt.subplot(3,1,3)
+plt.imshow(pdhg.get_output()[0].as_array())
+plt.title('TGV Reconstruction')
+plt.colorbar()
+plt.show()
+##
+plt.plot(np.linspace(0,N,N), data.as_array()[int(N/2),:], label = 'GTruth')
+plt.plot(np.linspace(0,N,N), pdhg.get_output()[0].as_array()[int(N/2),:], label = 'TV reconstruction')
+plt.legend()
+plt.title('Middle Line Profiles')
+plt.show()
+
+
+#%% Check with CVX solution
+
+from ccpi.optimisation.operators import SparseFiniteDiff
+
+try:
+ from cvxpy import *
+ cvx_not_installable = True
+except ImportError:
+ cvx_not_installable = False
+
+if cvx_not_installable:
+
+ u = Variable(ig.shape)
+ w1 = Variable((N, N))
+ w2 = Variable((N, N))
+
+ # create TGV regulariser
+ DY = SparseFiniteDiff(ig, direction=0, bnd_cond='Neumann')
+ DX = SparseFiniteDiff(ig, direction=1, bnd_cond='Neumann')
+
+ regulariser = alpha * sum(norm(vstack([DX.matrix() * vec(u) - vec(w1), \
+ DY.matrix() * vec(u) - vec(w2)]), 2, axis = 0)) + \
+ beta * sum(norm(vstack([ DX.matrix().transpose() * vec(w1), DY.matrix().transpose() * vec(w2), \
+ 0.5 * ( DX.matrix().transpose() * vec(w2) + DY.matrix().transpose() * vec(w1) ), \
+ 0.5 * ( DX.matrix().transpose() * vec(w2) + DY.matrix().transpose() * vec(w1) ) ]), 2, axis = 0 ) )
+
+ constraints = []
+ fidelity = pnorm(u - noisy_data.as_array(),1)
+ solver = MOSEK
+
+ # choose solver
+ if 'MOSEK' in installed_solvers():
+ solver = MOSEK
+ else:
+ solver = SCS
+
+ obj = Minimize( regulariser + fidelity)
+ prob = Problem(obj)
+ result = prob.solve(verbose = True, solver = solver)
+
+ diff_cvx = numpy.abs( pdhg.get_output()[0].as_array() - u.value )
+
+ plt.figure(figsize=(15,15))
+ plt.subplot(3,1,1)
+ plt.imshow(pdhg.get_output()[0].as_array())
+ plt.title('PDHG solution')
+ plt.colorbar()
+ plt.subplot(3,1,2)
+ plt.imshow(u.value)
+ plt.title('CVX solution')
+ plt.colorbar()
+ plt.subplot(3,1,3)
+ plt.imshow(diff_cvx)
+ plt.title('Difference')
+ plt.colorbar()
+ plt.show()
+
+ plt.plot(np.linspace(0,N,N), pdhg.get_output()[0].as_array()[int(N/2),:], label = 'PDHG')
+ plt.plot(np.linspace(0,N,N), u.value[int(N/2),:], label = 'CVX')
+ plt.legend()
+ plt.title('Middle Line Profiles')
+ plt.show()
+
+ print('Primal Objective (CVX) {} '.format(obj.value))
+ print('Primal Objective (PDHG) {} '.format(pdhg.objective[-1][0]))
+
+
+
+
+
diff --git a/Wrappers/Python/wip/Demos/PDHG_TV_Denoising_Gaussian_DiagPrecond.py b/Wrappers/Python/wip/Demos/PDHG_TV_Denoising_Gaussian_DiagPrecond.py
new file mode 100644
index 0000000..d65478c
--- /dev/null
+++ b/Wrappers/Python/wip/Demos/PDHG_TV_Denoising_Gaussian_DiagPrecond.py
@@ -0,0 +1,208 @@
+# -*- coding: utf-8 -*-
+# This work is part of the Core Imaging Library developed by
+# Visual Analytics and Imaging System Group of the Science Technology
+# Facilities Council, STFC
+
+# Copyright 2018-2019 Evangelos Papoutsellis and Edoardo Pasca
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+"""
+Total Variation Denoising using PDHG algorithm
+
+Problem: min_x \alpha * ||\nabla x||_{1} + || x - g ||_{2}^{2}
+
+ \nabla: Gradient operator
+ g: Noisy Data with Gaussian Noise
+ \alpha: Regularization parameter
+
+"""
+
+from ccpi.framework import ImageData, ImageGeometry
+
+import numpy as np
+import numpy
+import matplotlib.pyplot as plt
+
+from ccpi.optimisation.algorithms import PDHG
+
+from ccpi.optimisation.operators import BlockOperator, Identity, Gradient
+from ccpi.optimisation.functions import ZeroFunction, L2NormSquared, \
+ MixedL21Norm, BlockFunction
+
+
+# Create phantom for TV Gaussian denoising
+N = 400
+
+data = np.zeros((N,N))
+data[round(N/4):round(3*N/4),round(N/4):round(3*N/4)] = 0.5
+data[round(N/8):round(7*N/8),round(3*N/8):round(5*N/8)] = 1
+data = ImageData(data)
+ig = ImageGeometry(voxel_num_x = N, voxel_num_y = N)
+ag = ig
+
+# Create noisy data. Add Gaussian noise
+np.random.seed(10)
+noisy_data = ImageData( data.as_array() + np.random.normal(0, 0.05, size=ig.shape) )
+
+# Regularisation Parameter
+alpha = 2
+
+method = '1'
+
+if method == '0':
+
+ # Create operators
+ op1 = Gradient(ig)
+ op2 = Identity(ig, ag)
+
+ # Create BlockOperator
+ operator = BlockOperator(op1, op2, shape=(2,1) )
+
+ # Create functions
+
+ f1 = alpha * MixedL21Norm()
+ f2 = 0.5 * L2NormSquared(b = noisy_data)
+ f = BlockFunction(f1, f2)
+
+ g = ZeroFunction()
+
+else:
+
+ # Without the "Block Framework"
+ operator = Gradient(ig)
+ f = alpha * MixedL21Norm()
+ g = 0.5 * L2NormSquared(b = noisy_data)
+
+
+diag_precon = False
+
+
+if diag_precon:
+
+ def tau_sigma_precond(operator):
+
+ tau = 1/operator.sum_abs_col()
+ sigma = 1/operator.sum_abs_row()
+
+ sigma[0].as_array()[sigma[0].as_array()==np.inf]=0
+ sigma[1].as_array()[sigma[1].as_array()==np.inf]=0
+
+ return tau, sigma
+
+ tau, sigma = tau_sigma_precond(operator)
+
+else:
+ # Compute operator Norm
+ normK = operator.norm()
+
+ # Primal & dual stepsizes
+ sigma = 1
+ tau = 1/(sigma*normK**2)
+
+
+# Setup and run the PDHG algorithm
+pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma, memopt=True)
+pdhg.max_iteration = 3000
+pdhg.update_objective_interval = 200
+pdhg.run(3000, verbose=False)
+
+#%%
+plt.figure(figsize=(15,15))
+plt.subplot(3,1,1)
+plt.imshow(data.as_array())
+plt.title('Ground Truth')
+plt.colorbar()
+plt.subplot(3,1,2)
+plt.imshow(noisy_data.as_array())
+plt.title('Noisy Data')
+plt.colorbar()
+plt.subplot(3,1,3)
+plt.imshow(pdhg.get_output().as_array())
+plt.title('TV Reconstruction')
+plt.colorbar()
+plt.show()
+##
+plt.plot(np.linspace(0,N,N), data.as_array()[int(N/2),:], label = 'GTruth')
+plt.plot(np.linspace(0,N,N), pdhg.get_output().as_array()[int(N/2),:], label = 'TV reconstruction')
+plt.legend()
+plt.title('Middle Line Profiles')
+plt.show()
+
+
+#%% Check with CVX solution
+
+from ccpi.optimisation.operators import SparseFiniteDiff
+
+try:
+ from cvxpy import *
+ cvx_not_installable = True
+except ImportError:
+ cvx_not_installable = False
+
+
+if cvx_not_installable:
+
+ ##Construct problem
+ u = Variable(ig.shape)
+
+ DY = SparseFiniteDiff(ig, direction=0, bnd_cond='Neumann')
+ DX = SparseFiniteDiff(ig, direction=1, bnd_cond='Neumann')
+
+ # Define Total Variation as a regulariser
+ regulariser = alpha * sum(norm(vstack([DX.matrix() * vec(u), DY.matrix() * vec(u)]), 2, axis = 0))
+ fidelity = 0.5 * sum_squares(u - noisy_data.as_array())
+
+ # choose solver
+ if 'MOSEK' in installed_solvers():
+ solver = MOSEK
+ else:
+ solver = SCS
+
+ obj = Minimize( regulariser + fidelity)
+ prob = Problem(obj)
+ result = prob.solve(verbose = True, solver = MOSEK)
+
+ diff_cvx = numpy.abs( pdhg.get_output().as_array() - u.value )
+
+ plt.figure(figsize=(15,15))
+ plt.subplot(3,1,1)
+ plt.imshow(pdhg.get_output().as_array())
+ plt.title('PDHG solution')
+ plt.colorbar()
+ plt.subplot(3,1,2)
+ plt.imshow(u.value)
+ plt.title('CVX solution')
+ plt.colorbar()
+ plt.subplot(3,1,3)
+ plt.imshow(diff_cvx)
+ plt.title('Difference')
+ plt.colorbar()
+ plt.show()
+
+ plt.plot(np.linspace(0,N,N), pdhg.get_output().as_array()[int(N/2),:], label = 'PDHG')
+ plt.plot(np.linspace(0,N,N), u.value[int(N/2),:], label = 'CVX')
+ plt.plot(np.linspace(0,N,N), data.as_array()[int(N/2),:], label = 'Truth')
+
+ plt.legend()
+ plt.title('Middle Line Profiles')
+ plt.show()
+
+ print('Primal Objective (CVX) {} '.format(obj.value))
+ print('Primal Objective (PDHG) {} '.format(pdhg.objective[-1][0]))
+
+
+
+
+
diff --git a/Wrappers/Python/wip/Demos/PDHG_TV_Tomo2D.py b/Wrappers/Python/wip/Demos/PDHG_TV_Tomo2D.py
new file mode 100644
index 0000000..87d5328
--- /dev/null
+++ b/Wrappers/Python/wip/Demos/PDHG_TV_Tomo2D.py
@@ -0,0 +1,245 @@
+# -*- coding: utf-8 -*-
+# This work is part of the Core Imaging Library developed by
+# Visual Analytics and Imaging System Group of the Science Technology
+# Facilities Council, STFC
+
+# Copyright 2018-2019 Evangelos Papoutsellis and Edoardo Pasca
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from ccpi.framework import ImageData, ImageGeometry, AcquisitionGeometry, AcquisitionData
+
+import numpy as np
+import numpy
+import matplotlib.pyplot as plt
+
+from ccpi.optimisation.algorithms import PDHG
+
+from ccpi.optimisation.operators import BlockOperator, Identity, Gradient
+from ccpi.optimisation.functions import ZeroFunction, KullbackLeibler, \
+ MixedL21Norm, BlockFunction
+
+from ccpi.astra.ops import AstraProjectorSimple
+
+"""
+
+Total Variation Denoising using PDHG algorithm:
+
+ min_{x} max_{y} < K x, y > + g(x) - f^{*}(y)
+
+
+Problem: min_x, x>0 \alpha * ||\nabla x||_{1} + int A x -g log(Ax + \eta)
+
+ \nabla: Gradient operator
+
+ A: Projection Matrix
+ g: Noisy sinogram corrupted with Poisson Noise
+
+ \eta: Background Noise
+ \alpha: Regularization parameter
+
+"""
+
+# Create phantom for TV 2D tomography
+N = 75
+x = np.zeros((N,N))
+x[round(N/4):round(3*N/4),round(N/4):round(3*N/4)] = 0.5
+x[round(N/8):round(7*N/8),round(3*N/8):round(5*N/8)] = 1
+
+data = ImageData(x)
+ig = ImageGeometry(voxel_num_x = N, voxel_num_y = N)
+
+detectors = N
+angles = np.linspace(0, np.pi, N, dtype=np.float32)
+
+ag = AcquisitionGeometry('parallel','2D',angles, detectors)
+Aop = AstraProjectorSimple(ig, ag, 'cpu')
+sin = Aop.direct(data)
+
+# Create noisy data. Apply Poisson noise
+scale = 2
+n1 = scale * np.random.poisson(sin.as_array()/scale)
+noisy_data = AcquisitionData(n1, ag)
+
+# Regularisation Parameter
+alpha = 5
+
+# Create operators
+op1 = Gradient(ig)
+op2 = Aop
+
+# Create BlockOperator
+operator = BlockOperator(op1, op2, shape=(2,1) )
+
+# Create functions
+
+f1 = alpha * MixedL21Norm()
+f2 = KullbackLeibler(noisy_data)
+f = BlockFunction(f1, f2)
+
+g = ZeroFunction()
+
+diag_precon = True
+
+if diag_precon:
+
+ def tau_sigma_precond(operator):
+
+ tau = 1/operator.sum_abs_row()
+ sigma = 1/ operator.sum_abs_col()
+
+ return tau, sigma
+
+ tau, sigma = tau_sigma_precond(operator)
+
+else:
+ # Compute operator Norm
+ normK = operator.norm()
+ # Primal & dual stepsizes
+ sigma = 10
+ tau = 1/(sigma*normK**2)
+
+
+# Setup and run the PDHG algorithm
+pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma, memopt=True)
+pdhg.max_iteration = 2000
+pdhg.update_objective_interval = 50
+pdhg.run(2000)
+
+
+#%%
+plt.figure(figsize=(15,15))
+plt.subplot(3,1,1)
+plt.imshow(data.as_array())
+plt.title('Ground Truth')
+plt.colorbar()
+plt.subplot(3,1,2)
+plt.imshow(noisy_data.as_array())
+plt.title('Noisy Data')
+plt.colorbar()
+plt.subplot(3,1,3)
+plt.imshow(pdhg.get_output().as_array())
+plt.title('TV Reconstruction')
+plt.colorbar()
+plt.show()
+##
+plt.plot(np.linspace(0,N,N), data.as_array()[int(N/2),:], label = 'GTruth')
+plt.plot(np.linspace(0,N,N), pdhg.get_output().as_array()[int(N/2),:], label = 'TV reconstruction')
+plt.legend()
+plt.title('Middle Line Profiles')
+plt.show()
+
+
+#%% Check with CVX solution
+
+from ccpi.optimisation.operators import SparseFiniteDiff
+import astra
+import numpy
+
+try:
+ from cvxpy import *
+ cvx_not_installable = True
+except ImportError:
+ cvx_not_installable = False
+
+
+if cvx_not_installable:
+
+
+ ##Construct problem
+ u = Variable(N*N)
+ #q = Variable()
+
+ DY = SparseFiniteDiff(ig, direction=0, bnd_cond='Neumann')
+ DX = SparseFiniteDiff(ig, direction=1, bnd_cond='Neumann')
+
+ regulariser = alpha * sum(norm(vstack([DX.matrix() * vec(u), DY.matrix() * vec(u)]), 2, axis = 0))
+
+ # create matrix representation for Astra operator
+
+ vol_geom = astra.create_vol_geom(N, N)
+ proj_geom = astra.create_proj_geom('parallel', 1.0, detectors, angles)
+
+ proj_id = astra.create_projector('strip', proj_geom, vol_geom)
+
+ matrix_id = astra.projector.matrix(proj_id)
+
+ ProjMat = astra.matrix.get(matrix_id)
+
+ fidelity = sum( ProjMat * u - noisy_data.as_array().ravel() * log(ProjMat * u))
+ #constraints = [q>= fidelity, u>=0]
+ constraints = [u>=0]
+
+ solver = SCS
+ obj = Minimize( regulariser + fidelity)
+ prob = Problem(obj, constraints)
+ result = prob.solve(verbose = True, solver = solver)
+
+
+##%% Check with CVX solution
+
+from ccpi.optimisation.operators import SparseFiniteDiff
+
+try:
+ from cvxpy import *
+ cvx_not_installable = True
+except ImportError:
+ cvx_not_installable = False
+
+
+if cvx_not_installable:
+
+ ##Construct problem
+ u = Variable(ig.shape)
+
+ DY = SparseFiniteDiff(ig, direction=0, bnd_cond='Neumann')
+ DX = SparseFiniteDiff(ig, direction=1, bnd_cond='Neumann')
+
+ # Define Total Variation as a regulariser
+ regulariser = alpha * sum(norm(vstack([DX.matrix() * vec(u), DY.matrix() * vec(u)]), 2, axis = 0))
+ fidelity = pnorm( u - noisy_data.as_array(),1)
+
+ # choose solver
+ if 'MOSEK' in installed_solvers():
+ solver = MOSEK
+ else:
+ solver = SCS
+
+ obj = Minimize( regulariser + fidelity)
+ prob = Problem(obj)
+ result = prob.solve(verbose = True, solver = solver)
+
+
+ plt.figure(figsize=(15,15))
+ plt.subplot(3,1,1)
+ plt.imshow(pdhg.get_output().as_array())
+ plt.title('PDHG solution')
+ plt.colorbar()
+ plt.subplot(3,1,2)
+ plt.imshow(np.reshape(u.value, (N, N)))
+ plt.title('CVX solution')
+ plt.colorbar()
+ plt.subplot(3,1,3)
+ plt.imshow(diff_cvx)
+ plt.title('Difference')
+ plt.colorbar()
+ plt.show()
+
+ plt.plot(np.linspace(0,N,N), pdhg.get_output().as_array()[int(N/2),:], label = 'PDHG')
+ plt.plot(np.linspace(0,N,N), u.value[int(N/2),:], label = 'CVX')
+ plt.legend()
+ plt.title('Middle Line Profiles')
+ plt.show()
+
+ print('Primal Objective (CVX) {} '.format(obj.value))
+ print('Primal Objective (PDHG) {} '.format(pdhg.objective[-1][0])) \ No newline at end of file
diff --git a/Wrappers/Python/wip/Demos/PDHG_TV_Tomo2D_time.py b/Wrappers/Python/wip/Demos/PDHG_TV_Tomo2D_time.py
new file mode 100644
index 0000000..045458a
--- /dev/null
+++ b/Wrappers/Python/wip/Demos/PDHG_TV_Tomo2D_time.py
@@ -0,0 +1,169 @@
+# -*- coding: utf-8 -*-
+# This work is part of the Core Imaging Library developed by
+# Visual Analytics and Imaging System Group of the Science Technology
+# Facilities Council, STFC
+
+# Copyright 2018-2019 Evangelos Papoutsellis and Edoardo Pasca
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from ccpi.framework import ImageData, ImageGeometry, AcquisitionGeometry, AcquisitionData
+
+import numpy as np
+import numpy
+import matplotlib.pyplot as plt
+
+from ccpi.optimisation.algorithms import PDHG
+
+from ccpi.optimisation.operators import BlockOperator, Gradient
+from ccpi.optimisation.functions import ZeroFunction, KullbackLeibler, \
+ MixedL21Norm, BlockFunction
+
+from ccpi.astra.ops import AstraProjectorMC
+
+import os
+import tomophantom
+from tomophantom import TomoP2D
+
+# Create phantom for TV 2D dynamic tomography
+
+model = 102 # note that the selected model is temporal (2D + time)
+N = 50 # set dimension of the phantom
+# one can specify an exact path to the parameters file
+# path_library2D = '../../../PhantomLibrary/models/Phantom2DLibrary.dat'
+path = os.path.dirname(tomophantom.__file__)
+path_library2D = os.path.join(path, "Phantom2DLibrary.dat")
+#This will generate a N_size x N_size x Time frames phantom (2D + time)
+phantom_2Dt = TomoP2D.ModelTemporal(model, N, path_library2D)
+
+plt.close('all')
+plt.figure(1)
+plt.rcParams.update({'font.size': 21})
+plt.title('{}''{}'.format('2D+t phantom using model no.',model))
+for sl in range(0,np.shape(phantom_2Dt)[0]):
+ im = phantom_2Dt[sl,:,:]
+ plt.imshow(im, vmin=0, vmax=1)
+ plt.pause(.1)
+ plt.draw
+
+
+ig = ImageGeometry(voxel_num_x = N, voxel_num_y = N, channels = np.shape(phantom_2Dt)[0])
+data = ImageData(phantom_2Dt, geometry=ig)
+
+detectors = N
+angles = np.linspace(0,np.pi,N)
+
+ag = AcquisitionGeometry('parallel','2D', angles, detectors, channels = np.shape(phantom_2Dt)[0])
+Aop = AstraProjectorMC(ig, ag, 'gpu')
+sin = Aop.direct(data)
+
+scale = 2
+n1 = scale * np.random.poisson(sin.as_array()/scale)
+noisy_data = AcquisitionData(n1, ag)
+
+tindex = [3, 6, 10]
+
+fig, axes = plt.subplots(nrows=1, ncols=3, figsize=(10, 10))
+plt.subplot(1,3,1)
+plt.imshow(noisy_data.as_array()[tindex[0],:,:])
+plt.axis('off')
+plt.title('Time {}'.format(tindex[0]))
+plt.subplot(1,3,2)
+plt.imshow(noisy_data.as_array()[tindex[1],:,:])
+plt.axis('off')
+plt.title('Time {}'.format(tindex[1]))
+plt.subplot(1,3,3)
+plt.imshow(noisy_data.as_array()[tindex[2],:,:])
+plt.axis('off')
+plt.title('Time {}'.format(tindex[2]))
+
+fig.subplots_adjust(bottom=0.1, top=0.9, left=0.1, right=0.8,
+ wspace=0.02, hspace=0.02)
+
+plt.show()
+
+#%%
+# Regularisation Parameter
+alpha = 5
+
+# Create operators
+#op1 = Gradient(ig)
+op1 = Gradient(ig, correlation='SpaceChannels')
+op2 = Aop
+
+# Create BlockOperator
+operator = BlockOperator(op1, op2, shape=(2,1) )
+
+# Create functions
+
+f1 = alpha * MixedL21Norm()
+f2 = KullbackLeibler(noisy_data)
+f = BlockFunction(f1, f2)
+
+g = ZeroFunction()
+
+# Compute operator Norm
+normK = operator.norm()
+
+# Primal & dual stepsizes
+sigma = 1
+tau = 1/(sigma*normK**2)
+
+
+# Setup and run the PDHG algorithm
+pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma, memopt=True)
+pdhg.max_iteration = 2000
+pdhg.update_objective_interval = 200
+pdhg.run(2000)
+
+
+#%%
+fig, axes = plt.subplots(nrows=2, ncols=3, figsize=(10, 8))
+
+plt.subplot(2,3,1)
+plt.imshow(phantom_2Dt[tindex[0],:,:],vmin=0, vmax=1)
+plt.axis('off')
+plt.title('Time {}'.format(tindex[0]))
+
+plt.subplot(2,3,2)
+plt.imshow(phantom_2Dt[tindex[1],:,:],vmin=0, vmax=1)
+plt.axis('off')
+plt.title('Time {}'.format(tindex[1]))
+
+plt.subplot(2,3,3)
+plt.imshow(phantom_2Dt[tindex[2],:,:],vmin=0, vmax=1)
+plt.axis('off')
+plt.title('Time {}'.format(tindex[2]))
+
+
+plt.subplot(2,3,4)
+plt.imshow(pdhg.get_output().as_array()[tindex[0],:,:])
+plt.axis('off')
+plt.subplot(2,3,5)
+plt.imshow(pdhg.get_output().as_array()[tindex[1],:,:])
+plt.axis('off')
+plt.subplot(2,3,6)
+plt.imshow(pdhg.get_output().as_array()[tindex[2],:,:])
+plt.axis('off')
+im = plt.imshow(pdhg.get_output().as_array()[tindex[0],:,:])
+
+
+fig.subplots_adjust(bottom=0.1, top=0.9, left=0.1, right=0.8,
+ wspace=0.02, hspace=0.02)
+
+cb_ax = fig.add_axes([0.83, 0.1, 0.02, 0.8])
+cbar = fig.colorbar(im, cax=cb_ax)
+
+
+plt.show()
+
diff --git a/Wrappers/Python/wip/Demos/PDHG_Tikhonov_Tomo2D.py b/Wrappers/Python/wip/Demos/PDHG_Tikhonov_Tomo2D.py
new file mode 100644
index 0000000..f17c4fe
--- /dev/null
+++ b/Wrappers/Python/wip/Demos/PDHG_Tikhonov_Tomo2D.py
@@ -0,0 +1,108 @@
+# -*- coding: utf-8 -*-
+# This work is part of the Core Imaging Library developed by
+# Visual Analytics and Imaging System Group of the Science Technology
+# Facilities Council, STFC
+
+# Copyright 2018-2019 Evangelos Papoutsellis and Edoardo Pasca
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from ccpi.framework import ImageData, ImageGeometry, AcquisitionGeometry, AcquisitionData
+
+import numpy as np
+import numpy
+import matplotlib.pyplot as plt
+
+from ccpi.optimisation.algorithms import PDHG
+
+from ccpi.optimisation.operators import BlockOperator, Gradient
+from ccpi.optimisation.functions import ZeroFunction, L2NormSquared, BlockFunction
+from skimage.util import random_noise
+from ccpi.astra.ops import AstraProjectorSimple
+
+# Create phantom for TV 2D tomography
+N = 75
+x = np.zeros((N,N))
+x[round(N/4):round(3*N/4),round(N/4):round(3*N/4)] = 0.5
+x[round(N/8):round(7*N/8),round(3*N/8):round(5*N/8)] = 1
+
+data = ImageData(x)
+ig = ImageGeometry(voxel_num_x = N, voxel_num_y = N)
+
+detectors = N
+angles = np.linspace(0, np.pi, N, dtype=np.float32)
+
+ag = AcquisitionGeometry('parallel','2D',angles, detectors)
+Aop = AstraProjectorSimple(ig, ag, 'gpu')
+sin = Aop.direct(data)
+
+# Create noisy data. Apply Gaussian noise
+
+np.random.seed(10)
+noisy_data = sin + AcquisitionData(np.random.normal(0, 3, sin.shape))
+
+# Regularisation Parameter
+alpha = 500
+
+# Create operators
+op1 = Gradient(ig)
+op2 = Aop
+
+# Create BlockOperator
+operator = BlockOperator(op1, op2, shape=(2,1) )
+
+# Create functions
+
+f1 = alpha * L2NormSquared()
+f2 = 0.5 * L2NormSquared(b=noisy_data)
+f = BlockFunction(f1, f2)
+
+g = ZeroFunction()
+
+# Compute operator Norm
+normK = operator.norm()
+
+# Primal & dual stepsizes
+sigma = 1
+tau = 1/(sigma*normK**2)
+
+
+# Setup and run the PDHG algorithm
+pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma, memopt=True)
+pdhg.max_iteration = 5000
+pdhg.update_objective_interval = 50
+pdhg.run(2000)
+
+#%%
+plt.figure(figsize=(15,15))
+plt.subplot(3,1,1)
+plt.imshow(data.as_array())
+plt.title('Ground Truth')
+plt.colorbar()
+plt.subplot(3,1,2)
+plt.imshow(noisy_data.as_array())
+plt.title('Noisy Data')
+plt.colorbar()
+plt.subplot(3,1,3)
+plt.imshow(pdhg.get_output().as_array())
+plt.title('Tikhonov Reconstruction')
+plt.colorbar()
+plt.show()
+##
+plt.plot(np.linspace(0,N,N), data.as_array()[int(N/2),:], label = 'GTruth')
+plt.plot(np.linspace(0,N,N), pdhg.get_output().as_array()[int(N/2),:], label = 'Tikhonov reconstruction')
+plt.legend()
+plt.title('Middle Line Profiles')
+plt.show()
+
+
diff --git a/Wrappers/Python/wip/Demos/PDHG_vs_CGLS.py b/Wrappers/Python/wip/Demos/PDHG_vs_CGLS.py
new file mode 100644
index 0000000..3155654
--- /dev/null
+++ b/Wrappers/Python/wip/Demos/PDHG_vs_CGLS.py
@@ -0,0 +1,127 @@
+# -*- coding: utf-8 -*-
+# This work is part of the Core Imaging Library developed by
+# Visual Analytics and Imaging System Group of the Science Technology
+# Facilities Council, STFC
+
+# Copyright 2018-2019 Evangelos Papoutsellis and Edoardo Pasca
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from ccpi.framework import ImageData, ImageGeometry, AcquisitionGeometry, AcquisitionData
+
+import numpy as np
+import numpy
+import matplotlib.pyplot as plt
+
+from ccpi.optimisation.algorithms import PDHG, CGLS
+
+from ccpi.optimisation.operators import Gradient
+from ccpi.optimisation.functions import ZeroFunction, L2NormSquared, FunctionOperatorComposition
+from skimage.util import random_noise
+from ccpi.astra.ops import AstraProjectorSimple
+
+#%%
+
+N = 128
+x = np.zeros((N,N))
+x[round(N/4):round(3*N/4),round(N/4):round(3*N/4)] = 0.5
+x[round(N/8):round(7*N/8),round(3*N/8):round(5*N/8)] = 1
+
+data = ImageData(x)
+ig = ImageGeometry(voxel_num_x = N, voxel_num_y = N)
+
+detectors = N
+angles = np.linspace(0, np.pi, N, dtype=np.float32)
+
+ag = AcquisitionGeometry('parallel','2D',angles, detectors)
+Aop = AstraProjectorSimple(ig, ag, 'cpu')
+sin = Aop.direct(data)
+
+noisy_data = sin
+
+x_init = ig.allocate()
+
+## Setup and run the CGLS algorithm
+cgls = CGLS(x_init=x_init, operator=Aop, data=noisy_data)
+cgls.max_iteration = 500
+cgls.update_objective_interval = 50
+cgls.run(500, verbose=True)
+
+# Create BlockOperator
+operator = Aop
+f = 0.5 * L2NormSquared(b = noisy_data)
+g = ZeroFunction()
+
+## Compute operator Norm
+normK = operator.norm()
+
+## Primal & dual stepsizes
+sigma = 0.1
+tau = 1/(sigma*normK**2)
+#
+#
+## Setup and run the PDHG algorithm
+pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma, memopt=True)
+pdhg.max_iteration = 2000
+pdhg.update_objective_interval = 50
+pdhg.run(2000)
+
+#%%
+
+diff = pdhg.get_output() - cgls.get_output()
+print( diff.norm())
+#
+plt.figure(figsize=(15,15))
+plt.subplot(3,1,1)
+plt.imshow(pdhg.get_output().as_array())
+plt.title('PDHG reconstruction')
+plt.colorbar()
+plt.subplot(3,1,2)
+plt.imshow(cgls.get_output().as_array())
+plt.title('CGLS reconstruction')
+plt.colorbar()
+plt.subplot(3,1,3)
+plt.imshow(diff.abs().as_array())
+plt.title('Difference reconstruction')
+plt.colorbar()
+plt.show()
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+#
+#
+#
+#
+#
+#
+#
+#
diff --git a/Wrappers/Python/wip/Demos/check_blockOperator_sum_row_cols.py b/Wrappers/Python/wip/Demos/check_blockOperator_sum_row_cols.py
new file mode 100644
index 0000000..bdb2c38
--- /dev/null
+++ b/Wrappers/Python/wip/Demos/check_blockOperator_sum_row_cols.py
@@ -0,0 +1,89 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Created on Fri May 3 13:10:09 2019
+
+@author: evangelos
+"""
+
+from ccpi.optimisation.operators import FiniteDiff, SparseFiniteDiff, BlockOperator, Gradient
+from ccpi.framework import ImageGeometry, AcquisitionGeometry, BlockDataContainer, ImageData
+from ccpi.astra.ops import AstraProjectorSimple
+
+from scipy import sparse
+import numpy as np
+
+N = 3
+ig = ImageGeometry(voxel_num_x = N, voxel_num_y = N)
+u = ig.allocate('random_int')
+
+# Compare FiniteDiff with SparseFiniteDiff
+
+DY = FiniteDiff(ig, direction = 0, bnd_cond = 'Neumann')
+DX = FiniteDiff(ig, direction = 1, bnd_cond = 'Neumann')
+
+DXu = DX.direct(u)
+DYu = DY.direct(u)
+
+DX_sparse = SparseFiniteDiff(ig, direction=1, bnd_cond='Neumann')
+DY_sparse = SparseFiniteDiff(ig, direction=0, bnd_cond='Neumann')
+
+DXu_sparse = DX_sparse.direct(u)
+DYu_sparse = DY_sparse.direct(u)
+
+#np.testing.assert_array_almost_equal(DYu.as_array(), DYu_sparse.as_array(), decimal=4)
+#np.testing.assert_array_almost_equal(DXu.as_array(), DXu_sparse.as_array(), decimal=4)
+
+#%% Tau/ Sigma
+
+A1 = DY_sparse.matrix()
+A2 = DX_sparse.matrix()
+A3 = sparse.eye(np.prod(ig.shape))
+
+sum_rows1 = np.array(np.sum(abs(A1), axis=1))
+sum_rows2 = np.array(np.sum(abs(A2), axis=1))
+sum_rows3 = np.array(np.sum(abs(A3), axis=1))
+
+sum_cols1 = np.array(np.sum(abs(A1), axis=0))
+sum_cols2 = np.array(np.sum(abs(A2), axis=0))
+sum_cols3 = np.array(np.sum(abs(A2), axis=0))
+
+# Check if Grad sum row/cols is OK
+Grad = Gradient(ig)
+
+Sum_Block_row = Grad.sum_abs_row()
+Sum_Block_col = Grad.sum_abs_col()
+
+tmp1 = BlockDataContainer( ImageData(np.reshape(sum_rows1, ig.shape, order='F')),\
+ ImageData(np.reshape(sum_rows2, ig.shape, order='F')))
+
+
+#np.testing.assert_array_almost_equal(tmp1[0].as_array(), Sum_Block_row[0].as_array(), decimal=4)
+#np.testing.assert_array_almost_equal(tmp1[1].as_array(), Sum_Block_row[1].as_array(), decimal=4)
+
+tmp2 = ImageData(np.reshape(sum_cols1 + sum_cols2, ig.shape, order='F'))
+
+#np.testing.assert_array_almost_equal(tmp2.as_array(), Sum_Block_col.as_array(), decimal=4)
+
+
+#%% BlockOperator with Gradient, Identity
+
+Id = Identity(ig)
+Block_GrId = BlockOperator(Grad, Id, shape=(2,1))
+
+
+Sum_Block_GrId_row = Block_GrId.sum_abs_row()
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Wrappers/Python/wip/Demos/check_precond.py b/Wrappers/Python/wip/Demos/check_precond.py
new file mode 100644
index 0000000..8cf95fa
--- /dev/null
+++ b/Wrappers/Python/wip/Demos/check_precond.py
@@ -0,0 +1,182 @@
+# -*- coding: utf-8 -*-
+# This work is part of the Core Imaging Library developed by
+# Visual Analytics and Imaging System Group of the Science Technology
+# Facilities Council, STFC
+
+# Copyright 2018-2019 Evangelos Papoutsellis and Edoardo Pasca
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from ccpi.framework import ImageData, ImageGeometry, AcquisitionGeometry
+
+import numpy as np
+import numpy
+import matplotlib.pyplot as plt
+
+from ccpi.optimisation.algorithms import PDHG
+
+from ccpi.optimisation.operators import BlockOperator, Gradient
+from ccpi.optimisation.functions import ZeroFunction, L2NormSquared, \
+ MixedL21Norm, BlockFunction
+
+from ccpi.astra.ops import AstraProjectorSimple
+
+# Create phantom for TV 2D tomography
+N = 75
+x = np.zeros((N,N))
+x[round(N/4):round(3*N/4),round(N/4):round(3*N/4)] = 0.5
+x[round(N/8):round(7*N/8),round(3*N/8):round(5*N/8)] = 1
+
+data = ImageData(x)
+ig = ImageGeometry(voxel_num_x = N, voxel_num_y = N)
+
+detectors = N
+angles = np.linspace(0, np.pi, N, dtype=np.float32)
+
+ag = AcquisitionGeometry('parallel','2D',angles, detectors)
+Aop = AstraProjectorSimple(ig, ag, 'cpu')
+sin = Aop.direct(data)
+
+# Create noisy data
+np.random.seed(10)
+n1 = np.random.random(sin.shape)
+noisy_data = sin + ImageData(5*n1)
+
+#%%
+
+# Regularisation Parameter
+alpha = 50
+
+# Create operators
+op1 = Gradient(ig)
+op2 = Aop
+
+# Create BlockOperator
+operator = BlockOperator(op1, op2, shape=(2,1) )
+
+
+
+# Create functions
+
+f1 = alpha * MixedL21Norm()
+f2 = L2NormSquared(b=noisy_data)
+f = BlockFunction(f1, f2)
+
+g = ZeroFunction()
+
+diag_precon = True
+
+if diag_precon:
+
+ def tau_sigma_precond(operator):
+
+ tau = 1/operator.sum_abs_row()
+ sigma = 1/ operator.sum_abs_col()
+
+ return tau, sigma
+
+ tau, sigma = tau_sigma_precond(operator)
+
+else:
+ # Compute operator Norm
+ normK = operator.norm()
+ # Primal & dual stepsizes
+ sigma = 10
+ tau = 1/(sigma*normK**2)
+
+
+# Setup and run the PDHG algorithm
+pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma, memopt=True)
+pdhg.max_iteration = 1000
+pdhg.update_objective_interval = 200
+pdhg.run(1000)
+
+#%% Check with CVX solution
+
+from ccpi.optimisation.operators import SparseFiniteDiff
+import astra
+import numpy
+
+try:
+ from cvxpy import *
+ cvx_not_installable = True
+except ImportError:
+ cvx_not_installable = False
+
+
+if cvx_not_installable:
+
+ ##Construct problem
+ u = Variable(N*N)
+
+ DY = SparseFiniteDiff(ig, direction=0, bnd_cond='Neumann')
+ DX = SparseFiniteDiff(ig, direction=1, bnd_cond='Neumann')
+
+ regulariser = alpha * sum(norm(vstack([DX.matrix() * vec(u), DY.matrix() * vec(u)]), 2, axis = 0))
+
+ # create matrix representation for Astra operator
+
+ vol_geom = astra.create_vol_geom(N, N)
+ proj_geom = astra.create_proj_geom('parallel', 1.0, detectors, angles)
+
+ proj_id = astra.create_projector('line', proj_geom, vol_geom)
+
+ matrix_id = astra.projector.matrix(proj_id)
+
+ ProjMat = astra.matrix.get(matrix_id)
+
+ fidelity = sum_squares( ProjMat * u - noisy_data.as_array().ravel())
+ #constraints = [q>=fidelity]
+# constraints = [u>=0]
+
+ solver = MOSEK
+ obj = Minimize( regulariser + fidelity)
+ prob = Problem(obj)
+ result = prob.solve(verbose = True, solver = solver)
+
+
+#%%
+
+plt.figure(figsize=(15,15))
+plt.subplot(2,2,1)
+plt.imshow(data.as_array())
+plt.title('Ground Truth')
+
+plt.subplot(2,2,2)
+plt.imshow(noisy_data.as_array())
+plt.title('Noisy Data')
+
+plt.subplot(2,2,3)
+plt.imshow(pdhg.get_output().as_array())
+plt.title('PDHG Reconstruction')
+
+plt.subplot(2,2,4)
+plt.imshow(np.reshape(u.value, ig.shape))
+plt.title('CVX Reconstruction')
+
+plt.show()
+
+#%%
+plt.plot(np.linspace(0,N,N), pdhg.get_output().as_array()[int(N/2),:], label = 'PDHG')
+plt.plot(np.linspace(0,N,N), np.reshape(u.value, ig.shape)[int(N/2),:], label = 'CVX')
+plt.legend()
+plt.title('Middle Line Profiles')
+plt.show()
+
+
+
+
+
+
+
+
diff --git a/Wrappers/Python/wip/compare_CGLS_algos.py b/Wrappers/Python/wip/compare_CGLS_algos.py
new file mode 100644
index 0000000..52f3f31
--- /dev/null
+++ b/Wrappers/Python/wip/compare_CGLS_algos.py
@@ -0,0 +1,133 @@
+# This demo illustrates how to use the SIRT algorithm without and with
+# nonnegativity and box constraints. The ASTRA 2D projectors are used.
+
+# First make all imports
+from ccpi.framework import ImageData, ImageGeometry, AcquisitionGeometry, \
+ AcquisitionData
+from ccpi.optimisation.algs import FISTA, FBPD, CGLS, SIRT
+from ccpi.astra.operators import AstraProjectorSimple
+
+from ccpi.optimisation.algorithms import CGLS as CGLSalg
+
+import numpy as np
+import matplotlib.pyplot as plt
+
+from ccpi.optimisation.functions import Norm2Sq
+
+# Choose either a parallel-beam (1=parallel2D) or fan-beam (2=cone2D) test case
+test_case = 1
+
+# Set up phantom size NxN by creating ImageGeometry, initialising the
+# ImageData object with this geometry and empty array and finally put some
+# data into its array, and display as image.
+N = 128
+ig = ImageGeometry(voxel_num_x=N,voxel_num_y=N)
+Phantom = ImageData(geometry=ig)
+
+x = Phantom.as_array()
+x[round(N/4):round(3*N/4),round(N/4):round(3*N/4)] = 0.5
+x[round(N/8):round(7*N/8),round(3*N/8):round(5*N/8)] = 1
+
+#plt.figure()
+#plt.imshow(x)
+#plt.title('Phantom image')
+#plt.show()
+
+# Set up AcquisitionGeometry object to hold the parameters of the measurement
+# setup geometry: # Number of angles, the actual angles from 0 to
+# pi for parallel beam and 0 to 2pi for fanbeam, set the width of a detector
+# pixel relative to an object pixel, the number of detector pixels, and the
+# source-origin and origin-detector distance (here the origin-detector distance
+# set to 0 to simulate a "virtual detector" with same detector pixel size as
+# object pixel size).
+angles_num = 20
+det_w = 1.0
+det_num = N
+SourceOrig = 200
+OrigDetec = 0
+
+if test_case==1:
+ angles = np.linspace(0,np.pi,angles_num,endpoint=False)
+ ag = AcquisitionGeometry('parallel',
+ '2D',
+ angles,
+ det_num,det_w)
+elif test_case==2:
+ angles = np.linspace(0,2*np.pi,angles_num,endpoint=False)
+ ag = AcquisitionGeometry('cone',
+ '2D',
+ angles,
+ det_num,
+ det_w,
+ dist_source_center=SourceOrig,
+ dist_center_detector=OrigDetec)
+else:
+ NotImplemented
+
+# Set up Operator object combining the ImageGeometry and AcquisitionGeometry
+# wrapping calls to ASTRA as well as specifying whether to use CPU or GPU.
+Aop = AstraProjectorSimple(ig, ag, 'cpu')
+
+# Forward and backprojection are available as methods direct and adjoint. Here
+# generate test data b and do simple backprojection to obtain z.
+b = Aop.direct(Phantom)
+z = Aop.adjoint(b)
+
+#plt.figure()
+#plt.imshow(b.array)
+#plt.title('Simulated data')
+#plt.show()
+
+#plt.figure()
+#plt.imshow(z.array)
+#plt.title('Backprojected data')
+#plt.show()
+
+# Using the test data b, different reconstruction methods can now be set up as
+# demonstrated in the rest of this file. In general all methods need an initial
+# guess and some algorithm options to be set:
+x_init = ImageData(np.zeros(x.shape),geometry=ig)
+opt = {'tol': 1e-4, 'iter': 7}
+
+# First a CGLS reconstruction using the function version of CGLS can be done:
+x_CGLS, it_CGLS, timing_CGLS, criter_CGLS = CGLS(x_init, Aop, b, opt)
+
+#plt.figure()
+#plt.imshow(x_CGLS.array)
+#plt.title('CGLS')
+#plt.colorbar()
+#plt.show()
+
+#plt.figure()
+#plt.semilogy(criter_CGLS)
+#plt.title('CGLS criterion')
+#plt.show()
+
+f = Norm2Sq(Aop, b, c=1.)
+
+def callback(it, objective, solution):
+ print (objective, f(solution))
+
+# Now CLGS using the algorithm class
+CGLS_alg = CGLSalg()
+CGLS_alg.set_up(x_init, Aop, b )
+CGLS_alg.max_iteration = 500
+CGLS_alg.update_objective_interval = 10
+CGLS_alg.run(300, callback=callback)
+x_CGLS_alg = CGLS_alg.get_output()
+
+plt.figure()
+plt.imshow(x_CGLS_alg.as_array())
+plt.title('CGLS ALG')
+plt.colorbar()
+plt.show()
+
+plt.figure()
+plt.semilogy(CGLS_alg.objective)
+plt.title('CGLS criterion')
+plt.show()
+
+print(criter_CGLS)
+print(CGLS_alg.objective)
+
+print((x_CGLS - x_CGLS_alg).norm()) \ No newline at end of file
diff --git a/Wrappers/Python/wip/demo_SIRT.py b/Wrappers/Python/wip/demo_SIRT.py
new file mode 100644
index 0000000..5a85d41
--- /dev/null
+++ b/Wrappers/Python/wip/demo_SIRT.py
@@ -0,0 +1,205 @@
+# This demo illustrates how to use the SIRT algorithm without and with
+# nonnegativity and box constraints. The ASTRA 2D projectors are used.
+
+# First make all imports
+from ccpi.framework import ImageData, ImageGeometry, AcquisitionGeometry, \
+ AcquisitionData
+from ccpi.optimisation.functions import IndicatorBox
+from ccpi.astra.ops import AstraProjectorSimple
+from ccpi.optimisation.algorithms import SIRT, CGLS
+
+import numpy as np
+import matplotlib.pyplot as plt
+
+# Choose either a parallel-beam (1=parallel2D) or fan-beam (2=cone2D) test case
+test_case = 1
+
+# Set up phantom size NxN by creating ImageGeometry, initialising the
+# ImageData object with this geometry and empty array and finally put some
+# data into its array, and display as image.
+N = 128
+ig = ImageGeometry(voxel_num_x=N,voxel_num_y=N)
+Phantom = ImageData(geometry=ig)
+
+x = Phantom.as_array()
+x[round(N/4):round(3*N/4),round(N/4):round(3*N/4)] = 0.5
+x[round(N/8):round(7*N/8),round(3*N/8):round(5*N/8)] = 1
+
+plt.figure()
+plt.imshow(x)
+plt.title('Phantom image')
+plt.show()
+
+# Set up AcquisitionGeometry object to hold the parameters of the measurement
+# setup geometry: # Number of angles, the actual angles from 0 to
+# pi for parallel beam and 0 to 2pi for fanbeam, set the width of a detector
+# pixel relative to an object pixel, the number of detector pixels, and the
+# source-origin and origin-detector distance (here the origin-detector distance
+# set to 0 to simulate a "virtual detector" with same detector pixel size as
+# object pixel size).
+angles_num = 20
+det_w = 1.0
+det_num = N
+SourceOrig = 200
+OrigDetec = 0
+
+if test_case==1:
+ angles = np.linspace(0,np.pi,angles_num,endpoint=False)
+ ag = AcquisitionGeometry('parallel',
+ '2D',
+ angles,
+ det_num,det_w)
+elif test_case==2:
+ angles = np.linspace(0,2*np.pi,angles_num,endpoint=False)
+ ag = AcquisitionGeometry('cone',
+ '2D',
+ angles,
+ det_num,
+ det_w,
+ dist_source_center=SourceOrig,
+ dist_center_detector=OrigDetec)
+else:
+ NotImplemented
+
+# Set up Operator object combining the ImageGeometry and AcquisitionGeometry
+# wrapping calls to ASTRA as well as specifying whether to use CPU or GPU.
+Aop = AstraProjectorSimple(ig, ag, 'gpu')
+
+# Forward and backprojection are available as methods direct and adjoint. Here
+# generate test data b and do simple backprojection to obtain z.
+b = Aop.direct(Phantom)
+z = Aop.adjoint(b)
+
+plt.figure()
+plt.imshow(b.as_array())
+plt.title('Simulated data')
+plt.show()
+
+plt.figure()
+plt.imshow(z.as_array())
+plt.title('Backprojected data')
+plt.show()
+
+# Using the test data b, different reconstruction methods can now be set up as
+# demonstrated in the rest of this file. In general all methods need an initial
+# guess and some algorithm options to be set:
+x_init = ImageData(np.zeros(x.shape),geometry=ig)
+opt = {'tol': 1e-4, 'iter': 100}
+
+
+# First run a simple CGLS reconstruction:
+CGLS_alg = CGLS()
+CGLS_alg.set_up(x_init, Aop, b )
+CGLS_alg.max_iteration = 2000
+CGLS_alg.run(opt['iter'])
+x_CGLS_alg = CGLS_alg.get_output()
+
+plt.figure()
+plt.imshow(x_CGLS_alg.as_array())
+plt.title('CGLS ALG')
+plt.colorbar()
+plt.show()
+
+plt.figure()
+plt.semilogy(CGLS_alg.objective)
+plt.title('CGLS criterion')
+plt.show()
+
+
+# A SIRT reconstruction can be done simply by replacing CGLS by SIRT.
+# In the first instance, no constraints are enforced.
+SIRT_alg = SIRT()
+SIRT_alg.set_up(x_init, Aop, b )
+SIRT_alg.max_iteration = 2000
+SIRT_alg.run(opt['iter'])
+x_SIRT_alg = SIRT_alg.get_output()
+
+plt.figure()
+plt.imshow(x_SIRT_alg.as_array())
+plt.title('SIRT unconstrained')
+plt.colorbar()
+plt.show()
+
+plt.figure()
+plt.semilogy(SIRT_alg.objective)
+plt.title('SIRT unconstrained criterion')
+plt.show()
+
+# The SIRT algorithm is stopped after the specified number of iterations has
+# been run. It can be resumed by calling the run command again, which will run
+# it for the specificed number of iterations
+SIRT_alg.run(opt['iter'])
+x_SIRT_alg2 = SIRT_alg.get_output()
+
+plt.figure()
+plt.imshow(x_SIRT_alg2.as_array())
+plt.title('SIRT unconstrained, extra iterations')
+plt.colorbar()
+plt.show()
+
+plt.figure()
+plt.semilogy(SIRT_alg.objective)
+plt.title('SIRT unconstrained criterion, extra iterations')
+plt.show()
+
+
+# A SIRT nonnegativity constrained reconstruction can be done using the
+# additional input "constraint" set to a box indicator function with 0 as the
+# lower bound and the default upper bound of infinity. First setup a new
+# instance of the SIRT algorithm.
+SIRT_alg0 = SIRT()
+SIRT_alg0.set_up(x_init, Aop, b, constraint=IndicatorBox(lower=0) )
+SIRT_alg0.max_iteration = 2000
+SIRT_alg0.run(opt['iter'])
+x_SIRT_alg0 = SIRT_alg0.get_output()
+
+plt.figure()
+plt.imshow(x_SIRT_alg0.as_array())
+plt.title('SIRT nonnegativity constrained')
+plt.colorbar()
+plt.show()
+
+plt.figure()
+plt.semilogy(SIRT_alg0.objective)
+plt.title('SIRT nonnegativity criterion')
+plt.show()
+
+
+# A SIRT reconstruction with box constraints on [0,1] can also be done.
+SIRT_alg01 = SIRT()
+SIRT_alg01.set_up(x_init, Aop, b, constraint=IndicatorBox(lower=0,upper=1) )
+SIRT_alg01.max_iteration = 2000
+SIRT_alg01.run(opt['iter'])
+x_SIRT_alg01 = SIRT_alg01.get_output()
+
+plt.figure()
+plt.imshow(x_SIRT_alg01.as_array())
+plt.title('SIRT boc(0,1)')
+plt.colorbar()
+plt.show()
+
+plt.figure()
+plt.semilogy(SIRT_alg01.objective)
+plt.title('SIRT box(0,1) criterion')
+plt.show()
+
+# The test image has values in the range [0,1], so enforcing values in the
+# reconstruction to be within this interval improves a lot. Just for fun
+# we can also easily see what happens if we choose a narrower interval as
+# constrint in the reconstruction, lower bound 0.2, upper bound 0.8.
+SIRT_alg0208 = SIRT()
+SIRT_alg0208.set_up(x_init,Aop,b,constraint=IndicatorBox(lower=0.2,upper=0.8))
+SIRT_alg0208.max_iteration = 2000
+SIRT_alg0208.run(opt['iter'])
+x_SIRT_alg0208 = SIRT_alg0208.get_output()
+
+plt.figure()
+plt.imshow(x_SIRT_alg0208.as_array())
+plt.title('SIRT boc(0.2,0.8)')
+plt.colorbar()
+plt.show()
+
+plt.figure()
+plt.semilogy(SIRT_alg0208.objective)
+plt.title('SIRT box(0.2,0.8) criterion')
+plt.show() \ No newline at end of file
diff --git a/Wrappers/Python/wip/demo_box_constraints_FISTA.py b/Wrappers/Python/wip/demo_box_constraints_FISTA.py
new file mode 100644
index 0000000..b15dd45
--- /dev/null
+++ b/Wrappers/Python/wip/demo_box_constraints_FISTA.py
@@ -0,0 +1,158 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Created on Wed Apr 17 14:46:21 2019
+
+@author: jakob
+
+Demonstrate the use of box constraints in FISTA
+"""
+
+# First make all imports
+from ccpi.framework import ImageData, ImageGeometry, AcquisitionGeometry, \
+ AcquisitionData
+from ccpi.optimisation.algorithms import FISTA
+from ccpi.optimisation.functions import Norm2sq, IndicatorBox
+from ccpi.astra.ops import AstraProjectorSimple
+
+from ccpi.optimisation.operators import Identity
+
+import numpy as np
+import matplotlib.pyplot as plt
+
+
+# Set up phantom size NxN by creating ImageGeometry, initialising the
+# ImageData object with this geometry and empty array and finally put some
+# data into its array, and display as image.
+N = 128
+ig = ImageGeometry(voxel_num_x=N,voxel_num_y=N)
+Phantom = ImageData(geometry=ig)
+
+x = Phantom.as_array()
+x[round(N/4):round(3*N/4),round(N/4):round(3*N/4)] = 0.5
+x[round(N/8):round(7*N/8),round(3*N/8):round(5*N/8)] = 1
+
+plt.figure()
+plt.imshow(x)
+plt.title('Phantom image')
+plt.show()
+
+# Set up AcquisitionGeometry object to hold the parameters of the measurement
+# setup geometry: # Number of angles, the actual angles from 0 to
+# pi for parallel beam and 0 to 2pi for fanbeam, set the width of a detector
+# pixel relative to an object pixel, the number of detector pixels, and the
+# source-origin and origin-detector distance (here the origin-detector distance
+# set to 0 to simulate a "virtual detector" with same detector pixel size as
+# object pixel size).
+angles_num = 20
+det_w = 1.0
+det_num = N
+SourceOrig = 200
+OrigDetec = 0
+
+test_case = 1
+
+if test_case==1:
+ angles = np.linspace(0,np.pi,angles_num,endpoint=False)
+ ag = AcquisitionGeometry('parallel',
+ '2D',
+ angles,
+ det_num,det_w)
+elif test_case==2:
+ angles = np.linspace(0,2*np.pi,angles_num,endpoint=False)
+ ag = AcquisitionGeometry('cone',
+ '2D',
+ angles,
+ det_num,
+ det_w,
+ dist_source_center=SourceOrig,
+ dist_center_detector=OrigDetec)
+else:
+ NotImplemented
+
+# Set up Operator object combining the ImageGeometry and AcquisitionGeometry
+# wrapping calls to ASTRA as well as specifying whether to use CPU or GPU.
+Aop = AstraProjectorSimple(ig, ag, 'cpu')
+
+Aop = Identity(ig,ig)
+
+# Forward and backprojection are available as methods direct and adjoint. Here
+# generate test data b and do simple backprojection to obtain z.
+b = Aop.direct(Phantom)
+z = Aop.adjoint(b)
+
+plt.figure()
+plt.imshow(b.array)
+plt.title('Simulated data')
+plt.show()
+
+plt.figure()
+plt.imshow(z.array)
+plt.title('Backprojected data')
+plt.show()
+
+# Using the test data b, different reconstruction methods can now be set up as
+# demonstrated in the rest of this file. In general all methods need an initial
+# guess and some algorithm options to be set:
+x_init = ImageData(np.zeros(x.shape),geometry=ig)
+opt = {'tol': 1e-4, 'iter': 100}
+
+
+
+# Create least squares object instance with projector, test data and a constant
+# coefficient of 0.5:
+f = Norm2sq(Aop,b,c=0.5)
+
+# Run FISTA for least squares without constraints
+FISTA_alg = FISTA()
+FISTA_alg.set_up(x_init=x_init, f=f, opt=opt)
+FISTA_alg.max_iteration = 2000
+FISTA_alg.run(opt['iter'])
+x_FISTA = FISTA_alg.get_output()
+
+plt.figure()
+plt.imshow(x_FISTA.array)
+plt.title('FISTA unconstrained')
+plt.colorbar()
+plt.show()
+
+plt.figure()
+plt.semilogy(FISTA_alg.objective)
+plt.title('FISTA unconstrained criterion')
+plt.show()
+
+# Run FISTA for least squares with lower bound 0.1
+FISTA_alg0 = FISTA()
+FISTA_alg0.set_up(x_init=x_init, f=f, g=IndicatorBox(lower=0.1), opt=opt)
+FISTA_alg0.max_iteration = 2000
+FISTA_alg0.run(opt['iter'])
+x_FISTA0 = FISTA_alg0.get_output()
+
+plt.figure()
+plt.imshow(x_FISTA0.array)
+plt.title('FISTA lower bound 0.1')
+plt.colorbar()
+plt.show()
+
+plt.figure()
+plt.semilogy(FISTA_alg0.objective)
+plt.title('FISTA criterion, lower bound 0.1')
+plt.show()
+
+# Run FISTA for least squares with box constraint [0.1,0.8]
+FISTA_alg0 = FISTA()
+FISTA_alg0.set_up(x_init=x_init, f=f, g=IndicatorBox(lower=0.1,upper=0.8), opt=opt)
+FISTA_alg0.max_iteration = 2000
+FISTA_alg0.run(opt['iter'])
+x_FISTA0 = FISTA_alg0.get_output()
+
+plt.figure()
+plt.imshow(x_FISTA0.array)
+plt.title('FISTA box(0.1,0.8) constrained')
+plt.colorbar()
+plt.show()
+
+plt.figure()
+plt.semilogy(FISTA_alg0.objective)
+plt.title('FISTA criterion, box(0.1,0.8) constrained criterion')
+plt.show() \ No newline at end of file
diff --git a/Wrappers/Python/wip/demo_colourbay.py b/Wrappers/Python/wip/demo_colourbay.py
index 5dbf2e1..0536b07 100644
--- a/Wrappers/Python/wip/demo_colourbay.py
+++ b/Wrappers/Python/wip/demo_colourbay.py
@@ -18,7 +18,7 @@ from ccpi.optimisation.funcs import Norm2sq, Norm1
# Permute (numpy.transpose) puts into our default ordering which is
# (channel, angle, vertical, horizontal).
-pathname = '/media/jakob/050d8d45-fab3-4285-935f-260e6c5f162c1/Data/ColourBay/spectral_data_sets/CarbonPd/'
+pathname = '/media/newhd/shared/Data/ColourBay/spectral_data_sets/CarbonPd/'
filename = 'carbonPd_full_sinogram_stripes_removed.mat'
X = loadmat(pathname + filename)
diff --git a/Wrappers/Python/wip/fix_test.py b/Wrappers/Python/wip/fix_test.py
new file mode 100755
index 0000000..b1006c0
--- /dev/null
+++ b/Wrappers/Python/wip/fix_test.py
@@ -0,0 +1,208 @@
+import numpy as np
+import numpy
+from ccpi.optimisation.operators import *
+from ccpi.optimisation.algorithms import *
+from ccpi.optimisation.functions import *
+from ccpi.framework import *
+
+def isSizeCorrect(data1 ,data2):
+ if issubclass(type(data1), DataContainer) and \
+ issubclass(type(data2), DataContainer):
+ # check dimensionality
+ if data1.check_dimensions(data2):
+ return True
+ elif issubclass(type(data1) , numpy.ndarray) and \
+ issubclass(type(data2) , numpy.ndarray):
+ return data1.shape == data2.shape
+ else:
+ raise ValueError("{0}: getting two incompatible types: {1} {2}"\
+ .format('Function', type(data1), type(data2)))
+ return False
+
+class Norm1(Function):
+
+ def __init__(self,gamma):
+ super(Norm1, self).__init__()
+ self.gamma = gamma
+ self.L = 1
+ self.sign_x = None
+
+ def __call__(self,x,out=None):
+ if out is None:
+ return self.gamma*(x.abs().sum())
+ else:
+ if not x.shape == out.shape:
+ raise ValueError('Norm1 Incompatible size:',
+ x.shape, out.shape)
+ x.abs(out=out)
+ return out.sum() * self.gamma
+
+ def prox(self,x,tau):
+ return (x.abs() - tau*self.gamma).maximum(0) * x.sign()
+
+ def proximal(self, x, tau, out=None):
+ if out is None:
+ return self.prox(x, tau)
+ else:
+ if isSizeCorrect(x,out):
+ # check dimensionality
+ if issubclass(type(out), DataContainer):
+ v = (x.abs() - tau*self.gamma).maximum(0)
+ x.sign(out=out)
+ out *= v
+ #out.fill(self.prox(x,tau))
+ elif issubclass(type(out) , numpy.ndarray):
+ v = (x.abs() - tau*self.gamma).maximum(0)
+ out[:] = x.sign()
+ out *= v
+ #out[:] = self.prox(x,tau)
+ else:
+ raise ValueError ('Wrong size: x{0} out{1}'.format(x.shape,out.shape) )
+
+opt = {'memopt': True}
+# Problem data.
+m = 500
+n = 200
+
+# if m < n then the problem is under-determined and algorithms will struggle to find a solution.
+# One approach is to add regularisation
+
+#np.random.seed(1)
+Amat = np.asarray( np.random.randn(m, n), dtype=numpy.float32)
+Amat = np.asarray( np.random.random_integers(1,10, (m, n)), dtype=numpy.float32)
+#Amat = np.asarray(np.eye(m), dtype=np.float32) * 2
+A = LinearOperatorMatrix(Amat)
+bmat = np.asarray( np.random.randn(m), dtype=numpy.float32)
+#bmat *= 0
+#bmat += 2
+print ("bmat", bmat.shape)
+print ("A", A.A)
+#bmat.shape = (bmat.shape[0], 1)
+
+# A = Identity()
+# Change n to equal to m.
+vgb = VectorGeometry(m)
+vgx = VectorGeometry(n)
+b = vgb.allocate(VectorGeometry.RANDOM_INT, dtype=numpy.float32)
+# b.fill(bmat)
+#b = DataContainer(bmat)
+
+# Regularization parameter
+lam = 10
+opt = {'memopt': True}
+# Create object instances with the test data A and b.
+f = Norm2Sq(A, b, c=1., memopt=True)
+#f = FunctionOperatorComposition(A, L2NormSquared(b=bmat))
+g0 = ZeroFunction()
+
+#f.L = 30.003
+x_init = vgx.allocate(VectorGeometry.RANDOM, dtype=numpy.float32)
+x_initcgls = x_init.copy()
+
+a = VectorData(x_init.as_array(), deep_copy=True)
+
+assert id(x_init.as_array()) != id(a.as_array())
+
+
+#f.L = LinearOperator.PowerMethod(A, 25, x_init)[0]
+#print ('f.L', f.L)
+rate = (1 / f.L) / 6
+#f.L *= 12
+
+# Initial guess
+#x_init = DataContainer(np.zeros((n, 1)))
+print ('x_init', x_init.as_array())
+print ('b', b.as_array())
+# Create 1-norm object instance
+g1_new = lam * L1Norm()
+g1 = Norm1(lam)
+
+g1 = ZeroFunction()
+#g1(x_init)
+x = g1.prox(x_init, 1/f.L )
+print ("g1.proximal ", x.as_array())
+
+x = g1.prox(x_init, 0.03 )
+print ("g1.proximal ", x.as_array())
+x = g1_new.proximal(x_init, 0.03 )
+print ("g1.proximal ", x.as_array())
+
+x1 = vgx.allocate(VectorGeometry.RANDOM, dtype=numpy.float32)
+pippo = vgx.allocate()
+
+print ("x_init", x_init.as_array())
+print ("x1", x1.as_array())
+a = x_init.subtract(x1, out=pippo)
+
+print ("pippo", pippo.as_array())
+print ("x_init", x_init.as_array())
+print ("x1", x1.as_array())
+
+y = A.direct(x_init)
+y *= 0
+A.direct(x_init, out=y)
+
+# Combine with least squares and solve using generic FISTA implementation
+#x_fista1, it1, timing1, criter1 = FISTA(x_init, f, g1, opt=opt)
+def callback(it, objective, solution):
+ print ("callback " , it , objective, f(solution))
+
+fa = FISTA(x_init=x_init, f=f, g=g1)
+fa.max_iteration = 1000
+fa.update_objective_interval = int( fa.max_iteration / 10 )
+fa.run(fa.max_iteration, callback = None, verbose=True)
+
+gd = GradientDescent(x_init=x_init, objective_function=f, rate = rate )
+gd.max_iteration = 5000
+gd.update_objective_interval = int( gd.max_iteration / 10 )
+gd.run(gd.max_iteration, callback = None, verbose=True)
+
+
+
+cgls = CGLS(x_init= x_initcgls, operator=A, data=b)
+cgls.max_iteration = 1000
+cgls.update_objective_interval = int( cgls.max_iteration / 10 )
+
+#cgls.should_stop = stop_criterion(cgls)
+cgls.run(cgls.max_iteration, callback = callback, verbose=True)
+
+
+
+# Print for comparison
+print("FISTA least squares plus 1-norm solution and objective value:")
+print(fa.get_output().as_array())
+print(fa.get_last_objective())
+
+print ("data ", b.as_array())
+print ('FISTA ', A.direct(fa.get_output()).as_array())
+print ('GradientDescent', A.direct(gd.get_output()).as_array())
+print ('CGLS ', A.direct(cgls.get_output()).as_array())
+
+cond = numpy.linalg.cond(A.A)
+
+print ("cond" , cond)
+
+#%%
+try:
+ import cvxpy as cp
+ # Construct the problem.
+ x = cp.Variable(n)
+ objective = cp.Minimize(cp.sum_squares(A.A*x - bmat))
+ prob = cp.Problem(objective)
+ # The optimal objective is returned by prob.solve().
+ result = prob.solve(solver = cp.MOSEK)
+
+ print ('CGLS ', cgls.get_output().as_array())
+ print ('CVX ', x.value)
+
+ print ('FISTA ', fa.get_output().as_array())
+ print ('GD ', gd.get_output().as_array())
+except ImportError as ir:
+ pass
+
+ #%%
+
+
+
+
+
diff --git a/Wrappers/Python/wip/old_demos/demo_colourbay.py b/Wrappers/Python/wip/old_demos/demo_colourbay.py
new file mode 100644
index 0000000..5dbf2e1
--- /dev/null
+++ b/Wrappers/Python/wip/old_demos/demo_colourbay.py
@@ -0,0 +1,137 @@
+# This script demonstrates how to load a mat-file with UoM colour-bay data
+# into the CIL optimisation framework and run (simple) multichannel
+# reconstruction methods.
+
+# All third-party imports.
+import numpy
+from scipy.io import loadmat
+import matplotlib.pyplot as plt
+
+# All own imports.
+from ccpi.framework import AcquisitionData, AcquisitionGeometry, ImageGeometry, ImageData
+from ccpi.astra.ops import AstraProjectorMC
+from ccpi.optimisation.algs import CGLS, FISTA
+from ccpi.optimisation.funcs import Norm2sq, Norm1
+
+# Load full data and permute to expected ordering. Change path as necessary.
+# The loaded X has dims 80x60x80x150, which is pix x angle x pix x channel.
+# Permute (numpy.transpose) puts into our default ordering which is
+# (channel, angle, vertical, horizontal).
+
+pathname = '/media/jakob/050d8d45-fab3-4285-935f-260e6c5f162c1/Data/ColourBay/spectral_data_sets/CarbonPd/'
+filename = 'carbonPd_full_sinogram_stripes_removed.mat'
+
+X = loadmat(pathname + filename)
+X = numpy.transpose(X['SS'],(3,1,2,0))
+
+# Store geometric variables for reuse
+num_channels = X.shape[0]
+num_pixels_h = X.shape[3]
+num_pixels_v = X.shape[2]
+num_angles = X.shape[1]
+
+# Display a single projection in a single channel
+plt.imshow(X[100,5,:,:])
+plt.title('Example of a projection image in one channel' )
+plt.show()
+
+# Set angles to use
+angles = numpy.linspace(-numpy.pi,numpy.pi,num_angles,endpoint=False)
+
+# Define full 3D acquisition geometry and data container.
+# Geometric info is taken from the txt-file in the same dir as the mat-file
+ag = AcquisitionGeometry('cone',
+ '3D',
+ angles,
+ pixel_num_h=num_pixels_h,
+ pixel_size_h=0.25,
+ pixel_num_v=num_pixels_v,
+ pixel_size_v=0.25,
+ dist_source_center=233.0,
+ dist_center_detector=245.0,
+ channels=num_channels)
+data = AcquisitionData(X, geometry=ag)
+
+# Reduce to central slice by extracting relevant parameters from data and its
+# geometry. Perhaps create function to extract central slice automatically?
+data2d = data.subset(vertical=40)
+ag2d = AcquisitionGeometry('cone',
+ '2D',
+ ag.angles,
+ pixel_num_h=ag.pixel_num_h,
+ pixel_size_h=ag.pixel_size_h,
+ pixel_num_v=1,
+ pixel_size_v=ag.pixel_size_h,
+ dist_source_center=ag.dist_source_center,
+ dist_center_detector=ag.dist_center_detector,
+ channels=ag.channels)
+data2d.geometry = ag2d
+
+# Set up 2D Image Geometry.
+# First need the geometric magnification to scale the voxel size relative
+# to the detector pixel size.
+mag = (ag.dist_source_center + ag.dist_center_detector)/ag.dist_source_center
+ig2d = ImageGeometry(voxel_num_x=ag2d.pixel_num_h,
+ voxel_num_y=ag2d.pixel_num_h,
+ voxel_size_x=ag2d.pixel_size_h/mag,
+ voxel_size_y=ag2d.pixel_size_h/mag,
+ channels=X.shape[0])
+
+# Create GPU multichannel projector/backprojector operator with ASTRA.
+Aall = AstraProjectorMC(ig2d,ag2d,'gpu')
+
+# Compute and simple backprojction and display one channel as image.
+Xbp = Aall.adjoint(data2d)
+plt.imshow(Xbp.subset(channel=100).array)
+plt.show()
+
+# Set initial guess ImageData with zeros for algorithms, and algorithm options.
+x_init = ImageData(numpy.zeros((num_channels,num_pixels_v,num_pixels_h)),
+ geometry=ig2d,
+ dimension_labels=['channel','horizontal_y','horizontal_x'])
+opt_CGLS = {'tol': 1e-4, 'iter': 5}
+
+# Run CGLS algorithm and display one channel.
+x_CGLS, it_CGLS, timing_CGLS, criter_CGLS = CGLS(x_init, Aall, data2d, opt_CGLS)
+
+plt.imshow(x_CGLS.subset(channel=100).array)
+plt.title('CGLS')
+plt.show()
+
+plt.semilogy(criter_CGLS)
+plt.title('CGLS Criterion vs iterations')
+plt.show()
+
+# Create least squares object instance with projector, test data and a constant
+# coefficient of 0.5. Note it is least squares over all channels.
+f = Norm2sq(Aall,data2d,c=0.5)
+
+# Options for FISTA algorithm.
+opt = {'tol': 1e-4, 'iter': 100}
+
+# Run FISTA for least squares without regularization and display one channel
+# reconstruction as image.
+x_fista0, it0, timing0, criter0 = FISTA(x_init, f, None, opt)
+
+plt.imshow(x_fista0.subset(channel=100).array)
+plt.title('FISTA LS')
+plt.show()
+
+plt.semilogy(criter0)
+plt.title('FISTA LS Criterion vs iterations')
+plt.show()
+
+# Set up 1-norm regularisation (over all channels), solve with FISTA, and
+# display one channel of reconstruction.
+lam = 0.1
+g0 = Norm1(lam)
+
+x_fista1, it1, timing1, criter1 = FISTA(x_init, f, g0, opt)
+
+plt.imshow(x_fista1.subset(channel=100).array)
+plt.title('FISTA LS+1')
+plt.show()
+
+plt.semilogy(criter1)
+plt.title('FISTA LS+1 Criterion vs iterations')
+plt.show() \ No newline at end of file
diff --git a/Wrappers/Python/wip/old_demos/demo_compare_cvx.py b/Wrappers/Python/wip/old_demos/demo_compare_cvx.py
new file mode 100644
index 0000000..27b1c97
--- /dev/null
+++ b/Wrappers/Python/wip/old_demos/demo_compare_cvx.py
@@ -0,0 +1,306 @@
+
+from ccpi.framework import ImageData, ImageGeometry, AcquisitionGeometry, DataContainer
+from ccpi.optimisation.algs import FISTA, FBPD, CGLS
+from ccpi.optimisation.funcs import Norm2sq, ZeroFun, Norm1, TV2D, Norm2
+
+from ccpi.optimisation.ops import LinearOperatorMatrix, TomoIdentity
+from ccpi.optimisation.ops import Identity
+from ccpi.optimisation.ops import FiniteDiff2D
+
+# Requires CVXPY, see http://www.cvxpy.org/
+# CVXPY can be installed in anaconda using
+# conda install -c cvxgrp cvxpy libgcc
+
+# Whether to use or omit CVXPY
+use_cvxpy = True
+if use_cvxpy:
+ from cvxpy import *
+
+import numpy as np
+import matplotlib.pyplot as plt
+
+# Problem data.
+m = 30
+n = 20
+np.random.seed(1)
+Amat = np.random.randn(m, n)
+A = LinearOperatorMatrix(Amat)
+bmat = np.random.randn(m)
+bmat.shape = (bmat.shape[0],1)
+
+# A = Identity()
+# Change n to equal to m.
+
+b = DataContainer(bmat)
+
+# Regularization parameter
+lam = 10
+opt = {'memopt':True}
+# Create object instances with the test data A and b.
+f = Norm2sq(A,b,c=0.5, memopt=True)
+g0 = ZeroFun()
+
+# Initial guess
+x_init = DataContainer(np.zeros((n,1)))
+
+f.grad(x_init)
+
+# Run FISTA for least squares plus zero function.
+x_fista0, it0, timing0, criter0 = FISTA(x_init, f, g0 , opt=opt)
+
+# Print solution and final objective/criterion value for comparison
+print("FISTA least squares plus zero function solution and objective value:")
+print(x_fista0.array)
+print(criter0[-1])
+
+if use_cvxpy:
+ # Compare to CVXPY
+
+ # Construct the problem.
+ x0 = Variable(n)
+ objective0 = Minimize(0.5*sum_squares(Amat*x0 - bmat.T[0]) )
+ prob0 = Problem(objective0)
+
+ # The optimal objective is returned by prob.solve().
+ result0 = prob0.solve(verbose=False,solver=SCS,eps=1e-9)
+
+ # The optimal solution for x is stored in x.value and optimal objective value
+ # is in result as well as in objective.value
+ print("CVXPY least squares plus zero function solution and objective value:")
+ print(x0.value)
+ print(objective0.value)
+
+# Plot criterion curve to see FISTA converge to same value as CVX.
+iternum = np.arange(1,1001)
+plt.figure()
+plt.loglog(iternum[[0,-1]],[objective0.value, objective0.value], label='CVX LS')
+plt.loglog(iternum,criter0,label='FISTA LS')
+plt.legend()
+plt.show()
+
+# Create 1-norm object instance
+g1 = Norm1(lam)
+
+g1(x_init)
+x_rand = DataContainer(np.reshape(np.random.rand(n),(n,1)))
+x_rand2 = DataContainer(np.reshape(np.random.rand(n-1),(n-1,1)))
+v = g1.prox(x_rand,0.02)
+#vv = g1.prox(x_rand2,0.02)
+vv = v.copy()
+vv *= 0
+print (">>>>>>>>>>vv" , vv.as_array())
+vv.fill(v)
+print (">>>>>>>>>>fill" , vv.as_array())
+g1.proximal(x_rand, 0.02, out=vv)
+print (">>>>>>>>>>v" , v.as_array())
+print (">>>>>>>>>>gradient" , vv.as_array())
+
+print (">>>>>>>>>>" , (v-vv).as_array())
+import sys
+#sys.exit(0)
+# Combine with least squares and solve using generic FISTA implementation
+x_fista1, it1, timing1, criter1 = FISTA(x_init, f, g1,opt=opt)
+
+# Print for comparison
+print("FISTA least squares plus 1-norm solution and objective value:")
+print(x_fista1)
+print(criter1[-1])
+
+if use_cvxpy:
+ # Compare to CVXPY
+
+ # Construct the problem.
+ x1 = Variable(n)
+ objective1 = Minimize(0.5*sum_squares(Amat*x1 - bmat.T[0]) + lam*norm(x1,1) )
+ prob1 = Problem(objective1)
+
+ # The optimal objective is returned by prob.solve().
+ result1 = prob1.solve(verbose=False,solver=SCS,eps=1e-9)
+
+ # The optimal solution for x is stored in x.value and optimal objective value
+ # is in result as well as in objective.value
+ print("CVXPY least squares plus 1-norm solution and objective value:")
+ print(x1.value)
+ print(objective1.value)
+
+# Now try another algorithm FBPD for same problem:
+x_fbpd1, itfbpd1, timingfbpd1, criterfbpd1 = FBPD(x_init,Identity(), None, f, g1)
+print(x_fbpd1)
+print(criterfbpd1[-1])
+
+# Plot criterion curve to see both FISTA and FBPD converge to same value.
+# Note that FISTA is very efficient for 1-norm minimization so it beats
+# FBPD in this test by a lot. But FBPD can handle a larger class of problems
+# than FISTA can.
+plt.figure()
+plt.loglog(iternum[[0,-1]],[objective1.value, objective1.value], label='CVX LS+1')
+plt.loglog(iternum,criter1,label='FISTA LS+1')
+plt.legend()
+plt.show()
+
+plt.figure()
+plt.loglog(iternum[[0,-1]],[objective1.value, objective1.value], label='CVX LS+1')
+plt.loglog(iternum,criter1,label='FISTA LS+1')
+plt.loglog(iternum,criterfbpd1,label='FBPD LS+1')
+plt.legend()
+plt.show()
+
+# Now try 1-norm and TV denoising with FBPD, first 1-norm.
+
+# Set up phantom size NxN by creating ImageGeometry, initialising the
+# ImageData object with this geometry and empty array and finally put some
+# data into its array, and display as image.
+N = 64
+ig = ImageGeometry(voxel_num_x=N,voxel_num_y=N)
+Phantom = ImageData(geometry=ig)
+
+x = Phantom.as_array()
+x[round(N/4):round(3*N/4),round(N/4):round(3*N/4)] = 0.5
+x[round(N/8):round(7*N/8),round(3*N/8):round(5*N/8)] = 1
+
+plt.imshow(x)
+plt.title('Phantom image')
+plt.show()
+
+# Identity operator for denoising
+I = TomoIdentity(ig)
+
+# Data and add noise
+y = I.direct(Phantom)
+y.array = y.array + 0.1*np.random.randn(N, N)
+
+plt.imshow(y.array)
+plt.title('Noisy image')
+plt.show()
+
+
+###################
+# Data fidelity term
+f_denoise = Norm2sq(I,y,c=0.5,memopt=True)
+
+# 1-norm regulariser
+lam1_denoise = 1.0
+g1_denoise = Norm1(lam1_denoise)
+
+# Initial guess
+x_init_denoise = ImageData(np.zeros((N,N)))
+
+# Combine with least squares and solve using generic FISTA implementation
+x_fista1_denoise, it1_denoise, timing1_denoise, criter1_denoise = FISTA(x_init_denoise, f_denoise, g1_denoise, opt=opt)
+
+print(x_fista1_denoise)
+print(criter1_denoise[-1])
+
+#plt.imshow(x_fista1_denoise.as_array())
+#plt.title('FISTA LS+1')
+#plt.show()
+
+# Now denoise LS + 1-norm with FBPD
+x_fbpd1_denoise, itfbpd1_denoise, timingfbpd1_denoise, \
+ criterfbpd1_denoise = FBPD(x_init_denoise, I, None, f_denoise, g1_denoise)
+print(x_fbpd1_denoise)
+print(criterfbpd1_denoise[-1])
+
+#plt.imshow(x_fbpd1_denoise.as_array())
+#plt.title('FBPD LS+1')
+#plt.show()
+
+if use_cvxpy:
+ # Compare to CVXPY
+
+ # Construct the problem.
+ x1_denoise = Variable(N**2,1)
+ objective1_denoise = Minimize(0.5*sum_squares(x1_denoise - y.array.flatten()) + lam1_denoise*norm(x1_denoise,1) )
+ prob1_denoise = Problem(objective1_denoise)
+
+ # The optimal objective is returned by prob.solve().
+ result1_denoise = prob1_denoise.solve(verbose=False,solver=SCS,eps=1e-12)
+
+ # The optimal solution for x is stored in x.value and optimal objective value
+ # is in result as well as in objective.value
+ print("CVXPY least squares plus 1-norm solution and objective value:")
+ print(x1_denoise.value)
+ print(objective1_denoise.value)
+
+x1_cvx = x1_denoise.value
+x1_cvx.shape = (N,N)
+
+
+
+#plt.imshow(x1_cvx)
+#plt.title('CVX LS+1')
+#plt.show()
+
+fig = plt.figure()
+plt.subplot(1,4,1)
+plt.imshow(y.array)
+plt.title("LS+1")
+plt.subplot(1,4,2)
+plt.imshow(x_fista1_denoise.as_array())
+plt.title("fista")
+plt.subplot(1,4,3)
+plt.imshow(x_fbpd1_denoise.as_array())
+plt.title("fbpd")
+plt.subplot(1,4,4)
+plt.imshow(x1_cvx)
+plt.title("cvx")
+plt.show()
+
+##############################################################
+# Now TV with FBPD and Norm2
+lam_tv = 0.1
+gtv = TV2D(lam_tv)
+norm2 = Norm2(lam_tv)
+op = FiniteDiff2D()
+#gtv(gtv.op.direct(x_init_denoise))
+
+opt_tv = {'tol': 1e-4, 'iter': 10000}
+
+x_fbpdtv_denoise, itfbpdtv_denoise, timingfbpdtv_denoise, \
+ criterfbpdtv_denoise = FBPD(x_init_denoise, op, None, \
+ f_denoise, norm2 ,opt=opt_tv)
+print(x_fbpdtv_denoise)
+print(criterfbpdtv_denoise[-1])
+
+plt.imshow(x_fbpdtv_denoise.as_array())
+plt.title('FBPD TV')
+#plt.show()
+
+if use_cvxpy:
+ # Compare to CVXPY
+
+ # Construct the problem.
+ xtv_denoise = Variable((N,N))
+ #print (xtv_denoise.value.shape)
+ objectivetv_denoise = Minimize(0.5*sum_squares(xtv_denoise - y.array) + lam_tv*tv(xtv_denoise) )
+ probtv_denoise = Problem(objectivetv_denoise)
+
+ # The optimal objective is returned by prob.solve().
+ resulttv_denoise = probtv_denoise.solve(verbose=False,solver=SCS,eps=1e-12)
+
+ # The optimal solution for x is stored in x.value and optimal objective value
+ # is in result as well as in objective.value
+ print("CVXPY least squares plus 1-norm solution and objective value:")
+ print(xtv_denoise.value)
+ print(objectivetv_denoise.value)
+
+plt.imshow(xtv_denoise.value)
+plt.title('CVX TV')
+#plt.show()
+
+fig = plt.figure()
+plt.subplot(1,3,1)
+plt.imshow(y.array)
+plt.title("TV2D")
+plt.subplot(1,3,2)
+plt.imshow(x_fbpdtv_denoise.as_array())
+plt.title("fbpd tv denoise")
+plt.subplot(1,3,3)
+plt.imshow(xtv_denoise.value)
+plt.title("CVX tv")
+plt.show()
+
+
+
+plt.loglog([0,opt_tv['iter']], [objectivetv_denoise.value,objectivetv_denoise.value], label='CVX TV')
+plt.loglog(criterfbpdtv_denoise, label='FBPD TV')
diff --git a/Wrappers/Python/wip/old_demos/demo_gradient_descent.py b/Wrappers/Python/wip/old_demos/demo_gradient_descent.py
new file mode 100755
index 0000000..4d6647e
--- /dev/null
+++ b/Wrappers/Python/wip/old_demos/demo_gradient_descent.py
@@ -0,0 +1,295 @@
+
+from ccpi.framework import ImageData, ImageGeometry, AcquisitionGeometry, DataContainer
+from ccpi.optimisation.algs import FISTA, FBPD, CGLS
+from ccpi.optimisation.funcs import Norm2sq, ZeroFun, Norm1, TV2D, Norm2
+
+from ccpi.optimisation.ops import LinearOperatorMatrix, TomoIdentity
+from ccpi.optimisation.ops import Identity
+from ccpi.optimisation.ops import FiniteDiff2D
+
+# Requires CVXPY, see http://www.cvxpy.org/
+# CVXPY can be installed in anaconda using
+# conda install -c cvxgrp cvxpy libgcc
+
+# Whether to use or omit CVXPY
+
+import numpy as np
+import matplotlib.pyplot as plt
+
+class Algorithm(object):
+ def __init__(self, *args, **kwargs):
+ pass
+ def set_up(self, *args, **kwargs):
+ raise NotImplementedError()
+ def update(self):
+ raise NotImplementedError()
+
+ def should_stop(self):
+ raise NotImplementedError()
+
+ def __iter__(self):
+ return self
+
+ def __next__(self):
+ if self.should_stop():
+ raise StopIteration()
+ else:
+ self.update()
+
+class GradientDescent(Algorithm):
+ x = None
+ rate = 0
+ objective_function = None
+ regulariser = None
+ iteration = 0
+ stop_cryterion = 'max_iter'
+ __max_iteration = 0
+ __loss = []
+ def __init__(self, **kwargs):
+ args = ['x_init', 'objective_function', 'rate']
+ present = True
+ for k,v in kwargs.items():
+ if k in args:
+ args.pop(args.index(k))
+ if len(args) == 0:
+ return self.set_up(x_init=kwargs['x_init'],
+ objective_function=kwargs['objective_function'],
+ rate=kwargs['rate'])
+
+ def should_stop(self):
+ return self.iteration >= self.max_iteration
+
+ def set_up(self, x_init, objective_function, rate):
+ self.x = x_init.copy()
+ self.x_update = x_init.copy()
+ self.objective_function = objective_function
+ self.rate = rate
+ self.__loss.append(objective_function(x_init))
+
+ def update(self):
+
+ self.objective_function.gradient(self.x, out=self.x_update)
+ self.x_update *= -self.rate
+ self.x += self.x_update
+ self.__loss.append(self.objective_function(self.x))
+ self.iteration += 1
+
+ def get_output(self):
+ return self.x
+ def get_current_loss(self):
+ return self.__loss[-1]
+ @property
+ def loss(self):
+ return self.__loss
+ @property
+ def max_iteration(self):
+ return self.__max_iteration
+ @max_iteration.setter
+ def max_iteration(self, value):
+ assert isinstance(value, int)
+ self.__max_iteration = value
+
+
+
+
+
+# Problem data.
+m = 30
+n = 20
+np.random.seed(1)
+Amat = np.random.randn(m, n)
+A = LinearOperatorMatrix(Amat)
+bmat = np.random.randn(m)
+bmat.shape = (bmat.shape[0],1)
+
+# A = Identity()
+# Change n to equal to m.
+
+b = DataContainer(bmat)
+
+# Regularization parameter
+lam = 10
+opt = {'memopt':True}
+# Create object instances with the test data A and b.
+f = Norm2sq(A,b,c=0.5, memopt=True)
+g0 = ZeroFun()
+
+# Initial guess
+x_init = DataContainer(np.zeros((n,1)))
+
+f.grad(x_init)
+
+# Run FISTA for least squares plus zero function.
+x_fista0, it0, timing0, criter0 = FISTA(x_init, f, g0 , opt=opt)
+
+# Print solution and final objective/criterion value for comparison
+print("FISTA least squares plus zero function solution and objective value:")
+print(x_fista0.array)
+print(criter0[-1])
+
+gd = GradientDescent(x_init=x_init, objective_function=f, rate=0.001)
+gd.max_iteration = 5000
+
+for i,el in enumerate(gd):
+ if i%100 == 0:
+ print ("\rIteration {} Loss: {}".format(gd.iteration,
+ gd.get_current_loss()))
+
+
+#%%
+
+
+#
+#if use_cvxpy:
+# # Compare to CVXPY
+#
+# # Construct the problem.
+# x0 = Variable(n)
+# objective0 = Minimize(0.5*sum_squares(Amat*x0 - bmat.T[0]) )
+# prob0 = Problem(objective0)
+#
+# # The optimal objective is returned by prob.solve().
+# result0 = prob0.solve(verbose=False,solver=SCS,eps=1e-9)
+#
+# # The optimal solution for x is stored in x.value and optimal objective value
+# # is in result as well as in objective.value
+# print("CVXPY least squares plus zero function solution and objective value:")
+# print(x0.value)
+# print(objective0.value)
+#
+## Plot criterion curve to see FISTA converge to same value as CVX.
+#iternum = np.arange(1,1001)
+#plt.figure()
+#plt.loglog(iternum[[0,-1]],[objective0.value, objective0.value], label='CVX LS')
+#plt.loglog(iternum,criter0,label='FISTA LS')
+#plt.legend()
+#plt.show()
+#
+## Create 1-norm object instance
+#g1 = Norm1(lam)
+#
+#g1(x_init)
+#x_rand = DataContainer(np.reshape(np.random.rand(n),(n,1)))
+#x_rand2 = DataContainer(np.reshape(np.random.rand(n-1),(n-1,1)))
+#v = g1.prox(x_rand,0.02)
+##vv = g1.prox(x_rand2,0.02)
+#vv = v.copy()
+#vv *= 0
+#print (">>>>>>>>>>vv" , vv.as_array())
+#vv.fill(v)
+#print (">>>>>>>>>>fill" , vv.as_array())
+#g1.proximal(x_rand, 0.02, out=vv)
+#print (">>>>>>>>>>v" , v.as_array())
+#print (">>>>>>>>>>gradient" , vv.as_array())
+#
+#print (">>>>>>>>>>" , (v-vv).as_array())
+#import sys
+##sys.exit(0)
+## Combine with least squares and solve using generic FISTA implementation
+#x_fista1, it1, timing1, criter1 = FISTA(x_init, f, g1,opt=opt)
+#
+## Print for comparison
+#print("FISTA least squares plus 1-norm solution and objective value:")
+#print(x_fista1)
+#print(criter1[-1])
+#
+#if use_cvxpy:
+# # Compare to CVXPY
+#
+# # Construct the problem.
+# x1 = Variable(n)
+# objective1 = Minimize(0.5*sum_squares(Amat*x1 - bmat.T[0]) + lam*norm(x1,1) )
+# prob1 = Problem(objective1)
+#
+# # The optimal objective is returned by prob.solve().
+# result1 = prob1.solve(verbose=False,solver=SCS,eps=1e-9)
+#
+# # The optimal solution for x is stored in x.value and optimal objective value
+# # is in result as well as in objective.value
+# print("CVXPY least squares plus 1-norm solution and objective value:")
+# print(x1.value)
+# print(objective1.value)
+#
+## Now try another algorithm FBPD for same problem:
+#x_fbpd1, itfbpd1, timingfbpd1, criterfbpd1 = FBPD(x_init,Identity(), None, f, g1)
+#print(x_fbpd1)
+#print(criterfbpd1[-1])
+#
+## Plot criterion curve to see both FISTA and FBPD converge to same value.
+## Note that FISTA is very efficient for 1-norm minimization so it beats
+## FBPD in this test by a lot. But FBPD can handle a larger class of problems
+## than FISTA can.
+#plt.figure()
+#plt.loglog(iternum[[0,-1]],[objective1.value, objective1.value], label='CVX LS+1')
+#plt.loglog(iternum,criter1,label='FISTA LS+1')
+#plt.legend()
+#plt.show()
+#
+#plt.figure()
+#plt.loglog(iternum[[0,-1]],[objective1.value, objective1.value], label='CVX LS+1')
+#plt.loglog(iternum,criter1,label='FISTA LS+1')
+#plt.loglog(iternum,criterfbpd1,label='FBPD LS+1')
+#plt.legend()
+#plt.show()
+
+# Now try 1-norm and TV denoising with FBPD, first 1-norm.
+
+# Set up phantom size NxN by creating ImageGeometry, initialising the
+# ImageData object with this geometry and empty array and finally put some
+# data into its array, and display as image.
+N = 64
+ig = ImageGeometry(voxel_num_x=N,voxel_num_y=N)
+Phantom = ImageData(geometry=ig)
+
+x = Phantom.as_array()
+x[round(N/4):round(3*N/4),round(N/4):round(3*N/4)] = 0.5
+x[round(N/8):round(7*N/8),round(3*N/8):round(5*N/8)] = 1
+
+plt.imshow(x)
+plt.title('Phantom image')
+plt.show()
+
+# Identity operator for denoising
+I = TomoIdentity(ig)
+
+# Data and add noise
+y = I.direct(Phantom)
+y.array = y.array + 0.1*np.random.randn(N, N)
+
+plt.imshow(y.array)
+plt.title('Noisy image')
+plt.show()
+
+
+###################
+# Data fidelity term
+f_denoise = Norm2sq(I,y,c=0.5,memopt=True)
+
+# 1-norm regulariser
+lam1_denoise = 1.0
+g1_denoise = Norm1(lam1_denoise)
+
+# Initial guess
+x_init_denoise = ImageData(np.zeros((N,N)))
+
+# Combine with least squares and solve using generic FISTA implementation
+x_fista1_denoise, it1_denoise, timing1_denoise, criter1_denoise = \
+ FISTA(x_init_denoise, f_denoise, g1_denoise, opt=opt)
+
+print(x_fista1_denoise)
+print(criter1_denoise[-1])
+
+f_2 =
+gd = GradientDescent(x_init=x_init_denoise,
+ objective_function=f, rate=0.001)
+gd.max_iteration = 5000
+
+for i,el in enumerate(gd):
+ if i%100 == 0:
+ print ("\rIteration {} Loss: {}".format(gd.iteration,
+ gd.get_current_loss()))
+
+plt.imshow(gd.get_output().as_array())
+plt.title('GD image')
+plt.show()
+
diff --git a/Wrappers/Python/wip/old_demos/demo_imat_multichan_RGLTK.py b/Wrappers/Python/wip/old_demos/demo_imat_multichan_RGLTK.py
new file mode 100644
index 0000000..8370c78
--- /dev/null
+++ b/Wrappers/Python/wip/old_demos/demo_imat_multichan_RGLTK.py
@@ -0,0 +1,151 @@
+# This script demonstrates how to load IMAT fits data
+# into the CIL optimisation framework and run reconstruction methods.
+#
+# Demo to reconstruct energy-discretized channels of IMAT data
+
+# needs dxchange: conda install -c conda-forge dxchange
+# needs astropy: conda install astropy
+
+
+# All third-party imports.
+import numpy as np
+import matplotlib.pyplot as plt
+from dxchange.reader import read_fits
+from astropy.io import fits
+
+# All own imports.
+from ccpi.framework import AcquisitionData, AcquisitionGeometry, ImageGeometry, ImageData, DataContainer
+from ccpi.astra.ops import AstraProjectorSimple, AstraProjector3DSimple
+from ccpi.optimisation.algs import CGLS, FISTA
+from ccpi.optimisation.funcs import Norm2sq, Norm1
+from ccpi.plugins.regularisers import FGP_TV
+
+# set main parameters here
+n = 512
+totalAngles = 250 # total number of projection angles
+# spectral discretization parameter
+num_average = 145 # channel discretization frequency (total number of averaged channels)
+numChannels = 2970 # 2970
+totChannels = round(numChannels/num_average) # the resulting number of channels
+Projections_stack = np.zeros((num_average,n,n),dtype='uint16')
+ProjAngleChannels = np.zeros((totalAngles,totChannels,n,n),dtype='float32')
+
+#########################################################################
+print ("Loading the data...")
+MainPath = '/media/jakob/050d8d45-fab3-4285-935f-260e6c5f162c1/Data/neutrondata/' # path to data
+pathname0 = '{!s}{!s}'.format(MainPath,'PSI_phantom_IMAT/DATA/Sample/')
+counterFileName = 4675
+# A main loop over all available angles
+for ll in range(0,totalAngles,1):
+ pathnameData = '{!s}{!s}{!s}/'.format(pathname0,'angle',str(ll))
+ filenameCurr = '{!s}{!s}{!s}'.format('IMAT0000',str(counterFileName),'_Tomo_test_000_')
+ counterT = 0
+ # loop over reduced channels (discretized)
+ for i in range(0,totChannels,1):
+ sumCount = 0
+ # loop over actual channels to obtain averaged one
+ for j in range(0,num_average,1):
+ if counterT < 10:
+ outfile = '{!s}{!s}{!s}{!s}.fits'.format(pathnameData,filenameCurr,'0000',str(counterT))
+ if ((counterT >= 10) & (counterT < 100)):
+ outfile = '{!s}{!s}{!s}{!s}.fits'.format(pathnameData,filenameCurr,'000',str(counterT))
+ if ((counterT >= 100) & (counterT < 1000)):
+ outfile = '{!s}{!s}{!s}{!s}.fits'.format(pathnameData,filenameCurr,'00',str(counterT))
+ if ((counterT >= 1000) & (counterT < 10000)):
+ outfile = '{!s}{!s}{!s}{!s}.fits'.format(pathnameData,filenameCurr,'0',str(counterT))
+ try:
+ Projections_stack[j,:,:] = read_fits(outfile)
+ except:
+ print("Fits is corrupted, skipping no.", counterT)
+ sumCount -= 1
+ counterT += 1
+ sumCount += 1
+ AverageProj=np.sum(Projections_stack,axis=0)/sumCount # averaged projection over "num_average" channels
+ ProjAngleChannels[ll,i,:,:] = AverageProj
+ print("Angle is processed", ll)
+ counterFileName += 1
+#########################################################################
+
+flat1 = read_fits('{!s}{!s}{!s}'.format(MainPath,'PSI_phantom_IMAT/DATA/','OpenBeam_aft1/IMAT00004932_Tomo_test_000_SummedImg.fits'))
+nonzero = flat1 > 0
+# Apply flat field and take negative log
+for ll in range(0,totalAngles,1):
+ for i in range(0,totChannels,1):
+ ProjAngleChannels[ll,i,nonzero] = ProjAngleChannels[ll,i,nonzero]/flat1[nonzero]
+
+eqzero = ProjAngleChannels == 0
+ProjAngleChannels[eqzero] = 1
+ProjAngleChannels_NormLog = -np.log(ProjAngleChannels) # normalised and neg-log data
+
+# extact sinogram over energy channels
+selectedVertical_slice = 256
+sino_all_channels = ProjAngleChannels_NormLog[:,:,:,selectedVertical_slice]
+# Set angles to use
+angles = np.linspace(-np.pi,np.pi,totalAngles,endpoint=False)
+
+# set the geometry
+ig = ImageGeometry(n,n)
+ag = AcquisitionGeometry('parallel',
+ '2D',
+ angles,
+ n,1)
+Aop = AstraProjectorSimple(ig, ag, 'gpu')
+
+
+# loop to reconstruct energy channels
+REC_chan = np.zeros((totChannels, n, n), 'float32')
+for i in range(0,totChannels,1):
+ sino_channel = sino_all_channels[:,i,:] # extract a sinogram for i-th channel
+
+ print ("Initial guess")
+ x_init = ImageData(geometry=ig)
+
+ # Create least squares object instance with projector and data.
+ print ("Create least squares object instance with projector and data.")
+ f = Norm2sq(Aop,DataContainer(sino_channel),c=0.5)
+
+ print ("Run FISTA-TV for least squares")
+ lamtv = 5
+ opt = {'tol': 1e-4, 'iter': 200}
+ g_fgp = FGP_TV(lambdaReg = lamtv,
+ iterationsTV=50,
+ tolerance=1e-6,
+ methodTV=0,
+ nonnegativity=0,
+ printing=0,
+ device='gpu')
+
+ x_fista_fgp, it1, timing1, criter_fgp = FISTA(x_init, f, g_fgp, opt)
+ REC_chan[i,:,:] = x_fista_fgp.array
+ """
+ plt.figure()
+ plt.subplot(121)
+ plt.imshow(x_fista_fgp.array, vmin=0, vmax=0.05)
+ plt.title('FISTA FGP TV')
+ plt.subplot(122)
+ plt.semilogy(criter_fgp)
+ plt.show()
+ """
+ """
+ print ("Run CGLS for least squares")
+ opt = {'tol': 1e-4, 'iter': 20}
+ x_init = ImageData(geometry=ig)
+ x_CGLS, it_CGLS, timing_CGLS, criter_CGLS = CGLS(x_init, Aop, DataContainer(sino_channel), opt=opt)
+
+ plt.figure()
+ plt.imshow(x_CGLS.array,vmin=0, vmax=0.05)
+ plt.title('CGLS')
+ plt.show()
+ """
+# Saving images into fits using astrapy if required
+counter = 0
+filename = 'FISTA_TV_imat_slice'
+for i in range(totChannels):
+ im = REC_chan[i,:,:]
+ add_val = np.min(im[:])
+ im += abs(add_val)
+ im = im/np.max(im[:])*65535
+ outfile = '{!s}_{!s}_{!s}.fits'.format(filename,str(selectedVertical_slice),str(counter))
+ hdu = fits.PrimaryHDU(np.uint16(im))
+ hdu.writeto(outfile, overwrite=True)
+ counter += 1 \ No newline at end of file
diff --git a/Wrappers/Python/wip/old_demos/demo_imat_whitebeam.py b/Wrappers/Python/wip/old_demos/demo_imat_whitebeam.py
new file mode 100644
index 0000000..e0d213e
--- /dev/null
+++ b/Wrappers/Python/wip/old_demos/demo_imat_whitebeam.py
@@ -0,0 +1,138 @@
+# This script demonstrates how to load IMAT fits data
+# into the CIL optimisation framework and run reconstruction methods.
+#
+# This demo loads the summedImg files which are the non-spectral images
+# resulting from summing projections over all spectral channels.
+
+# needs dxchange: conda install -c conda-forge dxchange
+# needs astropy: conda install astropy
+
+
+# All third-party imports.
+import numpy
+from scipy.io import loadmat
+import matplotlib.pyplot as plt
+from dxchange.reader import read_fits
+
+# All own imports.
+from ccpi.framework import AcquisitionData, AcquisitionGeometry, ImageGeometry, ImageData
+from ccpi.astra.ops import AstraProjectorSimple, AstraProjector3DSimple
+from ccpi.optimisation.algs import CGLS, FISTA
+from ccpi.optimisation.funcs import Norm2sq, Norm1
+
+# Load and display a couple of summed projection as examples
+pathname0 = '/media/newhd/shared/Data/neutrondata/PSI_phantom_IMAT/DATA/Sample/angle0/'
+filename0 = 'IMAT00004675_Tomo_test_000_SummedImg.fits'
+
+data0 = read_fits(pathname0 + filename0)
+
+pathname10 = '/media/newhd/shared/Data/neutrondata/PSI_phantom_IMAT/DATA/Sample/angle10/'
+filename10 = 'IMAT00004685_Tomo_test_000_SummedImg.fits'
+
+data10 = read_fits(pathname10 + filename10)
+
+# Load a flat field (more are available, should we average over them?)
+flat1 = read_fits('/media/newhd/shared/Data/neutrondata/PSI_phantom_IMAT/DATA/OpenBeam_aft1/IMAT00004932_Tomo_test_000_SummedImg.fits')
+
+# Apply flat field and display after flat-field correction and negative log
+data0_rel = numpy.zeros(numpy.shape(flat1), dtype = float)
+nonzero = flat1 > 0
+data0_rel[nonzero] = data0[nonzero] / flat1[nonzero]
+data10_rel = numpy.zeros(numpy.shape(flat1), dtype = float)
+data10_rel[nonzero] = data10[nonzero] / flat1[nonzero]
+
+plt.imshow(data0_rel)
+plt.colorbar()
+plt.show()
+
+plt.imshow(-numpy.log(data0_rel))
+plt.colorbar()
+plt.show()
+
+plt.imshow(data10_rel)
+plt.colorbar()
+plt.show()
+
+plt.imshow(-numpy.log(data10_rel))
+plt.colorbar()
+plt.show()
+
+# Set up for loading all summed images at 250 angles.
+pathname = '/media/newhd/shared/Data/neutrondata/PSI_phantom_IMAT/DATA/Sample/angle{}/'
+filename = 'IMAT0000{}_Tomo_test_000_SummedImg.fits'
+
+# Dimensions
+num_angles = 250
+imsize = 512
+
+# Initialise array
+data = numpy.zeros((num_angles,imsize,imsize))
+
+# Load only 0-249, as 250 is at repetition of zero degrees just like image 0
+for i in range(0,250):
+ curimfile = (pathname + filename).format(i, i+4675)
+ data[i,:,:] = read_fits(curimfile)
+
+# Apply flat field and take negative log
+nonzero = flat1 > 0
+for i in range(0,250):
+ data[i,nonzero] = data[i,nonzero]/flat1[nonzero]
+
+eqzero = data == 0
+data[eqzero] = 1
+
+data_rel = -numpy.log(data)
+
+# Permute order to get: angles, vertical, horizontal, as default in framework.
+data_rel = numpy.transpose(data_rel,(0,2,1))
+
+# Set angles to use
+angles = numpy.linspace(-numpy.pi,numpy.pi,num_angles,endpoint=False)
+
+# Create 3D acquisition geometry and acquisition data
+ag = AcquisitionGeometry('parallel',
+ '3D',
+ angles,
+ pixel_num_h=imsize,
+ pixel_num_v=imsize)
+b = AcquisitionData(data_rel, geometry=ag)
+
+# Reduce to single (noncentral) slice by extracting relevant parameters from data and its
+# geometry. Perhaps create function to extract central slice automatically?
+b2d = b.subset(vertical=128)
+ag2d = AcquisitionGeometry('parallel',
+ '2D',
+ ag.angles,
+ pixel_num_h=ag.pixel_num_h)
+b2d.geometry = ag2d
+
+# Create 2D image geometry
+ig2d = ImageGeometry(voxel_num_x=ag2d.pixel_num_h,
+ voxel_num_y=ag2d.pixel_num_h)
+
+# Create GPU projector/backprojector operator with ASTRA.
+Aop = AstraProjectorSimple(ig2d,ag2d,'gpu')
+
+# Demonstrate operator is working by applying simple backprojection.
+z = Aop.adjoint(b2d)
+plt.imshow(z.array)
+plt.title('Simple backprojection')
+plt.colorbar()
+plt.show()
+
+# Set initial guess ImageData with zeros for algorithms, and algorithm options.
+x_init = ImageData(numpy.zeros((imsize,imsize)),
+ geometry=ig2d)
+opt_CGLS = {'tol': 1e-4, 'iter': 20}
+
+# Run CGLS algorithm and display reconstruction.
+x_CGLS, it_CGLS, timing_CGLS, criter_CGLS = CGLS(x_init, Aop, b2d, opt_CGLS)
+
+plt.imshow(x_CGLS.array)
+plt.title('CGLS')
+plt.colorbar()
+plt.show()
+
+plt.semilogy(criter_CGLS)
+plt.title('CGLS Criterion vs iterations')
+plt.show() \ No newline at end of file
diff --git a/Wrappers/Python/wip/old_demos/demo_memhandle.py b/Wrappers/Python/wip/old_demos/demo_memhandle.py
new file mode 100755
index 0000000..db48d73
--- /dev/null
+++ b/Wrappers/Python/wip/old_demos/demo_memhandle.py
@@ -0,0 +1,193 @@
+
+from ccpi.framework import ImageData, ImageGeometry, AcquisitionGeometry, DataContainer
+from ccpi.optimisation.algs import FISTA, FBPD, CGLS
+from ccpi.optimisation.funcs import Norm2sq, ZeroFun, Norm1, TV2D
+
+from ccpi.optimisation.ops import LinearOperatorMatrix, Identity
+from ccpi.optimisation.ops import TomoIdentity
+
+# Requires CVXPY, see http://www.cvxpy.org/
+# CVXPY can be installed in anaconda using
+# conda install -c cvxgrp cvxpy libgcc
+
+
+import numpy as np
+import matplotlib.pyplot as plt
+
+# Problem data.
+m = 30
+n = 20
+np.random.seed(1)
+Amat = np.random.randn(m, n)
+A = LinearOperatorMatrix(Amat)
+bmat = np.random.randn(m)
+bmat.shape = (bmat.shape[0],1)
+
+
+
+# A = Identity()
+# Change n to equal to m.
+
+b = DataContainer(bmat)
+
+# Regularization parameter
+lam = 10
+
+# Create object instances with the test data A and b.
+f = Norm2sq(A,b,c=0.5, memopt=True)
+g0 = ZeroFun()
+
+# Initial guess
+x_init = DataContainer(np.zeros((n,1)))
+
+f.grad(x_init)
+opt = {'memopt': True}
+# Run FISTA for least squares plus zero function.
+x_fista0, it0, timing0, criter0 = FISTA(x_init, f, g0)
+x_fista0_m, it0_m, timing0_m, criter0_m = FISTA(x_init, f, g0, opt=opt)
+
+iternum = [i for i in range(len(criter0))]
+# Print solution and final objective/criterion value for comparison
+print("FISTA least squares plus zero function solution and objective value:")
+print(x_fista0.array)
+print(criter0[-1])
+
+
+# Plot criterion curve to see FISTA converge to same value as CVX.
+#iternum = np.arange(1,1001)
+plt.figure()
+plt.loglog(iternum,criter0,label='FISTA LS')
+plt.loglog(iternum,criter0_m,label='FISTA LS memopt')
+plt.legend()
+plt.show()
+#%%
+# Create 1-norm object instance
+g1 = Norm1(lam)
+
+g1(x_init)
+g1.prox(x_init,0.02)
+
+# Combine with least squares and solve using generic FISTA implementation
+x_fista1, it1, timing1, criter1 = FISTA(x_init, f, g1)
+x_fista1_m, it1_m, timing1_m, criter1_m = FISTA(x_init, f, g1, opt=opt)
+iternum = [i for i in range(len(criter1))]
+# Print for comparison
+print("FISTA least squares plus 1-norm solution and objective value:")
+print(x_fista1)
+print(criter1[-1])
+
+
+# Now try another algorithm FBPD for same problem:
+x_fbpd1, itfbpd1, timingfbpd1, criterfbpd1 = FBPD(x_init, None, f, g1)
+iternum = [i for i in range(len(criterfbpd1))]
+print(x_fbpd1)
+print(criterfbpd1[-1])
+
+# Plot criterion curve to see both FISTA and FBPD converge to same value.
+# Note that FISTA is very efficient for 1-norm minimization so it beats
+# FBPD in this test by a lot. But FBPD can handle a larger class of problems
+# than FISTA can.
+plt.figure()
+plt.loglog(iternum,criter1,label='FISTA LS+1')
+plt.loglog(iternum,criter1_m,label='FISTA LS+1 memopt')
+plt.legend()
+plt.show()
+
+plt.figure()
+plt.loglog(iternum,criter1,label='FISTA LS+1')
+plt.loglog(iternum,criterfbpd1,label='FBPD LS+1')
+plt.legend()
+plt.show()
+#%%
+# Now try 1-norm and TV denoising with FBPD, first 1-norm.
+
+# Set up phantom size NxN by creating ImageGeometry, initialising the
+# ImageData object with this geometry and empty array and finally put some
+# data into its array, and display as image.
+N = 1000
+ig = ImageGeometry(voxel_num_x=N,voxel_num_y=N)
+Phantom = ImageData(geometry=ig)
+
+x = Phantom.as_array()
+x[round(N/4):round(3*N/4),round(N/4):round(3*N/4)] = 0.5
+x[round(N/8):round(7*N/8),round(3*N/8):round(5*N/8)] = 1
+
+plt.imshow(x)
+plt.title('Phantom image')
+plt.show()
+
+# Identity operator for denoising
+I = TomoIdentity(ig)
+
+# Data and add noise
+y = I.direct(Phantom)
+y.array += 0.1*np.random.randn(N, N)
+
+plt.figure()
+plt.imshow(y.array)
+plt.title('Noisy image')
+plt.show()
+
+# Data fidelity term
+f_denoise = Norm2sq(I,y,c=0.5, memopt=True)
+
+# 1-norm regulariser
+lam1_denoise = 1.0
+g1_denoise = Norm1(lam1_denoise)
+
+# Initial guess
+x_init_denoise = ImageData(np.zeros((N,N)))
+opt = {'memopt': False, 'iter' : 50}
+# Combine with least squares and solve using generic FISTA implementation
+print ("no memopt")
+x_fista1_denoise, it1_denoise, timing1_denoise, \
+ criter1_denoise = FISTA(x_init_denoise, f_denoise, g1_denoise, opt=opt)
+opt = {'memopt': True, 'iter' : 50}
+print ("yes memopt")
+x_fista1_denoise_m, it1_denoise_m, timing1_denoise_m, \
+ criter1_denoise_m = FISTA(x_init_denoise, f_denoise, g1_denoise, opt=opt)
+
+print(x_fista1_denoise)
+print(criter1_denoise[-1])
+
+plt.figure()
+plt.imshow(x_fista1_denoise.as_array())
+plt.title('FISTA LS+1')
+plt.show()
+
+plt.figure()
+plt.imshow(x_fista1_denoise_m.as_array())
+plt.title('FISTA LS+1 memopt')
+plt.show()
+
+plt.figure()
+plt.loglog(iternum,criter1_denoise,label='FISTA LS+1')
+plt.loglog(iternum,criter1_denoise_m,label='FISTA LS+1 memopt')
+plt.legend()
+plt.show()
+#%%
+# Now denoise LS + 1-norm with FBPD
+x_fbpd1_denoise, itfbpd1_denoise, timingfbpd1_denoise, criterfbpd1_denoise = FBPD(x_init_denoise, None, f_denoise, g1_denoise)
+print(x_fbpd1_denoise)
+print(criterfbpd1_denoise[-1])
+
+plt.figure()
+plt.imshow(x_fbpd1_denoise.as_array())
+plt.title('FBPD LS+1')
+plt.show()
+
+
+# Now TV with FBPD
+lam_tv = 0.1
+gtv = TV2D(lam_tv)
+gtv(gtv.op.direct(x_init_denoise))
+
+opt_tv = {'tol': 1e-4, 'iter': 10000}
+
+x_fbpdtv_denoise, itfbpdtv_denoise, timingfbpdtv_denoise, criterfbpdtv_denoise = FBPD(x_init_denoise, None, f_denoise, gtv,opt=opt_tv)
+print(x_fbpdtv_denoise)
+print(criterfbpdtv_denoise[-1])
+
+plt.imshow(x_fbpdtv_denoise.as_array())
+plt.title('FBPD TV')
+plt.show()
diff --git a/Wrappers/Python/wip/demo_test_sirt.py b/Wrappers/Python/wip/old_demos/demo_test_sirt.py
index 6f5a44d..6f5a44d 100644
--- a/Wrappers/Python/wip/demo_test_sirt.py
+++ b/Wrappers/Python/wip/old_demos/demo_test_sirt.py
diff --git a/Wrappers/Python/wip/old_demos/multifile_nexus.py b/Wrappers/Python/wip/old_demos/multifile_nexus.py
new file mode 100755
index 0000000..d1ad438
--- /dev/null
+++ b/Wrappers/Python/wip/old_demos/multifile_nexus.py
@@ -0,0 +1,307 @@
+# -*- coding: utf-8 -*-
+"""
+Created on Wed Aug 15 16:00:53 2018
+
+@author: ofn77899
+"""
+
+import os
+from ccpi.io.reader import NexusReader
+
+from sys import getsizeof
+
+import matplotlib.pyplot as plt
+
+from ccpi.framework import DataProcessor, DataContainer
+from ccpi.processors import Normalizer
+from ccpi.processors import CenterOfRotationFinder
+import numpy
+
+
+class averager(object):
+ def __init__(self):
+ self.reset()
+
+ def reset(self):
+ self.N = 0
+ self.avg = 0
+ self.min = 0
+ self.max = 0
+ self.var = 0
+ self.ske = 0
+ self.kur = 0
+
+ def add_reading(self, val):
+
+ if (self.N == 0):
+ self.avg = val;
+ self.min = val;
+ self.max = val;
+ elif (self.N == 1):
+ #//set min/max
+ self.max = val if val > self.max else self.max
+ self.min = val if val < self.min else self.min
+
+
+ thisavg = (self.avg + val)/2
+ #// initial value is in avg
+ self.var = (self.avg - thisavg)*(self.avg-thisavg) + (val - thisavg) * (val-thisavg)
+ self.ske = self.var * (self.avg - thisavg)
+ self.kur = self.var * self.var
+ self.avg = thisavg
+ else:
+ self.max = val if val > self.max else self.max
+ self.min = val if val < self.min else self.min
+
+ M = self.N
+
+ #// b-factor =(<v>_N + v_(N+1)) / (N+1)
+ #float b = (val -avg) / (M+1);
+ b = (val -self.avg) / (M+1)
+
+ self.kur = self.kur + (M *(b*b*b*b) * (1+M*M*M))- (4* b * self.ske) + (6 * b *b * self.var * (M-1))
+
+ self.ske = self.ske + (M * b*b*b *(M-1)*(M+1)) - (3 * b * self.var * (M-1))
+
+ #//var = var * ((M-1)/M) + ((val - avg)*(val - avg)/(M+1)) ;
+ self.var = self.var * ((M-1)/M) + (b * b * (M+1))
+
+ self.avg = self.avg * (M/(M+1)) + val / (M+1)
+
+ self.N += 1
+
+ def stats(self, vector):
+ i = 0
+ while i < vector.size:
+ self.add_reading(vector[i])
+ i+=1
+
+avg = averager()
+a = numpy.linspace(0,39,40)
+avg.stats(a)
+print ("average" , avg.avg, a.mean())
+print ("variance" , avg.var, a.var())
+b = a - a.mean()
+b *= b
+b = numpy.sqrt(sum(b)/(a.size-1))
+print ("std" , numpy.sqrt(avg.var), b)
+#%%
+
+class DataStatMoments(DataProcessor):
+ '''Normalization based on flat and dark
+
+ This processor read in a AcquisitionData and normalises it based on
+ the instrument reading with and without incident photons or neutrons.
+
+ Input: AcquisitionData
+ Parameter: 2D projection with flat field (or stack)
+ 2D projection with dark field (or stack)
+ Output: AcquisitionDataSetn
+ '''
+
+ def __init__(self, axis, skewness=False, kurtosis=False, offset=0):
+ kwargs = {
+ 'axis' : axis,
+ 'skewness' : skewness,
+ 'kurtosis' : kurtosis,
+ 'offset' : offset,
+ }
+ #DataProcessor.__init__(self, **kwargs)
+ super(DataStatMoments, self).__init__(**kwargs)
+
+
+ def check_input(self, dataset):
+ #self.N = dataset.get_dimension_size(self.axis)
+ return True
+
+ @staticmethod
+ def add_sample(dataset, N, axis, stats=None, offset=0):
+ #dataset = self.get_input()
+ if (N == 0):
+ # get the axis number along to calculate the stats
+
+
+ #axs = dataset.get_dimension_size(self.axis)
+ # create a placeholder for the output
+ if stats is None:
+ ax = dataset.get_dimension_axis(axis)
+ shape = [dataset.shape[i] for i in range(len(dataset.shape)) if i != ax]
+ # output has 4 components avg, std, skewness and kurtosis + last avg+ (val-thisavg)
+ shape.insert(0, 4+2)
+ stats = numpy.zeros(shape)
+
+ stats[0] = dataset.subset(**{axis:N-offset}).array[:]
+
+ #avg = val
+ elif (N == 1):
+ # val
+ stats[5] = dataset.subset(**{axis:N-offset}).array
+ stats[4] = stats[0] + stats[5]
+ stats[4] /= 2 # thisavg
+ stats[5] -= stats[4] # (val - thisavg)
+
+ #float thisavg = (avg + val)/2;
+
+ #// initial value is in avg
+ #var = (avg - thisavg)*(avg-thisavg) + (val - thisavg) * (val-thisavg);
+ stats[1] = stats[5] * stats[5] + stats[5] * stats[5]
+ #skewness = var * (avg - thisavg);
+ stats[2] = stats[1] * stats[5]
+ #kurtosis = var * var;
+ stats[3] = stats[1] * stats[1]
+ #avg = thisavg;
+ stats[0] = stats[4]
+ else:
+
+ #float M = (float)N;
+ M = N
+ #val
+ stats[4] = dataset.subset(**{axis:N-offset}).array
+ #// b-factor =(<v>_N + v_(N+1)) / (N+1)
+ stats[5] = stats[4] - stats[0]
+ stats[5] /= (M+1) # b factor
+ #float b = (val -avg) / (M+1);
+
+ #kurtosis = kurtosis + (M *(b*b*b*b) * (1+M*M*M))- (4* b * skewness) + (6 * b *b * var * (M-1));
+ #if self.kurtosis:
+ # stats[3] += (M * stats[5] * stats[5] * stats[5] * stats[5]) - \
+ # (4 * stats[5] * stats[2]) + \
+ # (6 * stats[5] * stats[5] * stats[1] * (M-1))
+
+ #skewness = skewness + (M * b*b*b *(M-1)*(M+1)) - (3 * b * var * (M-1));
+ #if self.skewness:
+ # stats[2] = stats[2] + (M * stats[5]* stats[5] * stats[5] * (M-1)*(M-1) ) -\
+ # 3 * stats[5] * stats[1] * (M-1)
+ #//var = var * ((M-1)/M) + ((val - avg)*(val - avg)/(M+1)) ;
+ #var = var * ((M-1)/M) + (b * b * (M+1));
+ stats[1] = ((M-1)/M) * stats[1] + (stats[5] * stats[5] * (M+1))
+
+ #avg = avg * (M/(M+1)) + val / (M+1)
+ stats[0] = stats[0] * (M/(M+1)) + stats[4] / (M+1)
+
+ N += 1
+ return stats , N
+
+
+ def process(self):
+
+ data = self.get_input()
+
+ #stats, i = add_sample(0)
+ N = data.get_dimension_size(self.axis)
+ ax = data.get_dimension_axis(self.axis)
+ stats = None
+ i = 0
+ while i < N:
+ stats , i = DataStatMoments.add_sample(data, i, self.axis, stats, offset=self.offset)
+ self.offset += N
+ labels = ['StatisticalMoments']
+
+ labels += [data.dimension_labels[i] \
+ for i in range(len(data.dimension_labels)) if i != ax]
+ y = DataContainer( stats[:4] , False,
+ dimension_labels=labels)
+ return y
+
+directory = r'E:\Documents\Dataset\CCPi\Nexus_test'
+data_path="entry1/instrument/pco1_hw_hdf_nochunking/data"
+
+reader = NexusReader(os.path.join( os.path.abspath(directory) , '74331.nxs'))
+
+print ("read flat")
+read_flat = NexusReader(os.path.join( os.path.abspath(directory) , '74240.nxs'))
+read_flat.data_path = data_path
+flatsslice = read_flat.get_acquisition_data_whole()
+avg = DataStatMoments('angle')
+
+avg.set_input(flatsslice)
+flats = avg.get_output()
+
+ave = averager()
+ave.stats(flatsslice.array[:,0,0])
+
+print ("avg" , ave.avg, flats.array[0][0][0])
+print ("var" , ave.var, flats.array[1][0][0])
+
+print ("read dark")
+read_dark = NexusReader(os.path.join( os.path.abspath(directory) , '74243.nxs'))
+read_dark.data_path = data_path
+
+## darks are very many so we proceed in batches
+total_size = read_dark.get_projection_dimensions()[0]
+print ("total_size", total_size)
+
+batchsize = 40
+if batchsize > total_size:
+ batchlimits = [batchsize * (i+1) for i in range(int(total_size/batchsize))] + [total_size-1]
+else:
+ batchlimits = [total_size]
+#avg.N = 0
+avg.offset = 0
+N = 0
+for batch in range(len(batchlimits)):
+ print ("running batch " , batch)
+ bmax = batchlimits[batch]
+ bmin = bmax-batchsize
+
+ darksslice = read_dark.get_acquisition_data_batch(bmin,bmax)
+ if batch == 0:
+ #create stats
+ ax = darksslice.get_dimension_axis('angle')
+ shape = [darksslice.shape[i] for i in range(len(darksslice.shape)) if i != ax]
+ # output has 4 components avg, std, skewness and kurtosis + last avg+ (val-thisavg)
+ shape.insert(0, 4+2)
+ print ("create stats shape ", shape)
+ stats = numpy.zeros(shape)
+ print ("N" , N)
+ #avg.set_input(darksslice)
+ i = bmin
+ while i < bmax:
+ stats , i = DataStatMoments.add_sample(darksslice, i, 'angle', stats, bmin)
+ print ("{0}-{1}-{2}".format(bmin, i , bmax ) )
+
+darks = stats
+#%%
+
+fig = plt.subplot(2,2,1)
+fig.imshow(flats.subset(StatisticalMoments=0).array)
+fig = plt.subplot(2,2,2)
+fig.imshow(numpy.sqrt(flats.subset(StatisticalMoments=1).array))
+fig = plt.subplot(2,2,3)
+fig.imshow(darks[0])
+fig = plt.subplot(2,2,4)
+fig.imshow(numpy.sqrt(darks[1]))
+plt.show()
+
+
+#%%
+norm = Normalizer(flat_field=flats.array[0,200,:], dark_field=darks[0,200,:])
+#norm.set_flat_field(flats.array[0,200,:])
+#norm.set_dark_field(darks.array[0,200,:])
+norm.set_input(reader.get_acquisition_data_slice(200))
+
+n = Normalizer.normalize_projection(norm.get_input().as_array(), flats.array[0,200,:], darks[0,200,:], 1e-5)
+#dn_n= Normalizer.estimate_normalised_error(norm.get_input().as_array(), flats.array[0,200,:], darks[0,200,:],
+# numpy.sqrt(flats.array[1,200,:]), numpy.sqrt(darks[1,200,:]))
+#%%
+
+
+#%%
+fig = plt.subplot(2,1,1)
+
+
+fig.imshow(norm.get_input().as_array())
+fig = plt.subplot(2,1,2)
+fig.imshow(n)
+
+#fig = plt.subplot(3,1,3)
+#fig.imshow(dn_n)
+
+
+plt.show()
+
+
+
+
+
+
diff --git a/Wrappers/Python/wip/pdhg_TV_denoising_precond.py b/Wrappers/Python/wip/pdhg_TV_denoising_precond.py
new file mode 100644
index 0000000..3fc9320
--- /dev/null
+++ b/Wrappers/Python/wip/pdhg_TV_denoising_precond.py
@@ -0,0 +1,156 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Created on Fri Feb 22 14:53:03 2019
+
+@author: evangelos
+"""
+
+from ccpi.framework import ImageData, ImageGeometry, BlockDataContainer
+
+import numpy as np
+import matplotlib.pyplot as plt
+
+from ccpi.optimisation.algorithms import PDHG, PDHG_old
+
+from ccpi.optimisation.operators import BlockOperator, Identity, Gradient
+from ccpi.optimisation.functions import ZeroFun, L2NormSquared, \
+ MixedL21Norm, FunctionOperatorComposition, BlockFunction, ScaledFunction
+
+from skimage.util import random_noise
+
+
+
+# ############################################################################
+# Create phantom for TV denoising
+
+N = 100
+data = np.zeros((N,N))
+data[round(N/4):round(3*N/4),round(N/4):round(3*N/4)] = 0.5
+data[round(N/8):round(7*N/8),round(3*N/8):round(5*N/8)] = 1
+
+ig = ImageGeometry(voxel_num_x = N, voxel_num_y = N)
+ag = ig
+
+# Create noisy data. Add Gaussian noise
+n1 = random_noise(data, mode='gaussian', seed=10)
+noisy_data = ImageData(n1)
+
+
+#%%
+
+# Regularisation Parameter
+alpha = 2
+
+#method = input("Enter structure of PDHG (0=Composite or 1=NotComposite): ")
+method = '0'
+if method == '0':
+
+ # Create operators
+ op1 = Gradient(ig)
+ op2 = Identity(ig, ag)
+
+ # Form Composite Operator
+ operator = BlockOperator(op1, op2, shape=(2,1) )
+
+ #### Create functions
+# f = FunctionComposition_new(operator, mixed_L12Norm(alpha), \
+# L2NormSq(0.5, b = noisy_data) )
+
+ f1 = alpha * MixedL21Norm()
+ f2 = 0.5 * L2NormSquared(b = noisy_data)
+
+ f = BlockFunction(f1, f2 )
+ g = ZeroFun()
+
+else:
+
+ ###########################################################################
+ # No Composite #
+ ###########################################################################
+ operator = Gradient(ig)
+ f = alpha * FunctionOperatorComposition(operator, MixedL21Norm())
+ g = 0.5 * L2NormSquared(b = noisy_data)
+ ###########################################################################
+#%%
+
+diag_precon = True
+
+if diag_precon:
+
+ def tau_sigma_precond(operator):
+
+ tau = 1/operator.sum_abs_row()
+ sigma = 1/ operator.sum_abs_col()
+
+ return tau, sigma
+
+ tau, sigma = tau_sigma_precond(operator)
+
+else:
+ # Compute operator Norm
+ normK = operator.norm()
+ print ("normK", normK)
+ # Primal & dual stepsizes
+ sigma = 1/normK
+ tau = 1/normK
+# tau = 1/(sigma*normK**2)
+
+#%%
+
+opt = {'niter':2000}
+
+res, time, primal, dual, pdgap = PDHG_old(f, g, operator, tau = tau, sigma = sigma, opt = opt)
+
+plt.figure(figsize=(5,5))
+plt.imshow(res.as_array())
+plt.colorbar()
+plt.show()
+
+#aaa = res[0].as_array()
+#
+#plt.imshow(aaa)
+#plt.colorbar()
+#plt.show()
+#c2 = aaa
+#del aaa
+#%%
+
+#c2 = aaa
+##%%
+#%%
+#z = c1 - c2
+#plt.imshow(np.abs(z[0:95,0:95]))
+#plt.colorbar()
+
+#%%
+#pdhg = PDHG(f=f,g=g,operator=operator, tau=tau, sigma=sigma)
+#pdhg.max_iteration = 2000
+#pdhg.update_objective_interval = 10
+#
+#pdhg.run(2000)
+#
+#
+#
+#sol = pdhg.get_output().as_array()
+##sol = result.as_array()
+##
+#fig = plt.figure()
+#plt.subplot(1,2,1)
+#plt.imshow(noisy_data.as_array())
+##plt.colorbar()
+#plt.subplot(1,2,2)
+#plt.imshow(sol)
+##plt.colorbar()
+#plt.show()
+##
+#
+###
+#plt.plot(np.linspace(0,N,N), data[int(N/2),:], label = 'GTruth')
+#plt.plot(np.linspace(0,N,N), sol[int(N/2),:], label = 'Recon')
+#plt.legend()
+#plt.show()
+#
+
+#%%
+#