diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml
index 2ed4960d..3ffcb6f4 100644
--- a/.github/workflows/ci-build.yml
+++ b/.github/workflows/ci-build.yml
@@ -53,6 +53,7 @@ jobs:
python -m pip install -r develop.txt
python -m pip install -r docs/requirements.txt
python -m pip install astropy scikit-image scikit-learn
+ python -m pip install tensorflow>=2.4.1
python -m pip install twine
python -m pip install .
diff --git a/MANIFEST.in b/MANIFEST.in
index 9a2f374e..74db0634 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -3,3 +3,4 @@ include develop.txt
include docs/requirements.txt
include README.rst
include LICENSE.txt
+include docs/source/modopt_logo.png
diff --git a/README.md b/README.md
index aa72d976..0f7501f0 100644
--- a/README.md
+++ b/README.md
@@ -58,6 +58,7 @@ Note that none of these are required for running on a CPU.
* [CuPy](https://cupy.dev/)
* [Torch](https://pytorch.org/)
+* [TensorFlow](https://www.tensorflow.org/)
## Citation
diff --git a/docs/source/dependencies.rst b/docs/source/dependencies.rst
index 782807f8..2a513158 100644
--- a/docs/source/dependencies.rst
+++ b/docs/source/dependencies.rst
@@ -82,6 +82,7 @@ For GPU compliance the following packages can also be installed:
* |link-to-cupy|
* |link-to-torch|
+* |link-to-tf|
.. |link-to-cupy| raw:: html
@@ -93,6 +94,11 @@ For GPU compliance the following packages can also be installed:
Torch
+.. |link-to-tf| raw:: html
+
+ TensorFlow
+
.. note::
Note that none of these are required for running on a CPU.
diff --git a/docs/source/index.rst b/docs/source/index.rst
index 8262d43d..238aa5b6 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -12,8 +12,8 @@ ModOpt Documentation
.. include:: toc.rst
:Author: Samuel Farrens `(samuel.farrens@cea.fr) `_
-:Version: 1.5.0
-:Release Date: 31/03/2021
+:Version: 1.5.1
+:Release Date: 22/04/2021
:Repository: |link-to-repo|
.. |link-to-repo| raw:: html
diff --git a/modopt/base/backend.py b/modopt/base/backend.py
index 1f5a7a3a..5fbe912f 100644
--- a/modopt/base/backend.py
+++ b/modopt/base/backend.py
@@ -8,35 +8,73 @@
"""
-import warnings
from importlib import util
import numpy as np
+from modopt.interface.errors import warn
+
try:
import torch
+ from torch.utils.dlpack import from_dlpack as torch_from_dlpack
+ from torch.utils.dlpack import to_dlpack as torch_to_dlpack
+
except ImportError: # pragma: no cover
import_torch = False
else:
import_torch = True
# Handle the compatibility with variable
-gpu_compatibility = {
- 'cupy': False,
- 'cupy-cudnn': False,
+LIBRARIES = {
+ 'cupy': None,
+ 'tensorflow': None,
+ 'numpy': np,
}
if util.find_spec('cupy') is not None:
try:
import cupy as cp
- gpu_compatibility['cupy'] = True
+ LIBRARIES['cupy'] = cp
+ except ImportError:
+ pass
- if util.find_spec('cupy.cuda.cudnn') is not None:
- gpu_compatibility['cupy-cudnn'] = True
+if util.find_spec('tensorflow') is not None:
+ try:
+ from tensorflow.experimental import numpy as tnp
+ LIBRARIES['tensorflow'] = tnp
except ImportError:
pass
+def get_backend(backend):
+ """Get backend.
+
+ Returns the backend module for input specified by string
+
+ Parameters
+ ----------
+ backend: str
+ String holding the backend name. One of `tensorflow`,
+ `numpy` or `cupy`.
+
+ Returns
+ -------
+ tuple
+ Returns the module for carrying out calculations and the actual backend
+ that was reverted towards. If the right libraries are not installed,
+ the function warns and reverts to `numpy` backend
+ """
+ if backend not in LIBRARIES.keys() or LIBRARIES[backend] is None:
+ msg = (
+ '{0} backend not possible, please ensure that '
+ + 'the optional libraries are installed.\n'
+ + 'Reverting to numpy'
+ )
+ warn(msg.format(backend))
+ backend = 'numpy'
+ return LIBRARIES[backend], backend
+
+
def get_array_module(input_data):
"""Get Array Module.
@@ -54,48 +92,47 @@ def get_array_module(input_data):
The numpy or cupy module
"""
- if gpu_compatibility['cupy']:
- return cp.get_array_module(input_data)
-
+ if LIBRARIES['tensorflow'] is not None:
+ if isinstance(input_data, LIBRARIES['tensorflow'].ndarray):
+ return LIBRARIES['tensorflow']
+ if LIBRARIES['cupy'] is not None:
+ if isinstance(input_data, LIBRARIES['cupy'].ndarray):
+ return LIBRARIES['cupy']
return np
-def move_to_device(input_data):
+def change_backend(input_data, backend='cupy'):
"""Move data to device.
- This method moves data from CPU to GPU if we have the
- compatibility to do so. It returns the same data if
- it is already on GPU.
+ This method changes the backend of an array
+ This can be used to copy data to GPU or to CPU
Parameters
----------
input_data : numpy.ndarray or cupy.ndarray
Input data array to be moved
+ backend: str, optional
+ The backend to use, one among `tensorflow`, `cupy` and
+ `numpy`. Default is `cupy`.
Returns
-------
- cupy.ndarray
- The CuPy array residing on GPU
+ backend.ndarray
+ An ndarray of specified backend
"""
xp = get_array_module(input_data)
-
- if xp == cp:
+ txp, target_backend = get_backend(backend)
+ if xp == txp:
return input_data
-
- if gpu_compatibility['cupy']:
- return cp.array(input_data)
-
- warnings.warn('Cupy is not installed, cannot move data to GPU')
-
- return input_data
+ return txp.array(input_data)
def move_to_cpu(input_data):
"""Move data to CPU.
- This method moves data from GPU to CPU.It returns the same data if it is
- already on CPU.
+ This method moves data from GPU to CPU.
+ It returns the same data if it is already on CPU.
Parameters
----------
@@ -107,13 +144,20 @@ def move_to_cpu(input_data):
numpy.ndarray
The NumPy array residing on CPU
+ Raises
+ ------
+ ValueError
+ if the input does not correspond to any array
"""
xp = get_array_module(input_data)
- if xp == np:
+ if xp == LIBRARIES['numpy']:
return input_data
-
- return input_data.get()
+ elif xp == LIBRARIES['cupy']:
+ return input_data.get()
+ elif xp == LIBRARIES['tensorflow']:
+ return input_data.data.numpy()
+ raise ValueError('Cannot identify the array type.')
def convert_to_tensor(input_data):
@@ -150,7 +194,7 @@ def convert_to_tensor(input_data):
if xp == np:
return torch.Tensor(input_data)
- return torch.utils.dlpack.from_dlpack(input_data.toDlpack()).float()
+ return torch_from_dlpack(input_data.toDlpack()).float()
def convert_to_cupy_array(input_data):
@@ -182,6 +226,6 @@ def convert_to_cupy_array(input_data):
)
if input_data.is_cuda:
- return cp.fromDlpack(torch.utils.dlpack.to_dlpack(input_data))
+ return cp.fromDlpack(torch_to_dlpack(input_data))
return input_data.detach().numpy()
diff --git a/modopt/math/matrix.py b/modopt/math/matrix.py
index cb54cebc..be737f52 100644
--- a/modopt/math/matrix.py
+++ b/modopt/math/matrix.py
@@ -12,12 +12,7 @@
import numpy as np
-from modopt.base.backend import get_array_module
-
-try:
- import cupy as cp
-except ImportError: # pragma: no cover
- pass
+from modopt.base.backend import get_array_module, get_backend
def gram_schmidt(matrix, return_opt='orthonormal'):
@@ -303,7 +298,7 @@ def __init__(
data_shape,
data_type=float,
auto_run=True,
- use_gpu=False,
+ compute_backend='numpy',
verbose=False,
):
@@ -311,10 +306,9 @@ def __init__(
self._data_shape = data_shape
self._data_type = data_type
self._verbose = verbose
- if use_gpu:
- self.xp = cp
- else:
- self.xp = np
+ xp, compute_backend = get_backend(compute_backend)
+ self.xp = xp
+ self.compute_backend = compute_backend
if auto_run:
self.get_spec_rad()
diff --git a/modopt/opt/algorithms.py b/modopt/opt/algorithms.py
index ccd8fe21..125ac84c 100644
--- a/modopt/opt/algorithms.py
+++ b/modopt/opt/algorithms.py
@@ -54,11 +54,6 @@
from modopt.opt.cost import costObj
from modopt.opt.linear import Identity
-try:
- import cupy as cp
-except ImportError: # pragma: no cover
- pass
-
class SetUp(Observable):
r"""Algorithm Set-Up.
@@ -92,7 +87,7 @@ def __init__(
verbose=False,
progress=True,
step_size=None,
- use_gpu=False,
+ compute_backend='numpy',
**dummy_kwargs,
):
@@ -123,20 +118,9 @@ def __init__(
)
self.add_observer('cv_metrics', observer)
- # Check for GPU
- if use_gpu:
- if backend.gpu_compatibility['cupy']:
- self.xp = cp
- else:
- warn(
- 'CuPy is not installed, cannot run on GPU!'
- + 'Running optimization on CPU.',
- )
- self.xp = np
- use_gpu = False
- else:
- self.xp = np
- self.use_gpu = use_gpu
+ xp, compute_backend = backend.get_backend(compute_backend)
+ self.xp = xp
+ self.compute_backend = compute_backend
@property
def metrics(self):
@@ -148,7 +132,9 @@ def metrics(self, metrics):
if isinstance(metrics, type(None)):
self._metrics = {}
- elif not isinstance(metrics, dict):
+ elif isinstance(metrics, dict):
+ self._metrics = metrics
+ else:
raise TypeError(
'Metrics must be a dictionary, not {0}.'.format(type(metrics)),
)
@@ -184,10 +170,10 @@ def copy_data(self, input_data):
Copy of input data
"""
- if self.use_gpu:
- return backend.move_to_device(input_data)
-
- return self.xp.copy(input_data)
+ return self.xp.copy(backend.change_backend(
+ input_data,
+ self.compute_backend,
+ ))
def _check_input_data(self, input_data):
"""Check input data type.
@@ -205,8 +191,10 @@ def _check_input_data(self, input_data):
For invalid input type
"""
- if not isinstance(input_data, self.xp.ndarray):
- raise TypeError('Input data must be a numpy array.')
+ if not (isinstance(input_data, (self.xp.ndarray, np.ndarray))):
+ raise TypeError(
+ 'Input data must be a numpy array or backend array',
+ )
def _check_param(self, param_val):
"""Check algorithm parameters.
@@ -779,8 +767,8 @@ def _update(self):
self._z_new = self._x_new
# Update old values for next iteration.
- self.xp.copyto(self._x_old, self._x_new)
- self.xp.copyto(self._z_old, self._z_new)
+ self._x_old = self.xp.copy(self._x_new)
+ self._z_old = self.xp.copy(self._z_new)
# Update parameter values for next iteration.
self._update_param()
@@ -789,7 +777,7 @@ def _update(self):
if self._cost_func:
self.converge = (
self.any_convergence_flag()
- or self._cost_func.get_cost(self._x_new),
+ or self._cost_func.get_cost(self._x_new)
)
def iterate(self, max_iter=150):
@@ -1548,7 +1536,7 @@ def _update(self):
if self._cost_func:
self.converge = (
self.any_convergence_flag()
- or self._cost_func.get_cost(self._x_new),
+ or self._cost_func.get_cost(self._x_new)
)
def iterate(self, max_iter=150):
diff --git a/modopt/tests/test_base.py b/modopt/tests/test_base.py
index 26e1e4ea..873a4506 100644
--- a/modopt/tests/test_base.py
+++ b/modopt/tests/test_base.py
@@ -9,12 +9,14 @@
"""
from builtins import range
-from unittest import TestCase
+from unittest import TestCase, skipIf
import numpy as np
import numpy.testing as npt
from modopt.base import np_adjust, transform, types
+from modopt.base.backend import (LIBRARIES, change_backend, get_array_module,
+ get_backend)
class NPAdjustTestCase(TestCase):
@@ -275,3 +277,53 @@ def test_check_npndarray(self):
self.data3,
dtype=np.integer,
)
+
+
+class TestBackend(TestCase):
+ """Test the backend codes."""
+
+ def setUp(self):
+ """Set test parameter values."""
+ self.input = np.array([10, 10])
+
+ @skipIf(LIBRARIES['tensorflow'] is None, 'tensorflow library not installed')
+ def test_tf_backend(self):
+ """Test tensorflow backend."""
+ xp, backend = get_backend('tensorflow')
+ if backend != 'tensorflow' or xp != LIBRARIES['tensorflow']:
+ raise AssertionError('tensorflow get_backend fails!')
+ tf_input = change_backend(self.input, 'tensorflow')
+ if (
+ get_array_module(LIBRARIES['tensorflow'].ones(1)) != LIBRARIES['tensorflow']
+ or get_array_module(tf_input) != LIBRARIES['tensorflow']
+ ):
+ raise AssertionError('tensorflow backend fails!')
+
+ @skipIf(LIBRARIES['cupy'] is None, 'cupy library not installed')
+ def test_cp_backend(self):
+ """Test cupy backend."""
+ xp, backend = get_backend('cupy')
+ if backend != 'cupy' or xp != LIBRARIES['cupy']:
+ raise AssertionError('cupy get_backend fails!')
+ cp_input = change_backend(self.input, 'cupy')
+ if (
+ get_array_module(LIBRARIES['cupy'].ones(1)) != LIBRARIES['cupy']
+ or get_array_module(cp_input) != LIBRARIES['cupy']
+ ):
+ raise AssertionError('cupy backend fails!')
+
+ def test_np_backend(self):
+ """Test numpy backend."""
+ xp, backend = get_backend('numpy')
+ if backend != 'numpy' or xp != LIBRARIES['numpy']:
+ raise AssertionError('numpy get_backend fails!')
+ np_input = change_backend(self.input, 'numpy')
+ if (
+ get_array_module(LIBRARIES['numpy'].ones(1)) != LIBRARIES['numpy']
+ or get_array_module(np_input) != LIBRARIES['numpy']
+ ):
+ raise AssertionError('numpy backend fails!')
+
+ def tearDown(self):
+ """Tear Down of objects."""
+ self.input = None
diff --git a/modopt/tests/test_opt.py b/modopt/tests/test_opt.py
index b6805006..3c33c948 100644
--- a/modopt/tests/test_opt.py
+++ b/modopt/tests/test_opt.py
@@ -58,6 +58,17 @@ def setUp(self):
reweight_inst = reweight.cwbReweight(self.data3)
cost_inst = cost.costObj([grad_inst, prox_inst, prox_dual_inst])
self.setup = algorithms.SetUp()
+ self.max_iter = 20
+
+ self.fb_all_iter = algorithms.ForwardBackward(
+ self.data1,
+ grad=grad_inst,
+ prox=prox_inst,
+ cost=None,
+ auto_iterate=False,
+ beta_update=func_identity,
+ )
+ self.fb_all_iter.iterate(self.max_iter)
self.fb1 = algorithms.ForwardBackward(
self.data1,
@@ -110,6 +121,17 @@ def setUp(self):
s_greedy=1.1,
)
+ self.gfb_all_iter = algorithms.GenForwardBackward(
+ self.data1,
+ grad=grad_inst,
+ prox_list=[prox_inst, prox_dual_inst],
+ cost=None,
+ auto_iterate=False,
+ gamma_update=func_identity,
+ beta_update=func_identity,
+ )
+ self.gfb_all_iter.iterate(self.max_iter)
+
self.gfb1 = algorithms.GenForwardBackward(
self.data1,
grad=grad_inst,
@@ -133,6 +155,20 @@ def setUp(self):
step_size=2,
)
+ self.condat_all_iter = algorithms.Condat(
+ self.data1,
+ self.data2,
+ grad=grad_inst,
+ prox=prox_inst,
+ cost=None,
+ prox_dual=prox_dual_inst,
+ sigma_update=func_identity,
+ tau_update=func_identity,
+ rho_update=func_identity,
+ auto_iterate=False,
+ )
+ self.condat_all_iter.iterate(self.max_iter)
+
self.condat1 = algorithms.Condat(
self.data1,
self.data2,
@@ -166,6 +202,18 @@ def setUp(self):
auto_iterate=False,
)
+ self.pogm_all_iter = algorithms.POGM(
+ u=self.data1,
+ x=self.data1,
+ y=self.data1,
+ z=self.data1,
+ grad=grad_inst,
+ prox=prox_inst,
+ auto_iterate=False,
+ cost=None,
+ )
+ self.pogm_all_iter.iterate(self.max_iter)
+
self.pogm1 = algorithms.POGM(
u=self.data1,
x=self.data1,
@@ -184,13 +232,18 @@ def tearDown(self):
self.data1 = None
self.data2 = None
self.setup = None
+ self.fb_all_iter = None
self.fb1 = None
self.fb2 = None
+ self.gfb_all_iter = None
self.gfb1 = None
self.gfb2 = None
+ self.condat_all_iter = None
self.condat1 = None
self.condat2 = None
self.condat3 = None
+ self.pogm1 = None
+ self.pogm_all_iter = None
self.dummy = None
def test_set_up(self):
@@ -201,6 +254,17 @@ def test_set_up(self):
npt.assert_raises(TypeError, self.setup._check_param_update, 1)
+ def test_all_iter(self):
+ """Test if all opt run for all iterations."""
+ opts = [
+ self.fb_all_iter,
+ self.gfb_all_iter,
+ self.condat_all_iter,
+ self.pogm_all_iter,
+ ]
+ for opt in opts:
+ npt.assert_equal(opt.idx, self.max_iter - 1)
+
def test_forward_backward(self):
"""Test forward_backward."""
npt.assert_array_equal(
diff --git a/setup.cfg b/setup.cfg
index 56a40d12..d2f544f0 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -26,7 +26,8 @@ per-file-ignores =
#Justification: Needed for keeping package version and current API
*__init__.py*: F401,F403,WPS347,WPS410,WPS412
#Todo: Rethink conditional imports
- modopt/base/backend.py: WPS229, WPS420
+ #Todo: How can we bypass mutable constants?
+ modopt/base/backend.py: WPS229, WPS420, WPS407
#Todo: Rethink conditional imports
modopt/base/observable.py: WPS420,WPS604
#Todo: Check string for log formatting
@@ -54,6 +55,8 @@ per-file-ignores =
modopt/signal/wavelet.py: S404,S603
#Todo: Clean up tests
modopt/tests/*.py: E731,F401,WPS301,WPS420,WPS425,WPS437,WPS604
+ #Todo: Import has bad parenthesis
+ modopt/tests/test_base.py: WPS318,WPS319,E501,WPS301
#WPS Settings
max-arguments = 25
max-attributes = 40
diff --git a/setup.py b/setup.py
index 92040f6d..841ca1b1 100644
--- a/setup.py
+++ b/setup.py
@@ -7,7 +7,7 @@
# Set the package release version
major = 1
minor = 5
-patch = 0
+patch = 1
# Set the package details
name = 'modopt'
@@ -29,9 +29,10 @@
os_str = 'Operating System :: {0}'
classifiers = (
- [lc_str.format(license)] + [ln_str] +
- [py_str.format(ver) for ver in python_versions_supported] +
- [os_str.format(ops) for ops in os_platforms_supported]
+ [lc_str.format(license)]
+ + [ln_str]
+ + [py_str.format(ver) for ver in python_versions_supported]
+ + [os_str.format(ops) for ops in os_platforms_supported]
)
# Source package description from README.md