diff --git a/README.md b/README.md index e30fa380f42..99b960a4a5f 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,6 @@ For more details and an introduction into the inner workings of ODL, please refe Highlights ========== -- Well-tested data containers based on NumPy or CUDA allow high performance computing with minimal effort. - A versatile and efficient library of optimization routines for smooth and non-smooth problems, such as CGLS, BFGS, PDHG and Douglas-Rachford splitting. - Support for tomographic imaging with a unified geometry representation and bindings to external libraries for efficient computation of projections and back-projections. - And much more, including support for deep learning libraries, figures of merits, phantom generation, data handling, etc. diff --git a/doc/source/getting_started/about_odl.rst b/doc/source/getting_started/about_odl.rst index 4e1e8d3045f..1d6497ffa84 100644 --- a/doc/source/getting_started/about_odl.rst +++ b/doc/source/getting_started/about_odl.rst @@ -16,182 +16,6 @@ The main advantages of this approach is that 3. Solvers and application-specific code need to be written only once, in one place, and can be tested individually. 4. Adding new applications or solution methods becomes a much easier task. -ODL implements many abstract mathematical notions such as sets, vector spaces and operators. -In the following, a few are shown by example. - -Set -=== - -A `Set` is the fundamental building block of ODL objects. It mirrors the mathematical concept of a `set`_ in that it can tell if an object belongs to it or not: - -.. code-block:: python - - >>> interv = odl.IntervalProd(0, 1) - >>> 0.5 in interv - True - >>> 2.0 in interv - False - -The most commonly used sets in ODL are `RealNumbers` (set of all `real numbers`_) and `IntervalProd` ("Interval product", `rectangular boxes`_ of arbitrary dimension). - - -LinearSpace -=========== - -The `LinearSpace` class is the most important subclass of `Set`. -It is a general (abstract) implementation of a mathematical `vector space`_ and has a couple of widely used concrete realizations. - -Spaces of n-tuples -~~~~~~~~~~~~~~~~~~ - -Large parts of basic functionality, e.g. arithmetic or inner products, rest on array computations, i.e. computations on tuples of elements of the same kind. -Typically, these vector spaces are of the type :math:`\mathbb{F}^n`, where :math:`\mathbb{F}` is a `field`_ (usually :math:`\mathbb{R}` or :math:`\mathbb{C}`), and :math:`n` a positive integer. -Example: - -.. code-block:: python - - >>> c3 = odl.cn(3) - >>> u = c3.element([1 + 1j, 2 - 2j, 3]) - >>> v = c3.one() # vector of all ones - >>> u.inner(v) # sum of the elements - (6-1j) - -Function spaces -~~~~~~~~~~~~~~~ - -A `function space`_ is a set of functions :math:`f: \mathcal{X} \to \mathcal{Y}` with fixed domain and range (more accurately: `codomain`_), where :math:`\mathcal{Y}` is a vector space. -The ODL implementation `FunctionSpace` covers only the cases :math:`\mathcal{Y} = \mathbb{R}` or :math:`\mathbb{C}` since the general case has large overlaps with `Operator`. -Note that we do not make a distinction between different types of function spaces with respect to regularity, integrability etc. on an *abstract* level since there is no obvious way to check it. - -As linear spaces, function spaces support some interesting operations: - -.. code-block:: python - - >>> import numpy as np - >>> space = odl.FunctionSpace(odl.IntervalProd(0, 2)) - >>> exp = space.element(np.exp) - >>> exp(np.log(2)) - 2.0 - >>> exp_plus_one = exp + space.one() - >>> exp_plus_one(np.log(2)) - 3.0 - >>> ratio_func = exp_plus_one / exp # x -> (exp(x) + 1) / exp(x) - >>> ratio_func(np.log(2)) # 3 / 2 - 1.5 - -A big advantage of the function space implementation in ODL is that the evaluation of functions is `vectorized`_, i.e. that the values of a function can be computed from an array of input data "at once", without looping in Python (which is slow, in general). -What follows is a simple example, see the :ref:`vectorization_in_depth` guide for instructions on how to write vectorization-compatible functions. - -.. code-block:: python - - >>> import numpy as np - >>> space = odl.FunctionSpace(odl.IntervalProd(0, 2)) - >>> exp = space.element(np.exp) - >>> exp([0, 1, 2]) - array([ 1. , 2.71828183, 7.3890561 ]) - >>> x = np.linspace(0, 2, 1000) - >>> y = exp(x) # works - - -Discretizations -~~~~~~~~~~~~~~~ - -A discretization typically represents the finite-dimensional, concrete counterpart of an infinite-dimensional, abstract vector space, which makes it accessible to computations. -In ODL, a `Discretization` instance encompasses both continuous and discrete spaces as well as the mappings take one into the other. -The canonical example is the space :math:`L^2(\Omega)` of real-valued square-integrable functions on a rectangular domain (we take an interval for simplicity). -It is the default in the convenience function `uniform_discr`: - -.. code-block:: python - - >>> l2_discr = odl.uniform_discr(0, 1, 5) # Omega = [0, 1], 5 subintervals - >>> type(l2_discr) - odl.discr.lp_discr.DiscreteLp - >>> l2_discr.exponent - 2.0 - >>> l2_discr.domain - IntervalProd(0.0, 1.0) - -Discretizations have a large number of useful functionality, for example the direct and vectorized sampling of continuously defined functions. -If we, for example, want to discretize the function ``f(x) = exp(-x)``, we can simply pass it to the ``element()`` method: - -.. code-block:: python - - >>> exp_discr = l2_discr.element(lambda x: np.exp(-x)) - >>> type(exp_discr) - odl.discr.lp_discr.DiscreteLpElement - >>> print(exp_discr) - [ 0.90483742, 0.74081822, 0.60653066, 0.4965853 , 0.40656966] - >>> exp_discr.shape - (5,) - -Operators -========= - -This is the central class and general notion in ODL. -The concept is derived from the mathematical theory of `operators`_ and implements many of its core properties. -Any functionality that is implemented as an `Operator` has access to the full machinery of operator arithmetic, composition, differentiation and much more. -It is the universal interface between application-specific code (e.g. line projectors in tomography for a given geometry) and other parts of the library that are written in an abstract mathematical language. -The large benefit of this approach is that once an operator is fully implemented and functional, it can be used seamlessly by, e.g., optimization routines that expect an operator and data (among others) as input. - -As a small example, we study the problem of solving a linear system with 2 equations and 3 unknowns. -We use `Landweber's method`_ to get a least-squares solution and plot the intermediate residual norm. -The method needs a relaxation :math:`\lambda < 2 / \lVert A\rvert^2` to converge - in our case, the right-hand side is 0.14, so we choose 0.1. - -.. code-block:: python - - >>> matrix = np.array([[1.0, 3.0, 2.0], - ... [2.0, -1.0, 1.0]]) - >>> matrix_op = odl.MatrixOperator(matrix) # operator defined by the matrix - >>> matrix_op.domain - rn(3) - >>> matrix_op.range - rn(2) - >>> data = np.array([1.0, -1.0]) - >>> niter = 5 - >>> reco = matrix_op.domain.zero() # starting with the zero vector - >>> for i in range(niter): - ... residual = matrix_op(reco) - data - ... reco -= 0.1 * matrix_op.adjoint(residual) - ... print('{:.3}'.format(residual.norm())) - 1.41 - 0.583 - 0.24 - 0.0991 - 0.0409 - -If we now exchange ``matrix_op`` and ``data`` with a tomographic projector and line integral data, not a single line of code in the reconstruction method changes since the operator interface is exactly the same. - - -Further features -================ -* A unified structure `Geometry` for representing tomographic acquisition geometries -* Interfaces to fast external libraries, e.g. `ASTRA`_ for X-ray tomography, `pyFFTW`_ for fast Fourier transforms, ... -* A growing number of "must-have" operators like `Gradient`, `FourierTransform`, `WaveletTransform` -* Several solvers for variational inverse problems, ranging from simple `gradient methods ` to state-of-the-art non-smooth primal-dual splitting methods like `Douglas-Rachford ` -* Standardized tests for the correctness of implementations of operators and spaces, e.g. does the adjoint operator fulfill its defining relation? -* `CUDA-accelerated data containers`_ as a replacement for `Numpy`_ - - -Further reading -=============== -- :ref:`linearspace_in_depth` -- :ref:`operators_in_depth` -- :ref:`discretizations` - -.. _ASTRA: https://github.com/astra-toolbox/astra-toolbox -.. _codomain: https://en.wikipedia.org/wiki/Codomain -.. _field: https://en.wikipedia.org/wiki/Field_%28mathematics%29 -.. _function space: https://en.wikipedia.org/wiki/Function_space .. _KTH Royal Institute of Technology, Stockholm: https://www.kth.se/en/sci/institutioner/math .. _Centrum Wiskunde & Informatica (CWI), Amsterdam: https://www.cwi.nl -.. _Landweber's method: https://en.wikipedia.org/wiki/Landweber_iteration -.. _Numpy: http://www.numpy.org/ -.. _CUDA-accelerated data containers: https://github.com/odlgroup/odlcuda -.. _operators: https://en.wikipedia.org/wiki/Operator_%28mathematics%29 -.. _pyFFTW: https://pypi.python.org/pypi/pyFFTW -.. _real numbers: https://en.wikipedia.org/wiki/Real_number -.. _rectangular boxes: https://en.wikipedia.org/wiki/Hypercube -.. _set: https://en.wikipedia.org/wiki/Set_%28mathematics%29 -.. _vector space: https://en.wikipedia.org/wiki/Vector_space -.. _vectorized: https://en.wikipedia.org/wiki/Array_programming diff --git a/doc/source/guide/faq.rst b/doc/source/guide/faq.rst index 964dc16e0b2..c9232ee5cbb 100644 --- a/doc/source/guide/faq.rst +++ b/doc/source/guide/faq.rst @@ -51,7 +51,7 @@ General errors #. **Q:** When adding two space elements, the following error is shown:: - TypeError: unsupported operand type(s) for +: 'DiscreteLpElement' and 'DiscreteLpElement' + TypeError: unsupported operand type(s) for +: 'DiscretizedSpaceElement' and 'DiscretizedSpaceElement' This seems completely illogical since it works in other situations and clearly must be supported. Why is this error shown? @@ -83,7 +83,7 @@ General errors for example a "we identify X with Y" step has been omitted. * If the ``dtype`` or ``impl`` do not match, they need to be cast to each one of the others. - The most simple way to do this is by using the `DiscreteLpElement.astype` method. + The most simple way to do this is by using the `DiscretizedSpaceElement.astype` method. #. **Q:** I have installed ODL with the ``pip install --editable`` option, but I still get an ``AttributeError`` when I try to use a function/class I just implemented. The use-without-reinstall diff --git a/doc/source/guide/geometry_guide.rst b/doc/source/guide/geometry_guide.rst index 8dc2d66ad0f..247487950db 100644 --- a/doc/source/guide/geometry_guide.rst +++ b/doc/source/guide/geometry_guide.rst @@ -77,7 +77,7 @@ is a parametrization of the data manifold. Geometries in ODL ================= -The `RayTransform` in ODL is an `Operator` between `DiscreteLp` type discretized function spaces defined on rectangular domains. +The `RayTransform` in ODL is an `Operator` between `DiscretizedSpace` type discretized function spaces defined on rectangular domains. The **reconstruction space** ("volume"), i.e., the :term:`domain` of the ray transform, is naturally described as functions on a Euclidean space, and as derived above, the **data space**, i.e., the :term:`range` of the ray transform, can also be defined in terms of Euclidean coordinates. The missing component, which is the mapping from coordinates to points on the data manifold, is encoded in the `Geometry` class and its subclasses as described in the following. diff --git a/doc/source/guide/glossary.rst b/doc/source/guide/glossary.rst index 4628a265694..b6b64097c5e 100644 --- a/doc/source/guide/glossary.rst +++ b/doc/source/guide/glossary.rst @@ -4,14 +4,11 @@ Glossary ######## -.. _numpy vectorization: http://docs.scipy.org/doc/numpy/reference/generated/numpy.vectorize.html -.. _numpy dtype: http://docs.scipy.org/doc/numpy/reference/generated/numpy.dtype.html - .. glossary:: array-like - Any data structure which can be converted into a `numpy.ndarray` by the - `numpy.array` constructor. Includes all `Tensor` based classes. + Any data structure which can be converted into a `numpy.ndarray` by the `numpy.array` constructor. + Includes all `Tensor` based classes. convex conjugate The convex conjugate (also called Fenchel conjugate) is an important tool in convex optimization. @@ -21,93 +18,75 @@ Glossary f^*(x^*) = \sup_x \big( \langle x, x^* \rangle - f(x) \big). discretization - Structure to handle the mapping between abstract objects (e.g. functions) and concrete, finite realizations. - It encompasses an abstract `Set`, a `Tensor` as finite data container and the mappings between them, :term:`sampling` and :term:`interpolation`. + Mathematical structure to handle mapping between abstract objects (e.g. functions) and concrete, finite realizations, e.g., `Tensor`'s. + The mapping from abstract to concrete is here called :term:`sampling`, and the opposite mapping :term:`interpolation`. domain - Set of elements to which an operator can be applied. + Set of admissible inputs to a mapping, e.g., a function or an :term:`operator`. dtype - Short for data type, indicates the way data is represented internally. - For example ``float32`` means 32-bit floating point numbers. - See `numpy dtype`_ for more details. + Short for data type, indicating the way data is represented internally. + For instance, ``float32`` means 32-bit floating point numbers. + See `numpy.dtype` for more details. element - Saying that ``x`` is an element of a given `Set` ``my_set`` means that ``x in my_set`` - evaluates to `True`. The term is typically used as "element of " or "" element. - When referring to a `LinearSpace` like, e.g., `DiscreteLp`, an element is of the - corresponding type `LinearSpaceElement`, i.e. `DiscreteLpElement` in the above example. + Saying that ``x`` is an element of a given `Set` ``my_set`` means that ``x in my_set`` evaluates to ``True``. + The term is typically used as "element of " or " element". + When referring to a `LinearSpace` like, e.g., `DiscretizedSpace`, an element is of the corresponding type `LinearSpaceElement`, i.e. `DiscretizedSpaceElement` in the above example. Elements of a set can be created by the `Set.element` method. element-like - Any data structure which can be converted into an :term:`element` of a `Set` by - the `Set.element` method. For example, an ``rn(3) element-like`` is any :term:`array-like` - object with 3 real entries. - - Example: ```DiscreteLp` element-like`` means that - `DiscreteLp.element` can create a `DiscreteLpElement` from the input. + Any data structure which can be converted into an :term:`element` of a `Set` by the `Set.element` method. + For instance, an ``rn(3) element-like`` is any :term:`array-like` object with 3 real entries. in-place evaluation - Operator evaluation method which uses an existing data container to store - the result. Usually more efficient than :term:`out-of-place evaluation` - since no new memory is allocated and no data is copied. + Operator evaluation method which uses an existing data container to store the result. + Often, this mode of evaluation is more efficient than :term:`out-of-place evaluation` since memory allocation can be skipped. interpolation - Operator in a :term:`discretization` mapping a concrete - (finite-dimensional) object to an abstract (infinite-dimensional) one. - Example: `LinearInterpolation`. + Operation in the context of a :term:`discretization` that turns a finite data container into a function based on the values in the container. + For instance, linear interpolation creates a function that linearly interpolates between the values in the container based on grid nodes. meshgrid - Tuple of arrays defining a tensor grid by all possible combinations of entries, one from each - array. In 2 dimensions, for example, the arrays ``[1, 2]`` and ``[-1, 0, 1]`` define the grid - points ``(1, -1), (1, 0), (1, 1), (2, -1), (2, 0), (2, 1)``. + Tuple of arrays defining a tensor grid by all possible combinations of entries, one from each array. + In 2 dimensions, for example, the arrays ``[[1], [2]]`` and ``[[-1, 0, 1]]`` define the grid points ``(1, -1), (1, 0), (1, 1), (2, -1), (2, 0), (2, 1)``. + Note that the resulting grid has the broadcast shape, here ``(2, 3)``, broadcast from ``(2, 1)`` and ``(1, 3)`` + (expressed in code: ``result_shape = np.broadcast(shape1, shape2).shape``). operator - Mathematical notion for a mapping between arbitrary vector spaces. This includes the important - special case of an operator taking a (discretized) function as an input and returning another - function. For example, the Fourier Transform maps a function to its transformed version. - Operators of this type are the most prominent use case in ODL. See - :ref:`the in-depth guide on operators ` for details on their implementation. + Mathematical notion for a mapping between vector spaces. + This includes the important special case of an operator taking a (discretized) function as an input and returning another function. + See :ref:`the in-depth guide on operators ` for details on their usage and implementation. order Ordering of the axes in a multi-dimensional array with linear (one-dimensional) storage. - For C ordering (``'C'``), the last axis has smallest stride (varies fastest), and the first - axis has largest stride (varies slowest). Fortran ordering (``'F'``) is the exact opposite. + For C ordering (``'C'``), the last axis has smallest stride (varies fastest), and the first axis has largest stride (varies slowest). + Fortran ordering (``'F'``) is the exact opposite. out-of-place evaluation - Operator evaluation method which creates a new data container to store - the result. Usually less efficient than :term:`in-place evaluation` - since new memory is allocated and data needs to be copied. + Operator evaluation method that creates a new data container to store the result. + Often, this mode of evaluation is less efficient than :term:`in-place evaluation` since new memory must be allocated. proximal - Given a proper convex functional :math:`S`, the proximal operator is defined by - - .. math:: + Given a proper and convex functional :math:`S`, the proximal operator is defined by - \text{prox}_S(v) = \arg\min_x \big( S(x) + \frac{1}{2}||x - v||_2^2 \big) - - The term "proximal" is also occasionally used instead of ProxImaL, then refering to the proximal modelling language for the solution of convex optimization problems. + .. math:: + \text{prox}_S(v) = \arg\min_x \big( S(x) + \frac{1}{2}||x - v||_2^2 \big) proximal factory - A proximal factory associated with a functional :math:`S` is a `callable`, which returns the proximal of the scaled functional :math:`\sigma S` when called with a scalar :math:`\sigma`. - This is used due to the fact that optimization methods often use :math:`\text{prox}_{\sigma S}` for varying :math:`\sigma`. + A proximal factory associated with a functional :math:`S` is a function that takes a scalar :math:`\sigma` and returns the proximal of the scaled functional :math:`\sigma S`. + This indirection is needed since optimization methods typically use scaled proximals :math:`\text{prox}_{\sigma S}` for varying :math:`\sigma`, and that the scaled proximal cannot be inferred from the unscaled one alone. range - Set of elements to which an operator maps, i.e. in which the result of - an operator evaluation lies. + Set in which a mapping, e.g., a function or :term:`operator`, takes values. sampling - Operator in a :term:`discretization` mapping an abstract - (infinite-dimensional) object to a concrete (finite-dimensional) one. - Example: `PointCollocation`. + Operation in the context of :term:`discretization` that turns a function into a finite data container. + The primary example is the evaluation ("collocation") of the function on a set of points. vectorization - Ability of a function to be evaluated on a grid in a single call rather - than looping over the grid points. Vectorized evaluation gives a huge - performance boost compared to Python loops (at least if there is no - JIT) since loops are implemented in optimized C code. - - The vectorization concept in ODL differs slightly from the one in NumPy - in that arguments have to be passed as a single tuple rather than a - number of (positional) arguments. See `numpy vectorization`_ for more - details. + Ability of a function to be evaluated on a grid in a single call rather than looping over the grid points. + Vectorized evaluation gives a huge performance boost compared to Python loops (at least if there is no JIT) since loops are implemented in optimized C code. + + The vectorization concept in ODL differs slightly from the one in NumPy in that arguments have to be passed as a single tuple rather than a number of (positional) arguments. + See :ref:`the ODL vectorization guide ` and `the NumPy vectorization documentation `_ for more details. diff --git a/examples/space/vectorization.py b/examples/space/vectorization.py index bb2228c7276..849bacf9b50 100644 --- a/examples/space/vectorization.py +++ b/examples/space/vectorization.py @@ -1,21 +1,21 @@ -"""Example showing how to use vectorization of `FunctionSpaceElement`'s.""" +"""Example showing how to use vectorization.""" + +import timeit import numpy as np + import odl -import timeit +from odl.discr.discr_utils import sampling_function def performance_example(): - # Create a space of functions on the interval [0, 1]. - fspace = odl.FunctionSpace(odl.IntervalProd(0, 1)) - - # Simple function, already supports vectorization. - f_vec = fspace.element(lambda x: x ** 2) + # Simple function, already supports vectorization + f_vec = sampling_function( + lambda x: x ** 2, domain=odl.IntervalProd(0, 1) + ) - # If 'vectorized=False' is used, odl automatically vectorizes with - # the help of numpy.vectorize. This will be very slow, though, since - # the implementation is basically a Python loop. - f_novec = fspace.element(lambda x: x ** 2, vectorized=False) + # Vectorized with NumPy's poor man's vectorization function + f_novec = np.vectorize(lambda x: x ** 2) # We test both versions with 10000 evaluation points. The natively # vectorized version should be much faster than the one using @@ -51,11 +51,11 @@ def myfunc(x): # to wrap the Numba-vectorized function. vectorized = numba.vectorize(lambda x, y: x - y if x > y else x + y) - def myfunc_vec(x): + def myfunc_numba(x): """Return x - y if x > y, otherwise return x + y.""" return vectorized(x[0], x[1]) - def myfunc_native_vec(x): + def myfunc_vec(x): """Return x - y if x > y, otherwise return x + y.""" # This implementation uses Numpy's fast built-in vectorization # directly. The function np.where checks the condition in the @@ -68,14 +68,16 @@ def myfunc_native_vec(x): # Create (continuous) functions in the space of function defined # on the rectangle [0, 1] x [0, 1]. - fspace = odl.FunctionSpace(odl.IntervalProd([0, 0], [1, 1])) - f_default = fspace.element(myfunc, vectorized=False) - f_numba = fspace.element(myfunc_vec) - f_native = fspace.element(myfunc_native_vec, vectorized=True) + f_vec = sampling_function( + myfunc_vec, domain=odl.IntervalProd([0, 0], [1, 1]) + ) + f_numba = sampling_function( + myfunc_numba, domain=odl.IntervalProd([0, 0], [1, 1]) + ) # Create a unform grid in [0, 1] x [0, 1] (fspace.domain) with 2000 # samples per dimension. - grid = odl.uniform_grid_fromintv(fspace.domain, [2000, 2000]) + grid = odl.uniform_grid([0, 0], [1, 1], shape=(2000, 2000)) # The points() method really creates all grid points (2000^2) and # stores them one-by-one (row-wise) in a large array with shape # (2000*2000, 2). Since the function expects points[i] to be the @@ -89,18 +91,14 @@ def myfunc_native_vec(x): # See the numpy.meshgrid function for more information. mesh = grid.meshgrid # Returns a sparse meshgrid (2000 * 2) - print('Non-Vectorized runtime (points): {:5f}' - ''.format(timeit.timeit(lambda: f_default(points), number=1))) - print('Non-Vectorized runtime (meshgrid): {:5f}' - ''.format(timeit.timeit(lambda: f_default(mesh), number=1))) + print('Native vectorized runtime (points): {:5f}' + ''.format(timeit.timeit(lambda: f_vec(points), number=1))) + print('Native vectorized runtime (meshgrid): {:5f}' + ''.format(timeit.timeit(lambda: f_vec(mesh), number=1))) print('Numba vectorized runtime (points): {:5f}' ''.format(timeit.timeit(lambda: f_numba(points), number=1))) print('Numba vectorized runtime (meshgrid): {:5f}' ''.format(timeit.timeit(lambda: f_numba(mesh), number=1))) - print('Native vectorized runtime (points): {:5f}' - ''.format(timeit.timeit(lambda: f_native(points), number=1))) - print('Native vectorized runtime (meshgrid): {:5f}' - ''.format(timeit.timeit(lambda: f_native(mesh), number=1))) if __name__ == '__main__': diff --git a/examples/visualization/README.md b/examples/visualization/README.md index 3af27b10be8..d6dc31fe317 100644 --- a/examples/visualization/README.md +++ b/examples/visualization/README.md @@ -7,11 +7,11 @@ These examples show to use the visualization capabilities of ODL to view data. Example | Purpose | Complexity ------- | ------- | ---------- [`show_vector.py`](show_vector.py) | Using `Tensor.show` | low -[`show_1d.py`](show_1d.py) | Using `DiscreteLpElement.show` in 1D | low -[`show_2d.py`](show_2d.py) | Using `DiscreteLpElement.show` in 2D | low -[`show_2d.py`](show_2d_complex.py) | Using `DiscreteLpElement.show` in 2D with complex data | low +[`show_1d.py`](show_1d.py) | Using `DiscretizedSpaceElement.show` in 1D | low +[`show_2d.py`](show_2d.py) | Using `DiscretizedSpaceElement.show` in 2D | low +[`show_2d.py`](show_2d_complex.py) | Using `DiscretizedSpaceElement.show` in 2D with complex data | low [`show_productspace.py`](show_productspace.py) | Using `ProductSpaceElement.show` | low -[`visualize_vector_examples.py`](visualize_vector_examples.py) | Show all example vectors in `DiscreteLp.examples` | low +[`visualize_vector_examples.py`](visualize_vector_examples.py) | Show all example vectors in `DiscretizedSpace.examples` | low ## Real time updating diff --git a/examples/visualization/show_1d.py b/examples/visualization/show_1d.py index 2caae79910d..a8a13876a52 100644 --- a/examples/visualization/show_1d.py +++ b/examples/visualization/show_1d.py @@ -1,4 +1,4 @@ -"""Example for `DiscreteLpElement.show` in 1D. +"""Example for `DiscretizedSpaceElement.show` in 1D. Notes ----- @@ -7,9 +7,10 @@ """ import matplotlib.pyplot as plt -import odl import numpy as np +import odl + space = odl.uniform_discr(0, 5, 100) elem = space.element(np.sin) diff --git a/examples/visualization/show_2d.py b/examples/visualization/show_2d.py index 62dc854b37b..d02e65f7089 100644 --- a/examples/visualization/show_2d.py +++ b/examples/visualization/show_2d.py @@ -1,4 +1,4 @@ -"""Example for `DiscreteLpElement.show` in 2D. +"""Example for `DiscretizedSpaceElement.show` in 2D. Notes ----- diff --git a/examples/visualization/show_2d_complex.py b/examples/visualization/show_2d_complex.py index 460e1c9f99a..705f2113b40 100644 --- a/examples/visualization/show_2d_complex.py +++ b/examples/visualization/show_2d_complex.py @@ -1,4 +1,4 @@ -"""Example for `DiscreteLpElement.show` in 2D with complex data. +"""Example for `DiscretizedSpaceElement.show` in 2D with complex data. Notes ----- diff --git a/odl/README.md b/odl/README.md index 7e05e316c45..6524889fe87 100644 --- a/odl/README.md +++ b/odl/README.md @@ -1,29 +1,27 @@ # ODL -This directory contains all of the source code related to ODL. +Briefly overview of submodules: -Briefly, the hierarchy of submodules is: - -* [set](set) and [operator](operator) contains the core abstract functionality of ODL needed to define sets, vector spaces and operators acting on these. -* [space](space) and [discr](discr) contains the standard spaces such as Rn and the set of discretized functions on some domain. -* [solvers](solvers) defines equation solvers as well as various optimization algorithms. -* [tomo](tomo), [trafos](trafos), [deform](deform) contains application specific operators like Fourier transforms, Wavelet transforms, deformations, ray transforms, etc. +* [set](set) and [operator](operator): Contain the core abstract functionality of ODL needed to define sets, vector spaces and operators acting on these. +* [space](space) and [discr](discr): Contain the standard spaces such as Rn and the set of discretized functions on some domain. +* [solvers](solvers): Defines equation solvers as well as various optimization algorithms. +* [tomo](tomo), [trafos](trafos), [deform](deform): Contain application-specific operators like Fourier transforms, Wavelet transforms, deformations, ray transforms, etc. ## Content This is a brief description of the content of each submodule, see the individual modules for a more detailed description. -* [contrib](contrib) Sub-package for immature and/or very specific code. Examples includes vendor specific geometries, bindings to less used libraries and very new solution methods. -* [deform](deform) Functionality related to deformations. Defines the free function `linear_deform` which deforms a function according to a vector-field of displacements. Also defines the operators `LinDeformFixedTempl` and `LinDeformFixedDisp`. -* [diagnostics](diagnostics) Automated tests for user defined operators and spaces. `SpaceTest` verifies that various properties of linear spaces work as expected, while `OperatorTest` does the same for operators. -* [discr](discr) Discretizations of function-spaces. The main abstract class is `Discretization`, while `DiscreteLp` is an abstract class defining discretized functions on some hypercube. In addition, the classes `RectGrid` and `RectPartition` are used to exactly define what discretization is used under the hood. Finally this submodule defines several utilities like `uniform_discr` and `uniform_partition` which serve to create the most common special cases. -* [operator](operator) Operators between sets. Defines the class `Operator` which is the main abstract class used for any mapping between two `Set`'s. Further defines several general classes of operators applicable to general spaces. -* [phantom](phantom) Standardized test images. Functions for generating standardized test examples such as `shepp_logan`. -* [set](set) Sets of objects. Defines the abstract class `Set` and `LinearSpace` as well as some concrete implementations such as `RealNumbers`. -* [solvers](solvers) Solution of equations and optimization. Contains both general solvers for problems of the form `A(x) = b` where `A` is an `Operator` as well as solvers of minimization problems. In addition, it defines the class `Functional` with several concrete implementations such as `L2Norm`. -* [space](space) Concrete vector spaces. Contains concrete implementations of `LinearSpace`, including `NumpyTensorSpace` and `ProductSpace`. -* [test](test) Tests for the ODL package. This contains automated tests for all other ODL functionality. In general, users should not be calling anything from this submoduel. -* [tomo](tomo) Tomography. Defines the operator `RayTransform` as well as `Geometry` along with subclasses and utilities. Also defines problem dependent direct reconstruction such as `fbp_op`. +* [contrib](contrib): Sub-package for experimental and/or very specific code. Examples includes vendor-specific geometries, bindings to less used libraries and cutting-edge optimizers. +* [deform](deform): Functionality related to deformations. Defines the free function `linear_deform` which deforms a function according to a vector-field of displacements. Also defines the operators `LinDeformFixedTempl` and `LinDeformFixedDisp`. +* [diagnostics](diagnostics): Automated tests for user-defined operators and spaces. `SpaceTest` verifies that various properties of linear spaces work as expected, while `OperatorTest` does the same for operators. +* [discr](discr): Discretizations of function spaces. The main class is `DiscretizedSpace`, an implementation of a standard Lebesgue Lp space on a hypercube. In addition, the classes `RectGrid` and `RectPartition` are used to exactly define what discretization is being used under the hood. Finally this submodule defines several utilities like `uniform_discr` and `uniform_partition` which serve to create the most common special cases. +* [operator](operator): Operators between sets. Defines the class `Operator` which is the main abstract class used for any mapping between two `Set`'s. Further defines several general classes of operators applicable to general spaces. +* [phantom](phantom): Standardized test images. Functions for generating standardized test examples such as `shepp_logan`. +* [set](set): Sets of objects. Defines the abstract class `Set` and `LinearSpace` as well as some concrete implementations such as `RealNumbers`. +* [solvers](solvers): Optimizers and solution methods for systems of equations. Contains both general solvers for problems of the form `A(x) = b` where `A` is an `Operator` as well as solvers of minimization problems. In addition, it defines the class `Functional` with several concrete implementations such as `L2Norm`. +* [space](space): Concrete vector spaces. Contains concrete implementations of `LinearSpace`, including `NumpyTensorSpace` and `ProductSpace`. +* [test](test): Unit tests. This contains automated tests for all other ODL functionality. In general, users should not be calling anything from this submodule. +* [tomo](tomo): Tomography. Defines the operator `RayTransform` as well as `Geometry` along with subclasses and utilities. Also defines problem dependent direct reconstruction such as `fbp_op`. * [trafos](trafos) Transformations between spaces. Defines `FourierTransform` and `WaveletTransform`. -* [ufunc_ops](ufunc_ops) Ufuncs as operators. Defines operators like the `sin` and `abs` functions. +* [ufunc_ops](ufunc_ops) UFuncs as operators. Defines operators like the `sin` and `abs` functions. * [util](util) Utilities. Functionality mainly intended to be used by other ODL functions such as linear algebra and visualization. \ No newline at end of file diff --git a/odl/__init__.py b/odl/__init__.py index 2eb789b30ef..d6d0884f814 100644 --- a/odl/__init__.py +++ b/odl/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2014-2018 The ODL contributors +# Copyright 2014-2020 The ODL contributors # # This file is part of ODL. # @@ -47,23 +47,17 @@ except TypeError: pass -# Set printing linewidth to 71 to allow method docstrings to not extend +# Set printing line width to 71 to allow method docstrings to not extend # beyond 79 characters (2 times indent of 4) np.set_printoptions(linewidth=71) -# Propagate names defined in` __all__` of all "core" subpackages into -# the top-level namespace +# Import all names from "core" subpackages into the top-level namespace; +# the `__all__` collection is extended later to make import errors more +# visible (otherwise one gets errors like "... has no attribute __all__") from .set import * -__all__ += set.__all__ - from .space import * -__all__ += space.__all__ - from .operator import * -__all__ += operator.__all__ - from .discr import * -__all__ += discr.__all__ # More "advanced" subpackages keep their namespaces separate from top-level, # we only import the modules themselves @@ -79,4 +73,10 @@ # Add `test` function to global namespace so users can run `odl.test()` from .util import test + +# Amend `__all__` +__all__ += set.__all__ +__all__ += space.__all__ +__all__ += operator.__all__ +__all__ += discr.__all__ __all__ += ('test',) diff --git a/odl/contrib/fom/supervised.py b/odl/contrib/fom/supervised.py index a7b959574c8..60d65a8ed01 100644 --- a/odl/contrib/fom/supervised.py +++ b/odl/contrib/fom/supervised.py @@ -1,4 +1,4 @@ -# Copyright 2014-2019 The ODL contributors +# Copyright 2014-2020 The ODL contributors # # This file is part of ODL. # @@ -13,8 +13,8 @@ import numpy as np import odl -from odl.discr.grid import sparse_meshgrid from odl.contrib.fom.util import spherical_sum +from odl.discr.grid import sparse_meshgrid __all__ = ('mean_squared_error', 'mean_absolute_error', 'mean_value_difference', 'standard_deviation_difference', @@ -828,9 +828,10 @@ def noise_power_spectrum(data, ground_truth, radial=False, Parameters ---------- - data : `DiscreteLpElement` or `array-like` + data : `DiscretizedSpaceElement` or `array-like` Input data to compare to the ground truth. If not a - `DiscreteLpElement`, a default space with cell size 1 will be assumed. + `DiscretizedSpaceElement`, a default space with cell size 1 will be + assumed. ground_truth : `array-like` Reference to which ``data`` should be compared. radial : bool @@ -848,7 +849,7 @@ def noise_power_spectrum(data, ground_truth, radial=False, Returns ------- - noise_power_spectrum : `DiscreteLp`-element + noise_power_spectrum : `DiscretizedSpace`-element The space is the Fourier space corresponding to ``space``, and hence the axes indicate frequency. If ``radial`` is ``True``, an average over concentric annuli is @@ -860,7 +861,7 @@ def noise_power_spectrum(data, ground_truth, radial=False, """ try: space = data.space - assert isinstance(space, odl.DiscreteLp) + assert isinstance(space, odl.DiscretizedSpace) except (AttributeError, AssertionError): data = np.asarray(data) space = odl.uniform_discr( diff --git a/odl/contrib/fom/util.py b/odl/contrib/fom/util.py index 8d954e8eb68..5b47f09be94 100644 --- a/odl/contrib/fom/util.py +++ b/odl/contrib/fom/util.py @@ -1,4 +1,4 @@ -# Copyright 2014-2019 The ODL contributors +# Copyright 2014-2020 The ODL contributors # # This file is part of ODL. # @@ -405,7 +405,7 @@ def spherical_sum(image, binning_factor=1.0): Parameters ---------- - image : `DiscreteLp` element + image : `DiscretizedSpace` element Input data whose radial sum should be computed. binning_factor : positive float, optional Reduce the number of output bins by this factor. Increasing this @@ -420,7 +420,7 @@ def spherical_sum(image, binning_factor=1.0): Returns ------- - spherical_sum : 1D `DiscreteLp` element + spherical_sum : 1D `DiscretizedSpace` element The spherical sum of ``image``. Its space is one-dimensional with domain ``[0, rmax]``, where ``rmax`` is the radius of the smallest ball containing ``image.space.domain``. Its shape is ``(N,)`` with :: diff --git a/odl/contrib/mrc/examples/raw_binary_with_header_io.py b/odl/contrib/mrc/examples/raw_binary_with_header_io.py index 35548828491..ac1187a5ee8 100644 --- a/odl/contrib/mrc/examples/raw_binary_with_header_io.py +++ b/odl/contrib/mrc/examples/raw_binary_with_header_io.py @@ -12,20 +12,20 @@ sequence of dictionaries with a certain structure. """ +import tempfile from collections import OrderedDict + import matplotlib.pyplot as plt import numpy as np import scipy.misc -import tempfile from odl.contrib.mrc import ( FileReaderRawBinaryWithHeader, FileWriterRawBinaryWithHeader) - # --- Writing --- # # Create some test data. We arbitrarily define origin and pixel size. -# In practice, these could come from a `DiscreteLp` space as `mid_pt` +# In practice, these could come from a `DiscretizedSpace` space as `mid_pt` # and `cell_sides` properties. image = scipy.misc.ascent() shape = np.array(image.shape, dtype='int32') diff --git a/odl/contrib/pyshearlab/pyshearlab_operator.py b/odl/contrib/pyshearlab/pyshearlab_operator.py index 635c857f7c8..f33236ab4ae 100644 --- a/odl/contrib/pyshearlab/pyshearlab_operator.py +++ b/odl/contrib/pyshearlab/pyshearlab_operator.py @@ -1,4 +1,4 @@ -# Copyright 2014-2017 The ODL contributors +# Copyright 2014-2020 The ODL contributors # # This file is part of ODL. # @@ -9,11 +9,12 @@ """ODL integration with pyshearlab.""" -import odl -import numpy as np -import pyshearlab from threading import Lock +import numpy as np + +import odl +import pyshearlab __all__ = ('PyShearlabOperator',) @@ -31,7 +32,7 @@ def __init__(self, space, num_scales): Parameters ---------- - space : `DiscreteLp` + space : `DiscretizedSpace` The space on which the shearlet transform should act. Must be two-dimensional. num_scales : nonnegative `int` diff --git a/odl/contrib/shearlab/shearlab_operator.py b/odl/contrib/shearlab/shearlab_operator.py index 4e07300eef8..76b120b43c8 100644 --- a/odl/contrib/shearlab/shearlab_operator.py +++ b/odl/contrib/shearlab/shearlab_operator.py @@ -1,4 +1,4 @@ -# Copyright 2014-2018 The ODL contributors +# Copyright 2014-2020 The ODL contributors # # This file is part of ODL. # @@ -8,16 +8,15 @@ """ODL integration with shearlab.""" -import odl -import numpy as np from threading import Lock -# Library for the shearlab library - -import julia import matplotlib.pyplot as plt +import numpy as np from numpy import ceil -from numpy.fft import fft2, ifft2, fftshift, ifftshift +from numpy.fft import fft2, fftshift, ifft2, ifftshift + +import julia +import odl __all__ = ('ShearlabOperator',) @@ -34,7 +33,7 @@ def __init__(self, space, num_scales): Parameters ---------- - space : `DiscreteLp` + space : `DiscretizedSpace` The space on which the shearlet transform should act. Must be two-dimensional. num_scales : nonnegative `int` diff --git a/odl/contrib/solvers/spdhg/misc.py b/odl/contrib/solvers/spdhg/misc.py index 045e0054579..e0484a8faf2 100644 --- a/odl/contrib/solvers/spdhg/misc.py +++ b/odl/contrib/solvers/spdhg/misc.py @@ -1,4 +1,4 @@ -# Copyright 2014-2019 The ODL contributors +# Copyright 2014-2020 The ODL contributors # # This file is part of ODL. # @@ -9,14 +9,17 @@ """Functions for folders and files.""" from __future__ import print_function + from builtins import super -import numpy as np -import odl -import scipy.signal + import matplotlib import matplotlib.pyplot as plt +import numpy as np +import scipy.signal from skimage.io import imsave +import odl + __all__ = ('total_variation', 'TotalVariationNonNegative', 'bregman', 'save_image', 'save_signal', 'divide_1Darray_equally', 'Blur2D', 'KullbackLeiblerSmooth') @@ -472,7 +475,7 @@ def __init__(self, space, data, background): Parameters ---------- - space : `DiscreteLp` or `TensorSpace` + space : `DiscretizedSpace` or `TensorSpace` Domain of the functional. data : ``space`` `element-like` Data vector which has to be non-negative. @@ -597,7 +600,7 @@ def __init__(self, space, data, background): Parameters ---------- - space : `DiscreteLp` or `TensorSpace` + space : `DiscretizedSpace` or `TensorSpace` Domain of the functional. data : ``space`` `element-like` Data vector which has to be non-negative. diff --git a/odl/contrib/tomo/elekta.py b/odl/contrib/tomo/elekta.py index 273734fe8d9..629d7ac2800 100644 --- a/odl/contrib/tomo/elekta.py +++ b/odl/contrib/tomo/elekta.py @@ -1,4 +1,4 @@ -# Copyright 2014-2017 The ODL contributors +# Copyright 2014-2020 The ODL contributors # # This file is part of ODL. # @@ -9,8 +9,6 @@ """Tomography helpers for Elekta systems.""" import numpy as np -import odl - __all__ = ('elekta_icon_geometry', 'elekta_icon_space', @@ -129,7 +127,7 @@ def elekta_icon_space(shape=(448, 448, 448), **kwargs): Returns ------- - elekta_icon_space : `DiscreteLp` + elekta_icon_space : `DiscretizedSpace` Examples -------- @@ -182,7 +180,7 @@ def elekta_icon_fbp(ray_transform, Returns ------- - elekta_icon_fbp : `DiscreteLp` + elekta_icon_fbp : `DiscretizedSpace` Examples -------- @@ -305,7 +303,7 @@ def elekta_xvi_space(shape=(512, 512, 512), **kwargs): Returns ------- - elekta_xvi_space : `DiscreteLp` + elekta_xvi_space : `DiscretizedSpace` Examples -------- @@ -349,7 +347,7 @@ def elekta_xvi_fbp(ray_transform, Returns ------- - elekta_xvi_fbp : `DiscreteLp` + elekta_xvi_fbp : `DiscretizedSpace` Examples -------- diff --git a/odl/deform/linearized.py b/odl/deform/linearized.py index 453fbdf291b..cd28fc8c5e5 100644 --- a/odl/deform/linearized.py +++ b/odl/deform/linearized.py @@ -12,10 +12,9 @@ import numpy as np -from odl.discr import DiscreteLp, Divergence, Gradient -from odl.discr.discr_utils import ( - _all_interp_equal, _normalize_interp, per_axis_interpolator) -from odl.discr.lp_discr import DiscreteLpElement +from odl.discr import DiscretizedSpace, Divergence, Gradient +from odl.discr.discr_space import DiscretizedSpaceElement +from odl.discr.discr_utils import _normalize_interp, per_axis_interpolator from odl.operator import Operator, PointwiseInner from odl.space import ProductSpace from odl.space.pspace import ProductSpaceElement @@ -32,7 +31,7 @@ def linear_deform(template, displacement, interp='linear', out=None): Parameters ---------- - template : `DiscreteLpElement` + template : `DiscretizedSpaceElement` Template to be deformed by a displacement field. displacement : element of power space of ``template.space`` Vector field (displacement field) used to deform the @@ -136,9 +135,9 @@ def __init__(self, template, domain=None, interp='linear'): Parameters ---------- - template : `DiscreteLpElement` + template : `DiscretizedSpaceElement` Fixed template that is to be deformed. - domain : power space of `DiscreteLp`, optional + domain : power space of `DiscretizedSpace`, optional The space of all allowed coordinates in the deformation. A `ProductSpace` of ``template.ndim`` copies of a function-space. It must fulfill @@ -155,7 +154,7 @@ def __init__(self, template, domain=None, interp='linear'): Supported values: ``'nearest'``, ``'linear'`` - .. note:: + .. warning:: Choosing ``'nearest'`` interpolation results in a formally non-differentiable operator since the gradient of the template is not well-defined. If the operator derivative @@ -186,9 +185,9 @@ def __init__(self, template, domain=None, interp='linear'): >>> print(op(disp_field)) [ 0. , 0. , 1. , 0.5, 0. ] """ - if not isinstance(template, DiscreteLpElement): + if not isinstance(template, DiscretizedSpaceElement): raise TypeError( - '`template` must be a `DiscreteLpElement, got {!r}`' + '`template` must be a `DiscretizedSpaceElement, got {!r}`' ''.format(template) ) self.__template = template @@ -203,8 +202,8 @@ def __init__(self, template, domain=None, interp='linear'): if not domain.is_power_space: raise TypeError('`domain` must be a power space, ' 'got {!r}'.format(domain)) - if not isinstance(domain[0], DiscreteLp): - raise TypeError('`domain[0]` must be a `DiscreteLp` ' + if not isinstance(domain[0], DiscretizedSpace): + raise TypeError('`domain[0]` must be a `DiscretizedSpace` ' 'instance, got {!r}'.format(domain[0])) if template.space.partition != domain[0].partition: @@ -231,7 +230,10 @@ def interp_byaxis(self): @property def interp(self): """Interpolation scheme or tuple of per-axis interpolation schemes.""" - if _all_interp_equal(self.interp_byaxis): + if ( + len(self.interp_byaxis) != 0 + and all(s == self.interp_byaxis[0] for s in self.interp_byaxis[1:]) + ): return self.interp_byaxis[0] else: return self.interp_byaxis @@ -320,9 +322,9 @@ def __init__(self, displacement, templ_space=None, interp='linear'): Parameters ---------- - displacement : element of a power space of `DiscreteLp` + displacement : element of a power space of `DiscretizedSpace` Fixed displacement field used in the deformation. - templ_space : `DiscreteLp`, optional + templ_space : `DiscretizedSpace`, optional Template space on which this operator is applied, i.e. the operator domain and range. It must fulfill ``templ_space[0].partition == displacement.space.partition``, so @@ -375,18 +377,18 @@ def __init__(self, displacement, templ_space=None, interp='linear'): '`displacement.space` must be a power space, got {!r}' ''.format(displacement.space) ) - if not isinstance(displacement.space[0], DiscreteLp): + if not isinstance(displacement.space[0], DiscretizedSpace): raise ValueError( - '`displacement.space[0]` must be a `DiscreteLp`, got {!r}' - ''.format(displacement.space[0])) + '`displacement.space[0]` must be a `DiscretizedSpace`, ' + 'got {!r}'.format(displacement.space[0])) self.__displacement = displacement if templ_space is None: templ_space = displacement.space[0] else: - if not isinstance(templ_space, DiscreteLp): - raise TypeError('`templ_space` must be a `DiscreteLp` ' + if not isinstance(templ_space, DiscretizedSpace): + raise TypeError('`templ_space` must be a `DiscretizedSpace` ' 'instance, got {!r}'.format(templ_space)) if templ_space.partition != displacement.space[0].partition: raise ValueError( @@ -409,7 +411,10 @@ def interp_byaxis(self): @property def interp(self): """Interpolation scheme or tuple of per-axis interpolation schemes.""" - if _all_interp_equal(self.interp_byaxis): + if ( + len(self.interp_byaxis) != 0 + and all(s == self.interp_byaxis[0] for s in self.interp_byaxis[1:]) + ): return self.interp_byaxis[0] else: return self.interp_byaxis diff --git a/odl/discr/__init__.py b/odl/discr/__init__.py index aa4a92fa4fd..6d500f3c0ce 100644 --- a/odl/discr/__init__.py +++ b/odl/discr/__init__.py @@ -6,28 +6,21 @@ # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. -"""Discretizations in ODL.""" +"""Discretization-related functionality like grids and discrete spaces.""" from __future__ import absolute_import -__all__ = () - +from . import discr_utils +from .diff_ops import * +from .discr_ops import * +from .discr_space import * from .grid import * -__all__ += grid.__all__ - from .partition import * -__all__ += partition.__all__ - -from .discretization import * -__all__ += discretization.__all__ -from .lp_discr import * -__all__ += lp_discr.__all__ +__all__ = () -from .discr_ops import * +__all__ += grid.__all__ +__all__ += partition.__all__ +__all__ += discr_space.__all__ __all__ += discr_ops.__all__ - -from .diff_ops import * __all__ += diff_ops.__all__ - -from . import discr_utils diff --git a/odl/discr/diff_ops.py b/odl/discr/diff_ops.py index 1f8f8f9db45..e7ba9d7f168 100644 --- a/odl/discr/diff_ops.py +++ b/odl/discr/diff_ops.py @@ -1,4 +1,4 @@ -# Copyright 2014-2019 The ODL contributors +# Copyright 2014-2020 The ODL contributors # # This file is part of ODL. # @@ -8,14 +8,14 @@ """Operators defined for tensor fields.""" -from __future__ import print_function, division, absolute_import +from __future__ import absolute_import, division, print_function + import numpy as np -from odl.discr.lp_discr import DiscreteLp +from odl.discr.discr_space import DiscretizedSpace from odl.operator.tensor_ops import PointwiseTensorFieldOperator from odl.space import ProductSpace -from odl.util import writable_array, signature_string, indent - +from odl.util import indent, signature_string, writable_array __all__ = ('PartialDerivative', 'Gradient', 'Divergence', 'Laplacian') @@ -57,11 +57,11 @@ def __init__(self, domain, axis, range=None, method='forward', Parameters ---------- - domain : `DiscreteLp` + domain : `DiscretizedSpace` Space of elements on which the operator can act. axis : int Axis along which the partial derivative is evaluated. - range : `DiscreteLp`, optional + range : `DiscretizedSpace`, optional Space of elements to which the operator maps, must have the same shape as ``domain``. For the default ``None``, the range is the same as ``domain``. @@ -106,8 +106,8 @@ def __init__(self, domain, axis, range=None, method='forward', [ 0., 1., 2., 3., 4.]] ) """ - if not isinstance(domain, DiscreteLp): - raise TypeError('`domain` {!r} is not a DiscreteLp instance' + if not isinstance(domain, DiscretizedSpace): + raise TypeError('`domain` {!r} is not a DiscretizedSpace instance' ''.format(domain)) if range is None: @@ -196,7 +196,7 @@ def __str__(self): class Gradient(PointwiseTensorFieldOperator): - """Spatial gradient operator for `DiscreteLp` spaces. + """Spatial gradient operator for `DiscretizedSpace` spaces. Calls helper function `finite_diff` to calculate each component of the resulting product space element. For the adjoint of the `Gradient` @@ -213,10 +213,10 @@ def __init__(self, domain=None, range=None, method='forward', Parameters ---------- - domain : `DiscreteLp`, optional + domain : `DiscretizedSpace`, optional Space of elements which the operator acts on. This is required if ``range`` is not given. - range : power space of `DiscreteLp`, optional + range : power space of `DiscretizedSpace`, optional Space of elements to which the operator maps. This is required if ``domain`` is not given. method : {'forward', 'backward', 'central'}, optional @@ -317,8 +317,8 @@ def __init__(self, domain=None, range=None, method='forward', raise ValueError('`range` {!r} is not a power space' ''.format(range)) - if not isinstance(domain, DiscreteLp): - raise TypeError('`domain` {!r} is not a `DiscreteLp` ' + if not isinstance(domain, DiscretizedSpace): + raise TypeError('`domain` {!r} is not a `DiscretizedSpace` ' 'instance'.format(domain)) if len(range) != domain.ndim: @@ -420,7 +420,7 @@ def __str__(self): class Divergence(PointwiseTensorFieldOperator): - """Divergence operator for `DiscreteLp` spaces. + """Divergence operator for `DiscretizedSpace` spaces. Calls helper function `finite_diff` for each component of the input product space vector. For the adjoint of the `Divergence` operator to @@ -436,10 +436,10 @@ def __init__(self, domain=None, range=None, method='forward', Parameters ---------- - domain : power space of `DiscreteLp`, optional + domain : power space of `DiscretizedSpace`, optional Space of elements which the operator acts on. This is required if ``range`` is not given. - range : `DiscreteLp`, optional + range : `DiscretizedSpace`, optional Space of elements to which the operator maps. This is required if ``domain`` is not given. method : {'forward', 'backward', 'central'}, optional @@ -527,8 +527,8 @@ def __init__(self, domain=None, range=None, method='forward', raise ValueError('`domain` {!r} is not a power space' ''.format(domain)) - if not isinstance(range, DiscreteLp): - raise TypeError('`range` {!r} is not a `DiscreteLp` ' + if not isinstance(range, DiscretizedSpace): + raise TypeError('`range` {!r} is not a `DiscretizedSpace` ' 'instance'.format(range)) if len(domain) != range.ndim: @@ -629,7 +629,7 @@ def __str__(self): class Laplacian(PointwiseTensorFieldOperator): - """Spatial Laplacian operator for `DiscreteLp` spaces. + """Spatial Laplacian operator for `DiscretizedSpace` spaces. Calls helper function `finite_diff` to calculate each component of the resulting product space vector. @@ -642,7 +642,7 @@ def __init__(self, domain, range=None, pad_mode='constant', pad_const=0): Parameters ---------- - domain : `DiscreteLp` + domain : `DiscretizedSpace` Space of elements which the operator is acting on. pad_mode : string, optional The padding mode to use outside the domain. @@ -685,8 +685,8 @@ def __init__(self, domain, range=None, pad_mode='constant', pad_const=0): [ 0., 1., 0.]] ) """ - if not isinstance(domain, DiscreteLp): - raise TypeError('`domain` {!r} is not a DiscreteLp instance' + if not isinstance(domain, DiscretizedSpace): + raise TypeError('`domain` {!r} is not a DiscretizedSpace instance' ''.format(domain)) if range is None: @@ -785,7 +785,8 @@ def __str__(self): return '{}:\n{}'.format(self.__class__.__name__, indent(dom_ran_str)) -def finite_diff(f, axis, dx=1.0, method='forward', out=None, **kwargs): +def finite_diff(f, axis, dx=1.0, method='forward', out=None, + pad_mode='constant', pad_const=0): """Calculate the partial derivative of ``f`` along a given ``axis``. In the interior of the domain of f, the partial derivative is computed @@ -904,12 +905,10 @@ def finite_diff(f, axis, dx=1.0, method='forward', out=None, **kwargs): if method not in _SUPPORTED_DIFF_METHODS: raise ValueError('`method` {} was not understood'.format(method_in)) - pad_mode = kwargs.pop('pad_mode', 'constant') if pad_mode not in _SUPPORTED_PAD_MODES: raise ValueError('`pad_mode` {} not understood' ''.format(pad_mode)) - pad_const = kwargs.pop('pad_const', 0) pad_const = f.dtype.type(pad_const) if out is None: @@ -926,9 +925,6 @@ def finite_diff(f, axis, dx=1.0, method='forward', out=None, **kwargs): raise ValueError("size of array to small to use 'order2', needs at " "least 3 elements along axis {}.".format(axis)) - if kwargs: - raise ValueError('unkown keyword argument(s): {}'.format(kwargs)) - # create slice objects: initially all are [:, :, ..., :] # Swap axes so that the axis of interest is first. This is a O(1) diff --git a/odl/discr/discr_ops.py b/odl/discr/discr_ops.py index 6c5fe065232..bebaa195973 100644 --- a/odl/discr/discr_ops.py +++ b/odl/discr/discr_ops.py @@ -6,23 +6,22 @@ # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. -"""Operators defined on `DiscreteLp`.""" +"""Operators defined on `DiscretizedSpace`.""" from __future__ import absolute_import, division, print_function import numpy as np -from odl.discr import DiscreteLp, uniform_partition +from odl.discr.discr_space import DiscretizedSpace from odl.discr.discr_utils import ( - _all_interp_equal, _normalize_interp, per_axis_interpolator, - point_collocation) + _normalize_interp, per_axis_interpolator, point_collocation) +from odl.discr.partition import uniform_partition from odl.operator import Operator -from odl.set import IntervalProd -from odl.space import FunctionSpace, tensor_space +from odl.space import tensor_space from odl.util import ( - none_context, normalized_scalar_param_list, resize_array, safe_int_conv, - writable_array) + normalized_scalar_param_list, resize_array, safe_int_conv, writable_array) from odl.util.numerics import _SUPPORTED_RESIZE_PAD_MODES +from odl.util.utility import nullcontext __all__ = ('Resampling', 'ResizingOperator') @@ -39,7 +38,8 @@ def __init__(self, domain, range, interp): domain : `DiscretizedSpace` Set of elements that are to be resampled. range : `DiscretizedSpace` - Set in which the resampled elements lie. + Set in which the resampled elements lie. Must have the same + `DiscretizedSpace.domain` as ``domain``. interp : str or sequence of str Interpolation type that should be used to resample. A single value applies to all axes, and a sequence gives the interpolation @@ -73,10 +73,11 @@ def __init__(self, domain, range, interp): >>> print(resampling([0, 1, 0])) [ 0. , 0.25, 0.75, 0.75, 0.25, 0. ] """ - if domain.fspace != range.fspace: - raise ValueError('`domain.fspace` ({}) does not match ' - '`range.fspace` ({})' - ''.format(domain.fspace, range.fspace)) + if domain.domain != range.domain: + raise ValueError( + '`domain.domain` ({}) does not match `range.domain` ({})' + ''.format(domain.domain, range.domain) + ) super(Resampling, self).__init__( domain=domain, range=range, linear=True) @@ -91,7 +92,10 @@ def interp_byaxis(self): @property def interp(self): """Interpolation scheme or tuple of per-axis interpolation schemes.""" - if _all_interp_equal(self.interp_byaxis): + if ( + len(self.interp_byaxis) != 0 + and all(s == self.interp_byaxis[0] for s in self.interp_byaxis[1:]) + ): return self.interp_byaxis[0] else: return self.interp_byaxis @@ -106,8 +110,8 @@ def _call(self, x, out=None): x, self.domain.grid.coord_vectors, self.interp ) - context = none_context if out is None else writable_array - with context(out) as out_arr: + out_ctx = nullcontext() if out is None else writable_array(out) + with out_ctx as out_arr: return point_collocation( interpolator, self.range.meshgrid, out=out_arr ) @@ -162,12 +166,23 @@ def adjoint(self): return self.inverse -class ResizingOperatorBase(Operator): +class ResizingOperator(Operator): + + """Operator mapping a discretized function to a new domain. - """Base class for `ResizingOperator` and its adjoint. + This operator is a mapping between uniformly discretized + `DiscretizedSpace` spaces with the same `DiscretizedSpace.cell_sides`, + but different `DiscretizedSpace.shape`. The underlying operation is array + resizing, i.e. no resampling is performed. + In axes where the domain is enlarged, the new entries are filled + ("padded") according to a provided parameter ``pad_mode``. - This is an abstract class used to share code between the forward and - adjoint variants of the resizing operator. + All resizing operator variants are linear, except constant padding + with constant != 0. + + See `the online documentation + `_ + on resizing operators for mathematical details. """ def __init__(self, domain, range=None, ran_shp=None, **kwargs): @@ -175,10 +190,10 @@ def __init__(self, domain, range=None, ran_shp=None, **kwargs): Parameters ---------- - domain : uniform `DiscreteLp` + domain : uniform `DiscretizedSpace` Uniformly discretized space, the operator can be applied to its elements. - range : uniform `DiscreteLp`, optional + range : uniform `DiscretizedSpace`, optional Uniformly discretized space in which the result of the application of this operator lies. For the default ``None``, a space with the same attributes @@ -281,8 +296,8 @@ def __init__(self, domain, range=None, ran_shp=None, **kwargs): import builtins ran, range = range, builtins.range - if not isinstance(domain, DiscreteLp): - raise TypeError('`domain` must be a `DiscreteLp` instance, ' + if not isinstance(domain, DiscretizedSpace): + raise TypeError('`domain` must be a `DiscretizedSpace` instance, ' 'got {!r}'.format(domain)) offset = kwargs.pop('offset', None) @@ -334,8 +349,7 @@ def __init__(self, domain, range=None, ran_shp=None, **kwargs): # padding mode 'constant' with `pad_const != 0` is not linear linear = (self.pad_mode != 'constant' or self.pad_const == 0.0) - super(ResizingOperatorBase, self).__init__( - domain, ran, linear=linear) + super(ResizingOperator, self).__init__(domain, ran, linear=linear) @property def offset(self): @@ -358,26 +372,6 @@ def axes(self): return tuple(i for i in range(self.domain.ndim) if self.domain.shape[i] != self.range.shape[i]) - -class ResizingOperator(ResizingOperatorBase): - - """Operator mapping a discretized function to a new domain. - - This operator is a mapping between uniformly discretized - `DiscreteLp` spaces with the same `DiscreteLp.cell_sides`, - but different `DiscreteLp.shape`. The underlying operation is array - resizing, i.e. no resampling is performed. - In axes where the domain is enlarged, the new entries are filled - ("padded") according to a provided parameter ``pad_mode``. - - All resizing operator variants are linear, except constant padding - with constant != 0. - - See `the online documentation - `_ - on resizing operators for mathematical details. - """ - def _call(self, x, out): """Implement ``self(x, out)``.""" with writable_array(out) as out_arr: @@ -407,9 +401,9 @@ def adjoint(self): raise NotImplementedError('this operator is not linear and ' 'thus has no adjoint') - forward_op = self + op = self - class ResizingOperatorAdjoint(ResizingOperatorBase): + class ResizingOperatorAdjoint(Operator): """Adjoint of `ResizingOperator`. @@ -421,15 +415,15 @@ class ResizingOperatorAdjoint(ResizingOperatorBase): def _call(self, x, out): """Implement ``self(x, out)``.""" with writable_array(out) as out_arr: - resize_array(x.asarray(), self.range.shape, - offset=self.offset, pad_mode=self.pad_mode, + resize_array(x.asarray(), op.domain.shape, + offset=op.offset, pad_mode=op.pad_mode, pad_const=0, direction='adjoint', out=out_arr) @property def adjoint(self): """Adjoint of the adjoint, i.e. the original operator.""" - return forward_op + return op @property def inverse(self): @@ -440,11 +434,10 @@ def inverse(self): operation is not invertible. """ return ResizingOperatorAdjoint( - domain=self.range, range=self.domain, - pad_mode=self.pad_mode) + domain=self.range, range=self.domain, pad_mode=op.pad_mode + ) - return ResizingOperatorAdjoint(domain=self.range, range=self.domain, - pad_mode=self.pad_mode) + return ResizingOperatorAdjoint(op.range, op.domain, linear=True) @property def inverse(self): @@ -454,7 +447,7 @@ def inverse(self): acts as left inverse, while in restriction axes, it is a right inverse. """ - return ResizingOperator(domain=self.range, range=self.domain, + return ResizingOperator(self.range, self.domain, pad_mode=self.pad_mode, pad_const=self.pad_const) @@ -543,8 +536,6 @@ def _resize_discr(discr, newshp, offset, discr_kwargs): else: new_maxpt.append(grid_max[axis] + (num_r + 0.5) * cell_size[axis]) - fspace = FunctionSpace(IntervalProd(new_minpt, new_maxpt), - out_dtype=dtype) tspace = tensor_space(newshp, dtype=dtype, impl=impl, exponent=exponent, weighting=weighting) @@ -559,7 +550,7 @@ def _resize_discr(discr, newshp, offset, discr_kwargs): else: part = part.append(discr.partition.byaxis[i]) - return DiscreteLp(fspace, part, tspace) + return DiscretizedSpace(part, tspace) if __name__ == '__main__': diff --git a/odl/discr/lp_discr.py b/odl/discr/discr_space.py similarity index 83% rename from odl/discr/lp_discr.py rename to odl/discr/discr_space.py index f0f43be8cf8..8a1443ae342 100644 --- a/odl/discr/lp_discr.py +++ b/odl/discr/discr_space.py @@ -14,43 +14,42 @@ import numpy as np -from odl.discr.discr_utils import point_collocation -from odl.discr.discretization import ( - DiscretizedSpace, DiscretizedSpaceElement, tspace_type) +from odl.discr.discr_utils import point_collocation, sampling_function from odl.discr.partition import ( RectPartition, uniform_partition, uniform_partition_fromintv) -from odl.set import ComplexNumbers, IntervalProd, RealNumbers -from odl.space import FunctionSpace, ProductSpace +from odl.set import IntervalProd, RealNumbers +from odl.space import ProductSpace +from odl.space.base_tensors import Tensor, TensorSpace from odl.space.entry_points import tensor_space_impl from odl.space.weighting import ConstWeighting from odl.util import ( - apply_on_boundary, array_str, dtype_str, is_complex_floating_dtype, - is_floating_dtype, is_numeric_dtype, is_real_dtype, - normalized_nodes_on_bdry, normalized_scalar_param_list, repr_string, - safe_int_conv, signature_string_parts) + apply_on_boundary, array_str, dtype_str, is_floating_dtype, + is_numeric_dtype, normalized_nodes_on_bdry, normalized_scalar_param_list, + repr_string, safe_int_conv, signature_string_parts) -__all__ = ('DiscreteLp', 'DiscreteLpElement', - 'uniform_discr_frompartition', 'uniform_discr_fromspace', - 'uniform_discr_fromintv', 'uniform_discr', - 'uniform_discr_fromdiscr', 'discr_sequence_space') +__all__ = ( + 'DiscretizedSpace', + 'DiscretizedSpaceElement', + 'uniform_discr_frompartition', + 'uniform_discr_fromintv', + 'uniform_discr', + 'uniform_discr_fromdiscr', +) -class DiscreteLp(DiscretizedSpace): +class DiscretizedSpace(TensorSpace): """Discretization of a Lebesgue :math:`L^p` space.""" - def __init__(self, fspace, partition, tspace, **kwargs): + def __init__(self, partition, tspace, **kwargs): """Initialize a new instance. Parameters ---------- - fspace : `FunctionSpace` - The continuous space to be discretized. partition : `RectPartition` - Partition of (a subset of) ``fspace.domain``. + Partition of a rectangular spatial domain. tspace : `TensorSpace` - Space of elements used for data storage. It must have the - same `TensorSpace.field` as ``fspace`` and the same + Space of elements used for data storage. It must have the same `TensorSpace.shape` as ``partition``. axis_labels : sequence of str, optional Names of the axes to use for plotting etc. @@ -63,32 +62,23 @@ def __init__(self, fspace, partition, tspace, **kwargs): Note: The ``$`` signs ensure rendering as LaTeX. """ - if not isinstance(fspace, FunctionSpace): - raise TypeError('{!r} is not a FunctionSpace instance' - ''.format(fspace)) - if not isinstance(fspace.domain, IntervalProd): - raise TypeError('function space domain {!r} is not an ' - 'IntervalProd instance'.format(fspace.domain)) if not isinstance(partition, RectPartition): - raise TypeError('`partition` {!r} is not a RectPartition ' - 'instance'.format(partition)) - if not fspace.domain.contains_set(partition.set): - raise ValueError('`partition` {} is not a subset of the function ' - 'domain {}'.format(partition, fspace.domain)) - if fspace.scalar_out_dtype != tspace.dtype: - raise ValueError('`fspace.scalar_out_dtype` does not match ' - '`tspace.dtype`: {} != {}' - ''.format(fspace.scalar_out_dtype, tspace.dtype)) + raise TypeError('`partition` must be a `RectPartition`, got {!r}' + ''.format(partition)) + if not isinstance(tspace, TensorSpace): + raise TypeError('`tspace` must be a `TensorSpace`, got {!r}' + ''.format(tspace)) if partition.shape != tspace.shape: raise ValueError( - "`partition.shape` must be equal to `tspace.shape`, but " - "{} != {}".format(partition.shape, tspace.shape) + '`partition.shape` must be equal to `tspace.shape`, but ' + '{} != {}'.format(partition.shape, tspace.shape) ) - super(DiscreteLp, self).__init__(fspace, tspace) - + self.__tspace = tspace self.__partition = partition + super(DiscretizedSpace, self).__init__(tspace.shape, tspace.dtype) + # Set axis labels axis_labels = kwargs.pop('axis_labels', None) if axis_labels is None: @@ -104,19 +94,61 @@ def __init__(self, fspace, partition, tspace, **kwargs): raise ValueError('got unexpected keyword arguments {}' ''.format(kwargs)) + # --- Meta-info + @property - def axis_labels(self): - """Labels for axes when displaying space elements.""" - return self.__axis_labels + def element_type(self): + """`DiscretizedSpaceElement`""" + return DiscretizedSpaceElement + + # --- Constructor args @property def partition(self): """`RectPartition` of the function domain.""" return self.__partition + @property + def tspace(self): + """Space for the coefficients of the elements of this space.""" + return self.__tspace + + @property + def axis_labels(self): + """Labels for axes when displaying space elements.""" + return self.__axis_labels + + # --- Pass-through `partition` attributes + + @property + def domain(self): + """Set on which functions are defined before discretization.""" + return self.partition.set + + # --- Pass-through `tspace` attributes + + @property + def weighting(self): + """This space's weighting scheme.""" + # TODO(kohr-h): `weighting` is optional in `tspace`, how should we + # handle that? + return self.tspace.weighting + + @property + def is_weighted(self): + """``True`` if the ``tspace`` is weighted.""" + return getattr(self.tspace, 'is_weighted', False) + + @property + def impl(self): + """Name of the implementation back-end.""" + return self.tspace.impl + @property def exponent(self): """Exponent of this space, the ``p`` in ``L^p``.""" + # TODO(kohr-h): `exponent` is optional in `tspace`, how should we + # handle that? return self.tspace.exponent @property @@ -198,6 +230,27 @@ def default_order(self): """ return self.tspace.default_order + def default_dtype(self, field=None): + """Default data type for new elements in this space. + + This is equal to the default data type of `tspace`. + """ + return self.tspace.default_dtype(field) + + def available_dtypes(self): + """Available data types for new elements in this space. + + This is equal to the available data types of `tspace`. + """ + return self.tspace.available_dtypes() + + # --- Derived properties + + @property + def tspace_type(self): + """Tensor space type of this space.""" + return type(self.tspace) + @property def tangent_bundle(self): """The tangent bundle associated with `domain` using `partition`. @@ -228,6 +281,8 @@ def is_uniformly_weighted(self): return is_uniformly_weighted + # --- Element creation + def element(self, inp=None, order=None, **kwargs): """Create an element from ``inp`` or from scratch. @@ -258,10 +313,6 @@ def element(self, inp=None, order=None, **kwargs): Storage order of the returned element. For ``'C'`` and ``'F'``, contiguous memory in the respective ordering is enforced. The default ``None`` enforces no contiguousness. - vectorized : bool, optional - If ``True``, assume that a provided callable ``inp`` supports - vectorized evaluation. Otherwise, wrap it in a vectorizer. - Default: ``True``. kwargs : Additional arguments passed on to `point_collocation` when called on ``inp``, in the form @@ -270,7 +321,7 @@ def element(self, inp=None, order=None, **kwargs): Returns ------- - element : `DiscreteLpElement` + element : `DiscretizedSpaceElement` The discretized element, calculated as ``point_collocation(inp)`` or ``tspace.element(inp)``, tried in this order. @@ -310,68 +361,38 @@ def element(self, inp=None, order=None, **kwargs): elif inp in self.tspace and order is None: return self.element_type(self, inp) elif callable(inp): - vectorized = kwargs.pop('vectorized', True) - # fspace element -> discretize - inp_elem = self.fspace.element(inp, vectorized=vectorized) - sampled = point_collocation(inp_elem, self.meshgrid, **kwargs) + func = sampling_function( + inp, self.domain, out_dtype=self.dtype, + ) + sampled = point_collocation(func, self.meshgrid, **kwargs) return self.element_type( - self, self.tspace.element(sampled, order=order)) + self, self.tspace.element(sampled, order=order) + ) else: # Sequence-type input return self.element_type( - self, self.tspace.element(inp, order=order)) + self, self.tspace.element(inp, order=order) + ) + + def zero(self): + """Return the element of all zeros.""" + return self.element_type(self, self.tspace.zero()) + + def one(self): + """Return the element of all ones.""" + return self.element_type(self, self.tspace.one()) + + # --- Casting def _astype(self, dtype): """Internal helper for ``astype``.""" - fspace = self.fspace.astype(dtype) tspace = self.tspace.astype(dtype) return type(self)( - fspace, self.partition, tspace, axis_labels=self.axis_labels - ) + self.partition, tspace, axis_labels=self.axis_labels) - # Overrides for space functions depending on partition - # - # The inherited methods by default use a weighting by a constant - # (the grid cell size). In dimensions where the partitioned set contains - # only a fraction of the outermost cells (e.g. if the outermost grid - # points lie at the boundary), the corresponding contribuitons to - # discretized integrals need to be scaled by that fraction. - def _inner(self, x, y): - """Return ``self.inner(x, y)``.""" - if self.is_uniform and not self.is_uniformly_weighted: - # TODO: implement without copying x - bdry_fracs = self.partition.boundary_cell_fractions - func_list = _scaling_func_list(bdry_fracs, exponent=1.0) - x_arr = apply_on_boundary(x, func=func_list, only_once=False) - return super(DiscreteLp, self)._inner(self.element(x_arr), y) - else: - return super(DiscreteLp, self)._inner(x, y) + # --- Slicing - def _norm(self, x): - """Return ``self.norm(x)``.""" - if self.is_uniform and not self.is_uniformly_weighted: - # TODO: implement without copying x - bdry_fracs = self.partition.boundary_cell_fractions - func_list = _scaling_func_list(bdry_fracs, exponent=self.exponent) - x_arr = apply_on_boundary(x, func=func_list, only_once=False) - return super(DiscreteLp, self)._norm(self.element(x_arr)) - else: - return super(DiscreteLp, self)._norm(x) - - def _dist(self, x, y): - """Return ``self.dist(x, y)``.""" - if self.is_uniform and not self.is_uniformly_weighted: - bdry_fracs = self.partition.boundary_cell_fractions - func_list = _scaling_func_list(bdry_fracs, exponent=self.exponent) - arrs = [apply_on_boundary(vec, func=func_list, only_once=False) - for vec in (x, y)] - - return super(DiscreteLp, self)._dist( - self.element(arrs[0]), self.element(arrs[1])) - else: - return super(DiscreteLp, self)._dist(x, y) - - # TODO: add byaxis_out when discretized tensor-valued functions are + # TODO: add `byaxis`_out when discretized tensor-valued functions are # available @property @@ -397,7 +418,7 @@ def byaxis_in(self): """ space = self - class DiscreteLpByaxisIn(object): + class DiscretizedSpaceByaxisIn(object): """Helper class for indexing by domain axes.""" @@ -411,11 +432,10 @@ def __getitem__(self, indices): Returns ------- - space : `DiscreteLp` + space : `DiscretizedSpace` The resulting space with indexed domain and otherwise same properties (except possibly weighting). """ - fspace = space.fspace.byaxis_in[indices] part = space.partition.byaxis[indices] if isinstance(space.weighting, ConstWeighting): @@ -435,6 +455,8 @@ def __getitem__(self, indices): else: # Other weighting schemes are handled correctly by # the tensor space + # TODO(kohr-h): `byaxis` is not guaranteed to exist in + # `tspace`, how to handle that? tspace = space.tspace.byaxis[indices] try: @@ -445,29 +467,117 @@ def __getitem__(self, indices): labels = tuple(space.axis_labels[int(i)] for i in indices) - return DiscreteLp(fspace, part, tspace, axis_labels=labels) + return DiscretizedSpace(part, tspace, axis_labels=labels) def __repr__(self): """Return ``repr(self)``.""" return repr(space) + '.byaxis_in' - return DiscreteLpByaxisIn() + return DiscretizedSpaceByaxisIn() + + # --- Identity + + def __eq__(self, other): + """Return ``self == other``. + + Returns + ------- + equals : bool + ``True`` if ``other`` is a `DiscretizedSpace` with equal + `tspace`, ``False`` otherwise. + """ + # Optimizations for simple cases + if other is self: + return True + elif other is None: + return False + else: + return ( + super(DiscretizedSpace, self).__eq__(other) + and other.tspace == self.tspace + and other.partition == self.partition + ) + + def __hash__(self): + """Return ``hash(self)``.""" + return hash( + (super(DiscretizedSpace, self).__hash__(), + self.tspace, + self.partition) + ) + + # --- Space functions + + def _lincomb(self, a, x1, b, x2, out): + """Raw linear combination.""" + self.tspace._lincomb(a, x1.tensor, b, x2.tensor, out.tensor) + + def _multiply(self, x1, x2, out): + """Raw pointwise multiplication of two elements.""" + self.tspace._multiply(x1.tensor, x2.tensor, out.tensor) + + def _divide(self, x1, x2, out): + """Raw pointwise multiplication of two elements.""" + self.tspace._divide(x1.tensor, x2.tensor, out.tensor) + + # The inherited methods by default use a weighting by a constant + # (the grid cell size). In dimensions where the partitioned set contains + # only a fraction of the outermost cells (e.g. if the outermost grid + # points lie at the boundary), the corresponding contributions to + # discretized integrals need to be scaled by that fraction. + def _inner(self, x, y): + """Return ``self.inner(x, y)``.""" + if self.is_uniform and not self.is_uniformly_weighted: + # TODO: implement without copying x + bdry_fracs = self.partition.boundary_cell_fractions + func_list = _scaling_func_list(bdry_fracs, exponent=1.0) + x_arr = apply_on_boundary(x, func=func_list, only_once=False) + return self.tspace.inner(self.tspace.element(x_arr), y.tensor) + else: + return self.tspace.inner(x.tensor, y.tensor) + + def _norm(self, x): + """Return ``self.norm(x)``.""" + if self.is_uniform and not self.is_uniformly_weighted: + # TODO: implement without copying x + bdry_fracs = self.partition.boundary_cell_fractions + func_list = _scaling_func_list(bdry_fracs, exponent=self.exponent) + x_arr = apply_on_boundary(x, func=func_list, only_once=False) + return self.tspace.norm(self.tspace.element(x_arr)) + else: + return self.tspace.norm(x.tensor) + + def _dist(self, x, y): + """Return ``self.dist(x, y)``.""" + if self.is_uniform and not self.is_uniformly_weighted: + bdry_fracs = self.partition.boundary_cell_fractions + func_list = _scaling_func_list(bdry_fracs, exponent=self.exponent) + arrs = [apply_on_boundary(vec, func=func_list, only_once=False) + for vec in (x, y)] + + return self.tspace.dist( + self.tspace.element(arrs[0]), + self.tspace.element(arrs[1]), + ) + else: + return self.tspace.dist(x.tensor, y.tensor) def __repr__(self): """Return ``repr(self)``.""" # Clunky check if the factory repr can be used if uniform_partition_fromintv( - self.fspace.domain, self.shape, nodes_on_bdry=False + self.partition.set, self.shape, nodes_on_bdry=False ) == self.partition: use_uniform = True nodes_on_bdry = False elif uniform_partition_fromintv( - self.fspace.domain, self.shape, nodes_on_bdry=True + self.partition.set, self.shape, nodes_on_bdry=True ) == self.partition: use_uniform = True nodes_on_bdry = True else: use_uniform = False + nodes_on_bdry = None if use_uniform: ctor = 'uniform_discr' @@ -519,7 +629,7 @@ def __repr__(self): else: ctor = self.__class__.__name__ - posargs = [self.fspace, self.partition, self.tspace] + posargs = [self.partition, self.tspace] inner_parts = signature_string_parts(posargs, []) return repr_string(ctor, inner_parts, allow_mixed_seps=False) @@ -527,15 +637,24 @@ def __str__(self): """Return ``str(self)``.""" return repr(self) - @property - def element_type(self): - """`DiscreteLpElement`""" - return DiscreteLpElement +class DiscretizedSpaceElement(Tensor): + + """Representation of a `DiscretizedSpace` element.""" -class DiscreteLpElement(DiscretizedSpaceElement): + def __init__(self, space, tensor): + """Initialize a new instance.""" + super(DiscretizedSpaceElement, self).__init__(space) + self.__tensor = tensor - """Representation of a `DiscreteLp` element.""" + # --- Constructor args + + @property + def tensor(self): + """Structure for data storage.""" + return self.__tensor + + # --- Pass-through `space` properties @property def cell_sides(self): @@ -547,24 +666,113 @@ def cell_volume(self): """Cell volume of an underlying regular grid.""" return self.space.cell_volume + # --- Pass-through `tensor` properties + @property def data(self): """Data container of ``self``, depends on ``space.impl``.""" return self.tensor.data + @property + def dtype(self): + """Type of data storage.""" + return self.tensor.dtype + + @property + def size(self): + """Size of data storage.""" + return self.tensor.size + + def __len__(self): + """Return ``len(self)``. + + Equivalent to ``self.shape[0]`` if possible. Zero-dimensional + tensors have no length and produce a `TypeError`. + """ + return len(self.tensor) + + def copy(self): + """Create an identical (deep) copy of this element.""" + return self.space.element(self.tensor.copy()) + + def asarray(self, out=None): + """Extract the data of this array as a numpy array. + + Parameters + ---------- + out : `numpy.ndarray`, optional + Array in which the result should be written in-place. + Has to be contiguous and of the correct dtype. + """ + return self.tensor.asarray(out=out) + + def astype(self, dtype): + """Return a copy of this element with new ``dtype``. + + Parameters + ---------- + dtype : + Scalar data type of the returned space. Can be provided + in any way the `numpy.dtype` constructor understands, e.g. + as built-in type or as a string. Data types with non-trivial + shapes are not allowed. + + Returns + ------- + newelem : `DisceteLpElement` + Version of this element with given data type. + """ + return self.space.astype(dtype).element(self.tensor.astype(dtype)) + + def __eq__(self, other): + """Return ``self == other``. + + Returns + ------- + equals : bool + ``True`` if all entries of ``other`` are equal to this + element's entries, ``False`` otherwise. + """ + return other in self.space and other.tensor == self.tensor + + def __getitem__(self, indices): + """Return ``self[indices]``. + + Parameters + ---------- + indices : int or `slice` + The position(s) that should be accessed. + + Returns + ------- + values : `Tensor` + The value(s) at the index (indices). + """ + if isinstance(indices, type(self)): + indices = indices.tensor + return self.tensor[indices] + + def __ipow__(self, p): + """Implement ``self **= p``.""" + # The concrete `tensor` can specialize `__ipow__` for non-integer + # `p` so we want to use it here. Otherwise we get the default + # `LinearSpaceElement.__ipow__` which only works for integer `p`. + self.tensor.__ipow__(p) + return self + @property def real(self): """Real part of this element. Returns ------- - real : `DiscreteLpElement` + real : `DiscretizedSpaceElement` Examples -------- Get the real part: - >>> discr = uniform_discr(0, 1, 3, dtype=complex) + >>> discr = odl.uniform_discr(0, 1, 3, dtype=complex) >>> x = discr.element([5+1j, 3, 2-2j]) >>> x.real uniform_discr(0.0, 1.0, 3).element([ 5., 3., 2.]) @@ -607,7 +815,7 @@ def imag(self): Returns ------- - imag : `DiscreteLpElement` + imag : `DiscretizedSpaceElement` Examples -------- @@ -662,13 +870,13 @@ def conj(self, out=None): Parameters ---------- - out : `DiscreteLpElement`, optional + out : `DiscretizedSpaceElement`, optional Element to which the complex conjugate is written. Must be an element of this element's space. Returns ------- - out : `DiscreteLpElement` + out : `DiscretizedSpaceElement` The complex conjugate element. If ``out`` is provided, the returned object is a reference to it. @@ -724,7 +932,11 @@ def __setitem__(self, indices, values): if values in self.space: self.tensor[indices] = values.tensor else: - super(DiscreteLpElement, self).__setitem__(indices, values) + if isinstance(indices, type(self)): + indices = indices.tensor + if isinstance(values, type(self)): + values = values.tensor + self.tensor.__setitem__(indices, values) def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): """Interface to Numpy's ufunc machinery. @@ -772,9 +984,9 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): Returns ------- - ufunc_result : `DiscreteLpElement`, `numpy.ndarray` or tuple + ufunc_result : `DiscretizedSpaceElement`, `numpy.ndarray` or tuple Result of the ufunc evaluation. If no ``out`` keyword argument - was given, the result is a `DiscreteLpElement` or a tuple + was given, the result is a `DiscretizedSpaceElement` or a tuple of such, depending on the number of outputs of ``ufunc``. If ``out`` was provided, the returned object or sequence members refer(s) to ``out``. @@ -791,7 +1003,7 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): >>> np.add(x, y) # same mechanism for Numpy >= 1.13 uniform_discr(0.0, 1.0, 3).element([ 0., 0., 0.]) - As ``out``, a `DiscreteLpElement` can be provided as well as a + As ``out``, a `DiscretizedSpaceElement` can be provided as well as a `Tensor` of appropriate type, or its underlying data container type (wrapped in a sequence): @@ -943,7 +1155,8 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): # --- Process `inputs` --- # - # Pull out the `tensor` attributes from DiscreteLpElement instances + # Pull out the `tensor` attributes from `DiscretizedSpaceElement` + # instances # since we want to pass them to `self.tensor.__array_ufunc__` input_tensors = tuple( elem.tensor if isinstance(elem, type(self)) else elem @@ -967,8 +1180,6 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): reduced_axes = [i for i in range(self.ndim) if i not in axis] - weighting = self.space.weighting - # --- Evaluate ufunc --- # if method == '__call__': @@ -978,15 +1189,8 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): ufunc, '__call__', *input_tensors, **kwargs) if out is None: - # Wrap result tensor in appropriate DiscreteLp space. - # Make new function space based on result dtype, - # keep everything else, and get `tspace` from the result - # tensor. - out_dtype = (res_tens.dtype, self.space.fspace.out_shape) - fspace = FunctionSpace(self.space.fspace.domain, - out_dtype) - res_space = DiscreteLp( - fspace, + # Wrap result tensor in appropriate DiscretizedSpace space. + res_space = DiscretizedSpace( self.space.partition, res_tens.space, axis_labels=self.space.axis_labels @@ -1004,11 +1208,7 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): if out1 is None: # Wrap as for nout = 1 - out_dtype = (res1_tens.dtype, self.space.fspace.out_shape) - fspace = FunctionSpace(self.space.fspace.domain, - out_dtype) - res_space = DiscreteLp( - fspace, + res_space = DiscretizedSpace( self.space.partition, res1_tens.space, axis_labels=self.space.axis_labels @@ -1019,11 +1219,7 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): if out2 is None: # Wrap as for nout = 1 - out_dtype = (res2_tens.dtype, self.space.fspace.out_shape) - fspace = FunctionSpace(self.space.fspace.domain, - out_dtype) - res_space = DiscreteLp( - fspace, + res_space = DiscretizedSpace( self.space.partition, res2_tens.space, axis_labels=self.space.axis_labels @@ -1076,15 +1272,10 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): return res_tens if out is None: - # Wrap in appropriate DiscreteLp space depending on `method` + # Wrap in appropriate DiscretizedSpace space depending + # on `method` if method == 'accumulate': - # Make `fspace` with appropriate dtype, get `tspace` - # from the result tensor and keep the rest - fspace = FunctionSpace(self.space.domain, - out_dtype=res_tens.dtype) - - res_space = DiscreteLp( - fspace, + res_space = DiscretizedSpace( self.space.partition, res_tens.space, axis_labels=self.space.axis_labels @@ -1092,11 +1283,9 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): result = res_space.element(res_tens) elif method == 'outer': - # Concatenate domains, partitions, axis_labels, + # Concatenate partitions and axis_labels, # and determine `tspace` from the result tensor inp1, inp2 = inputs - domain = inp1.space.domain.append(inp2.space.domain) - fspace = FunctionSpace(domain, out_dtype=res_tens.dtype) part = inp1.space.partition.append(inp2.space.partition) labels1 = [lbl + ' (1)' for lbl in inp1.space.axis_labels] labels2 = [lbl + ' (2)' for lbl in inp2.space.axis_labels] @@ -1118,8 +1307,8 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): # Otherwise `TensorSpace` knows how to handle this tspace = res_tens.space - res_space = DiscreteLp( - fspace, part, tspace, axis_labels=labels + res_space = DiscretizedSpace( + part, tspace, axis_labels=labels ) result = res_space.element(res_tens) @@ -1211,6 +1400,8 @@ def show(self, title=None, method='', coords=None, indices=None, is that ``fig`` is the return value of an earlier call to this function. + Other Parameters + ---------------- interp : {'linear', 'nearest'}, optional Interpolation type that should be used for the plot. @@ -1346,7 +1537,7 @@ def uniform_discr_frompartition(partition, dtype=None, impl='numpy', **kwargs): Returns ------- - discr : `DiscreteLp` + discr : `DiscretizedSpace` The uniformly discretized function space. Examples @@ -1372,11 +1563,9 @@ def uniform_discr_frompartition(partition, dtype=None, impl='numpy', **kwargs): if dtype is not None: dtype = np.dtype(dtype) - fspace = FunctionSpace(partition.set, out_dtype=dtype) - ds_type = tspace_type(fspace, impl, dtype) - + tspace_type = tensor_space_impl(impl) if dtype is None: - dtype = ds_type.default_dtype() + dtype = tspace_type.default_dtype() weighting = kwargs.pop('weighting', None) exponent = kwargs.pop('exponent', 2.0) @@ -1386,84 +1575,9 @@ def uniform_discr_frompartition(partition, dtype=None, impl='numpy', **kwargs): else: weighting = partition.cell_volume - tspace = ds_type(partition.shape, dtype, exponent=exponent, - weighting=weighting) - return DiscreteLp(fspace, partition, tspace, **kwargs) - - -def uniform_discr_fromspace(fspace, shape, dtype=None, impl='numpy', **kwargs): - """Return a uniformly discretized L^p function space. - - Parameters - ---------- - fspace : `FunctionSpace` - Continuous function space. Its domain must be an `IntervalProd`. - shape : int or sequence of ints - Number of samples per axis. - dtype : optional - Data type for the discretized space, must be understood by the - `numpy.dtype` constructor. The default for ``None`` depends on the - ``impl`` backend, usually it is ``'float64'`` or ``'float32'``. - impl : string, optional - Implementation of the data storage arrays - kwargs : - Additional keyword parameters, see `uniform_discr` for details. - - Returns - ------- - discr : `DiscreteLp` - The uniformly discretized function space - - Examples - -------- - >>> intv = odl.IntervalProd(0, 1) - >>> space = odl.FunctionSpace(intv) - >>> uniform_discr_fromspace(space, 10) - uniform_discr(0.0, 1.0, 10) - - See Also - -------- - uniform_discr : implicit uniform Lp discretization - uniform_discr_frompartition : uniform Lp discretization using a given - uniform partition of a function domain - uniform_discr_fromintv : uniform discretization from an existing - interval product - odl.discr.partition.uniform_partition : - partition of the function domain - """ - if not isinstance(fspace, FunctionSpace): - raise TypeError('`fspace` {!r} is not a `FunctionSpace` instance' - ''.format(fspace)) - if not isinstance(fspace.domain, IntervalProd): - raise TypeError('domain {!r} of the function space is not an ' - '`IntervalProd` instance'.format(fspace.domain)) - - # Set data type. If given, check consistency with fspace's field and - # out_dtype. If not given, take the latter. - if dtype is None: - dtype = fspace.out_dtype - else: - dtype, dtype_in = np.dtype(dtype), dtype - if not np.can_cast(fspace.scalar_out_dtype, dtype, casting='safe'): - raise ValueError('cannot safely cast from output data {} type of ' - 'the function space to given data type {}' - ''.format(fspace.out, dtype_in)) - - if fspace.field == RealNumbers() and not is_real_dtype(dtype): - raise ValueError('cannot discretize real space {} with ' - 'non-real data type {}' - ''.format(fspace, dtype)) - elif (fspace.field == ComplexNumbers() and - not is_complex_floating_dtype(dtype)): - raise ValueError('cannot discretize complex space {} with ' - 'non-complex-floating data type {}' - ''.format(fspace, dtype)) - - nodes_on_bdry = kwargs.pop('nodes_on_bdry', False) - partition = uniform_partition_fromintv(fspace.domain, shape, - nodes_on_bdry) - - return uniform_discr_frompartition(partition, dtype, impl, **kwargs) + tspace = tspace_type(partition.shape, dtype, exponent=exponent, + weighting=weighting) + return DiscretizedSpace(partition, tspace, **kwargs) def uniform_discr_fromintv(intv_prod, shape, dtype=None, impl='numpy', @@ -1487,7 +1601,7 @@ def uniform_discr_fromintv(intv_prod, shape, dtype=None, impl='numpy', Returns ------- - discr : `DiscreteLp` + discr : `DiscretizedSpace` The uniformly discretized function space Examples @@ -1501,14 +1615,13 @@ def uniform_discr_fromintv(intv_prod, shape, dtype=None, impl='numpy', uniform_discr : implicit uniform Lp discretization uniform_discr_frompartition : uniform Lp discretization using a given uniform partition of a function domain - uniform_discr_fromspace : uniform discretization from an existing - function space """ if dtype is None: dtype = tensor_space_impl(str(impl).lower()).default_dtype() - fspace = FunctionSpace(intv_prod, out_dtype=dtype) - return uniform_discr_fromspace(fspace, shape, dtype, impl, **kwargs) + nodes_on_bdry = kwargs.pop('nodes_on_bdry', False) + partition = uniform_partition_fromintv(intv_prod, shape, nodes_on_bdry) + return uniform_discr_frompartition(partition, dtype, impl, **kwargs) def uniform_discr(min_pt, max_pt, shape, dtype=None, impl='numpy', **kwargs): @@ -1555,7 +1668,7 @@ def uniform_discr(min_pt, max_pt, shape, dtype=None, impl='numpy', **kwargs): Returns ------- - discr : `DiscreteLp` + discr : `DiscretizedSpace` The uniformly discretized function space Examples @@ -1595,45 +1708,6 @@ def uniform_discr(min_pt, max_pt, shape, dtype=None, impl='numpy', **kwargs): return uniform_discr_fromintv(intv_prod, shape, dtype, impl, **kwargs) -def discr_sequence_space(shape, dtype=None, impl='numpy', **kwargs): - """Return an object mimicing the sequence space ``l^p(R^d)``. - - The returned object is a `DiscreteLp` on the domain ``[0, shape - 1]``, - using a uniform grid with stride 1. - - Parameters - ---------- - shape : int or sequence of ints - Number of element entries per axis. - dtype : optional - Data type for the discretized space, must be understood by the - `numpy.dtype` constructor. The default for ``None`` depends on the - ``impl`` backend, usually it is ``'float64'`` or ``'float32'``. - impl : string, optional - Implementation of the data storage arrays. - kwargs : - Additional keyword parameters, see `uniform_discr` for details. - Note that ``nodes_on_bdry`` cannot be given. - - Returns - ------- - seqspc : `DiscreteLp` - Sequence-space-like discrete Lp. - - Examples - -------- - >>> seq_spc = discr_sequence_space((3, 3)) - >>> seq_spc.one().norm() == 3.0 - True - >>> seq_spc = discr_sequence_space((3, 3), exponent=1) - >>> seq_spc.one().norm() == 9.0 - True - """ - shape = np.atleast_1d(shape) - return uniform_discr([0] * len(shape), shape - 1, shape, dtype, impl, - nodes_on_bdry=True, **kwargs) - - def uniform_discr_fromdiscr(discr, min_pt=None, max_pt=None, shape=None, cell_sides=None, **kwargs): """Return a discretization based on an existing one. @@ -1644,7 +1718,7 @@ def uniform_discr_fromdiscr(discr, min_pt=None, max_pt=None, Parameters ---------- - discr : `DiscreteLp` + discr : `DiscretizedSpace` Uniformly discretized space used as a template. min_pt, max_pt: float or sequence of floats, optional Desired minimum/maximum corners of the new space domain. @@ -1652,6 +1726,9 @@ def uniform_discr_fromdiscr(discr, min_pt=None, max_pt=None, Desired number of samples per axis of the new space. cell_sides : float or sequence of floats, optional Desired cell side lenghts of the new space's partition. + + Other Parameters + ---------------- nodes_on_bdry : bool or sequence, optional If a sequence is provided, it determines per axis whether to place the last grid point on the boundary (``True``) or shift it @@ -1667,7 +1744,7 @@ def uniform_discr_fromdiscr(discr, min_pt=None, max_pt=None, Default: ``False``. kwargs : - Additional keyword parameters passed to the `DiscreteLp` + Additional keyword parameters passed to the `DiscretizedSpace` initializer. Notes @@ -1760,8 +1837,8 @@ def uniform_discr_fromdiscr(discr, min_pt=None, max_pt=None, >>> new_discr.cell_sides array([ 0.1 , 0.25]) """ - if not isinstance(discr, DiscreteLp): - raise TypeError('`discr` {!r} is not a DiscreteLp instance' + if not isinstance(discr, DiscretizedSpace): + raise TypeError('`discr` {!r} is not a DiscretizedSpace instance' ''.format(discr)) if not discr.is_uniform: raise ValueError('`discr` {} is not uniformly discretized' diff --git a/odl/discr/discr_utils.py b/odl/discr/discr_utils.py index 366b09a9682..25eb08a8e38 100644 --- a/odl/discr/discr_utils.py +++ b/odl/discr/discr_utils.py @@ -6,23 +6,34 @@ # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. -"""Helpers for discretization-related functionality.""" +"""Helpers for discretization-related functionality. + +Most functions deal with interpolation of arrays, sampling of functions and +providing a single interface for the sampler by wrapping functions or +arrays of functions appropriately. +""" from __future__ import absolute_import, division, print_function +import inspect +import sys +from builtins import object +from functools import partial from itertools import product import numpy as np from odl.util import ( - is_string, is_valid_input_array, is_valid_input_meshgrid, - out_shape_from_array, out_shape_from_meshgrid) + dtype_repr, is_real_dtype, is_string, is_valid_input_array, + is_valid_input_meshgrid, out_shape_from_array, out_shape_from_meshgrid, + writable_array) __all__ = ( 'point_collocation', 'nearest_interpolator', 'linear_interpolator', 'per_axis_interpolator', + 'sampling_function', ) SUPPORTED_INTERP = ['nearest', 'linear'] @@ -61,8 +72,7 @@ def point_collocation(func, points, out=None, **kwargs): >>> from odl.discr.grid import sparse_meshgrid >>> domain = odl.IntervalProd(0, 5) - >>> fspace = odl.FunctionSpace(domain) - >>> func = fspace.element(lambda x: x ** 2) + >>> func = sampling_function(lambda x: x ** 2, domain) >>> mesh = sparse_meshgrid([1, 2, 3]) >>> point_collocation(func, mesh) array([ 1., 4., 9.]) @@ -79,11 +89,10 @@ def point_collocation(func, points, out=None, **kwargs): around the call would iterate over all points: >>> domain = odl.IntervalProd([0, 0], [5, 5]) - >>> fspace = odl.FunctionSpace(domain) >>> xs = [1, 2] >>> ys = [3, 4, 5] >>> mesh = sparse_meshgrid(xs, ys) - >>> func = fspace.element(lambda x: x[0] - x[1]) + >>> func = sampling_function(lambda x: x[0] - x[1], domain) >>> point_collocation(func, mesh) array([[-2., -3., -4.], [-1., -2., -3.]]) @@ -93,7 +102,7 @@ def point_collocation(func, points, out=None, **kwargs): >>> def f(x, c=0): ... return x[0] + c - >>> func = fspace.element(f) + >>> func = sampling_function(f, domain) >>> point_collocation(func, mesh) # uses default c=0 array([[ 1., 1., 1.], [ 2., 2., 2.]]) @@ -106,14 +115,16 @@ def point_collocation(func, points, out=None, **kwargs): array-like of results, or as an array-like of member functions: >>> domain = odl.IntervalProd([0, 0], [5, 5]) - >>> # Need to tell the wrapper that we want a 3-component function - >>> fspace = odl.FunctionSpace(domain, out_dtype=(float, (3,))) >>> xs = [1, 2] >>> ys = [3, 4] >>> mesh = sparse_meshgrid(xs, ys) >>> def vec_valued(x): ... return (x[0] - 1, 0, x[0] + x[1]) # broadcasting - >>> func1 = fspace.element(vec_valued) + >>> # For a function with several output components, we must specify the + >>> # shape explicitly in the `out_dtype` parameter + >>> func1 = sampling_function( + ... vec_valued, domain, out_dtype=(float, (3,)) + ... ) >>> point_collocation(func1, mesh) array([[[ 0., 0.], [ 1., 1.]], @@ -123,12 +134,13 @@ def point_collocation(func, points, out=None, **kwargs): [[ 4., 5.], [ 5., 6.]]]) - >>> list_of_funcs = [ + >>> list_of_funcs = [ # equivalent to `vec_valued` ... lambda x: x[0] - 1, ... 0, # constants are allowed ... lambda x: x[0] + x[1] ... ] - >>> func2 = fspace.element(list_of_funcs) + >>> # For an array of functions, the output shape can be inferred + >>> func2 = sampling_function(list_of_funcs, domain) >>> point_collocation(func2, mesh) array([[[ 0., 0.], [ 1., 1.]], @@ -147,8 +159,7 @@ def point_collocation(func, points, out=None, **kwargs): See Also -------- - make_func_for_sampling : - wrap a function so it can handle all valid types of input + make_func_for_sampling : wrap a function odl.discr.grid.RectGrid.meshgrid numpy.meshgrid @@ -164,13 +175,6 @@ def point_collocation(func, points, out=None, **kwargs): return out -def _all_interp_equal(interp_byaxis): - """Whether all entries are equal, with ``False`` for length 0.""" - if len(interp_byaxis) == 0: - return False - return all(itp == interp_byaxis[0] for itp in interp_byaxis) - - def _normalize_interp(interp, ndim): """Turn interpolation type into a tuple with one entry per axis.""" interp_in = interp @@ -183,7 +187,7 @@ def _normalize_interp(interp, ndim): if len(interp_byaxis) != ndim: raise ValueError( 'length of `interp` ({}) does not match number of axes ({})' - ''.format(len(interp_byaxis, ndim)) + ''.format(len(interp_byaxis), ndim) ) if not all( @@ -349,7 +353,7 @@ def nearest_interp(x, out=None): def linear_interpolator(f, coord_vecs): - """Return the linear interpolator for discrete values. + """Return the linear interpolator for discrete function values. Parameters ---------- @@ -514,11 +518,11 @@ def __init__(self, coord_vecs, values, input_type): """Initialize a new instance. coord_vecs : sequence of `numpy.ndarray`'s - Coordinate vectors defining the interpolation grid + Coordinate vectors defining the interpolation grid. values : `array-like` - Grid values to use for interpolation + Grid values to use for interpolation. input_type : {'array', 'meshgrid'} - Type of expected input values in ``__call__`` + Type of expected input values in ``__call__``. """ values = np.asarray(values) typ_ = str(input_type).lower() @@ -526,10 +530,11 @@ def __init__(self, coord_vecs, values, input_type): raise ValueError('`input_type` ({}) not understood' ''.format(input_type)) - if len(coord_vecs) > values.ndim: - raise ValueError('there are {} point arrays, but `values` has {} ' - 'dimensions'.format(len(coord_vecs), - values.ndim)) + if len(coord_vecs) != values.ndim: + raise ValueError( + 'there are {} point arrays, but `values` has {} dimensions' + ''.format(len(coord_vecs), values.ndim) + ) for i, p in enumerate(coord_vecs): if not np.asarray(p).ndim == 1: raise ValueError('the points in dimension {} must be ' @@ -560,8 +565,15 @@ def __call__(self, x, out=None): Interpolated values. If ``out`` was given, the returned object is a reference to it. """ + x = np.asarray(x) ndim = len(self.coord_vecs) + scalar_out = False + if self.input_type == 'array': + if ndim == 1: + scalar_out = x.ndim == 0 + else: + scalar_out = x.shape == (ndim,) # Make a (1, n) array from one with shape (n,) x = x.reshape([ndim, -1]) out_shape = out_shape_from_array(x) @@ -585,7 +597,11 @@ def __call__(self, x, out=None): ''.format(out.dtype, self.values.dtype)) indices, norm_distances = self._find_indices(x) - return self._evaluate(indices, norm_distances, out) + values = self._evaluate(indices, norm_distances, out) + if scalar_out: + return values.item() + else: + return values def _find_indices(self, x): """Find indices and distances of the given nodes. @@ -616,11 +632,12 @@ def _evaluate(self, indices, norm_distances, out=None): class _NearestInterpolator(_Interpolator): - r"""Nearest neighbor interpolator. + + """Nearest neighbor interpolator. The code is adapted from SciPy's `RegularGridInterpolator - `_ - class. + `_ class. This implementation is faster than the more generic one in the `_PerAxisPointwiseInterpolator`. @@ -710,7 +727,7 @@ def _compute_linear_weights_edge(idcs, ndist): def _create_weight_edge_lists(indices, norm_distances, interp): - # Precalculate indices and weights (per axis) + # Pre-calculate indices and weights (per axis) low_weights = [] high_weights = [] edge_indices = [] @@ -777,7 +794,7 @@ def _evaluate(self, indices, norm_distances, out=None): for lo_hi, edge in zip(product(*([['l', 'h']] * len(indices))), product(*edge_indices)): weight = 1.0 - # TODO: determine best summation order from array strides + # TODO(kohr-h): determine best summation order from array strides for lh, w_lo, w_hi in zip(lo_hi, low_weights, high_weights): # We don't multiply in-place to exploit the cheap operations @@ -817,6 +834,576 @@ def __init__(self, coord_vecs, values, input_type): ) +def _check_func_out_arg(func): + """Check if ``func`` has an (optional) ``out`` argument. + + Also verify that the signature of ``func`` has no ``*args`` since + they make argument propagation a huge hassle. + + Note: this function only works for objects that can be inspected + with the ``inspect`` module, i.e., Python functions and callables, + but not, e.g., NumPy UFuncs. + + Parameters + ---------- + func : callable + Object that should be inspected. + + Returns + ------- + has_out : bool + ``True`` if the signature has an ``out`` argument, ``False`` + otherwise. + out_is_optional : bool + ``True`` if ``out`` is present and optional in the signature, + ``False`` otherwise. + + Raises + ------ + TypeError + If ``func``'s signature has ``*args``. + """ + if sys.version_info.major > 2: + spec = inspect.getfullargspec(func) + kw_only = spec.kwonlyargs + else: + spec = inspect.getargspec(func) + kw_only = () + + if spec.varargs is not None: + raise TypeError('*args not allowed in function signature') + + pos_args = spec.args + pos_defaults = () if spec.defaults is None else spec.defaults + + if 'out' in pos_args: + has_out = True + out_optional = ( + pos_args.index('out') >= len(pos_args) - len(pos_defaults) + ) + elif 'out' in kw_only: + has_out = out_optional = True + else: + has_out = out_optional = False + + return has_out, out_optional + + +def _func_out_type(func): + """Determine the output argument type (if any) of a function-like object. + + This function is intended to work with all types of callables + that are used as input to `sampling_function`. + """ + # Numpy `UFuncs` and similar objects (e.g. Numba `DUFuncs`) + if hasattr(func, 'nin') and hasattr(func, 'nout'): + if func.nin != 1: + raise ValueError( + 'ufunc {} takes {} input arguments, expected 1' + ''.format(func.__name__, func.nin) + ) + if func.nout > 1: + raise ValueError( + 'ufunc {} returns {} outputs, expected 0 or 1' + ''.format(func.__name__, func.nout) + ) + has_out = out_optional = (func.nout == 1) + elif inspect.isfunction(func): + has_out, out_optional = _check_func_out_arg(func) + elif callable(func): + has_out, out_optional = _check_func_out_arg(func.__call__) + else: + raise TypeError('object {!r} not callable'.format(func)) + + return has_out, out_optional + + +def sampling_function(func_or_arr, domain, out_dtype=None): + """Return a function that can be used for sampling. + + For examples on this function's usage, see `point_collocation`. + + Parameters + ---------- + func_or_arr : callable or array-like + Either a single callable object (possibly with multiple output + components), or an array or callables and constants. + A callable (or each callable) must take a single input and may + accept one output parameter called ``out``, and should return + its result. + domain : IntervalProd + Set in which inputs to the function are assumed to lie. It is used + to determine the type of input (point/meshgrid/array) based on + ``domain.ndim``, and (unless switched off) to check whether all + inputs are in bounds. + out_dtype : optional + Data type of a *single* output of ``func_or_arr``, i.e., when + called with a single point as input. In particular: + + - If ``func_or_arr`` is a scalar-valued function, ``out_dtype`` is + expected to be a basic dtype with empty shape. + - If ``func_or_arr`` is a vector- or tensor-valued function, + ``out_dtype`` should be a shaped data type, e.g., ``(float, (3,))`` + for a vector-valued function with 3 components. + - If ``func_or_arr`` is an array-like, ``out_dtype`` should be a + shaped dtype whose shape matches that of ``func_or_arr``. It can + also be ``None``, in which case the shape is inferred, and the + scalar data type is set to ``float``. + + Returns + ------- + func : function + Wrapper function that has an optional ``out`` argument. + """ + if out_dtype is None: + val_shape = None + scalar_out_dtype = np.dtype('float64') + else: + out_dtype = np.dtype(out_dtype) + val_shape = out_dtype.shape + scalar_out_dtype = out_dtype.base + + # Provide default implementations of missing function signature types + + def _default_oop(func_ip, x, **kwargs): + """Default out-of-place variant of an in-place-only function.""" + if is_valid_input_array(x, domain.ndim): + scalar_out_shape = out_shape_from_array(x) + elif is_valid_input_meshgrid(x, domain.ndim): + scalar_out_shape = out_shape_from_meshgrid(x) + else: + raise TypeError('invalid input `x`') + + out_shape = val_shape + scalar_out_shape + out = np.empty(out_shape, dtype=scalar_out_dtype) + func_ip(x, out=out, **kwargs) + return out + + def _default_ip(func_oop, x, out, **kwargs): + """Default in-place variant of an out-of-place-only function.""" + result = np.array(func_oop(x, **kwargs), copy=False) + if result.dtype == object: + # Different shapes encountered, need to broadcast + flat_results = result.ravel() + if is_valid_input_array(x, domain.ndim): + scalar_out_shape = out_shape_from_array(x) + elif is_valid_input_meshgrid(x, domain.ndim): + scalar_out_shape = out_shape_from_meshgrid(x) + else: + raise TypeError('invalid input `x`') + + bcast_results = [np.broadcast_to(res, scalar_out_shape) + for res in flat_results] + # New array that is flat in the `out_shape` axes, reshape it + # to the final `out_shape + scalar_shape`, using the same + # order ('C') as the initial `result.ravel()`. + result = np.array(bcast_results, dtype=scalar_out_dtype) + result = result.reshape(val_shape + scalar_out_shape) + + # The following code is required to remove extra axes, e.g., when + # the result has shape (2, 1, 3) but should have shape (2, 3). + # For those cases, broadcasting doesn't apply. + try: + reshaped = result.reshape(out.shape) + except ValueError: + # This is the case when `result` must be broadcast + out[:] = result + else: + out[:] = reshaped + + return out + + # Now prepare the in-place and out-of-place functions for the final + # wrapping. + + if callable(func_or_arr): + # Assume scalar float out dtype for single function + if out_dtype is None: + out_dtype = np.dtype('float64') + + # Got a (single) function, possibly need to vectorize + func = func_or_arr + + # Get default implementations if necessary + has_out, out_optional = _func_out_type(func) + if not has_out: + # Out-of-place-only + func_ip = partial(_default_ip, func) + func_oop = func + elif out_optional: + # Dual-use + func_ip = func_oop = func + else: + # In-place-only + func_ip = func + func_oop = partial(_default_oop, func) + + else: + # This is for the case that an array-like of callables is provided. + # We need to convert this into a single function that returns an + # array. + + arr = np.array(func_or_arr, dtype=object) + + if val_shape is None: + # Infer value shape if `out_dtype is None` + val_shape = arr.shape + elif arr.shape != val_shape: + # Otherwise, check that the value shape matches the dtype shape + raise ValueError( + 'invalid `func_or_arr` {!r}: expected `None`, a callable or ' + 'an array-like of callables whose shape matches ' + '`out_dtype.shape` {}'.format(func_or_arr, val_shape) + ) + + out_dtype = np.dtype((scalar_out_dtype, val_shape)) + + arr = arr.ravel().tolist() + + def array_wrapper_func(x, out=None, **kwargs): + """Function wrapping an array of callables and constants. + + This wrapper does the following for out-of-place + evaluation (when ``out=None``): + + 1. Collect the results of all function evaluations into + a list, handling all kinds of sequence entries + (normal function, ufunc, constant, etc.). + 2. Broadcast all results to the desired shape that is + determined by the space's ``out_shape`` and the + shape(s) of the input. + 3. Form a big array containing the final result. + + The in-place version is simpler because broadcasting + happens automatically when assigning to the components + of ``out``. Hence, we only have + + 1. Assign the result of the evaluation of the i-th + function to ``out_flat[i]``, possibly using the + ``out`` parameter of the function. + """ + if is_valid_input_meshgrid(x, domain.ndim): + scalar_out_shape = out_shape_from_meshgrid(x) + elif is_valid_input_array(x, domain.ndim): + scalar_out_shape = out_shape_from_array(x) + else: + raise RuntimeError('bad input') + + if out is None: + # Out-of-place evaluation + + # Collect results of member functions into a list. + # Put simply, all that happens here is + # `results.append(f(x))`, just for a bunch of cases + # and with or without `out`. + results = [] + for f in arr: + if np.isscalar(f): + # Constant function + results.append(f) + elif not callable(f): + raise TypeError( + 'element {!r} of `func_or_arr` not callable' + ''.format(f) + ) + elif hasattr(f, 'nin') and hasattr(f, 'nout'): + # ufunc-like object + results.append(f(x, **kwargs)) + else: + has_out, _ = _func_out_type(f) + if has_out: + out = np.empty( + scalar_out_shape, dtype=scalar_out_dtype + ) + f(x, out=out, **kwargs) + results.append(out) + else: + results.append(f(x, **kwargs)) + + # Broadcast to required shape and convert to array. + # This will raise an error if the shape of some member + # array is wrong, since in that case the resulting + # dtype would be `object`. + bcast_results = [] + for res in results: + try: + reshaped = np.reshape(res, scalar_out_shape) + except ValueError: + bcast_results.append( + np.broadcast_to(res, scalar_out_shape)) + else: + bcast_results.append(reshaped) + + out_arr = np.array( + bcast_results, dtype=scalar_out_dtype + ) + + return out_arr.reshape(val_shape + scalar_out_shape) + + else: + # In-place evaluation + + # This is a precaution in case out is not contiguous + with writable_array(out) as out_arr: + # Flatten tensor axes to work on one tensor + # component (= scalar function) at a time + out_comps = out_arr.reshape((-1,) + scalar_out_shape) + for f, out_comp in zip(arr, out_comps): + if np.isscalar(f): + out_comp[:] = f + else: + has_out, _ = _func_out_type(f) + if has_out: + f(x, out=out_comp, **kwargs) + else: + out_comp[:] = f(x, **kwargs) + + func_ip = func_oop = array_wrapper_func + + return _make_dual_use_func(func_ip, func_oop, domain, out_dtype) + + +def _make_dual_use_func(func_ip, func_oop, domain, out_dtype): + """Return a unifying wrapper function with optional ``out`` argument.""" + + # Default to `ndim=1` for unusual domains that do not define a dimension + # (like `Strings(3)`) + ndim = getattr(domain, 'ndim', 1) + if out_dtype is None: + # Don't let `np.dtype` convert `None` to `float64` + raise TypeError('`out_dtype` cannot be `None`') + + out_dtype = np.dtype(out_dtype) + val_shape = out_dtype.shape + scalar_out_dtype = out_dtype.base + + tensor_valued = val_shape != () + + def dual_use_func(x, out=None, **kwargs): + """Wrapper function with optional ``out`` argument. + + This function closes over two other functions, one for in-place, + the other for out-of-place evaluation. Its purpose is to unify their + interfaces to a single one with optional ``out`` argument, and to + automate all details of input/output checking, broadcasting and + type casting. + + The closure also contains ``domain``, an `IntervalProd` where points + should lie, and the expected ``out_dtype``. + + For usage examples, see `point_collocation`. + + Parameters + ---------- + x : point, `meshgrid` or `numpy.ndarray` + Input argument for the function evaluation. Conditions + on ``x`` depend on its type: + + - point: must be castable to an element of the enclosed ``domain``. + - meshgrid: length must be ``domain.ndim``, and the arrays must + be broadcastable against each other. + - array: shape must be ``(ndim, N)``, where ``ndim`` equals + ``domain.ndim``. + + out : `numpy.ndarray`, optional + Output argument holding the result of the function evaluation. + Its shape must be ``out_dtype.shape + np.broadcast(*x).shape``. + + Other Parameters + ---------------- + bounds_check : bool, optional + If ``True``, check if all input points lie in ``domain``. This + requires ``domain`` to implement `Set.contains_all`. + Default: ``True`` + + Returns + ------- + out : `numpy.ndarray` + Result of the function evaluation. If ``out`` was provided, + the returned object is a reference to it. + + Raises + ------ + TypeError + If ``x`` is not a valid vectorized evaluation argument. + + If ``out`` is neither ``None`` nor a `numpy.ndarray` of + adequate shape and data type. + + ValueError + If ``bounds_check == True`` and some evaluation points fall + outside the valid domain. + """ + bounds_check = kwargs.pop('bounds_check', True) + if bounds_check and not hasattr(domain, 'contains_all'): + raise AttributeError( + 'bounds check not possible for domain {!r}, missing ' + '`contains_all()` method' + ''.format(domain) + ) + + # Check for input type and determine output shape + if is_valid_input_meshgrid(x, ndim): + scalar_in = False + scalar_out_shape = out_shape_from_meshgrid(x) + scalar_out = False + # Avoid operations on tuples like x * 2 by casting to array + if ndim == 1: + x = x[0][None, ...] + elif is_valid_input_array(x, ndim): + x = np.asarray(x) + scalar_in = False + scalar_out_shape = out_shape_from_array(x) + scalar_out = False + elif x in domain: + x = np.atleast_2d(x).T # make a (d, 1) array + scalar_in = True + scalar_out_shape = (1,) + scalar_out = (out is None and not tensor_valued) + else: + # Unknown input + txt_1d = ' or (n,)' if ndim == 1 else '' + raise TypeError( + 'argument {!r} not a valid function input. ' + 'Expected an element of the domain {domain!r}, an array-like ' + 'with shape ({domain.ndim}, n){} or a length-{domain.ndim} ' + 'meshgrid tuple.' + ''.format(x, txt_1d, domain=domain) + ) + + # Check bounds if specified + if bounds_check and not domain.contains_all(x): + raise ValueError('input contains points outside the domain {!r}' + ''.format(domain)) + + if scalar_in: + out_shape = val_shape + else: + out_shape = val_shape + scalar_out_shape + + # Call the function and check out shape, before or after + if out is None: + + # The out-of-place evaluation path + + if ndim == 1: + try: + out = func_oop(x, **kwargs) + except (TypeError, IndexError): + # TypeError is raised if a meshgrid was used but the + # function expected an array (1d only). In this case we try + # again with the first meshgrid vector. + # IndexError is raised in expressions like x[x > 0] since + # "x > 0" evaluates to 'True', i.e. 1, and that index is + # out of range for a meshgrid tuple of length 1 :-). To get + # the real errors with indexing, we check again for the + # same scenario (scalar output when not valid) as in the + # first case. + out = func_oop(x[0], **kwargs) + + else: + # Here we don't catch exceptions since they are likely true + # errors + out = func_oop(x, **kwargs) + + if isinstance(out, np.ndarray) or np.isscalar(out): + # Cast to proper dtype if needed, also convert to array if out + # is a scalar. + out = np.asarray(out, dtype=scalar_out_dtype) + if scalar_in: + out = np.squeeze(out) + elif ndim == 1 and out.shape == (1,) + out_shape: + out = out.reshape(out_shape) + + if out_shape != () and out.shape != out_shape: + # Broadcast the returned element, but not in the + # scalar case. The resulting array may be read-only, + # in which case we copy. + out = np.broadcast_to(out, out_shape) + if not out.flags.writeable: + out = out.copy() + + elif tensor_valued: + # The out object can be any array-like of objects with shapes + # that should all be broadcastable to scalar_out_shape. + results = np.array(out) + if results.dtype == object or scalar_in: + # Some results don't have correct shape, need to + # broadcast + bcast_res = [] + for res in results.ravel(): + if ndim == 1: + # As usual, 1d is tedious to deal with. This + # code deals with extra dimensions in result + # components that stem from using x instead of + # x[0] in a function. + # Without this, broadcasting fails. + shp = getattr(res, 'shape', ()) + if shp and shp[0] == 1: + res = res.reshape(res.shape[1:]) + bcast_res.append( + np.broadcast_to(res, scalar_out_shape)) + + out_arr = np.array(bcast_res, dtype=scalar_out_dtype) + elif results.dtype != scalar_out_dtype: + raise ValueError( + 'result is of dtype {}, expected {}' + ''.format(dtype_repr(results.dtype), + dtype_repr(scalar_out_dtype)) + ) + else: + out_arr = results + + out = out_arr.reshape(out_shape) + + else: + # TODO(kohr-h): improve message + raise RuntimeError('bad output of function call') + + else: + # The in-place evaluation path + + if not isinstance(out, np.ndarray): + raise TypeError( + 'output must be a `numpy.ndarray` got {!r}' + ''.format(out) + ) + if out_shape != (1,) and out.shape != out_shape: + raise ValueError( + 'output has shape, expected {} from input' + ''.format(out.shape, out_shape) + ) + if out.dtype != scalar_out_dtype: + raise ValueError( + '`out` is of dtype {}, expected {}' + ''.format(out.dtype, scalar_out_dtype) + ) + + if ndim == 1 and not tensor_valued: + # TypeError for meshgrid in 1d, but expected array (see above) + try: + func_ip(x, out, **kwargs) + except TypeError: + func_ip(x[0], out, **kwargs) + else: + func_ip(x, out=out, **kwargs) + + # If we are to output a scalar, convert the result + + # Numpy < 1.12 does not implement __complex__ for arrays (in contrast + # to __float__), so we have to fish out the scalar ourselves. + if scalar_out: + scalar = out.ravel()[0].item() + if is_real_dtype(out_dtype): + return float(scalar) + else: + return complex(scalar) + else: + return out + + return dual_use_func + + if __name__ == '__main__': from odl.util.testutils import run_doctests run_doctests() diff --git a/odl/discr/discretization.py b/odl/discr/discretization.py deleted file mode 100644 index 55aaa4efa1b..00000000000 --- a/odl/discr/discretization.py +++ /dev/null @@ -1,383 +0,0 @@ -# Copyright 2014-2020 The ODL contributors -# -# This file is part of ODL. -# -# This Source Code Form is subject to the terms of the Mozilla Public License, -# v. 2.0. If a copy of the MPL was not distributed with this file, You can -# obtain one at https://mozilla.org/MPL/2.0/. - -"""Base classes for discretization.""" - -from __future__ import absolute_import, division, print_function - -from odl.set import ComplexNumbers, RealNumbers -from odl.set.sets import Set -from odl.space.base_tensors import Tensor, TensorSpace -from odl.space.entry_points import tensor_space_impl -from odl.util import ( - is_complex_floating_dtype, is_numeric_dtype, is_real_floating_dtype) - -__all__ = ('DiscretizedSpace',) - - -class DiscretizedSpace(TensorSpace): - - """Abstract discretization class for general sets or spaces. - - A discretization in ODL is a way to encode the transition from - an arbitrary set to a set of discrete values explicitly representable - in a computer. The most common use case is the discretization of - an infinite-dimensional vector space of functions by means of - storing coefficients in a finite basis. - - The minimal information required to create a discretization is - the set to be discretized ("function space" or ``fspace``), and a - backend for storage and processing of the discrete values - ("tensor space" or ``tspace``). - Since function spaces represent by far the most significant application, - the non-discretized set is called ``fspace``. - """ - - def __init__(self, fspace, tspace): - """Abstract initialization method. - - Intended to be called by subclasses for proper type checking - and setting of attributes. - - Parameters - ---------- - fspace : `Set` - The non-discretized (abstract) set to be discretized. - tspace : `TensorSpace` - Space providing containers for the values/coefficients of a - discretized object. - """ - if not isinstance(fspace, Set): - raise TypeError('`fspace` must be a `Set` instance, ' - 'got {!r}'.format(fspace)) - if not isinstance(tspace, TensorSpace): - raise TypeError('`tspace` {!r} not a `TensorSpace` instance' - ''.format(tspace)) - - super(DiscretizedSpace, self).__init__(tspace.shape, tspace.dtype) - self.__fspace = fspace - self.__tspace = tspace - - @property - def fspace(self): - """Non-discretized space of this discretization.""" - return self.__fspace - - @property - def tspace(self): - """Space for the coefficients of the elements of this space.""" - return self.__tspace - - @property - def tspace_type(self): - """Tensor space type of this discretization.""" - return type(self.tspace) - - def element(self, inp=None, order=None, **kwargs): - """Create an element from ``inp`` or from scratch. - - Parameters - ---------- - inp : optional - Input data to create an element from. - order : {None, 'C', 'F'}, optional - Storage order of the returned element. For ``'C'`` and ``'F'``, - contiguous memory in the respective ordering is enforced. - The default ``None`` enforces no contiguousness. - kwargs : - Additional keyword arguments passed on to the sampling function - `point_collocation` when called on ``inp``. This can be used for, - e.g., functions with parameters. - - Returns - ------- - element : `DiscretizedSpaceElement` - The discretized element. - """ - raise NotImplementedError('abstract method') - - def __eq__(self, other): - """Return ``self == other``. - - Returns - ------- - equals : bool - ``True`` if ``other`` is a `DiscretizedSpace` - instance and all attributes `fspace`, `tspace`, - of ``other`` and this discretization are equal, ``False`` - otherwise. - """ - # Optimizations for simple cases - if other is self: - return True - elif other is None: - return False - else: - return (super(DiscretizedSpace, self).__eq__(other) and - other.fspace == self.fspace and - other.tspace == self.tspace) - - def __hash__(self): - """Return ``hash(self)``.""" - return hash( - (super(DiscretizedSpace, self).__hash__(), - self.fspace, - self.tspace) - ) - - @property - def domain(self): - """Domain of the continuous space.""" - return self.fspace.domain - - def zero(self): - """Return the element of all zeros.""" - return self.element_type(self, self.tspace.zero()) - - def one(self): - """Return the element of all ones.""" - return self.element_type(self, self.tspace.one()) - - @property - def weighting(self): - """This space's weighting scheme.""" - return self.tspace.weighting - - @property - def is_weighted(self): - """``True`` if the ``tspace`` is weighted.""" - return getattr(self.tspace, 'is_weighted', False) - - @property - def impl(self): - """Name of the implementation back-end.""" - return self.tspace.impl - - def _lincomb(self, a, x1, b, x2, out): - """Raw linear combination.""" - self.tspace._lincomb(a, x1.tensor, b, x2.tensor, out.tensor) - - def _dist(self, x1, x2): - """Raw distance between two elements.""" - return self.tspace._dist(x1.tensor, x2.tensor) - - def _norm(self, x): - """Raw norm of an element.""" - return self.tspace._norm(x.tensor) - - def _inner(self, x1, x2): - """Raw inner product of two elements.""" - return self.tspace._inner(x1.tensor, x2.tensor) - - def _multiply(self, x1, x2, out): - """Raw pointwise multiplication of two elements.""" - self.tspace._multiply(x1.tensor, x2.tensor, out.tensor) - - def _divide(self, x1, x2, out): - """Raw pointwise multiplication of two elements.""" - self.tspace._divide(x1.tensor, x2.tensor, out.tensor) - - @property - def examples(self): - """Return example functions in the space. - - These are created by discretizing the examples in the underlying - `fspace`. - - See Also - -------- - odl.space.fspace.FunctionSpace.examples - """ - for name, elem in self.fspace.examples: - yield (name, self.element(elem)) - - @property - def element_type(self): - """Type of elements in this space: `DiscretizedSpaceElement`.""" - return DiscretizedSpaceElement - - -class DiscretizedSpaceElement(Tensor): - - """Representation of a `DiscretizedSpace` element. - - Basically only a wrapper class for tspace's element class.""" - - def __init__(self, space, tensor): - """Initialize a new instance.""" - super(DiscretizedSpaceElement, self).__init__(space) - self.__tensor = tensor - - @property - def tensor(self): - """Structure for data storage.""" - return self.__tensor - - @property - def dtype(self): - """Type of data storage.""" - return self.tensor.dtype - - @property - def size(self): - """Size of data storage.""" - return self.tensor.size - - def __len__(self): - """Return ``len(self)``. - - Size of data storage. - """ - return self.size - - def copy(self): - """Create an identical (deep) copy of this element.""" - return self.space.element(self.tensor.copy()) - - def asarray(self, out=None): - """Extract the data of this array as a numpy array. - - Parameters - ---------- - out : `numpy.ndarray`, optional - Array in which the result should be written in-place. - Has to be contiguous and of the correct dtype. - """ - return self.tensor.asarray(out=out) - - def astype(self, dtype): - """Return a copy of this element with new ``dtype``. - - Parameters - ---------- - dtype : - Scalar data type of the returned space. Can be provided - in any way the `numpy.dtype` constructor understands, e.g. - as built-in type or as a string. Data types with non-trivial - shapes are not allowed. - - Returns - ------- - newelem : `DiscretizedSpaceElement` - Version of this element with given data type. - """ - return self.space.astype(dtype).element(self.tensor.astype(dtype)) - - def __eq__(self, other): - """Return ``self == other``. - - Returns - ------- - equals : bool - ``True`` if all entries of ``other`` are equal to this - element's entries, ``False`` otherwise. - """ - return (other in self.space and - self.tensor == other.tensor) - - def __getitem__(self, indices): - """Return ``self[indices]``. - - Parameters - ---------- - indices : int or `slice` - The position(s) that should be accessed. - - Returns - ------- - values : `Tensor` - The value(s) at the index (indices). - """ - if isinstance(indices, type(self)): - indices = indices.tensor - return self.tensor[indices] - - def __setitem__(self, indices, values): - """Implement ``self[indices] = values``. - - Parameters - ---------- - indices : int or `slice` - The position(s) that should be set - values : scalar, `array-like` or `Tensor` - The value(s) that are to be assigned. - - If ``index`` is an int, ``value`` must be single value. - - If ``index`` is a slice, ``value`` must be broadcastable - to the size of the slice (same size, shape (1,) - or single value). - """ - if isinstance(indices, type(self)): - indices = indices.tensor - if isinstance(values, type(self)): - values = values.tensor - self.tensor.__setitem__(indices, values) - - def __ipow__(self, p): - """Implement ``self **= p``.""" - # The concrete `tensor` can specialize `__ipow__` for non-integer - # `p` so we want to use it here. Otherwise we get the default - # `LinearSpaceElement.__ipow__` which only works for integer `p`. - self.tensor.__ipow__(p) - return self - - -def tspace_type(space, impl, dtype=None): - """Select the correct corresponding tensor space. - - Parameters - ---------- - space : `LinearSpace` - Template space from which to infer an adequate tensor space. If - it has a ``field`` attribute, ``dtype`` must be consistent with it. - impl : string - Implementation backend for the tensor space. - dtype : optional - Data type which the space is supposed to use. If ``None`` is - given, the space type is purely determined from ``space`` and - ``impl``. Otherwise, it must be compatible with the - field of ``space``. - - Returns - ------- - stype : type - Space type selected after the space's field, the backend and - the data type. - """ - field_type = type(getattr(space, 'field', None)) - - if dtype is None: - pass - elif is_real_floating_dtype(dtype): - if field_type is None or field_type == ComplexNumbers: - raise TypeError('real floating data type {!r} requires space ' - 'field to be of type RealNumbers, got {}' - ''.format(dtype, field_type)) - elif is_complex_floating_dtype(dtype): - if field_type is None or field_type == RealNumbers: - raise TypeError('complex floating data type {!r} requires space ' - 'field to be of type ComplexNumbers, got {!r}' - ''.format(dtype, field_type)) - elif is_numeric_dtype(dtype): - if field_type == ComplexNumbers: - raise TypeError('non-floating data type {!r} requires space field ' - 'to be of type RealNumbers, got {!r}' - .format(dtype, field_type)) - - try: - return tensor_space_impl(impl) - except ValueError: - raise NotImplementedError('no corresponding tensor space available ' - 'for space {!r} and implementation {!r}' - ''.format(space, impl)) - - -if __name__ == '__main__': - from odl.util.testutils import run_doctests - run_doctests() diff --git a/odl/discr/grid.py b/odl/discr/grid.py index bbc12a76ea9..ef0d9bd2c35 100644 --- a/odl/discr/grid.py +++ b/odl/discr/grid.py @@ -1,4 +1,4 @@ -# Copyright 2014-2019 The ODL contributors +# Copyright 2014-2020 The ODL contributors # # This file is part of ODL. # @@ -21,7 +21,12 @@ array_str, signature_string, indent, npy_printoptions) -__all__ = ('RectGrid', 'uniform_grid', 'uniform_grid_fromintv') +__all__ = ( + 'sparse_meshgrid', + 'RectGrid', + 'uniform_grid', + 'uniform_grid_fromintv', +) def sparse_meshgrid(*x): @@ -972,11 +977,11 @@ def __getitem__(self, indices): True """ if isinstance(indices, list): - if indices == []: - new_coord_vecs = [] - else: + if indices: new_coord_vecs = [self.coord_vectors[0][indices]] new_coord_vecs += self.coord_vectors[1:] + else: + new_coord_vecs = [] return RectGrid(*new_coord_vecs) indices = normalized_index_expression(indices, self.shape, diff --git a/odl/discr/partition.py b/odl/discr/partition.py index 6eea0a11b4c..aabacca2b24 100644 --- a/odl/discr/partition.py +++ b/odl/discr/partition.py @@ -553,14 +553,14 @@ def __getitem__(self, indices): """ # Special case of index list: slice along first axis if isinstance(indices, list): - if indices == []: - new_min_pt = new_max_pt = [] - else: + if indices: new_min_pt = [self.cell_boundary_vecs[0][:-1][indices][0]] new_max_pt = [self.cell_boundary_vecs[0][1:][indices][-1]] for cvec in self.cell_boundary_vecs[1:]: new_min_pt.append(cvec[0]) new_max_pt.append(cvec[-1]) + else: + new_min_pt = new_max_pt = [] new_intvp = IntervalProd(new_min_pt, new_max_pt) new_grid = self.grid[indices] @@ -1375,10 +1375,11 @@ def nonuniform_partition(*coord_vecs, **kwargs): min_pt=-2.0, max_pt=3.0 ) """ - # Get parameters from kwargs min_pt = kwargs.pop('min_pt', None) max_pt = kwargs.pop('max_pt', None) nodes_on_bdry = kwargs.pop('nodes_on_bdry', False) + if kwargs: + raise TypeError('unexpected keyword arguments: {}'.format(kwargs)) # np.size(None) == 1 sizes = [len(coord_vecs)] + [np.size(p) for p in (min_pt, max_pt)] diff --git a/odl/operator/default_ops.py b/odl/operator/default_ops.py index f31175b20ee..4ae19c004cd 100644 --- a/odl/operator/default_ops.py +++ b/odl/operator/default_ops.py @@ -1,6 +1,6 @@ # coding=utf-8 -# Copyright 2014-2019 The ODL contributors +# Copyright 2014-2020 The ODL contributors # # This file is part of ODL. # @@ -10,16 +10,17 @@ """Default operators defined on any (reasonable) space.""" -from __future__ import print_function, division, absolute_import +from __future__ import absolute_import, division, print_function + from copy import copy + import numpy as np from odl.operator.operator import Operator -from odl.set import LinearSpace, Field, RealNumbers, ComplexNumbers +from odl.set import ComplexNumbers, Field, LinearSpace, RealNumbers from odl.set.space import LinearSpaceElement from odl.space import ProductSpace - __all__ = ('ScalingOperator', 'ZeroOperator', 'IdentityOperator', 'LinCombOperator', 'MultiplyOperator', 'PowerOperator', 'InnerProductOperator', 'NormOperator', 'DistOperator', @@ -984,7 +985,7 @@ def __init__(self, space): rn(3).element([ 1., 2., 3.]) The operator also works on other `TensorSpace` spaces such as - `DiscreteLp` spaces: + `DiscretizedSpace` spaces: >>> r3 = odl.uniform_discr(0, 1, 3, dtype=complex) >>> op = RealPart(r3) @@ -1387,7 +1388,7 @@ def __init__(self, space): rn(2).element([ 1., 2.]) The operator also works on other `TensorSpace`'s such as - `DiscreteLp`: + `DiscretizedSpace`: >>> space = odl.uniform_discr(0, 1, 2, dtype=complex) >>> op = odl.ComplexModulus(space) @@ -1587,7 +1588,7 @@ def __init__(self, space): rn(2).element([ 1., 4.]) The operator also works on other `TensorSpace`'s such as - `DiscreteLp`: + `DiscretizedSpace`: >>> space = odl.uniform_discr(0, 1, 2, dtype=complex) >>> op = odl.ComplexModulusSquared(space) diff --git a/odl/operator/operator.py b/odl/operator/operator.py index 3695d658a0e..125ca0fccb8 100644 --- a/odl/operator/operator.py +++ b/odl/operator/operator.py @@ -1,4 +1,4 @@ -# Copyright 2014-2019 The ODL contributors +# Copyright 2014-2020 The ODL contributors # # This file is part of ODL. # @@ -8,24 +8,33 @@ """Abstract mathematical operators.""" -from __future__ import print_function, division, absolute_import -from builtins import object +from __future__ import absolute_import, division, print_function + import inspect -from numbers import Number, Integral import sys +from builtins import object +from numbers import Integral, Number -from odl.set import LinearSpace, Set, Field +from odl.set import Field, LinearSpace, Set from odl.set.space import LinearSpaceElement from odl.util import cache_arguments - -__all__ = ('Operator', 'OperatorComp', 'OperatorSum', 'OperatorVectorSum', - 'OperatorLeftScalarMult', 'OperatorRightScalarMult', - 'FunctionalLeftVectorMult', - 'OperatorLeftVectorMult', 'OperatorRightVectorMult', - 'OperatorPointwiseProduct', - 'OpTypeError', 'OpDomainError', 'OpRangeError', - 'OpNotImplementedError') +__all__ = ( + 'Operator', + 'OperatorComp', + 'OperatorSum', + 'OperatorVectorSum', + 'OperatorLeftScalarMult', + 'OperatorRightScalarMult', + 'FunctionalLeftVectorMult', + 'OperatorLeftVectorMult', + 'OperatorRightVectorMult', + 'OperatorPointwiseProduct', + 'OpTypeError', + 'OpDomainError', + 'OpRangeError', + 'OpNotImplementedError', +) def _default_call_out_of_place(op, x, **kwargs): diff --git a/odl/operator/tensor_ops.py b/odl/operator/tensor_ops.py index f6a44b5da51..1e589ceee18 100644 --- a/odl/operator/tensor_ops.py +++ b/odl/operator/tensor_ops.py @@ -1,4 +1,4 @@ -# Copyright 2014-2019 The ODL contributors +# Copyright 2014-2020 The ODL contributors # # This file is part of ODL. # @@ -38,7 +38,7 @@ class PointwiseTensorFieldOperator(Operator): number ``k`` of components. For ``k == 1``, the base space ``X`` can be used instead. - For example, if ``X`` is a `DiscreteLp` space, then + For example, if ``X`` is a `DiscretizedSpace` space, then ``ProductSpace(X, d)`` is a valid domain for any positive integer ``d``. It is also possible to have tensor fields over tensor fields, i.e. ``ProductSpace(ProductSpace(X, n), m)``. @@ -104,7 +104,7 @@ class PointwiseNorm(PointwiseTensorFieldOperator): for ``p = inf``, where ``F`` is a vector field. This implies that the `Operator.domain` is a power space of a discretized function - space. For example, if ``X`` is a `DiscreteLp` space, then + space. For example, if ``X`` is a `DiscretizedSpace` space, then ``ProductSpace(X, d)`` is a valid domain for any positive integer ``d``. """ @@ -462,7 +462,7 @@ class PointwiseInner(PointwiseInnerBase): acting as a variable to this operator. This implies that the `Operator.domain` is a power space of a - discretized function space. For example, if ``X`` is a `DiscreteLp` + discretized function space. For example, if ``X`` is a `DiscretizedSpace` space, then ``ProductSpace(X, d)`` is a valid domain for any positive integer ``d``. """ @@ -655,7 +655,7 @@ class PointwiseSum(PointwiseInner): where ``F`` is a vector field. This implies that the `Operator.domain` is a power space of a discretized function - space. For example, if ``X`` is a `DiscreteLp` space, then + space. For example, if ``X`` is a `DiscretizedSpace` space, then ``ProductSpace(X, d)`` is a valid domain for any positive integer ``d``. """ diff --git a/odl/phantom/emission.py b/odl/phantom/emission.py index 3b12fa6a2fb..4052498e4cf 100644 --- a/odl/phantom/emission.py +++ b/odl/phantom/emission.py @@ -1,4 +1,4 @@ -# Copyright 2014-2019 The ODL contributors +# Copyright 2014-2020 The ODL contributors # # This file is part of ODL. # @@ -8,12 +8,11 @@ """Phantoms used in emission tomography.""" -from __future__ import print_function, division, absolute_import +from __future__ import absolute_import, division, print_function from odl.phantom.geometric import ellipsoid_phantom from odl.phantom.phantom_utils import cylinders_from_ellipses - __all__ = ('derenzo_sources',) @@ -114,7 +113,7 @@ def derenzo_sources(space, min_pt=None, max_pt=None): Parameters ---------- - space : `DiscreteLp` + space : `DiscretizedSpace` Space in which the phantom should be created, must be 2- or 3-dimensional. If ``space.shape`` is 1 in an axis, a corresponding slice of the phantom is created (instead of squashing the whole diff --git a/odl/phantom/geometric.py b/odl/phantom/geometric.py index 939b1a4a8d3..9f8e421c80b 100644 --- a/odl/phantom/geometric.py +++ b/odl/phantom/geometric.py @@ -1,4 +1,4 @@ -# Copyright 2014-2018 The ODL contributors +# Copyright 2014-2020 The ODL contributors # # This file is part of ODL. # @@ -8,14 +8,21 @@ """Phantoms given by simple geometric objects such as cubes or spheres.""" -from __future__ import print_function, division, absolute_import +from __future__ import absolute_import, division, print_function + import numpy as np -from odl.discr.lp_discr import uniform_discr_fromdiscr +from odl.discr.discr_space import uniform_discr_fromdiscr from odl.util.numerics import resize_array -__all__ = ('cuboid', 'defrise', 'ellipsoid_phantom', 'indicate_proj_axis', - 'smooth_cuboid', 'tgv_phantom') +__all__ = ( + 'cuboid', + 'defrise', + 'ellipsoid_phantom', + 'indicate_proj_axis', + 'smooth_cuboid', + 'tgv_phantom', +) def cuboid(space, min_pt=None, max_pt=None): @@ -23,7 +30,7 @@ def cuboid(space, min_pt=None, max_pt=None): Parameters ---------- - space : `DiscreteLp` + space : `DiscretizedSpace` Space in which the phantom should be created. min_pt : array-like of shape ``(space.ndim,)``, optional Lower left corner of the cuboid. If ``None`` is given, a quarter @@ -98,7 +105,7 @@ def defrise(space, nellipses=8, alternating=False, min_pt=None, max_pt=None): Parameters ---------- - space : `DiscreteLp` + space : `DiscretizedSpace` Space in which the phantom should be created, must be 2- or 3-dimensional. nellipses : int, optional @@ -200,7 +207,7 @@ def indicate_proj_axis(space, scale_structures=0.5): Parameters ---------- - space : `DiscreteLp` + space : `DiscretizedSpace` Space in which the phantom should be created, must be 2- or 3-dimensional. scale_structures : positive float in (0, 1], optional @@ -326,7 +333,7 @@ def _ellipse_phantom_2d(space, ellipses): Parameters ---------- - space : `DiscreteLp` + space : `DiscretizedSpace` Uniformly discretized space in which the phantom should be generated. If ``space.shape`` is 1 in an axis, a corresponding slice of the phantom is created (instead of squashing the whole phantom into the @@ -451,7 +458,7 @@ def _ellipsoid_phantom_3d(space, ellipsoids): Parameters ---------- - space : `DiscreteLp` + space : `DiscretizedSpace` Space in which the phantom should be generated. If ``space.shape`` is 1 in an axis, a corresponding slice of the phantom is created (instead of squashing the whole phantom into the slice). @@ -575,7 +582,7 @@ def ellipsoid_phantom(space, ellipsoids, min_pt=None, max_pt=None): Parameters ---------- - space : `DiscreteLp` + space : `DiscretizedSpace` Space in which the phantom should be created, must be 2- or 3-dimensional. If ``space.shape`` is 1 in an axis, a corresponding slice of the phantom is created (instead of squashing the whole @@ -708,7 +715,7 @@ def smooth_cuboid(space, min_pt=None, max_pt=None, axis=0): Parameters ---------- - space : `DiscreteLp` + space : `DiscretizedSpace` Discretized space in which the phantom is supposed to be created. min_pt : array-like of shape ``(space.ndim,)``, optional Lower left corner of the cuboid. If ``None`` is given, a quarter @@ -773,7 +780,7 @@ def tgv_phantom(space, edge_smoothing=0.2): Parameters ---------- - space : `DiscreteLp`, 2 dimensional + space : `DiscretizedSpace`, 2 dimensional Discretized space in which the phantom is supposed to be created. Needs to be two-dimensional. edge_smoothing : nonnegative float, optional @@ -810,7 +817,8 @@ def tgv_phantom(space, edge_smoothing=0.2): def sigmoid(val): if edge_smoothing != 0: val = val / scale - return 1 / (1 + np.exp(-val)) + with np.errstate(over="ignore", under="ignore"): + return 1 / (1 + np.exp(-val)) else: return (val > 0).astype(val.dtype) diff --git a/odl/phantom/misc_phantoms.py b/odl/phantom/misc_phantoms.py index 20716ae2166..bd35aad0526 100644 --- a/odl/phantom/misc_phantoms.py +++ b/odl/phantom/misc_phantoms.py @@ -1,4 +1,4 @@ -# Copyright 2014-2019 The ODL contributors +# Copyright 2014-2020 The ODL contributors # # This file is part of ODL. # @@ -8,10 +8,11 @@ """Miscellaneous phantoms that do not fit in other categories.""" -from __future__ import print_function, division, absolute_import -import numpy as np +from __future__ import absolute_import, division, print_function + import sys +import numpy as np __all__ = ('submarine', 'text') @@ -21,7 +22,7 @@ def submarine(space, smooth=True, taper=20.0): Parameters ---------- - space : `DiscreteLp` + space : `DiscretizedSpace` Discretized space in which the phantom is supposed to be created. smooth : bool, optional If ``True``, the boundaries are smoothed out. Otherwise, the @@ -155,7 +156,7 @@ def text(space, text, font=None, border=0.2, inverted=True): Parameters ---------- - space : `DiscreteLp` + space : `DiscretizedSpace` Discretized space in which the phantom is supposed to be created. Must be two-dimensional. text : str diff --git a/odl/phantom/transmission.py b/odl/phantom/transmission.py index 8f6565b2520..01ea73dd33c 100644 --- a/odl/phantom/transmission.py +++ b/odl/phantom/transmission.py @@ -1,4 +1,4 @@ -# Copyright 2014-2019 The ODL contributors +# Copyright 2014-2020 The ODL contributors # # This file is part of ODL. # @@ -8,13 +8,13 @@ """Phantoms typically used in transmission tomography.""" -from __future__ import print_function, division, absolute_import +from __future__ import absolute_import, division, print_function + import numpy as np -from odl.discr import DiscreteLp +from odl.discr import DiscretizedSpace from odl.phantom.geometric import ellipsoid_phantom - __all__ = ('shepp_logan_ellipsoids', 'shepp_logan', 'forbild') @@ -116,7 +116,7 @@ def shepp_logan(space, modified=False, min_pt=None, max_pt=None): Parameters ---------- - space : `DiscreteLp` + space : `DiscretizedSpace` Space in which the phantom is created, must be 2- or 3-dimensional. If ``space.shape`` is 1 in an axis, a corresponding slice of the phantom is created. @@ -279,7 +279,7 @@ def forbild(space, resolution=False, ear=True, value_type='density', Parameters ---------- - space : `DiscreteLp` + space : `DiscretizedSpace` The space in which the phantom should be corrected. Needs to be two- dimensional. resolution : bool, optional @@ -319,8 +319,8 @@ def transposeravel(arr): """Implement MATLAB's ``transpose(arr(:))``.""" return arr.T.ravel() - if not isinstance(space, DiscreteLp): - raise TypeError('`space` must be a `DiscreteLp`') + if not isinstance(space, DiscretizedSpace): + raise TypeError('`space` must be a `DiscretizedSpace`') if space.ndim != 2: raise TypeError('`space` must be two-dimensional') diff --git a/odl/solvers/functional/default_functionals.py b/odl/solvers/functional/default_functionals.py index eccacc40d35..abc4a095f99 100644 --- a/odl/solvers/functional/default_functionals.py +++ b/odl/solvers/functional/default_functionals.py @@ -1,4 +1,4 @@ -# Copyright 2014-2019 The ODL contributors +# Copyright 2014-2020 The ODL contributors # # This file is part of ODL. # @@ -67,7 +67,7 @@ def __init__(self, space, exponent): Parameters ---------- - space : `DiscreteLp` or `TensorSpace` + space : `DiscretizedSpace` or `TensorSpace` Domain of the functional. exponent : float Exponent for the norm (``p``). @@ -464,7 +464,7 @@ def __init__(self, space, exponent): Parameters ---------- - space : `DiscreteLp` or `TensorSpace` + space : `DiscretizedSpace` or `TensorSpace` Domain of the functional. exponent : int or infinity Specifies wich norm to use. @@ -576,7 +576,7 @@ def __init__(self, space): Parameters ---------- - space : `DiscreteLp` or `TensorSpace` + space : `DiscretizedSpace` or `TensorSpace` Domain of the functional. """ super(L1Norm, self).__init__(space=space, exponent=1) @@ -614,7 +614,7 @@ def __init__(self, space): Parameters ---------- - space : `DiscreteLp` or `TensorSpace` + space : `DiscretizedSpace` or `TensorSpace` Domain of the functional. """ super(L2Norm, self).__init__(space=space, exponent=2) @@ -660,7 +660,7 @@ def __init__(self, space): Parameters ---------- - space : `DiscreteLp` or `TensorSpace` + space : `DiscretizedSpace` or `TensorSpace` Domain of the functional. """ super(L2NormSquared, self).__init__( @@ -1073,7 +1073,7 @@ def __init__(self, space, prior=None): Parameters ---------- - space : `DiscreteLp` or `TensorSpace` + space : `DiscretizedSpace` or `TensorSpace` Domain of the functional. prior : ``space`` `element-like`, optional Depending on the context, the prior, target or data @@ -1227,7 +1227,7 @@ def __init__(self, space, prior=None): Parameters ---------- - space : `DiscreteLp` or `TensorSpace` + space : `DiscretizedSpace` or `TensorSpace` Domain of the functional. prior : ``space`` `element-like`, optional Depending on the context, the prior, target or data @@ -1374,7 +1374,7 @@ def __init__(self, space, prior=None): Parameters ---------- - space : `DiscreteLp` or `TensorSpace` + space : `DiscretizedSpace` or `TensorSpace` Domain of the functional. prior : ``space`` `element-like`, optional Depending on the context, the prior, target or data @@ -1505,7 +1505,7 @@ def __init__(self, space, prior=None): Parameters ---------- - space : `DiscreteLp` or `TensorSpace` + space : `DiscretizedSpace` or `TensorSpace` Domain of the functional. prior : ``space`` `element-like`, optional Depending on the context, the prior, target or data @@ -2239,7 +2239,7 @@ def __init__(self, space, diameter=1, sum_rtol=None): Parameters ---------- - space : `DiscreteLp` or `TensorSpace` + space : `DiscretizedSpace` or `TensorSpace` Domain of the functional. diameter : positive float, optional Diameter of the simplex. @@ -2358,7 +2358,7 @@ def __init__(self, space, sum_value=1, sum_rtol=None): Parameters ---------- - space : `DiscreteLp` or `TensorSpace` + space : `DiscretizedSpace` or `TensorSpace` Domain of the functional. sum_value : float Desired value of the sum constraint. diff --git a/odl/solvers/util/callback.py b/odl/solvers/util/callback.py index d890180ec7f..2b5058a0a8f 100644 --- a/odl/solvers/util/callback.py +++ b/odl/solvers/util/callback.py @@ -8,14 +8,16 @@ """Callback objects for per-iterate actions in iterative methods.""" -from __future__ import print_function, division, absolute_import -from builtins import object +from __future__ import absolute_import, division, print_function + +import contextlib import copy -import numpy as np import os import time import warnings -import contextlib +from builtins import object + +import numpy as np from odl.util import signature_string @@ -569,7 +571,7 @@ class CallbackShow(Callback): See Also -------- - odl.discr.lp_discr.DiscreteLpElement.show + odl.discr.discr_space.DiscretizedSpaceElement.show odl.space.base_tensors.Tensor.show """ diff --git a/odl/space/__init__.py b/odl/space/__init__.py index f2f9797ee1b..684f2fc2d99 100644 --- a/odl/space/__init__.py +++ b/odl/space/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2014-2018 The ODL contributors +# Copyright 2014-2020 The ODL contributors # # This file is part of ODL. # @@ -22,8 +22,5 @@ from .pspace import * __all__ += pspace.__all__ -from .fspace import * -__all__ += fspace.__all__ - from .space_utils import * __all__ += space_utils.__all__ diff --git a/odl/space/base_tensors.py b/odl/space/base_tensors.py index ec2c2b41923..1b65a440de4 100644 --- a/odl/space/base_tensors.py +++ b/odl/space/base_tensors.py @@ -1,4 +1,4 @@ -# Copyright 2014-2019 The ODL contributors +# Copyright 2014-2020 The ODL contributors # # This file is part of ODL. # @@ -21,7 +21,7 @@ is_numeric_dtype, is_real_dtype, is_real_floating_dtype, safe_int_conv, signature_string, writable_array) from odl.util.ufuncs import TensorSpaceUfuncs -from odl.util.utility import TYPE_MAP_C2R, TYPE_MAP_R2C, none_context +from odl.util.utility import TYPE_MAP_C2R, TYPE_MAP_R2C, nullcontext __all__ = ('TensorSpace',) @@ -819,7 +819,7 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): if ufunc.nout == 1: # Make context for output (trivial one returns `None`) if out is None: - out_ctx = none_context() + out_ctx = nullcontext() else: out_ctx = writable_array(out, **array_kwargs) @@ -836,11 +836,11 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): if out1 is not None: out1_ctx = writable_array(out1, **array_kwargs) else: - out1_ctx = none_context() + out1_ctx = nullcontext() if out2 is not None: out2_ctx = writable_array(out2, **array_kwargs) else: - out2_ctx = none_context() + out2_ctx = nullcontext() # Evaluate ufunc with out1_ctx as out1_arr, out2_ctx as out2_arr: @@ -857,7 +857,7 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): else: # method != '__call__' # Make context for output (trivial one returns `None`) if out is None: - out_ctx = none_context() + out_ctx = nullcontext() else: out_ctx = writable_array(out, **array_kwargs) diff --git a/odl/space/fspace.py b/odl/space/fspace.py deleted file mode 100644 index 2f02af8a996..00000000000 --- a/odl/space/fspace.py +++ /dev/null @@ -1,1533 +0,0 @@ -# Copyright 2014-2019 The ODL contributors -# -# This file is part of ODL. -# -# This Source Code Form is subject to the terms of the Mozilla Public License, -# v. 2.0. If a copy of the MPL was not distributed with this file, You can -# obtain one at https://mozilla.org/MPL/2.0/. - -"""Spaces of scalar-, vector- and tensor-valued functions on a given domain.""" - -from __future__ import print_function, division, absolute_import -from builtins import object -import inspect -import numpy as np -import sys -import warnings - -from odl.set import RealNumbers, ComplexNumbers, Set, LinearSpace -from odl.set.space import LinearSpaceElement -from odl.util import ( - is_real_dtype, is_complex_floating_dtype, dtype_repr, dtype_str, - complex_dtype, real_dtype, signature_string, is_real_floating_dtype, - is_valid_input_array, is_valid_input_meshgrid, - out_shape_from_array, out_shape_from_meshgrid, vectorize, writable_array) -from odl.util.utility import preload_first_arg, getargspec - - -__all__ = ('FunctionSpace',) - - -def _check_out_arg(func): - """Check if ``func`` has an (optional) ``out`` argument. - - Also verify that the signature of ``func`` has no ``*args`` since - they make argument propagation a hassle. - - Parameters - ---------- - func : callable - Object that should be inspected. - - Returns - ------- - has_out : bool - ``True`` if the signature has an ``out`` argument, ``False`` - otherwise. - out_is_optional : bool - ``True`` if ``out`` is present and optional in the signature, - ``False`` otherwise. - - Raises - ------ - TypeError - If ``func``'s signature has ``*args``. - """ - if sys.version_info.major > 2: - spec = inspect.getfullargspec(func) - kw_only = spec.kwonlyargs - else: - spec = inspect.getargspec(func) - kw_only = () - - if spec.varargs is not None: - raise TypeError('*args not allowed in function signature') - - pos_args = spec.args - pos_defaults = () if spec.defaults is None else spec.defaults - - has_out = 'out' in pos_args or 'out' in kw_only - if 'out' in pos_args: - has_out = True - out_is_optional = ( - pos_args.index('out') >= len(pos_args) - len(pos_defaults)) - elif 'out' in kw_only: - has_out = out_is_optional = True - else: - has_out = out_is_optional = False - - return has_out, out_is_optional - - -def _default_in_place(func, x, out, **kwargs): - """Default in-place evaluation method.""" - result = np.array(func._call_out_of_place(x, **kwargs), copy=False) - if result.dtype == object: - # Different shapes encountered, need to broadcast - flat_results = result.ravel() - if is_valid_input_array(x, func.domain.ndim): - scalar_out_shape = out_shape_from_array(x) - elif is_valid_input_meshgrid(x, func.domain.ndim): - scalar_out_shape = out_shape_from_meshgrid(x) - else: - raise RuntimeError('bad input') - - bcast_results = [np.broadcast_to(res, scalar_out_shape) - for res in flat_results] - # New array that is flat in the `out_shape` axes, reshape it - # to the final `out_shape + scalar_shape`, using the same - # order ('C') as the initial `result.ravel()`. - result = np.array(bcast_results, dtype=func.scalar_out_dtype) - result = result.reshape(func.out_shape + scalar_out_shape) - - # The following code is required to remove extra axes, e.g., when - # the result has shape (2, 1, 3) but should have shape (2, 3). - # For those cases, broadcasting doesn't apply. - try: - reshaped = result.reshape(out.shape) - except ValueError: - # This is the case when `result` must be broadcast - out[:] = result - else: - out[:] = reshaped - - return out - - -def _default_out_of_place(func, x, **kwargs): - """Default in-place evaluation method.""" - if is_valid_input_array(x, func.domain.ndim): - scalar_out_shape = out_shape_from_array(x) - elif is_valid_input_meshgrid(x, func.domain.ndim): - scalar_out_shape = out_shape_from_meshgrid(x) - else: - raise TypeError('cannot use in-place method to implement ' - 'out-of-place non-vectorized evaluation') - - dtype = func.space.scalar_out_dtype - if dtype is None: - dtype = np.result_type(*x) - - out_shape = func.out_shape + scalar_out_shape - out = np.empty(out_shape, dtype=dtype) - func._call_in_place(x, out=out, **kwargs) - return out - - -def _fcall_out_type(fcall): - """Check if ``fcall`` has (optional) output argument. - - This function is intended to work with all types of callables - that are used as input to `FunctionSpace.element`. - """ - if isinstance(fcall, FunctionSpaceElement): - call_has_out = fcall._call_has_out - call_out_optional = fcall._call_out_optional - - # Numpy Ufuncs and similar objects (e.g. Numba DUfuncs) - elif hasattr(fcall, 'nin') and hasattr(fcall, 'nout'): - if fcall.nin != 1: - raise ValueError('ufunc {} has {} input parameter(s), ' - 'expected 1' - ''.format(fcall.__name__, fcall.nin)) - if fcall.nout > 1: - raise ValueError('ufunc {} has {} output parameter(s), ' - 'expected at most 1' - ''.format(fcall.__name__, fcall.nout)) - call_has_out = call_out_optional = (fcall.nout == 1) - elif inspect.isfunction(fcall): - call_has_out, call_out_optional = _check_out_arg(fcall) - elif callable(fcall): - call_has_out, call_out_optional = _check_out_arg(fcall.__call__) - else: - raise TypeError('object {!r} not callable'.format(fcall)) - - return call_has_out, call_out_optional - - -class FunctionSpace(LinearSpace): - - r"""A vector space of functions. - - Elements in this space represent scalar-, vector- or tensor-valued - functions on some set, usually a subset of a Euclidean space - :math:`\mathbb{R}^d`. The functions support vectorized evaluation, - see `the vectorization guide - `_ - for details. - """ - - def __init__(self, domain, out_dtype=float): - """Initialize a new instance. - - Parameters - ---------- - domain : `Set` - The domain of the functions. - out_dtype : optional - Data type of the return value of a function in this - space. Can be provided in any way the `numpy.dtype` - constructor understands, e.g. as built-in type or as a string. - - To create a space of vector- or tensor-valued functions, - use a dtype with a shape, e.g., - ``np.dtype((float, (2, 3)))``. - - For ``None``, the data type of function outputs is inferred - lazily at runtime. - - Examples - -------- - Real-valued functions on the interval [0, 1]: - - >>> domain = odl.IntervalProd(0, 1) - >>> odl.FunctionSpace(domain) - FunctionSpace(IntervalProd(0.0, 1.0)) - - Complex-valued functions on the same domain can be created by - specifying ``out_dtype``: - - >>> odl.FunctionSpace(domain, out_dtype=complex) - FunctionSpace(IntervalProd(0.0, 1.0), out_dtype=complex) - - To get vector- or tensor-valued functions, specify - ``out_dtype`` with shape: - - >>> vec_dtype = np.dtype((float, (3,))) # 3 components - >>> odl.FunctionSpace(domain, out_dtype=vec_dtype) - FunctionSpace(IntervalProd(0.0, 1.0), out_dtype=('float64', (3,))) - """ - if not isinstance(domain, Set): - raise TypeError('`domain` must be a `Set` instance, got {!r}' - ''.format(domain)) - self.__domain = domain - - # Prevent None from being converted to float64 by np.dtype - if out_dtype is None: - self.__out_dtype = None - else: - self.__out_dtype = np.dtype(out_dtype) - - if is_real_dtype(self.out_dtype): - field = RealNumbers() - elif is_complex_floating_dtype(self.out_dtype): - field = ComplexNumbers() - else: - field = None - - super(FunctionSpace, self).__init__(field) - - # Init cache attributes for real / complex variants - if self.field == RealNumbers(): - self.__real_out_dtype = self.out_dtype - self.__real_space = self - self.__complex_out_dtype = complex_dtype(self.out_dtype, - default=np.dtype(object)) - self.__complex_space = None - elif self.field == ComplexNumbers(): - self.__real_out_dtype = real_dtype(self.out_dtype) - self.__real_space = None - self.__complex_out_dtype = self.out_dtype - self.__complex_space = self - else: - self.__real_out_dtype = None - self.__real_space = None - self.__complex_out_dtype = None - self.__complex_space = None - - @property - def domain(self): - """Set from which a function in this space can take inputs.""" - return self.__domain - - @property - def out_dtype(self): - """Output data type (including shape) of a function in this space. - - If ``None``, the output data type is not pre-defined and instead - inferred at run-time. - """ - return self.__out_dtype - - @property - def scalar_out_dtype(self): - """Scalar variant of ``out_dtype`` in case it has a shape.""" - return getattr(self.out_dtype, 'base', None) - - @property - def real_out_dtype(self): - """The real dtype corresponding to this space's `out_dtype`.""" - if self.__real_out_dtype is None: - raise AttributeError( - 'no real variant of output dtype {} defined' - ''.format(dtype_repr(self.scalar_out_dtype))) - else: - return self.__real_out_dtype - - @property - def complex_out_dtype(self): - """The complex dtype corresponding to this space's `out_dtype`.""" - if self.__complex_out_dtype is None: - raise AttributeError( - 'no complex variant of output dtype {} defined' - ''.format(dtype_repr(self.scalar_out_dtype))) - else: - return self.__complex_out_dtype - - @property - def is_real(self): - """True if this is a space of real valued functions.""" - return is_real_floating_dtype(self.scalar_out_dtype) - - @property - def is_complex(self): - """True if this is a space of complex valued functions.""" - return is_complex_floating_dtype(self.scalar_out_dtype) - - @property - def out_shape(self): - """Shape of function values, ``()`` for scalar output.""" - return getattr(self.out_dtype, 'shape', ()) - - @property - def tensor_valued(self): - """``True`` if functions have multi-dim. output, else ``False``.""" - return (self.out_shape != ()) - - @property - def real_space(self): - """The space corresponding to this space's `real_dtype`.""" - return self.astype(self.real_out_dtype) - - @property - def complex_space(self): - """The space corresponding to this space's `complex_dtype`.""" - return self.astype(self.complex_out_dtype) - - def element(self, fcall=None, vectorized=True): - """Create a `FunctionSpace` element. - - Parameters - ---------- - fcall : callable, optional - The actual instruction for out-of-place evaluation. - It must return a `FunctionSpace.range` element or a - `numpy.ndarray` of such (vectorized call). - If ``fcall`` is a `FunctionSpaceElement`, it is wrapped - as a new `FunctionSpaceElement`. - Default: `zero`. - vectorized : bool, optional - If ``True``, assume that ``fcall`` supports vectorized - evaluation. For ``False``, the function is decorated with a - vectorizer, which implies that two elements created this way - from the same function are regarded as not equal. - The ``False`` option cannot be used if ``fcall`` has an - ``out`` parameter. - - Returns - ------- - element : `FunctionSpaceElement` - The new element, always supporting vectorization. - - Examples - -------- - Scalar-valued functions are straightforward to create: - - >>> fspace = odl.FunctionSpace(odl.IntervalProd(0, 1)) - >>> func = fspace.element(lambda x: x - 1) - >>> func(0.5) - -0.5 - >>> func([0.1, 0.5, 0.6]) - array([-0.9, -0.5, -0.4]) - - It is also possible to use functions with parameters. Note that - such extra parameters have to be given by keyword when calling - the function: - - >>> def f(x, b): - ... return x + b - >>> func = fspace.element(f) - >>> func([0.1, 0.5, 0.6], b=1) - array([ 1.1, 1.5, 1.6]) - >>> func([0.1, 0.5, 0.6], b=-1) - array([-0.9, -0.5, -0.4]) - - Vector-valued functions can eiter be given as a sequence of - scalar-valued functions or as a single function that returns - a sequence: - - >>> # Space of vector-valued functions with 2 components - >>> fspace = odl.FunctionSpace(odl.IntervalProd(0, 1), - ... out_dtype=(float, (2,))) - >>> # Possibility 1: provide component functions - >>> func1 = fspace.element([lambda x: x + 1, np.negative]) - >>> func1(0.5) - array([ 1.5, -0.5]) - >>> func1([0.1, 0.5, 0.6]) - array([[ 1.1, 1.5, 1.6], - [-0.1, -0.5, -0.6]]) - >>> # Possibility 2: single function returning a sequence - >>> func2 = fspace.element(lambda x: (x + 1, -x)) - >>> func2(0.5) - array([ 1.5, -0.5]) - >>> func2([0.1, 0.5, 0.6]) - array([[ 1.1, 1.5, 1.6], - [-0.1, -0.5, -0.6]]) - - If the function(s) include(s) an ``out`` parameter, it can be - provided to hold the final result: - - >>> # Sequence of functions with `out` parameter - >>> def f1(x, out): - ... out[:] = x + 1 - >>> def f2(x, out): - ... out[:] = -x - >>> func = fspace.element([f1, f2]) - >>> out = np.empty((2, 3)) # needs to match expected output shape - >>> result = func([0.1, 0.5, 0.6], out=out) - >>> out - array([[ 1.1, 1.5, 1.6], - [-0.1, -0.5, -0.6]]) - >>> result is out - True - >>> # Single function assigning to components of `out` - >>> def f(x, out): - ... out[0] = x + 1 - ... out[1] = -x - >>> func = fspace.element(f) - >>> out = np.empty((2, 3)) # needs to match expected output shape - >>> result = func([0.1, 0.5, 0.6], out=out) - >>> out - array([[ 1.1, 1.5, 1.6], - [-0.1, -0.5, -0.6]]) - >>> result is out - True - - Tensor-valued functions and functions defined on higher-dimensional - domains work just analogously: - - >>> fspace = odl.FunctionSpace(odl.IntervalProd([0, 0], [1, 1]), - ... out_dtype=(float, (2, 3))) - >>> def pyfunc(x): - ... return [[x[0], x[1], x[0] + x[1]], - ... [1, 0, 2 * (x[0] + x[1])]] - >>> func1 = fspace.element(pyfunc) - >>> # Points are given such that the first axis indexes the - >>> # components and the second enumerates the points. - >>> # We evaluate at [0.0, 0.5] and [0.0, 1.0] here. - >>> eval_pts = np.array([[0.0, 0.5], - ... [0.0, 1.0]]).T - >>> func1(eval_pts).shape - (2, 3, 2) - >>> func1(eval_pts) - array([[[ 0. , 0. ], - [ 0.5, 1. ], - [ 0.5, 1. ]], - - [[ 1. , 1. ], - [ 0. , 0. ], - [ 1. , 2. ]]]) - - Furthermore, it is allowed to use scalar constants instead of - functions if the function is given as sequence: - - >>> seq = [[lambda x: x[0], lambda x: x[1], lambda x: x[0] + x[1]], - ... [1, 0, lambda x: 2 * (x[0] + x[1])]] - >>> func2 = fspace.element(seq) - >>> func2(eval_pts) - array([[[ 0. , 0. ], - [ 0.5, 1. ], - [ 0.5, 1. ]], - - [[ 1. , 1. ], - [ 0. , 0. ], - [ 1. , 2. ]]]) - """ - if fcall is None: - return self.zero() - elif fcall in self: - return fcall - elif callable(fcall): - if not vectorized: - if hasattr(fcall, 'nin') and hasattr(fcall, 'nout'): - warnings.warn('`fcall` {!r} is a ufunc-like object, ' - 'use vectorized=True'.format(fcall), - RuntimeWarning) - has_out, _ = _check_out_arg(fcall) - if has_out: - raise TypeError('non-vectorized `fcall` with `out` ' - 'parameter not allowed') - if self.field is not None: - otypes = [self.scalar_out_dtype] - else: - otypes = [] - - fcall = vectorize(otypes=otypes)(fcall) - return self.element_type(self, fcall) - else: - # This is for the case that an array-like of callables - # is provided - if np.shape(fcall) != self.out_shape: - raise ValueError( - 'invalid `fcall` {!r}: expected `None`, a callable or ' - 'an array-like of callables whose shape matches ' - '`out_shape` {}'.format(self.out_shape)) - - fcalls = np.array(fcall, dtype=object, ndmin=1).ravel().tolist() - if not vectorized: - if self.field == RealNumbers(): - otypes = ['float64'] - elif self.field == ComplexNumbers(): - otypes = ['complex128'] - else: - otypes = [] - - # Vectorize, preserving scalars - fcalls = [f if np.isscalar(f) else vectorize(otypes=otypes)(f) - for f in fcalls] - - def wrapper(x, out=None, **kwargs): - """Function wrapping an array of callables. - - This wrapper does the following for out-of-place - evaluation (when ``out=None``): - - 1. Collect the results of all function evaluations into - a list, handling all kinds of sequence entries - (normal function, ufunc, constant, etc.). - 2. Broadcast all results to the desired shape that is - determined by the space's ``out_shape`` and the - shape(s) of the input. - 3. Form a big array containing the final result. - - The in-place version is simpler because broadcasting - happens automatically when assigning to the components - of ``out``. Hence, we only have - - 1. Assign the result of the evaluation of the i-th - function to ``out_flat[i]``, possibly using the - ``out`` parameter of the function. - """ - if is_valid_input_meshgrid(x, self.domain.ndim): - scalar_out_shape = out_shape_from_meshgrid(x) - elif is_valid_input_array(x, self.domain.ndim): - scalar_out_shape = out_shape_from_array(x) - else: - raise RuntimeError('bad input') - - if out is None: - # Out-of-place evaluation - - # Collect results of member functions into a list. - # Put simply, all that happens here is - # `results.append(f(x))`, just for a bunch of cases - # and with or without `out`. - results = [] - for f in fcalls: - if np.isscalar(f): - # Constant function - results.append(f) - elif not callable(f): - raise TypeError('element {!r} of sequence not ' - 'callable'.format(f)) - elif hasattr(f, 'nin') and hasattr(f, 'nout'): - # ufunc-like object - results.append(f(x, **kwargs)) - else: - try: - has_out = 'out' in getargspec(f).args - except TypeError: - raise TypeError('unsupported callable {!r}' - ''.format(f)) - else: - if has_out: - out = np.empty(scalar_out_shape, - dtype=self.scalar_out_dtype) - f(x, out=out, **kwargs) - results.append(out) - else: - results.append(f(x, **kwargs)) - - # Broadcast to required shape and convert to array. - # This will raise an error if the shape of some member - # array is wrong, since in that case the resulting - # dtype would be `object`. - bcast_results = [] - for res in results: - try: - reshaped = np.reshape(res, scalar_out_shape) - except ValueError: - bcast_results.append( - np.broadcast_to(res, scalar_out_shape)) - else: - bcast_results.append(reshaped) - - out_arr = np.array(bcast_results, - dtype=self.scalar_out_dtype) - - return out_arr.reshape(self.out_shape + scalar_out_shape) - - else: - # In-place evaluation - - # This is a precaution in case out is not contiguous - with writable_array(out) as out_arr: - # Flatten tensor axes to work on one tensor - # component (= scalar function) at a time - out_comps = out.reshape((-1,) + scalar_out_shape) - for f, out_comp in zip(fcalls, out_comps): - if np.isscalar(f): - out_comp[:] = f - else: - has_out, _ = _fcall_out_type(f) - if has_out: - f(x, out=out_comp, **kwargs) - else: - out_comp[:] = f(x, **kwargs) - - return self.element_type(self, wrapper) - - def zero(self): - """Function mapping anything to zero.""" - # Since `FunctionSpace.lincomb` may be slow, we implement this - # function directly. - # The unused **kwargs are needed to support combination with - # functions that take parameters. - def zero_vec(x, out=None, **kwargs): - """Zero function, vectorized.""" - if is_valid_input_meshgrid(x, self.domain.ndim): - scalar_out_shape = out_shape_from_meshgrid(x) - elif is_valid_input_array(x, self.domain.ndim): - scalar_out_shape = out_shape_from_array(x) - else: - raise TypeError('invalid input type') - - # For tensor-valued functions - out_shape = self.out_shape + scalar_out_shape - - if out is None: - return np.zeros(out_shape, dtype=self.scalar_out_dtype) - else: - # Need to go through an array to fill with the correct - # zero value for all dtypes - fill_value = np.zeros(1, dtype=self.scalar_out_dtype)[0] - out.fill(fill_value) - - return self.element_type(self, zero_vec) - - def one(self): - """Function mapping anything to one.""" - # See zero() for remarks - def one_vec(x, out=None, **kwargs): - """One function, vectorized.""" - if is_valid_input_meshgrid(x, self.domain.ndim): - scalar_out_shape = out_shape_from_meshgrid(x) - elif is_valid_input_array(x, self.domain.ndim): - scalar_out_shape = out_shape_from_array(x) - else: - raise TypeError('invalid input type') - - out_shape = self.out_shape + scalar_out_shape - - if out is None: - return np.ones(out_shape, dtype=self.scalar_out_dtype) - else: - fill_value = np.ones(1, dtype=self.scalar_out_dtype)[0] - out.fill(fill_value) - - return self.element_type(self, one_vec) - - def __eq__(self, other): - """Return ``self == other``. - - Returns - ------- - equals : bool - ``True`` if ``other`` is a `FunctionSpace` with same - `FunctionSpace.domain`, `FunctionSpace.field` and - `FunctionSpace.out_dtype`, ``False`` otherwise. - """ - if other is self: - return True - - return (type(other) is type(self) and - self.domain == other.domain and - self.out_dtype == other.out_dtype) - - def __hash__(self): - """Return ``hash(self)``.""" - return hash((type(self), self.domain, self.out_dtype)) - - def __contains__(self, other): - """Return ``other in self``. - - Returns - ------- - equals : bool - ``True`` if ``other`` is a `FunctionSpaceElement` - whose `FunctionSpaceElement.space` attribute - equals this space, ``False`` otherwise. - """ - return (isinstance(other, self.element_type) and - other.space == self) - - def _astype(self, out_dtype): - """Internal helper for ``astype``.""" - return type(self)(self.domain, out_dtype=out_dtype) - - def astype(self, out_dtype): - """Return a copy of this space with new ``out_dtype``. - - Parameters - ---------- - out_dtype : - Output data type of the returned space. Can be given in any - way `numpy.dtype` understands, e.g. as string (``'complex64'``) - or built-in type (``complex``). ``None`` is interpreted as - ``'float64'``. - - Returns - ------- - newspace : `FunctionSpace` - The version of this space with given data type - """ - out_dtype = np.dtype(out_dtype) - if out_dtype == self.out_dtype: - return self - - # Try to use caching for real and complex versions (exact dtype - # mappings). This may fail for certain dtype, in which case we - # just go to `_astype` directly. - real_dtype = getattr(self, 'real_out_dtype', None) - if real_dtype is None: - return self._astype(out_dtype) - else: - if out_dtype == real_dtype: - if self.__real_space is None: - self.__real_space = self._astype(out_dtype) - return self.__real_space - elif out_dtype == self.complex_out_dtype: - if self.__complex_space is None: - self.__complex_space = self._astype(out_dtype) - return self.__complex_space - else: - return self._astype(out_dtype) - - def _lincomb(self, a, f1, b, f2, out): - """Linear combination of ``f1`` and ``f2``. - - Notes - ----- - The additions and multiplications are implemented via simple - Python functions, so non-vectorized versions are slow. - """ - # Avoid infinite recursions by making a copy of the functions - f1_copy = f1.copy() - f2_copy = f2.copy() - - def lincomb_oop(x, **kwargs): - """Linear combination, out-of-place version.""" - # Not optimized since that raises issues with alignment - # of input and partial results - out = a * np.asarray(f1_copy(x, **kwargs), - dtype=self.scalar_out_dtype) - tmp = b * np.asarray(f2_copy(x, **kwargs), - dtype=self.scalar_out_dtype) - out += tmp - return out - - out._call_out_of_place = lincomb_oop - decorator = preload_first_arg(out, 'in-place') - out._call_in_place = decorator(_default_in_place) - out._call_has_out = out._call_out_optional = False - return out - - def _multiply(self, f1, f2, out): - """Pointwise multiplication of ``f1`` and ``f2``. - - Notes - ----- - The multiplication is implemented with a simple Python - function, so the non-vectorized versions are slow. - """ - # Avoid infinite recursions by making a copy of the functions - f1_copy = f1.copy() - f2_copy = f2.copy() - - def product_oop(x, **kwargs): - """Product out-of-place evaluation function.""" - return np.asarray(f1_copy(x, **kwargs) * f2_copy(x, **kwargs), - dtype=self.scalar_out_dtype) - - out._call_out_of_place = product_oop - decorator = preload_first_arg(out, 'in-place') - out._call_in_place = decorator(_default_in_place) - out._call_has_out = out._call_out_optional = False - return out - - def _divide(self, f1, f2, out): - """Pointwise division of ``f1`` and ``f2``. - - Notes - ----- - The division is implemented with a simple Python - function, so the non-vectorized versions are slow. - """ - # Avoid infinite recursions by making a copy of the functions - f1_copy = f1.copy() - f2_copy = f2.copy() - - def quotient_oop(x, **kwargs): - """Quotient out-of-place evaluation function.""" - return np.asarray(f1_copy(x, **kwargs) / f2_copy(x, **kwargs), - dtype=self.scalar_out_dtype) - - out._call_out_of_place = quotient_oop - decorator = preload_first_arg(out, 'in-place') - out._call_in_place = decorator(_default_in_place) - out._call_has_out = out._call_out_optional = False - return out - - def _scalar_power(self, f, p, out): - """Compute ``p``-th power of ``f`` for ``p`` scalar.""" - # Avoid infinite recursions by making a copy of the function - f_copy = f.copy() - - def pow_posint(x, n): - """Power function for positive integer ``n``, out-of-place.""" - if isinstance(x, np.ndarray): - y = x.copy() - return ipow_posint(y, n) - else: - return x ** n - - def ipow_posint(x, n): - """Power function for positive integer ``n``, in-place.""" - if n == 1: - return x - elif n % 2 == 0: - x *= x - return ipow_posint(x, n // 2) - else: - tmp = x.copy() - x *= x - ipow_posint(x, n // 2) - x *= tmp - return x - - def power_oop(x, **kwargs): - """Power out-of-place evaluation function.""" - if p == 0: - return self.one() - elif p == int(p) and p >= 1: - return np.asarray(pow_posint(f_copy(x, **kwargs), int(p)), - dtype=self.scalar_out_dtype) - else: - result = np.power(f_copy(x, **kwargs), p) - return result.astype(self.scalar_out_dtype) - - out._call_out_of_place = power_oop - decorator = preload_first_arg(out, 'in-place') - out._call_in_place = decorator(_default_in_place) - out._call_has_out = out._call_out_optional = False - return out - - def _realpart(self, f): - """Function returning the real part of the result from ``f``.""" - def f_re(x, **kwargs): - result = np.asarray(f(x, **kwargs), - dtype=self.scalar_out_dtype) - return result.real - - if is_real_dtype(self.out_dtype): - return f - else: - return self.real_space.element(f_re) - - def _imagpart(self, f): - """Function returning the imaginary part of the result from ``f``.""" - def f_im(x, **kwargs): - result = np.asarray(f(x, **kwargs), - dtype=self.scalar_out_dtype) - return result.imag - - if is_real_dtype(self.out_dtype): - return self.zero() - else: - return self.real_space.element(f_im) - - def _conj(self, f): - """Function returning the complex conjugate of a result.""" - def f_conj(x, **kwargs): - result = np.asarray(f(x, **kwargs), - dtype=self.scalar_out_dtype) - return result.conj() - - if is_real_dtype(self.out_dtype): - return f - else: - return self.element(f_conj) - - @property - def byaxis_out(self): - """Object to index along output dimensions. - - This is only valid for non-trivial `out_shape`. - - Examples - -------- - Indexing with integers or slices: - - >>> domain = odl.IntervalProd(0, 1) - >>> fspace = odl.FunctionSpace(domain, out_dtype=(float, (2, 3, 4))) - >>> fspace.byaxis_out[0] - FunctionSpace(IntervalProd(0.0, 1.0), out_dtype=('float64', (2,))) - >>> fspace.byaxis_out[1] - FunctionSpace(IntervalProd(0.0, 1.0), out_dtype=('float64', (3,))) - >>> fspace.byaxis_out[1:] - FunctionSpace(IntervalProd(0.0, 1.0), out_dtype=('float64', (3, 4))) - - Lists can be used to stack spaces arbitrarily: - - >>> fspace.byaxis_out[[2, 1, 2]] - FunctionSpace(IntervalProd(0.0, 1.0), out_dtype=('float64', (4, 3, 4))) - """ - space = self - - class FspaceByaxisOut(object): - - """Helper class for indexing by output axes.""" - - def __getitem__(self, indices): - """Return ``self[indices]``. - - Parameters - ---------- - indices : index expression - Object used to index the output components. - - Returns - ------- - space : `FunctionSpace` - The resulting space with same domain and scalar output - data type, but indexed output components. - - Raises - ------ - IndexError - If this is a space of scalar-valued functions. - """ - try: - iter(indices) - except TypeError: - newshape = space.out_shape[indices] - else: - newshape = tuple(space.out_shape[int(i)] for i in indices) - - dtype = (space.scalar_out_dtype, newshape) - return FunctionSpace(space.domain, out_dtype=dtype) - - def __repr__(self): - """Return ``repr(self)``.""" - return repr(space) + '.byaxis_out' - - return FspaceByaxisOut() - - @property - def byaxis_in(self): - """Object to index ``self`` along input dimensions. - - Examples - -------- - Indexing with integers or slices: - - >>> domain = odl.IntervalProd([0, 0, 0], [1, 2, 3]) - >>> fspace = odl.FunctionSpace(domain) - >>> fspace.byaxis_in[0] - FunctionSpace(IntervalProd(0.0, 1.0)) - >>> fspace.byaxis_in[1] - FunctionSpace(IntervalProd(0.0, 2.0)) - >>> fspace.byaxis_in[1:] - FunctionSpace(IntervalProd([ 0., 0.], [ 2., 3.])) - - Lists can be used to stack spaces arbitrarily: - - >>> fspace.byaxis_in[[2, 1, 2]] - FunctionSpace(IntervalProd([ 0., 0., 0.], [ 3., 2., 3.])) - """ - space = self - - class FspaceByaxisIn(object): - - """Helper class for indexing by input axes.""" - - def __getitem__(self, indices): - """Return ``self[indices]``. - - Parameters - ---------- - indices : index expression - Object used to index the space domain. - - Returns - ------- - space : `FunctionSpace` - The resulting space with same output data type, but - indexed domain. - """ - domain = space.domain[indices] - return FunctionSpace(domain, out_dtype=space.out_dtype) - - def __repr__(self): - """Return ``repr(self)``.""" - return repr(space) + '.byaxis_in' - - return FspaceByaxisIn() - - @property - def examples(self): - """Return example functions in the space. - - Example functions include: - - Zero - One - Heaviside function - Hypercube characteristic function - Hypersphere characteristic function - Gaussian - Linear gradients - """ - # TODO: adapt for tensor-valued functions - - # Get the points and calculate some statistics on them - mins = self.domain.min() - maxs = self.domain.max() - means = (maxs + mins) / 2.0 - stds = (maxs - mins) / 4.0 - ndim = getattr(self.domain, 'ndim', 0) - - # Zero and One - yield ('Zero', self.zero()) - yield ('One', self.one()) - - # Indicator function in first dimension - def step_fun(x): - return (x[0] > means[0]) - - yield ('Step', self.element(step_fun)) - - # Indicator function on hypercube - def cube_fun(x): - result = True - for points, mean, std in zip(x, means, stds): - result = np.logical_and(result, points < mean + std) - result = np.logical_and(result, points > mean - std) - return result - - yield ('Cube', self.element(cube_fun)) - - # Indicator function on a ball - if ndim > 1: # Only if ndim > 1, don't duplicate cube - def ball_fun(x): - r = sum((xi - mean) ** 2 / std ** 2 - for xi, mean, std in zip(x, means, stds)) - return r < 1.0 - - yield ('Ball', self.element(ball_fun)) - - # Gaussian function - def gaussian_fun(x): - r2 = sum((xi - mean) ** 2 / (2 * std ** 2) - for xi, mean, std in zip(x, means, stds)) - return np.exp(-r2) - - yield ('Gaussian', self.element(gaussian_fun)) - - # Gradient in each dimensions - for axis in range(ndim): - def gradient_fun(x): - return (x[axis] - mins[axis]) / (maxs[axis] - mins[axis]) - - yield ('Grad {}'.format(axis), self.element(gradient_fun)) - - # Gradient in all dimensions - if ndim > 1: # Only if ndim > 1, don't duplicate grad 0 - def all_gradient_fun(x): - return sum((xi - xmin) / (xmax - xmin) - for xi, xmin, xmax in zip(x, mins, maxs)) - - yield ('Grad all', self.element(all_gradient_fun)) - - @property - def element_type(self): - """`FunctionSpaceElement`""" - return FunctionSpaceElement - - def __repr__(self): - """Return ``repr(self)``.""" - posargs = [self.domain] - optargs = [('out_dtype', dtype_str(self.out_dtype), 'float')] - if (self.tensor_valued or - self.scalar_out_dtype in (float, complex, int, bool)): - optmod = '!s' - else: - optmod = '' - inner_str = signature_string(posargs, optargs, mod=['!r', optmod]) - return '{}({})'.format(self.__class__.__name__, inner_str) - - def __str__(self): - """Return ``str(self)``.""" - return repr(self) - - -class FunctionSpaceElement(LinearSpaceElement): - - """Representation of a `FunctionSpace` element.""" - - def __init__(self, fspace, fcall): - """Initialize a new instance. - - Parameters - ---------- - fspace : `FunctionSpace` - Set of functions this element lives in. - fcall : callable - Object used to evaluate the function. Must support - vectorization and accept a sequence of - coordinate arrays ``x[0], ..., x[d]`` in sparse or dense - form, and return (or write to the ``out`` array) an - array of appropriate shape. - """ - super(FunctionSpaceElement, self).__init__(fspace) - self._call_has_out, self._call_out_optional = _fcall_out_type(fcall) - - if not self._call_has_out: - # Out-of-place-only - decorator = preload_first_arg(self, 'in-place') - self._call_in_place = decorator(_default_in_place) - self._call_out_of_place = fcall - elif self._call_out_optional: - # Dual-use - self._call_in_place = self._call_out_of_place = fcall - else: - # In-place-only - decorator = preload_first_arg(self, 'out-of-place') - self._call_out_of_place = decorator(_default_out_of_place) - self._call_in_place = fcall - - @property - def domain(self): - """Set of objects on which this function can be evaluated.""" - return self.space.domain - - @property - def out_dtype(self): - """Output data type of this function. - - If ``None``, the output data type is not uniquely pre-defined. - """ - return self.space.out_dtype - - @property - def scalar_out_dtype(self): - """Scalar variant of ``out_dtype`` in case it has a shape.""" - return self.space.scalar_out_dtype - - @property - def out_shape(self): - """Shape of function values, ``()`` for scalar output.""" - return self.space.out_shape - - @property - def tensor_valued(self): - """``True`` if the output is multi-dim. output, else ``False``.""" - return self.space.tensor_valued - - def _call(self, x, out=None, **kwargs): - """Raw evaluation method.""" - if out is None: - return self._call_out_of_place(x, **kwargs) - else: - self._call_in_place(x, out=out, **kwargs) - - def __call__(self, x, out=None, **kwargs): - """Return ``self(x[, out, **kwargs])``. - - Parameters - ---------- - x : `domain` `element-like`, `meshgrid` or `numpy.ndarray` - Input argument for the function evaluation. Conditions - on ``x`` depend on its type: - - element-like: must be a castable to a domain element - - meshgrid: length must be ``space.ndim``, and the arrays must - be broadcastable against each other. - - array: shape must be ``(d, N)``, where ``d`` is the number - of dimensions of the function domain - - out : `numpy.ndarray`, optional - Output argument holding the result of the function - evaluation, can only be used for vectorized - functions. Its shape must be equal to - ``np.broadcast(*x).shape``. - - Other Parameters - ---------------- - bounds_check : bool - If ``True``, check if all input points lie in the function - domain in the case of vectorized evaluation. This requires - the domain to implement `Set.contains_all`. - Default: ``True`` if `space` has a ``field``, ``False`` - otherwise. - - Returns - ------- - out : `range` element or `numpy.ndarray` of elements - Result of the function evaluation. If ``out`` was provided, - the returned object is a reference to it. - - Raises - ------ - TypeError - If ``x`` is not a valid vectorized evaluation argument. - - If ``out`` is neither ``None`` nor a `numpy.ndarray` of - adequate shape and data type. - - ValueError - If ``bounds_check == True`` and some evaluation points fall - outside the valid domain. - - Examples - -------- - In the following we have an ``ndim=2``-dimensional domain. The - following shows valid arrays and meshgrids for input: - - >>> fspace = odl.FunctionSpace(odl.IntervalProd([0, 0], [1, 1])) - >>> func = fspace.element(lambda x: x[1] - x[0]) - >>> # 3 evaluation points, given point per point, each of which - >>> # is contained in the function domain. - >>> points = [[0, 0], - ... [0, 1], - ... [0.5, 0.1]] - >>> # The array provided to `func` must be transposed since - >>> # the first axis must index the components of the points and - >>> # the second axis must enumerate them. - >>> array = np.array(points).T - >>> array.shape # should be `ndim` x N - (2, 3) - >>> func(array) - array([ 0. , 1. , -0.4]) - >>> # A meshgrid is an `ndim`-long sequence of 1D Numpy arrays - >>> # containing the coordinates of the points. We use - >>> # 2 * 3 = 6 points here. - >>> comp0 = np.array([0.0, 1.0]) # first components - >>> comp1 = np.array([0.0, 0.5, 1.0]) # second components - >>> # The following adds extra dimensions to enable broadcasting. - >>> mesh = odl.discr.grid.sparse_meshgrid(comp0, comp1) - >>> len(mesh) # should be `ndim` - 2 - >>> func(mesh) - array([[ 0. , 0.5, 1. ], - [-1. , -0.5, 0. ]]) - """ - bounds_check = kwargs.pop('bounds_check', self.space.field is not None) - if bounds_check and not hasattr(self.domain, 'contains_all'): - raise AttributeError('bounds check not possible for ' - 'domain {}, missing `contains_all()` ' - 'method'.format(self.domain)) - - if bounds_check and not hasattr(self.space.field, 'contains_all'): - raise AttributeError('bounds check not possible for ' - 'field {}, missing `contains_all()` ' - 'method'.format(self.space.field)) - - ndim = getattr(self.domain, 'ndim', None) - # Check for input type and determine output shape - if is_valid_input_meshgrid(x, ndim): - scalar_in = False - scalar_out_shape = out_shape_from_meshgrid(x) - scalar_out = False - # Avoid operations on tuples like x * 2 by casting to array - if ndim == 1: - x = x[0][None, ...] - elif is_valid_input_array(x, ndim): - x = np.asarray(x) - scalar_in = False - scalar_out_shape = out_shape_from_array(x) - scalar_out = False - elif x in self.domain: - x = np.atleast_2d(x).T # make a (d, 1) array - scalar_in = True - scalar_out_shape = (1,) - scalar_out = (out is None and not self.space.tensor_valued) - else: - # Unknown input - txt_1d = ' or (n,)' if ndim == 1 else '' - raise TypeError('argument {!r} not a valid function ' - 'input. Expected an element of the domain ' - '{domain}, an array-like with shape ' - '({domain.ndim}, n){} or a length-{domain.ndim} ' - 'meshgrid tuple.' - ''.format(x, txt_1d, domain=self.domain)) - - # Check bounds if specified - if bounds_check: - if not self.domain.contains_all(x): - raise ValueError('input contains points outside ' - 'the domain {}'.format(self.domain)) - - if scalar_in: - out_shape = self.out_shape - else: - out_shape = self.out_shape + scalar_out_shape - - # Call the function and check out shape, before or after - if out is None: - if ndim == 1: - try: - out = self._call(x, **kwargs) - except (TypeError, IndexError): - # TypeError is raised if a meshgrid was used but the - # function expected an array (1d only). In this case we try - # again with the first meshgrid vector. - # IndexError is raised in expressions like x[x > 0] since - # "x > 0" evaluates to 'True', i.e. 1, and that index is - # out of range for a meshgrid tuple of length 1 :-). To get - # the real errors with indexing, we check again for the - # same scenario (scalar output when not valid) as in the - # first case. - out = self._call(x[0], **kwargs) - - else: - # Here we don't catch exceptions since they are likely true - # errors - out = self._call(x, **kwargs) - - if isinstance(out, np.ndarray) or np.isscalar(out): - # Cast to proper dtype if needed, also convert to array if out - # is a scalar. - out = np.asarray(out, dtype=self.space.scalar_out_dtype) - if scalar_in: - out = np.squeeze(out) - elif ndim == 1 and out.shape == (1,) + out_shape: - out = out.reshape(out_shape) - - if out_shape != () and out.shape != out_shape: - # Broadcast the returned element, but not in the - # scalar case. The resulting array may be read-only, - # in which case we copy. - out = np.broadcast_to(out, out_shape) - if not out.flags.writeable: - out = out.copy() - - elif self.space.tensor_valued: - # The out object can be any array-like of objects with shapes - # that should all be broadcastable to scalar_out_shape. - results = np.array(out) - if results.dtype == object or scalar_in: - # Some results don't have correct shape, need to - # broadcast - bcast_res = [] - for res in results.ravel(): - if ndim == 1: - # As usual, 1d is tedious to deal with. This - # code deals with extra dimensions in result - # components that stem from using x instead of - # x[0] in a function. - # Without this, broadcasting fails. - shp = getattr(res, 'shape', ()) - if shp and shp[0] == 1: - res = res.reshape(res.shape[1:]) - bcast_res.append( - np.broadcast_to(res, scalar_out_shape)) - - out_arr = np.array(bcast_res, - dtype=self.space.scalar_out_dtype) - elif (self.scalar_out_dtype is not None and - results.dtype != self.scalar_out_dtype): - raise ValueError( - 'result is of dtype {}, expected {}' - ''.format(dtype_repr(results.dtype), - dtype_repr(self.space.scalar_out_dtype))) - else: - out_arr = results - - out = out_arr.reshape(out_shape) - - else: - # TODO: improve message - raise RuntimeError('bad output of function call') - - else: - if not isinstance(out, np.ndarray): - raise TypeError('output {!r} not a `numpy.ndarray` ' - 'instance') - if out_shape != (1,) and out.shape != out_shape: - raise ValueError('output shape {} not equal to shape ' - '{} expected from input' - ''.format(out.shape, out_shape)) - if (self.out_dtype is not None and - out.dtype != self.scalar_out_dtype): - raise ValueError('`out.dtype` ({}) does not match out_dtype ' - '({})'.format(out.dtype, self.out_dtype)) - - if ndim == 1 and not self.tensor_valued: - # TypeError for meshgrid in 1d, but expected array (see above) - try: - self._call(x, out=out, **kwargs) - except TypeError: - self._call(x[0], out=out, **kwargs) - else: - self._call(x, out=out, **kwargs) - - # Check output values - if bounds_check: - if not self.space.field.contains_all(out): - raise ValueError('output contains values not in the field ' - '{}' - ''.format(self.space.field)) - - # Numpy < 1.12 does not implement __complex__ for arrays (in contrast - # to __float__), so we have to fish out the scalar ourselves. - if scalar_out: - if self.space.field is None: - return out.ravel()[0] - else: - return self.space.field.element(out.ravel()[0]) - else: - return out - - def assign(self, other): - """Assign ``other`` to ``self``. - - This is implemented without `FunctionSpace.lincomb` to ensure that - ``self == other`` evaluates to True after ``self.assign(other)``. - """ - if other not in self.space: - raise TypeError('`other` {!r} is not an element of the space ' - '{} of this function' - ''.format(other, self.space)) - self._call_in_place = other._call_in_place - self._call_out_of_place = other._call_out_of_place - self._call_has_out = other._call_has_out - self._call_out_optional = other._call_out_optional - - def copy(self): - """Create an identical (deep) copy of this element.""" - result = self.space.element() - result.assign(self) - return result - - def __eq__(self, other): - """Return ``self == other``. - - Returns - ------- - equals : bool - ``True`` if ``other`` is a `FunctionSpaceElement` with - ``other.space == self.space``, and the functions for evaluation - of ``self`` and ``other`` are the same, ``False`` - otherwise. - - Notes - ----- - Since there is potentially a lot of function wrapping going on, - it is very hard to find the "true" function behind a - `FunctionSpaceElement` for comparison. Therefore, users - should be aware that very often, comparison evaluates to ``False`` - even if two elements were generated from the same function. - """ - if other is self: - return True - elif other not in self.space: - return False - - # We try to unwrap one level, which is better than nothing - if (self._call_has_out != other._call_has_out or - self._call_out_optional != other._call_out_optional): - return False - - if self._call_has_out: - # Out-of-place can be wrapped in this case, so we compare only - # the in-place methods. - funcs_equal = self._call_in_place == other._call_in_place - else: - # Just the opposite of the first case - funcs_equal = self._call_out_of_place == other._call_out_of_place - - return self.space == other.space and funcs_equal - - # Power functions are more general than the ones in LinearSpace - def __pow__(self, p): - """`f.__pow__(p) <==> f ** p`.""" - out = self.space.element() - self.space._scalar_power(self, p, out=out) - return out - - def __ipow__(self, p): - """`f.__ipow__(p) <==> f **= p`.""" - return self.space._scalar_power(self, p, out=self) - - @property - def real(self): - """Pointwise real part of this function.""" - return self.space._realpart(self) - - @property - def imag(self): - """Pointwise imaginary part of this function.""" - return self.space._imagpart(self) - - def conj(self): - """Pointwise complex conjugate of this function.""" - return self.space._conj(self) - - def __str__(self): - """Return ``str(self)``.""" - if self._call_has_out: - func = self._call_in_place - else: - func = self._call_out_of_place - - # Try to get a pretty-print name of the function - fname = getattr(func, '__name__', getattr(func, 'name', str(func))) - - return '{}: {} --> {}'.format(fname, self.domain, self.out_dtype) - - def __repr__(self): - """Return ``repr(self)``.""" - if self._call_has_out: - func = self._call_in_place - else: - func = self._call_out_of_place - - return '{!r}.element({!r})'.format(self.space, func) - - -if __name__ == '__main__': - from odl.util.testutils import run_doctests - run_doctests() diff --git a/odl/space/npy_tensors.py b/odl/space/npy_tensors.py index 4299e172555..d041a0e8f34 100644 --- a/odl/space/npy_tensors.py +++ b/odl/space/npy_tensors.py @@ -1,4 +1,4 @@ -# Copyright 2014-2019 The ODL contributors +# Copyright 2014-2020 The ODL contributors # # This file is part of ODL. # @@ -24,8 +24,8 @@ ArrayWeighting, ConstWeighting, CustomDist, CustomInner, CustomNorm, Weighting) from odl.util import ( - dtype_str, is_floating_dtype, is_numeric_dtype, is_real_dtype, - none_context, signature_string, writable_array) + dtype_str, is_floating_dtype, is_numeric_dtype, is_real_dtype, nullcontext, + signature_string, writable_array) __all__ = ('NumpyTensorSpace',) @@ -1659,7 +1659,7 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): if ufunc.nout == 1: # Make context for output (trivial one returns `None`) if out is None: - out_ctx = none_context() + out_ctx = nullcontext() else: out_ctx = writable_array(out, **array_kwargs) @@ -1687,11 +1687,11 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): if out1 is not None: out1_ctx = writable_array(out1, **array_kwargs) else: - out1_ctx = none_context() + out1_ctx = nullcontext() if out2 is not None: out2_ctx = writable_array(out2, **array_kwargs) else: - out2_ctx = none_context() + out2_ctx = nullcontext() # Evaluate ufunc with out1_ctx as out1_arr, out2_ctx as out2_arr: @@ -1717,7 +1717,7 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): else: # method != '__call__' # Make context for output (trivial one returns `None`) if out is None: - out_ctx = none_context() + out_ctx = nullcontext() else: out_ctx = writable_array(out, **array_kwargs) diff --git a/odl/space/pspace.py b/odl/space/pspace.py index 86a7ada9ef9..bffded0a58b 100644 --- a/odl/space/pspace.py +++ b/odl/space/pspace.py @@ -1,4 +1,4 @@ -# Copyright 2014-2019 The ODL contributors +# Copyright 2014-2020 The ODL contributors # # This file is part of ODL. # @@ -8,20 +8,21 @@ """Cartesian products of `LinearSpace` instances.""" -from __future__ import print_function, division, absolute_import +from __future__ import absolute_import, division, print_function + from itertools import product from numbers import Integral + import numpy as np from odl.set import LinearSpace from odl.set.space import LinearSpaceElement from odl.space.weighting import ( - Weighting, ArrayWeighting, ConstWeighting, - CustomInner, CustomNorm, CustomDist) -from odl.util import is_real_dtype, signature_string, indent + ArrayWeighting, ConstWeighting, CustomDist, CustomInner, CustomNorm, + Weighting) +from odl.util import indent, is_real_dtype, signature_string from odl.util.ufuncs import ProductSpaceUfuncs - __all__ = ('ProductSpace',) @@ -1428,7 +1429,7 @@ def show(self, title=None, indices=None, **kwargs): See Also -------- - odl.discr.lp_discr.DiscreteLpElement.show : + odl.discr.discr_space.DiscretizedSpaceElement.show : Display of a discretized function odl.space.base_tensors.Tensor.show : Display of sequence type data diff --git a/odl/test/deform/linearized_deform_test.py b/odl/test/deform/linearized_deform_test.py index ee17d4cb192..ebd2136d4ef 100644 --- a/odl/test/deform/linearized_deform_test.py +++ b/odl/test/deform/linearized_deform_test.py @@ -167,7 +167,7 @@ def test_fixed_templ_init(): # Invalid input with pytest.raises(TypeError): - # template_function not a DiscreteLpElement + # template_function not a DiscretizedSpaceElement LinDeformFixedTempl(template_function) @@ -229,12 +229,12 @@ def test_fixed_disp_init(): # Non-valid input with pytest.raises(TypeError): # displacement not ProductSpaceElement LinDeformFixedDisp(space.one()) - with pytest.raises(TypeError): # templ_space not DiscreteLp + with pytest.raises(TypeError): # templ_space not DiscretizedSpace LinDeformFixedDisp(disp_field, space.tangent_bundle) with pytest.raises(TypeError): # templ_space not a power space bad_pspace = odl.ProductSpace(space, odl.rn(3)) LinDeformFixedDisp(disp_field, bad_pspace) - with pytest.raises(TypeError): # templ_space not based on DiscreteLp + with pytest.raises(TypeError): # templ_space not based on DiscretizedSpace bad_pspace = odl.ProductSpace(odl.rn(2), 1) LinDeformFixedDisp(disp_field, bad_pspace) with pytest.raises(TypeError): # wrong dtype on templ_space diff --git a/odl/test/discr/diff_ops_test.py b/odl/test/discr/diff_ops_test.py index 5b37889d499..d8c6caab752 100644 --- a/odl/test/discr/diff_ops_test.py +++ b/odl/test/discr/diff_ops_test.py @@ -1,4 +1,4 @@ -# Copyright 2014-2017 The ODL contributors +# Copyright 2014-2020 The ODL contributors # # This file is part of ODL. # @@ -9,15 +9,15 @@ """Unit tests for `diff_ops`.""" from __future__ import division -import pytest + import numpy as np +import pytest import odl from odl.discr.diff_ops import ( - finite_diff, PartialDerivative, Gradient, Divergence, Laplacian) + Divergence, Gradient, Laplacian, PartialDerivative, finite_diff) from odl.util.testutils import ( - all_equal, all_almost_equal, dtype_tol, noise_element, simple_fixture) - + all_almost_equal, all_equal, dtype_tol, noise_element, simple_fixture) # --- pytest fixtures --- # @@ -47,10 +47,8 @@ def test_finite_diff_invalid_args(): """Test finite difference function for invalid arguments.""" # Test that old "edge order" argument fails. - with pytest.raises(ValueError): + with pytest.raises(TypeError): finite_diff(DATA_1D, axis=0, edge_order=0) - with pytest.raises(ValueError): - finite_diff(DATA_1D, axis=0, edge_order=3) # at least a two-element array is required with pytest.raises(ValueError): @@ -314,7 +312,7 @@ def test_gradient(space, method, padding): else: pad_mode, pad_const = padding, 0 - # DiscreteLp Vector + # DiscretizedSpaceElement dom_vec = noise_element(space) dom_vec_arr = dom_vec.asarray() diff --git a/odl/test/discr/discr_ops_test.py b/odl/test/discr/discr_ops_test.py index fcb84e349b8..fea5d16da46 100644 --- a/odl/test/discr/discr_ops_test.py +++ b/odl/test/discr/discr_ops_test.py @@ -1,4 +1,4 @@ -# Copyright 2014-2017 The ODL contributors +# Copyright 2014-2020 The ODL contributors # # This file is part of ODL. # @@ -64,42 +64,42 @@ def test_resizing_op_init(odl_tspace_impl, padding): def test_resizing_op_raise(): - # domain not a uniformely discretized Lp + """Validate error checking in ResizingOperator.""" + # Domain not a uniformly discretized Lp with pytest.raises(TypeError): odl.ResizingOperator(odl.rn(5), ran_shp=(10,)) grid = odl.RectGrid([0, 2, 3]) part = odl.RectPartition(odl.IntervalProd(0, 3), grid) - fspace = odl.FunctionSpace(odl.IntervalProd(0, 3)) tspace = odl.rn(3) - space = odl.DiscreteLp(fspace, part, tspace) + space = odl.DiscretizedSpace(part, tspace) with pytest.raises(ValueError): odl.ResizingOperator(space, ran_shp=(10,)) - # different cell sides in domain and range + # Different cell sides in domain and range space = odl.uniform_discr(0, 1, 10) res_space = odl.uniform_discr(0, 1, 15) with pytest.raises(ValueError): odl.ResizingOperator(space, res_space) - # non-integer multiple of cell sides used as shift (grid of the + # Non-integer multiple of cell sides used as shift (grid of the # resized space shifted) space = odl.uniform_discr(0, 1, 5) res_space = odl.uniform_discr(-0.5, 1.5, 10) with pytest.raises(ValueError): odl.ResizingOperator(space, res_space) - # need either range or ran_shp + # Need either range or ran_shp with pytest.raises(ValueError): odl.ResizingOperator(space) - # offset cannot be combined with range + # Offset cannot be combined with range space = odl.uniform_discr([0, -1], [1, 1], (10, 5)) res_space = odl.uniform_discr([0, -3], [2, 3], (20, 15)) with pytest.raises(ValueError): odl.ResizingOperator(space, res_space, offset=(0, 0)) - # bad pad_mode + # Bad pad_mode with pytest.raises(ValueError): odl.ResizingOperator(space, res_space, pad_mode='something') @@ -153,11 +153,15 @@ def test_resizing_op_call(odl_tspace_impl): for dtype in dtypes: # Minimal test since this operator only wraps resize_array - space = odl.uniform_discr([0, -1], [1, 1], (4, 5), impl=impl) - res_space = odl.uniform_discr([0, -0.6], [2, 0.2], (8, 2), impl=impl) + space = odl.uniform_discr( + [0, -1], [1, 1], (4, 5), dtype=dtype, impl=impl + ) + res_space = odl.uniform_discr( + [0, -0.6], [2, 0.2], (8, 2), dtype=dtype, impl=impl + ) res_op = odl.ResizingOperator(space, res_space) out = res_op(space.one()) - true_res = np.zeros((8, 2)) + true_res = np.zeros((8, 2), dtype=dtype) true_res[:4, :] = 1 assert np.array_equal(out, true_res) @@ -167,11 +171,15 @@ def test_resizing_op_call(odl_tspace_impl): # Test also mapping to default impl for other 'impl' if impl != 'numpy': - space = odl.uniform_discr([0, -1], [1, 1], (4, 5), impl=impl) - res_space = odl.uniform_discr([0, -0.6], [2, 0.2], (8, 2)) + space = odl.uniform_discr( + [0, -1], [1, 1], (4, 5), dtype=dtype, impl=impl + ) + res_space = odl.uniform_discr( + [0, -0.6], [2, 0.2], (8, 2), dtype=dtype + ) res_op = odl.ResizingOperator(space, res_space) out = res_op(space.one()) - true_res = np.zeros((8, 2)) + true_res = np.zeros((8, 2), dtype=dtype) true_res[:4, :] = 1 assert np.array_equal(out, true_res) @@ -211,7 +219,7 @@ def test_resizing_op_inverse(padding, odl_tspace_impl): res_op = odl.ResizingOperator(space, res_space, pad_mode=pad_mode, pad_const=pad_const) - # Only left inverse if the operator extentds in all axes + # Only left inverse if the operator extends in all axes x = noise_element(space) assert res_op.inverse(res_op(x)) == x @@ -250,9 +258,8 @@ def test_resizing_op_mixed_uni_nonuni(): nonuni_part = odl.nonuniform_partition([0, 1, 4]) uni_part = odl.uniform_partition(-1, 1, 4) part = uni_part.append(nonuni_part, uni_part, nonuni_part) - fspace = odl.FunctionSpace(odl.IntervalProd(part.min_pt, part.max_pt)) tspace = odl.rn(part.shape) - space = odl.DiscreteLp(fspace, part, tspace) + space = odl.DiscretizedSpace(part, tspace) # Keep non-uniform axes fixed res_op = odl.ResizingOperator(space, ran_shp=(6, 3, 6, 3)) @@ -262,9 +269,8 @@ def test_resizing_op_mixed_uni_nonuni(): # Evaluation test with a simpler case part = uni_part.append(nonuni_part) - fspace = odl.FunctionSpace(odl.IntervalProd(part.min_pt, part.max_pt)) tspace = odl.rn(part.shape) - space = odl.DiscreteLp(fspace, part, tspace) + space = odl.DiscretizedSpace(part, tspace) res_op = odl.ResizingOperator(space, ran_shp=(6, 3)) result = res_op(space.one()) true_result = [[0, 0, 0], diff --git a/odl/test/discr/lp_discr_test.py b/odl/test/discr/discr_space_test.py similarity index 89% rename from odl/test/discr/lp_discr_test.py rename to odl/test/discr/discr_space_test.py index 22471d82da1..4b254e5d494 100644 --- a/odl/test/discr/lp_discr_test.py +++ b/odl/test/discr/discr_space_test.py @@ -6,15 +6,15 @@ # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. -"""Unit tests for `DiscreteLp`.""" +"""Unit tests for `DiscretizedSpace`.""" from __future__ import division import numpy as np -import pytest import odl -from odl.discr.lp_discr import DiscreteLp, DiscreteLpElement +import pytest +from odl.discr.discr_space import DiscretizedSpace, DiscretizedSpaceElement from odl.space.base_tensors import TensorSpace from odl.space.npy_tensors import NumpyTensor from odl.space.weighting import ConstWeighting @@ -30,18 +30,16 @@ power = simple_fixture('power', [1.0, 2.0, 0.5, -0.5, -1.0, -2.0]) -# --- DiscreteLp --- # +# --- DiscretizedSpace --- # -def test_discretelp_init(): - """Test initialization and basic properties of DiscreteLp.""" +def test_discretizedspace_init(): + """Test initialization and basic properties of DiscretizedSpace.""" # Real space - fspace = odl.FunctionSpace(odl.IntervalProd([0, 0], [1, 1])) - part = odl.uniform_partition_fromintv(fspace.domain, (2, 4)) + part = odl.uniform_partition([0, 0], [1, 1], (2, 4)) tspace = odl.rn(part.shape) - discr = DiscreteLp(fspace, part, tspace) - assert discr.fspace == fspace + discr = DiscretizedSpace(part, tspace) assert discr.tspace == tspace assert discr.partition == part assert discr.exponent == tspace.exponent @@ -49,29 +47,21 @@ def test_discretelp_init(): assert discr.is_real # Complex space - fspace_c = odl.FunctionSpace(odl.IntervalProd([0, 0], [1, 1]), - out_dtype=complex) tspace_c = odl.cn(part.shape) - discr = DiscreteLp(fspace_c, part, tspace_c) + discr = DiscretizedSpace(part, tspace_c) assert discr.is_complex # Make sure repr shows something - assert repr(discr) + assert repr(discr) != '' # Error scenarios - with pytest.raises(ValueError): - DiscreteLp(fspace, part, tspace_c) # mixes real & complex - - with pytest.raises(ValueError): - DiscreteLp(fspace_c, part, tspace) # mixes complex & real - part_1d = odl.uniform_partition(0, 1, 2) with pytest.raises(ValueError): - DiscreteLp(fspace, part_1d, tspace) # wrong dimensionality + DiscretizedSpace(part_1d, tspace) # wrong dimensionality - part_diffshp = odl.uniform_partition_fromintv(fspace.domain, (3, 4)) + part_diffshp = odl.uniform_partition([0, 0], [1, 1], (3, 4)) with pytest.raises(ValueError): - DiscreteLp(fspace, part_diffshp, tspace) # shape mismatch + DiscretizedSpace(part_diffshp, tspace) # shape mismatch def test_empty(): @@ -140,7 +130,7 @@ def test_uniform_discr_init_real(odl_tspace_impl): # 1D discr = odl.uniform_discr(0, 1, 10, impl=impl) - assert isinstance(discr, DiscreteLp) + assert isinstance(discr, DiscretizedSpace) assert isinstance(discr.tspace, TensorSpace) assert discr.impl == impl assert discr.is_real @@ -180,11 +170,11 @@ def test_uniform_discr_init_complex(odl_tspace_impl): assert discr.dtype == discr.tspace.default_dtype(odl.ComplexNumbers()) -# --- DiscreteLp methods --- # +# --- DiscretizedSpace methods --- # -def test_discretelp_element(): - """Test creation and membership of DiscreteLp elements.""" +def test_discretizedspace_element(): + """Test creation and membership of DiscretizedSpace elements.""" # Creation from scratch # 1D discr = odl.uniform_discr(0, 1, 3) @@ -203,14 +193,14 @@ def test_discretelp_element(): assert elem.tensor in tspace -def test_discretelp_element_from_array(): - """Test creation of DiscreteLp elements from arrays.""" +def test_discretizedspace_element_from_array(): + """Test creation of DiscretizedSpace elements from arrays.""" # 1D discr = odl.uniform_discr(0, 1, 3) elem = discr.element([1, 2, 3]) assert np.array_equal(elem.tensor, [1, 2, 3]) - assert isinstance(elem, DiscreteLpElement) + assert isinstance(elem, DiscretizedSpaceElement) assert isinstance(elem.tensor, NumpyTensor) assert all_equal(elem.tensor, [1, 2, 3]) @@ -222,7 +212,7 @@ def test_element_from_array_2d(odl_elem_order): elem = discr.element([[1, 2], [3, 4]], order=order) - assert isinstance(elem, DiscreteLpElement) + assert isinstance(elem, DiscretizedSpaceElement) assert isinstance(elem.tensor, NumpyTensor) assert all_equal(elem, [[1, 2], [3, 4]]) @@ -244,7 +234,7 @@ def test_element_from_array_2d(odl_elem_order): def test_element_from_function_1d(): - """Test creation of DiscreteLp elements from functions in 1 dimension.""" + """Test creation of DiscretizedSpace elements from functions in 1D.""" space = odl.uniform_discr(-1, 1, 4) points = space.points().squeeze() @@ -284,16 +274,12 @@ def f(x, **kwargs): # Broadcast from constant function elem_lam = space.element(lambda x: 1.0) - true_elem = [1.0 for x in points] + true_elem = [1.0 for _ in points] assert all_equal(elem_lam, true_elem) - # Non vectorized - elem_lam = space.element(lambda x: x[0], vectorized=False) - assert all_equal(elem_lam, points) - def test_element_from_function_2d(): - """Test creation of DiscreteLp elements from functions in 2 dimensions.""" + """Test creation of DiscretizedSpace elements from functions in 2D.""" space = odl.uniform_discr([-1, -1], [1, 1], (2, 3)) points = space.points() @@ -302,8 +288,9 @@ def f(x): return x[0] ** 2 + np.maximum(x[1], 0) elem_f = space.element(f) - true_elem = np.reshape([x[0] ** 2 + max(x[1], 0) for x in points], - space.shape) + true_elem = np.reshape( + [x[0] ** 2 + max(x[1], 0) for x in points], space.shape + ) assert all_equal(elem_f, true_elem) # With parameter @@ -312,47 +299,39 @@ def f(x, **kwargs): return x[0] ** 2 + np.maximum(x[1], c) elem_f_default = space.element(f) - true_elem = np.reshape([x[0] ** 2 + max(x[1], 0) for x in points], - space.shape) + true_elem = np.reshape( + [x[0] ** 2 + max(x[1], 0) for x in points], space.shape + ) assert all_equal(elem_f_default, true_elem) elem_f_2 = space.element(f, c=1) - true_elem = np.reshape([x[0] ** 2 + max(x[1], 1) for x in points], - space.shape) + true_elem = np.reshape( + [x[0] ** 2 + max(x[1], 1) for x in points], space.shape + ) assert all_equal(elem_f_2, true_elem) # Using a lambda elem_lam = space.element(lambda x: x[0] - x[1]) - true_elem = np.reshape([x[0] - x[1] for x in points], - space.shape) + true_elem = np.reshape([x[0] - x[1] for x in points], space.shape) assert all_equal(elem_lam, true_elem) # Using broadcasting elem_lam = space.element(lambda x: x[0]) - true_elem = np.reshape([x[0] for x in points], - space.shape) + true_elem = np.reshape([x[0] for x in points], space.shape) assert all_equal(elem_lam, true_elem) elem_lam = space.element(lambda x: x[1]) - true_elem = np.reshape([x[1] for x in points], - space.shape) + true_elem = np.reshape([x[1] for x in points], space.shape) assert all_equal(elem_lam, true_elem) # Broadcast from constant function elem_lam = space.element(lambda x: 1.0) - true_elem = np.reshape([1.0 for x in points], - space.shape) - assert all_equal(elem_lam, true_elem) - - # Non vectorized - elem_lam = space.element(lambda x: x[0] + x[1], vectorized=False) - true_elem = np.reshape([x[0] + x[1] for x in points], - space.shape) + true_elem = np.reshape([1.0 for _ in points], space.shape) assert all_equal(elem_lam, true_elem) -def test_discretelp_zero_one(): - """Test the zero and one element creators of DiscreteLp.""" +def test_discretizedspace_zero_one(): + """Test the zero and one element creators of DiscretizedSpace.""" discr = odl.uniform_discr(0, 1, 3) zero = discr.zero() @@ -916,7 +895,7 @@ def test_ufunc_corner_cases(odl_tspace_impl): space_no_w = odl.uniform_discr([0, 0], [1, 1], (2, 3), impl=impl, weighting=1.0) - # --- Ufuncs with nin = 1, nout = 1 --- # + # --- UFuncs with nin = 1, nout = 1 --- # with pytest.raises(ValueError): # Too many arguments @@ -947,7 +926,7 @@ def test_ufunc_corner_cases(odl_tspace_impl): res = y.__array_ufunc__(np.sin, '__call__', y) assert res.space.weighting == space_no_w.weighting - # --- Ufuncs with nin = 2, nout = 1 --- # + # --- UFuncs with nin = 2, nout = 1 --- # with pytest.raises(ValueError): # Too few arguments @@ -1006,7 +985,7 @@ def test_ufunc_corner_cases(odl_tspace_impl): assert all_almost_equal(out_ax1, np.add.reduce(x.asarray(), axis=1)) assert res is out_ax1 - # Addition is reorderable, so we can give multiple axes + # Addition is re-orderable, so we can give multiple axes res = x.__array_ufunc__(np.add, 'reduce', x, axis=(0, 1)) assert res == pytest.approx(np.add.reduce(x.asarray(), axis=(0, 1))) @@ -1160,11 +1139,10 @@ def test_power(odl_tspace_impl, power): def test_inner_nonuniform(): """Check if inner products are correct in non-uniform discretizations.""" - fspace = odl.FunctionSpace(odl.IntervalProd(0, 5)) part = odl.nonuniform_partition([0, 2, 3, 5], min_pt=0, max_pt=5) weights = part.cell_sizes_vecs[0] tspace = odl.rn(part.size, weighting=weights) - discr = odl.DiscreteLp(fspace, part, tspace) + discr = odl.DiscretizedSpace(part, tspace) one = discr.one() linear = discr.element(lambda x: x) @@ -1177,11 +1155,10 @@ def test_inner_nonuniform(): def test_norm_nonuniform(): """Check if norms are correct in non-uniform discretizations.""" - fspace = odl.FunctionSpace(odl.IntervalProd(0, 5)) part = odl.nonuniform_partition([0, 2, 3, 5], min_pt=0, max_pt=5) weights = part.cell_sizes_vecs[0] tspace = odl.rn(part.size, weighting=weights) - discr = odl.DiscreteLp(fspace, part, tspace) + discr = odl.DiscretizedSpace(part, tspace) sqrt = discr.element(lambda x: np.sqrt(x)) @@ -1196,17 +1173,14 @@ def test_norm_interval(exponent): # Test the function f(x) = x^2 on the interval (0, 1). Its # L^p-norm is (1 + 2*p)^(-1/p) for finite p and 1 for p=inf p = exponent - fspace = odl.FunctionSpace(odl.IntervalProd(0, 1)) - lpdiscr = odl.uniform_discr_fromspace(fspace, 10, exponent=p) - - testfunc = fspace.element(lambda x: x ** 2) - discr_testfunc = lpdiscr.element(testfunc) + discr = odl.uniform_discr(0, 1, 10, exponent=p) + func = discr.element(lambda x: x ** 2) if p == float('inf'): - assert discr_testfunc.norm() <= 1 # Max at boundary not hit + assert func.norm() <= 1 # Max at boundary not hit else: true_norm = (1 + 2 * p) ** (-1 / p) - assert discr_testfunc.norm() == pytest.approx(true_norm, rel=1e-2) + assert func.norm() == pytest.approx(true_norm, rel=1e-2) def test_norm_rectangle(exponent): @@ -1214,17 +1188,14 @@ def test_norm_rectangle(exponent): # L^p-norm is ((1 + 2*p) * (1 + 3 * p) / 2)^(-1/p) for finite p # and 1 for p=inf p = exponent - fspace = odl.FunctionSpace(odl.IntervalProd([0, -1], [1, 1])) - lpdiscr = odl.uniform_discr_fromspace(fspace, (20, 30), exponent=p) - - testfunc = fspace.element(lambda x: x[0] ** 2 * x[1] ** 3) - discr_testfunc = lpdiscr.element(testfunc) + discr = odl.uniform_discr([0, -1], [1, 1], (20, 30), exponent=p) + func = discr.element(lambda x: x[0] ** 2 * x[1] ** 3) if p == float('inf'): - assert discr_testfunc.norm() <= 1 # Max at boundary not hit + assert func.norm() <= 1 # Max at boundary not hit else: true_norm = ((1 + 2 * p) * (1 + 3 * p) / 2) ** (-1 / p) - assert discr_testfunc.norm() == pytest.approx(true_norm, rel=1e-2) + assert func.norm() == pytest.approx(true_norm, rel=1e-2) def test_norm_rectangle_boundary(odl_tspace_impl, exponent): @@ -1233,62 +1204,74 @@ def test_norm_rectangle_boundary(odl_tspace_impl, exponent): impl = odl_tspace_impl dtype = 'float32' - rect = odl.IntervalProd([-1, -2], [1, 2]) - fspace = odl.FunctionSpace(rect, out_dtype=dtype) # Standard case - discr = odl.uniform_discr_fromspace(fspace, (4, 8), impl=impl, - exponent=exponent) + discr = odl.uniform_discr( + [-1, -2], [1, 2], (4, 8), dtype=dtype, impl=impl, exponent=exponent + ) if exponent == float('inf'): assert discr.one().norm() == 1 else: - assert (discr.one().norm() == - pytest.approx(rect.volume ** (1 / exponent))) + assert ( + discr.one().norm() + == pytest.approx(discr.domain.volume ** (1 / exponent)) + ) # Nodes on the boundary (everywhere) - discr = odl.uniform_discr_fromspace( - fspace, (4, 8), exponent=exponent, impl=impl, nodes_on_bdry=True) - + discr = odl.uniform_discr( + [-1, -2], [1, 2], (4, 8), dtype=dtype, impl=impl, exponent=exponent, + nodes_on_bdry=True + ) if exponent == float('inf'): assert discr.one().norm() == 1 else: - assert (discr.one().norm() == - pytest.approx(rect.volume ** (1 / exponent))) + assert ( + discr.one().norm() + == pytest.approx(discr.domain.volume ** (1 / exponent)) + ) # Nodes on the boundary (selective) - discr = odl.uniform_discr_fromspace( - fspace, (4, 8), exponent=exponent, - impl=impl, nodes_on_bdry=((False, True), False)) - + discr = odl.uniform_discr( + [-1, -2], [1, 2], (4, 8), dtype=dtype, impl=impl, exponent=exponent, + nodes_on_bdry=((False, True), False) + ) if exponent == float('inf'): assert discr.one().norm() == 1 else: - assert (discr.one().norm() == - pytest.approx(rect.volume ** (1 / exponent))) - - discr = odl.uniform_discr_fromspace( - fspace, (4, 8), exponent=exponent, - impl=impl, nodes_on_bdry=(False, (True, False))) - + assert ( + discr.one().norm() + == pytest.approx(discr.domain.volume ** (1 / exponent)) + ) + + discr = odl.uniform_discr( + [-1, -2], [1, 2], (4, 8), dtype=dtype, impl=impl, exponent=exponent, + nodes_on_bdry=(False, (True, False)) + ) if exponent == float('inf'): assert discr.one().norm() == 1 else: - assert (discr.one().norm() == - pytest.approx(rect.volume ** (1 / exponent))) + assert ( + discr.one().norm() + == pytest.approx(discr.domain.volume ** (1 / exponent)) + ) # Completely arbitrary boundary - grid = odl.uniform_grid([0, 0], [1, 1], (4, 4)) - part = odl.RectPartition(rect, grid) + part = odl.RectPartition( + odl.IntervalProd([-1, -2], [1, 2]), + odl.uniform_grid([0, 0], [1, 1], (4, 4)) + ) weight = 1.0 if exponent == float('inf') else part.cell_volume tspace = odl.rn(part.shape, dtype=dtype, impl=impl, exponent=exponent, weighting=weight) - discr = DiscreteLp(fspace, part, tspace) + discr = DiscretizedSpace(part, tspace) if exponent == float('inf'): assert discr.one().norm() == 1 else: - assert (discr.one().norm() == - pytest.approx(rect.volume ** (1 / exponent))) + assert ( + discr.one().norm() + == pytest.approx(discr.domain.volume ** (1 / exponent)) + ) def test_uniform_discr_fromdiscr_one_attr(): diff --git a/odl/test/discr/discr_utils_test.py b/odl/test/discr/discr_utils_test.py index ef30614d3fd..464af4228e9 100644 --- a/odl/test/discr/discr_utils_test.py +++ b/odl/test/discr/discr_utils_test.py @@ -10,15 +10,611 @@ from __future__ import division +from functools import partial + import numpy as np import pytest import odl from odl.discr.discr_utils import ( linear_interpolator, nearest_interpolator, per_axis_interpolator, - point_collocation) + point_collocation, sampling_function) from odl.discr.grid import sparse_meshgrid -from odl.util.testutils import all_almost_equal, all_equal +from odl.util.testutils import all_almost_equal, all_equal, simple_fixture + +# --- Helper functions --- # + + +def _test_eq(x, y): + """Test equality of x and y.""" + assert x == y + assert not x != y + assert hash(x) == hash(y) + + +def _test_neq(x, y): + """Test non-equality of x and y.""" + assert x != y + assert not x == y + assert hash(x) != hash(y) + + +def _points(domain, num): + """Helper to generate ``num`` points in ``domain``.""" + min_pt = domain.min_pt + max_pt = domain.max_pt + ndim = domain.ndim + points = np.random.uniform(low=0, high=1, size=(ndim, num)) + for i in range(ndim): + points[i, :] = min_pt[i] + (max_pt[i] - min_pt[i]) * points[i] + return points + + +def _meshgrid(domain, shape): + """Helper to generate a ``shape`` meshgrid of points in ``domain``.""" + min_pt = domain.min_pt + max_pt = domain.max_pt + ndim = domain.ndim + coord_vecs = [] + for i in range(ndim): + vec = np.random.uniform(low=min_pt[i], high=max_pt[i], size=shape[i]) + vec.sort() + coord_vecs.append(vec) + return sparse_meshgrid(*coord_vecs) + + +class FuncList(list): # So we can set __name__ + pass + + +# --- pytest fixtures (general) --- # + + +out_dtype = simple_fixture( + 'out_dtype', + ['float32', 'float64', 'complex64'], + fmt=' {name} = {value!r} ' +) +domain_ndim = simple_fixture('domain_ndim', [1, 2]) + + +# --- pytest fixtures (scalar test functions) --- # + + +def func_nd_oop(x): + return sum(x) + + +def func_nd_ip(x, out): + out[:] = sum(x) + + +def func_nd_dual(x, out=None): + if out is None: + return sum(x) + else: + out[:] = sum(x) + + +def func_nd_bcast_ref(x): + return x[0] + 0 * sum(x[1:]) + + +def func_nd_bcast_oop(x): + return x[0] + + +def func_nd_bcast_ip(x, out): + out[:] = x[0] + + +def func_nd_bcast_dual(x, out=None): + if out is None: + return x[0] + else: + out[:] = x[0] + + +func_nd_ref = func_nd_oop +func_nd_params = [(func_nd_ref, f) + for f in [func_nd_oop, func_nd_ip, func_nd_dual]] +func_nd_params.extend([(func_nd_bcast_ref, func_nd_bcast_oop), + (func_nd_bcast_ref, func_nd_bcast_ip)]) + +func_nd = simple_fixture('func_nd', func_nd_params, + fmt=' {name} = {value[1].__name__} ') + + +def func_nd_other(x): + return sum(x) + 1 + + +def func_param_nd_oop(x, c): + return sum(x) + c + + +def func_param_nd_ip(x, out, c): + out[:] = sum(x) + c + + +def func_param_switched_nd_ip(x, c, out): + out[:] = sum(x) + c + + +def func_param_bcast_nd_ref(x, c): + return x[0] + c + 0 * sum(x[1:]) + + +def func_param_bcast_nd_oop(x, c): + return x[0] + c + + +def func_param_bcast_nd_ip(x, out, c): + out[:] = x[0] + c + + +func_param_nd_ref = func_param_nd_oop +func_param_nd_params = [(func_param_nd_ref, f) + for f in [func_param_nd_oop, func_param_nd_ip, + func_param_switched_nd_ip]] +func_param_nd_params.extend( + [(func_param_bcast_nd_ref, func_param_bcast_nd_oop), + (func_param_bcast_nd_ref, func_param_bcast_nd_ip)]) +func_param_nd = simple_fixture('func_with_param', func_param_nd_params, + fmt=' {name} = {value[1].__name__} ') + + +def func_1d_ref(x): + return x[0] * 2 + + +def func_1d_oop(x): + return x * 2 + + +def func_1d_ip(x, out): + out[:] = x * 2 + + +func_1d_params = [ + (func_1d_ref, func_1d_oop), + (func_1d_ref, func_1d_ip), + (lambda x: -x[0], np.negative), +] +func_1d = simple_fixture('func_1d', func_1d_params, + fmt=' {name} = {value[1].__name__} ') + + +def func_complex_nd_oop(x): + return sum(x) + 1j + + +# --- pytest fixtures (vector-valued test functions) --- # + + +def func_vec_nd_ref(x): + return np.array([sum(x) + 1, sum(x) - 1]) + + +def func_vec_nd_oop(x): + return (sum(x) + 1, sum(x) - 1) + + +func_nd_oop_seq = FuncList([lambda x: sum(x) + 1, lambda x: sum(x) - 1]) +func_nd_oop_seq.__name__ = 'func_nd_oop_seq' + + +def func_vec_nd_ip(x, out): + out[0] = sum(x) + 1 + out[1] = sum(x) - 1 + + +def comp0_nd(x, out): + out[:] = sum(x) + 1 + + +def comp1_nd(x, out): + out[:] = sum(x) - 1 + + +def func_vec_nd_dual(x, out=None): + if out is None: + return (sum(x) + 1, sum(x) - 1) + else: + out[0] = sum(x) + 1 + out[1] = sum(x) - 1 + + +func_nd_ip_seq = FuncList([comp0_nd, comp1_nd]) +func_nd_ip_seq.__name__ = 'func_nd_ip_seq' + +func_vec_nd_params = [(func_vec_nd_ref, f) + for f in [func_vec_nd_oop, func_nd_oop_seq, + func_vec_nd_ip, func_nd_ip_seq]] +func_vec_nd = simple_fixture('func_vec_nd', func_vec_nd_params, + fmt=' {name} = {value[1].__name__} ') + + +def func_vec_nd_other(x): + return np.array([sum(x) + 2, sum(x) + 3]) + + +def func_vec_1d_ref(x): + return np.array([x[0] * 2, x[0] + 1]) + + +def func_vec_1d_oop(x): + return (x * 2, x + 1) + + +func_1d_oop_seq = FuncList([lambda x: x * 2, lambda x: x + 1]) +func_1d_oop_seq.__name__ = 'func_1d_oop_seq' + + +def func_vec_1d_ip(x, out): + out[0] = x * 2 + out[1] = x + 1 + + +def comp0_1d(x, out): + out[:] = x * 2 + + +def comp1_1d(x, out): + out[:] = x + 1 + + +func_1d_ip_seq = FuncList([comp0_1d, comp1_1d]) +func_1d_ip_seq.__name__ = 'func_1d_ip_seq' + +func_vec_1d_params = [(func_vec_1d_ref, f) + for f in [func_vec_1d_oop, func_1d_oop_seq, + func_vec_1d_ip, func_1d_ip_seq]] +func_vec_1d = simple_fixture('func_vec_1d', func_vec_1d_params, + fmt=' {name} = {value[1].__name__} ') + + +def func_vec_complex_nd_oop(x): + return (sum(x) + 1j, sum(x) - 1j) + + +# --- pytest fixtures (tensor-valued test functions) --- # + + +def func_tens_ref(x): + # Reference function where all shapes in the list are correct + # without broadcasting + shp = np.broadcast(*x).shape + return np.array([[x[0] - x[1], np.zeros(shp), x[1] + 0 * x[0]], + [np.ones(shp), x[0] + 0 * x[1], sum(x)]]) + + +def func_tens_oop(x): + # Output shape 2x3, input 2-dimensional. Broadcasting supported. + return [[x[0] - x[1], 0, x[1]], + [1, x[0], sum(x)]] + + +def func_tens_ip(x, out): + # In-place version + out[0, 0] = x[0] - x[1] + out[0, 1] = 0 + out[0, 2] = x[1] + out[1, 0] = 1 + out[1, 1] = x[0] + out[1, 2] = sum(x) + + +# Array of functions. May contain constants. Should yield the same as func. +func_tens_oop_seq = FuncList([[lambda x: x[0] - x[1], 0, lambda x: x[1]], + [1, lambda x: x[0], lambda x: sum(x)]]) +func_tens_oop_seq.__name__ = 'func_tens_oop_seq' + + +# In-place component functions, cannot use lambdas +def comp00(x, out): + out[:] = x[0] - x[1] + + +def comp01(x, out): + out[:] = 0 + + +def comp02(x, out): + out[:] = x[1] + + +def comp10(x, out): + out[:] = 1 + + +def comp11(x, out): + out[:] = x[0] + + +def comp12(x, out): + out[:] = sum(x) + + +func_tens_ip_seq = FuncList([[comp00, comp01, comp02], + [comp10, comp11, comp12]]) +func_tens_ip_seq.__name__ = 'func_tens_ip_seq' + + +def func_tens_dual(x, out=None): + if out is None: + return [[x[0] - x[1], 0, x[1]], + [1, x[0], sum(x)]] + else: + out[0, 0] = x[0] - x[1] + out[0, 1] = 0 + out[0, 2] = x[1] + out[1, 0] = 1 + out[1, 1] = x[0] + out[1, 2] = sum(x) + + +func_tens_params = [(func_tens_ref, f) + for f in [func_tens_oop, func_tens_oop_seq, + func_tens_ip, func_tens_ip_seq]] +func_tens = simple_fixture('func_tens', func_tens_params, + fmt=' {name} = {value[1].__name__} ') + + +def func_tens_other(x): + return np.array([[x[0] + x[1], sum(x), sum(x)], + [sum(x), 2 * x[0] - x[1], sum(x)]]) + + +def func_tens_complex_oop(x): + return [[x[0], 0, 1j * x[0]], + [1j, x, sum(x) + 1j]] + + +# --- point_collocation tests --- # + + +def test_point_collocation_scalar_valued(domain_ndim, out_dtype, func_nd): + """Check collocation of scalar-valued functions.""" + domain = odl.IntervalProd([0] * domain_ndim, [1] * domain_ndim) + points = _points(domain, 3) + mesh_shape = tuple(range(2, 2 + domain_ndim)) + mesh = _meshgrid(domain, mesh_shape) + point = [0.5] * domain_ndim + + func_ref, func = func_nd + + true_values_points = func_ref(points) + true_values_mesh = func_ref(mesh) + true_value_point = func_ref(point) + + sampl_func = sampling_function(func, domain, out_dtype) + collocator = partial(point_collocation, sampl_func) + + # Out of place + result_points = collocator(points) + result_mesh = collocator(mesh) + assert all_almost_equal(result_points, true_values_points) + assert all_almost_equal(result_mesh, true_values_mesh) + assert result_points.dtype == out_dtype + assert result_mesh.dtype == out_dtype + assert result_points.flags.writeable + assert result_mesh.flags.writeable + + # In place + out_points = np.empty(3, dtype=out_dtype) + out_mesh = np.empty(mesh_shape, dtype=out_dtype) + collocator(points, out=out_points) + collocator(mesh, out=out_mesh) + assert all_almost_equal(out_points, true_values_points) + assert all_almost_equal(out_mesh, true_values_mesh) + + # Single point evaluation + result_point = collocator(point) + assert all_almost_equal(result_point, true_value_point) + + +def test_point_collocation_scalar_valued_with_param(func_param_nd): + """Check collocation of scalar-valued functions with parameters.""" + domain = odl.IntervalProd([0, 0], [1, 1]) + points = _points(domain, 3) + mesh_shape = (2, 3) + mesh = _meshgrid(domain, mesh_shape) + + func_ref, func = func_param_nd + + true_values_points = func_ref(points, c=2.5) + true_values_mesh = func_ref(mesh, c=2.5) + + sampl_func = sampling_function(func, domain, out_dtype='float64') + collocator = partial(point_collocation, sampl_func) + + # Out of place + result_points = collocator(points, c=2.5) + result_mesh = collocator(mesh, c=2.5) + assert all_almost_equal(result_points, true_values_points) + assert all_almost_equal(result_mesh, true_values_mesh) + + # In place + out_points = np.empty(3, dtype='float64') + out_mesh = np.empty(mesh_shape, dtype='float64') + collocator(points, out=out_points, c=2.5) + collocator(mesh, out=out_mesh, c=2.5) + assert all_almost_equal(out_points, true_values_points) + assert all_almost_equal(out_mesh, true_values_mesh) + + # Complex output + true_values_points = func_ref(points, c=2j) + true_values_mesh = func_ref(mesh, c=2j) + + sampl_func = sampling_function(func, domain, out_dtype='complex128') + collocator = partial(point_collocation, sampl_func) + + result_points = collocator(points, c=2j) + result_mesh = collocator(mesh, c=2j) + assert all_almost_equal(result_points, true_values_points) + assert all_almost_equal(result_mesh, true_values_mesh) + + +def test_point_collocation_vector_valued(func_vec_nd): + """Check collocation of vector-valued functions.""" + domain = odl.IntervalProd([0, 0], [1, 1]) + points = _points(domain, 3) + mesh_shape = (2, 3) + mesh = _meshgrid(domain, mesh_shape) + point = [0.5, 0.5] + values_points_shape = (2, 3) + values_mesh_shape = (2, 2, 3) + + func_ref, func = func_vec_nd + + true_values_points = func_ref(points) + true_values_mesh = func_ref(mesh) + true_value_point = func_ref(point) + + sampl_func = sampling_function( + func, domain, out_dtype=('float64', (2,)) + ) + collocator = partial(point_collocation, sampl_func) + + # Out of place + result_points = collocator(points) + result_mesh = collocator(mesh) + assert all_almost_equal(result_points, true_values_points) + assert all_almost_equal(result_mesh, true_values_mesh) + assert result_points.dtype == 'float64' + assert result_mesh.dtype == 'float64' + assert result_points.flags.writeable + assert result_mesh.flags.writeable + + # In place + out_points = np.empty(values_points_shape, dtype='float64') + out_mesh = np.empty(values_mesh_shape, dtype='float64') + collocator(points, out=out_points) + collocator(mesh, out=out_mesh) + assert all_almost_equal(out_points, true_values_points) + assert all_almost_equal(out_mesh, true_values_mesh) + + # Single point evaluation + result_point = collocator(point) + assert all_almost_equal(result_point, true_value_point) + out_point = np.empty((2,), dtype='float64') + collocator(point, out=out_point) + assert all_almost_equal(out_point, true_value_point) + + +def test_point_collocation_tensor_valued(func_tens): + """Check collocation of tensor-valued functions.""" + domain = odl.IntervalProd([0, 0], [1, 1]) + points = _points(domain, 4) + mesh_shape = (4, 5) + mesh = _meshgrid(domain, mesh_shape) + point = [0.5, 0.5] + values_points_shape = (2, 3, 4) + values_mesh_shape = (2, 3, 4, 5) + value_point_shape = (2, 3) + + func_ref, func = func_tens + + true_result_points = np.array(func_ref(points)) + true_result_mesh = np.array(func_ref(mesh)) + true_result_point = np.array(func_ref(np.array(point)[:, None])).squeeze() + + sampl_func = sampling_function( + func, domain, out_dtype=('float64', (2, 3)) + ) + collocator = partial(point_collocation, sampl_func) + + result_points = collocator(points) + result_mesh = collocator(mesh) + result_point = collocator(point) + assert all_almost_equal(result_points, true_result_points) + assert all_almost_equal(result_mesh, true_result_mesh) + assert all_almost_equal(result_point, true_result_point) + assert result_points.flags.writeable + assert result_mesh.flags.writeable + assert result_point.flags.writeable + + out_points = np.empty(values_points_shape, dtype='float64') + out_mesh = np.empty(values_mesh_shape, dtype='float64') + out_point = np.empty(value_point_shape, dtype='float64') + collocator(points, out=out_points) + collocator(mesh, out=out_mesh) + collocator(point, out=out_point) + assert all_almost_equal(out_points, true_result_points) + assert all_almost_equal(out_mesh, true_result_mesh) + assert all_almost_equal(out_point, true_result_point) + + +def test_fspace_elem_eval_unusual_dtypes(): + """Check evaluation with unusual data types (int and string).""" + domain = odl.Strings(3) + strings = np.array(['aa', 'b', 'cab', 'aba']) + out_vec = np.empty((4,), dtype='int64') + + # Can be vectorized for arrays only + sampl_func = sampling_function( + lambda s: np.array([str(si).count('a') for si in s]), + domain, + out_dtype='int64' + ) + collocator = partial(point_collocation, sampl_func) + + true_values = [2, 0, 1, 2] + + assert collocator('abc') == 1 + assert all_equal(collocator(strings), true_values) + collocator(strings, out=out_vec) + assert all_equal(out_vec, true_values) + + +def test_fspace_elem_eval_vec_1d(func_vec_1d): + """Test evaluation in 1d since it's a corner case regarding shapes.""" + domain = odl.IntervalProd(0, 1) + points = _points(domain, 3) + mesh_shape = (4,) + mesh = _meshgrid(domain, mesh_shape) + point1 = 0.5 + point2 = [0.5] + values_points_shape = (2, 3) + values_mesh_shape = (2, 4) + value_point_shape = (2,) + + func_ref, func = func_vec_1d + + true_result_points = np.array(func_ref(points)) + true_result_mesh = np.array(func_ref(mesh)) + true_result_point = np.array(func_ref(np.array([point1]))).squeeze() + + sampl_func = sampling_function( + func, domain, out_dtype=('float64', (2,)) + ) + collocator = partial(point_collocation, sampl_func) + + result_points = collocator(points) + result_mesh = collocator(mesh) + result_point1 = collocator(point1) + result_point2 = collocator(point2) + assert all_almost_equal(result_points, true_result_points) + assert all_almost_equal(result_mesh, true_result_mesh) + assert all_almost_equal(result_point1, true_result_point) + assert all_almost_equal(result_point2, true_result_point) + + out_points = np.empty(values_points_shape, dtype='float64') + out_mesh = np.empty(values_mesh_shape, dtype='float64') + out_point1 = np.empty(value_point_shape, dtype='float64') + out_point2 = np.empty(value_point_shape, dtype='float64') + collocator(points, out=out_points) + collocator(mesh, out=out_mesh) + collocator(point1, out=out_point1) + collocator(point2, out=out_point2) + assert all_almost_equal(out_points, true_result_points) + assert all_almost_equal(out_mesh, true_result_mesh) + assert all_almost_equal(out_point1, true_result_point) + assert all_almost_equal(out_point2, true_result_point) + + +# --- interpolation tests --- # def test_nearest_interpolation_1d_complex(): diff --git a/odl/test/operator/operator_test.py b/odl/test/operator/operator_test.py index 00831211c7d..df7c771a97d 100644 --- a/odl/test/operator/operator_test.py +++ b/odl/test/operator/operator_test.py @@ -1,4 +1,4 @@ -# Copyright 2014-2019 The ODL contributors +# Copyright 2014-2020 The ODL contributors # # This file is part of ODL. # @@ -7,20 +7,27 @@ # obtain one at https://mozilla.org/MPL/2.0/. from __future__ import division -import pytest -import numpy as np + +import inspect import sys +import numpy as np +import pytest + import odl -from odl import (Operator, OperatorSum, OperatorComp, - OperatorLeftScalarMult, OperatorRightScalarMult, - FunctionalLeftVectorMult, OperatorRightVectorMult, - MatrixOperator, OperatorLeftVectorMult, - OpTypeError, OpDomainError, OpRangeError) -from odl.operator.operator import _function_signature, _dispatch_call_args +from odl import ( + FunctionalLeftVectorMult, MatrixOperator, OpDomainError, Operator, + OperatorComp, OperatorLeftScalarMult, OperatorLeftVectorMult, + OperatorRightScalarMult, OperatorRightVectorMult, OperatorSum, + OpRangeError, OpTypeError) +from odl.operator.operator import _dispatch_call_args, _function_signature from odl.util.testutils import ( all_almost_equal, noise_element, noise_elements, simple_fixture) -from odl.util.utility import getargspec + +try: + getargspec = inspect.getfullargspec +except AttributeError: + getargspec = inspect.getargspec # --- Fixtures --- # @@ -31,7 +38,7 @@ dom_eq_ran = simple_fixture('dom_eq_ran', [True, False]) -# --- Auxilliary --- # +# --- Auxiliary --- # class MultiplyAndSquareOp(Operator): diff --git a/odl/test/set/domain_test.py b/odl/test/set/domain_test.py index caa75590776..12a70ba63d5 100644 --- a/odl/test/set/domain_test.py +++ b/odl/test/set/domain_test.py @@ -1,4 +1,4 @@ -# Copyright 2014-2017 The ODL contributors +# Copyright 2014-2020 The ODL contributors # # This file is part of ODL. # @@ -7,8 +7,9 @@ # obtain one at https://mozilla.org/MPL/2.0/. from __future__ import division -import pytest + import numpy as np +import pytest import odl from odl.discr.grid import sparse_meshgrid @@ -239,12 +240,13 @@ def test_contains_set(): IntervalProd(1.2, 1.2)]: assert set_.contains_set(sub_set) - for non_sub_set in [np.array([0, 1, 1.1, 1.2, 1.3, 1.4]), - np.array([np.nan, 1.1, 1.3]), - IntervalProd(1.2, 3), - IntervalProd(0, 1.5), - IntervalProd(3, 4)]: - assert not set_.contains_set(non_sub_set) + with np.errstate(invalid="ignore"): + for non_sub_set in [np.array([0, 1, 1.1, 1.2, 1.3, 1.4]), + np.array([np.nan, 1.1, 1.3]), + IntervalProd(1.2, 3), + IntervalProd(0, 1.5), + IntervalProd(3, 4)]: + assert not set_.contains_set(non_sub_set) for non_set in [1, [1, 2], diff --git a/odl/test/space/fspace_test.py b/odl/test/space/fspace_test.py deleted file mode 100644 index da9dbddf334..00000000000 --- a/odl/test/space/fspace_test.py +++ /dev/null @@ -1,1181 +0,0 @@ -# Copyright 2014-2019 The ODL contributors -# -# This file is part of ODL. -# -# This Source Code Form is subject to the terms of the Mozilla Public License, -# v. 2.0. If a copy of the MPL was not distributed with this file, You can -# obtain one at https://mozilla.org/MPL/2.0/. - -from __future__ import division -import numpy as np -import pytest - -import odl -from odl import FunctionSpace -from odl.discr.grid import sparse_meshgrid -from odl.util.testutils import all_almost_equal, all_equal, simple_fixture - - -# --- Helper functions --- # - - -def _test_eq(x, y): - """Test equality of x and y.""" - assert x == y - assert not x != y - assert hash(x) == hash(y) - - -def _test_neq(x, y): - """Test non-equality of x and y.""" - assert x != y - assert not x == y - assert hash(x) != hash(y) - - -def _points(domain, num): - """Helper to generate ``num`` points in ``domain``.""" - min_pt = domain.min_pt - max_pt = domain.max_pt - ndim = domain.ndim - points = np.random.uniform(low=0, high=1, size=(ndim, num)) - for i in range(ndim): - points[i, :] = min_pt[i] + (max_pt[i] - min_pt[i]) * points[i] - return points - - -def _meshgrid(domain, shape): - """Helper to generate a ``shape`` meshgrid of points in ``domain``.""" - min_pt = domain.min_pt - max_pt = domain.max_pt - ndim = domain.ndim - coord_vecs = [] - for i in range(ndim): - vec = np.random.uniform(low=min_pt[i], high=max_pt[i], size=shape[i]) - vec.sort() - coord_vecs.append(vec) - return sparse_meshgrid(*coord_vecs) - - -class FuncList(list): # So we can set __name__ - pass - - -# --- pytest fixtures (general) --- # - - -out_dtype_params = ['float32', 'float64', 'complex64'] -out_dtype = simple_fixture('out_dtype', out_dtype_params, - fmt=' {name} = {value!r} ') - -out_shape = simple_fixture('out_shape', [(), (2,), (2, 3)]) -domain_ndim = simple_fixture('domain_ndim', [1, 2]) -vectorized = simple_fixture('vectorized', [True, False]) -a = simple_fixture('a', [0.0, 1.0, -2.0]) -b = simple_fixture('b', [0.0, 1.0, -2.0]) -power = simple_fixture('power', [3, 1.0, 0.5, -2.0]) - - -@pytest.fixture(scope='module') -def fspace_scal(domain_ndim, out_dtype): - """Fixture returning a function space with given properties.""" - domain = odl.IntervalProd([0] * domain_ndim, [1] * domain_ndim) - return FunctionSpace(domain, out_dtype=out_dtype) - - -# --- pytest fixtures (scalar test functions) --- # - - -def func_nd_oop(x): - return sum(x) - - -def func_nd_ip(x, out): - out[:] = sum(x) - - -def func_nd_dual(x, out=None): - if out is None: - return sum(x) - else: - out[:] = sum(x) - - -def func_nd_bcast_ref(x): - return x[0] + 0 * sum(x[1:]) - - -def func_nd_bcast_oop(x): - return x[0] - - -def func_nd_bcast_ip(x, out): - out[:] = x[0] - - -def func_nd_bcast_dual(x, out=None): - if out is None: - return x[0] - else: - out[:] = x[0] - - -func_nd_ref = func_nd_oop -func_nd_params = [(func_nd_ref, f) - for f in [func_nd_oop, func_nd_ip, func_nd_dual]] -func_nd_params.extend([(func_nd_bcast_ref, func_nd_bcast_oop), - (func_nd_bcast_ref, func_nd_bcast_ip)]) - -func_nd = simple_fixture('func_nd', func_nd_params, - fmt=' {name} = {value[1].__name__} ') - - -def func_nd_other(x): - return sum(x) + 1 - - -def func_param_nd_oop(x, c): - return sum(x) + c - - -def func_param_nd_ip(x, out, c): - out[:] = sum(x) + c - - -def func_param_switched_nd_ip(x, c, out): - out[:] = sum(x) + c - - -def func_param_bcast_nd_ref(x, c): - return x[0] + c + 0 * sum(x[1:]) - - -def func_param_bcast_nd_oop(x, c): - return x[0] + c - - -def func_param_bcast_nd_ip(x, out, c): - out[:] = x[0] + c - - -func_param_nd_ref = func_param_nd_oop -func_param_nd_params = [(func_param_nd_ref, f) - for f in [func_param_nd_oop, func_param_nd_ip, - func_param_switched_nd_ip]] -func_param_nd_params.extend( - [(func_param_bcast_nd_ref, func_param_bcast_nd_oop), - (func_param_bcast_nd_ref, func_param_bcast_nd_ip)]) -func_param_nd = simple_fixture('func_with_param', func_param_nd_params, - fmt=' {name} = {value[1].__name__} ') - - -def func_1d_ref(x): - return x[0] * 2 - - -def func_1d_oop(x): - return x * 2 - - -def func_1d_ip(x, out): - out[:] = x * 2 - - -func_1d_params = [(func_1d_ref, func_1d_oop), (func_1d_ref, func_1d_ip)] -func_1d_params.append((lambda x: -x[0], np.negative)) -func_1d = simple_fixture('func_1d', func_1d_params, - fmt=' {name} = {value[1].__name__} ') - - -def func_complex_nd_oop(x): - return sum(x) + 1j - - -# --- pytest fixtures (vector-valued test functions) --- # - - -def func_vec_nd_ref(x): - return np.array([sum(x) + 1, sum(x) - 1]) - - -def func_vec_nd_oop(x): - return (sum(x) + 1, sum(x) - 1) - - -func_nd_oop_seq = FuncList([lambda x: sum(x) + 1, lambda x: sum(x) - 1]) -func_nd_oop_seq.__name__ = 'func_nd_oop_seq' - - -def func_vec_nd_ip(x, out): - out[0] = sum(x) + 1 - out[1] = sum(x) - 1 - - -def comp0_nd(x, out): - out[:] = sum(x) + 1 - - -def comp1_nd(x, out): - out[:] = sum(x) - 1 - - -def func_vec_nd_dual(x, out=None): - if out is None: - return (sum(x) + 1, sum(x) - 1) - else: - out[0] = sum(x) + 1 - out[1] = sum(x) - 1 - - -func_nd_ip_seq = FuncList([comp0_nd, comp1_nd]) -func_nd_ip_seq.__name__ = 'func_nd_ip_seq' - -func_vec_nd_params = [(func_vec_nd_ref, f) - for f in [func_vec_nd_oop, func_nd_oop_seq, - func_vec_nd_ip, func_nd_ip_seq]] -func_vec_nd = simple_fixture('func_vec_nd', func_vec_nd_params, - fmt=' {name} = {value[1].__name__} ') - - -def func_vec_nd_other(x): - return np.array([sum(x) + 2, sum(x) + 3]) - - -def func_vec_1d_ref(x): - return np.array([x[0] * 2, x[0] + 1]) - - -def func_vec_1d_oop(x): - return (x * 2, x + 1) - - -func_1d_oop_seq = FuncList([lambda x: x * 2, lambda x: x + 1]) -func_1d_oop_seq.__name__ = 'func_1d_oop_seq' - - -def func_vec_1d_ip(x, out): - out[0] = x * 2 - out[1] = x + 1 - - -def comp0_1d(x, out): - out[:] = x * 2 - - -def comp1_1d(x, out): - out[:] = x + 1 - - -func_1d_ip_seq = FuncList([comp0_1d, comp1_1d]) -func_1d_ip_seq.__name__ = 'func_1d_ip_seq' - -func_vec_1d_params = [(func_vec_1d_ref, f) - for f in [func_vec_1d_oop, func_1d_oop_seq, - func_vec_1d_ip, func_1d_ip_seq]] -func_vec_1d = simple_fixture('func_vec_1d', func_vec_1d_params, - fmt=' {name} = {value[1].__name__} ') - - -def func_vec_complex_nd_oop(x): - return (sum(x) + 1j, sum(x) - 1j) - - -# --- pytest fixtures (tensor-valued test functions) --- # - - -def func_tens_ref(x): - # Reference function where all shapes in the list are correct - # without broadcasting - shp = np.broadcast(*x).shape - return np.array([[x[0] - x[1], np.zeros(shp), x[1] + 0 * x[0]], - [np.ones(shp), x[0] + 0 * x[1], sum(x)]]) - - -def func_tens_oop(x): - # Output shape 2x3, input 2-dimensional. Broadcasting supported. - return [[x[0] - x[1], 0, x[1]], - [1, x[0], sum(x)]] - - -def func_tens_ip(x, out): - # In-place version - out[0, 0] = x[0] - x[1] - out[0, 1] = 0 - out[0, 2] = x[1] - out[1, 0] = 1 - out[1, 1] = x[0] - out[1, 2] = sum(x) - - -# Array of functions. May contain constants. Should yield the same as func. -func_tens_oop_seq = FuncList([[lambda x: x[0] - x[1], 0, lambda x: x[1]], - [1, lambda x: x[0], lambda x: sum(x)]]) -func_tens_oop_seq.__name__ = 'func_tens_oop_seq' - - -# In-place component functions, cannot use lambdas -def comp00(x, out): - out[:] = x[0] - x[1] - - -def comp01(x, out): - out[:] = 0 - - -def comp02(x, out): - out[:] = x[1] - - -def comp10(x, out): - out[:] = 1 - - -def comp11(x, out): - out[:] = x[0] - - -def comp12(x, out): - out[:] = sum(x) - - -func_tens_ip_seq = FuncList([[comp00, comp01, comp02], - [comp10, comp11, comp12]]) -func_tens_ip_seq.__name__ = 'func_tens_ip_seq' - - -def func_tens_dual(x, out=None): - if out is None: - return [[x[0] - x[1], 0, x[1]], - [1, x[0], sum(x)]] - else: - out[0, 0] = x[0] - x[1] - out[0, 1] = 0 - out[0, 2] = x[1] - out[1, 0] = 1 - out[1, 1] = x[0] - out[1, 2] = sum(x) - - -func_tens_params = [(func_tens_ref, f) - for f in [func_tens_oop, func_tens_oop_seq, - func_tens_ip, func_tens_ip_seq]] -func_tens = simple_fixture('func_tens', func_tens_params, - fmt=' {name} = {value[1].__name__} ') - - -def func_tens_other(x): - return np.array([[x[0] + x[1], sum(x), sum(x)], - [sum(x), 2 * x[0] - x[1], sum(x)]]) - - -def func_tens_complex_oop(x): - return [[x[0], 0, 1j * x[0]], - [1j, x, sum(x) + 1j]] - - -# --- FunctionSpace tests --- # - - -def test_fspace_init(): - """Check if all initialization patterns work.""" - intv = odl.IntervalProd(0, 1) - FunctionSpace(intv) - FunctionSpace(intv, out_dtype=float) - FunctionSpace(intv, out_dtype=complex) - FunctionSpace(intv, out_dtype=(float, (2, 3))) - - str3 = odl.Strings(3) - FunctionSpace(str3, out_dtype=int) - - # Make sure repr shows something - assert repr(FunctionSpace(intv, out_dtype=(float, (2, 3)))) - - -def test_fspace_attributes(): - """Check attribute access and correct values.""" - intv = odl.IntervalProd(0, 1) - - # Scalar-valued function spaces - fspace = FunctionSpace(intv) - fspace_r = FunctionSpace(intv, out_dtype=float) - fspace_c = FunctionSpace(intv, out_dtype=complex) - fspace_s = FunctionSpace(intv, out_dtype='U1') - scalar_spaces = (fspace, fspace_r, fspace_c, fspace_s) - - assert fspace.domain == intv - assert fspace.field == odl.RealNumbers() - assert fspace_r.field == odl.RealNumbers() - assert fspace_c.field == odl.ComplexNumbers() - assert fspace_s.field is None - - assert fspace.out_dtype == float - assert fspace_r.out_dtype == float - assert fspace_r.real_out_dtype == float - assert fspace_r.complex_out_dtype == complex - assert fspace_c.out_dtype == complex - assert fspace_c.real_out_dtype == float - assert fspace_c.complex_out_dtype == complex - assert fspace_s.out_dtype == np.dtype('U1') - assert fspace.is_real - assert not fspace.is_complex - assert fspace_r.is_real - assert not fspace_r.is_complex - assert fspace_c.is_complex - assert not fspace_c.is_real - with pytest.raises(AttributeError): - fspace_s.real_out_dtype - with pytest.raises(AttributeError): - fspace_s.complex_out_dtype - - assert all(spc.scalar_out_dtype == spc.out_dtype for spc in scalar_spaces) - assert all(spc.out_shape == () for spc in scalar_spaces) - assert all(not spc.tensor_valued for spc in scalar_spaces) - - # Vector-valued function space - fspace_vec = FunctionSpace(intv, out_dtype=(float, (2,))) - assert fspace_vec.field == odl.RealNumbers() - assert fspace_vec.out_dtype == np.dtype((float, (2,))) - assert fspace_vec.scalar_out_dtype == float - assert fspace_vec.out_shape == (2,) - assert fspace_vec.tensor_valued - - -def test_equals(): - """Test equality check and hash.""" - intv = odl.IntervalProd(0, 1) - intv2 = odl.IntervalProd(-1, 1) - fspace = FunctionSpace(intv) - fspace_r = FunctionSpace(intv, out_dtype=float) - fspace_c = FunctionSpace(intv, out_dtype=complex) - fspace_intv2 = FunctionSpace(intv2) - fspace_vec = FunctionSpace(intv, out_dtype=(float, (2,))) - - _test_eq(fspace, fspace) - _test_eq(fspace, fspace_r) - _test_eq(fspace_c, fspace_c) - - _test_neq(fspace, fspace_c) - _test_neq(fspace, fspace_intv2) - _test_neq(fspace_r, fspace_vec) - - -def test_fspace_astype(): - """Check that converting function spaces to new out_dtype works.""" - rspace = FunctionSpace(odl.IntervalProd(0, 1)) - cspace = FunctionSpace(odl.IntervalProd(0, 1), out_dtype=complex) - rspace_s = FunctionSpace(odl.IntervalProd(0, 1), out_dtype='float32') - cspace_s = FunctionSpace(odl.IntervalProd(0, 1), out_dtype='complex64') - - assert rspace.astype('complex64') == cspace_s - assert rspace.astype('complex128') == cspace - assert rspace.astype('complex128') is rspace.complex_space - assert rspace.astype('float32') == rspace_s - assert rspace.astype('float64') is rspace.real_space - - assert cspace.astype('float32') == rspace_s - assert cspace.astype('float64') == rspace - assert cspace.astype('float64') is cspace.real_space - assert cspace.astype('complex64') == cspace_s - assert cspace.astype('complex128') is cspace.complex_space - - -# --- FunctionSpaceElement tests --- # - - -def test_fspace_elem_vectorized_init(vectorized): - """Check init of fspace elements with(out) vectorization.""" - intv = odl.IntervalProd(0, 1) - - fspace_scal = FunctionSpace(intv) - fspace_scal.element(func_nd_oop, vectorized=vectorized) - - fspace_vec = FunctionSpace(intv, out_dtype=(float, (2,))) - fspace_vec.element(func_vec_nd_oop, vectorized=vectorized) - fspace_vec.element(func_nd_oop_seq, vectorized=vectorized) - - -def test_fspace_scal_elem_eval(fspace_scal, func_nd): - """Check evaluation of scalar-valued function elements.""" - points = _points(fspace_scal.domain, 3) - mesh_shape = tuple(range(2, 2 + fspace_scal.domain.ndim)) - mesh = _meshgrid(fspace_scal.domain, mesh_shape) - point = [0.5] * fspace_scal.domain.ndim - - func_ref, func = func_nd - - true_values_points = func_ref(points) - true_values_mesh = func_ref(mesh) - true_value_point = func_ref(point) - - func_elem = fspace_scal.element(func) - - # Out of place - result_points = func_elem(points) - result_mesh = func_elem(mesh) - assert all_almost_equal(result_points, true_values_points) - assert all_almost_equal(result_mesh, true_values_mesh) - assert result_points.dtype == fspace_scal.scalar_out_dtype - assert result_mesh.dtype == fspace_scal.scalar_out_dtype - assert result_points.flags.writeable - assert result_mesh.flags.writeable - - # In place - out_points = np.empty(3, dtype=fspace_scal.scalar_out_dtype) - out_mesh = np.empty(mesh_shape, dtype=fspace_scal.scalar_out_dtype) - func_elem(points, out=out_points) - func_elem(mesh, out=out_mesh) - assert all_almost_equal(out_points, true_values_points) - assert all_almost_equal(out_mesh, true_values_mesh) - - # Single point evaluation - result_point = func_elem(point) - assert all_almost_equal(result_point, true_value_point) - - -def test_fspace_scal_elem_with_param_eval(func_param_nd): - """Check evaluation of scalar-valued function elements with parameters.""" - intv = odl.IntervalProd([0, 0], [1, 1]) - fspace_scal = FunctionSpace(intv) - points = _points(fspace_scal.domain, 3) - mesh_shape = (2, 3) - mesh = _meshgrid(fspace_scal.domain, mesh_shape) - - func_ref, func = func_param_nd - - true_values_points = func_ref(points, c=2.5) - true_values_mesh = func_ref(mesh, c=2.5) - - func_elem = fspace_scal.element(func) - - # Out of place - result_points = func_elem(points, c=2.5) - result_mesh = func_elem(mesh, c=2.5) - assert all_almost_equal(result_points, true_values_points) - assert all_almost_equal(result_mesh, true_values_mesh) - - # In place - out_points = np.empty(3, dtype=fspace_scal.scalar_out_dtype) - out_mesh = np.empty(mesh_shape, dtype=fspace_scal.scalar_out_dtype) - func_elem(points, out=out_points, c=2.5) - func_elem(mesh, out=out_mesh, c=2.5) - assert all_almost_equal(out_points, true_values_points) - assert all_almost_equal(out_mesh, true_values_mesh) - - # Complex output - fspace_complex = FunctionSpace(intv, out_dtype=complex) - true_values_points = func_ref(points, c=2j) - true_values_mesh = func_ref(mesh, c=2j) - - func_elem = fspace_complex.element(func) - - result_points = func_elem(points, c=2j) - result_mesh = func_elem(mesh, c=2j) - assert all_almost_equal(result_points, true_values_points) - assert all_almost_equal(result_mesh, true_values_mesh) - - -def test_fspace_vec_elem_eval(func_vec_nd, out_dtype): - """Check evaluation of scalar-valued function elements.""" - intv = odl.IntervalProd([0, 0], [1, 1]) - fspace_vec = FunctionSpace(intv, out_dtype=(float, (2,))) - points = _points(fspace_vec.domain, 3) - mesh_shape = (2, 3) - mesh = _meshgrid(fspace_vec.domain, mesh_shape) - point = [0.5, 0.5] - values_points_shape = (2, 3) - values_mesh_shape = (2, 2, 3) - - func_ref, func = func_vec_nd - - true_values_points = func_ref(points) - true_values_mesh = func_ref(mesh) - true_value_point = func_ref(point) - - func_elem = fspace_vec.element(func) - - # Out of place - result_points = func_elem(points) - result_mesh = func_elem(mesh) - assert all_almost_equal(result_points, true_values_points) - assert all_almost_equal(result_mesh, true_values_mesh) - assert result_points.dtype == fspace_vec.scalar_out_dtype - assert result_mesh.dtype == fspace_vec.scalar_out_dtype - assert result_points.flags.writeable - assert result_mesh.flags.writeable - - # In place - out_points = np.empty(values_points_shape, - dtype=fspace_vec.scalar_out_dtype) - out_mesh = np.empty(values_mesh_shape, - dtype=fspace_vec.scalar_out_dtype) - func_elem(points, out=out_points) - func_elem(mesh, out=out_mesh) - assert all_almost_equal(out_points, true_values_points) - assert all_almost_equal(out_mesh, true_values_mesh) - - # Single point evaluation - result_point = func_elem(point) - assert all_almost_equal(result_point, true_value_point) - out_point = np.empty((2,), dtype=fspace_vec.scalar_out_dtype) - func_elem(point, out=out_point) - assert all_almost_equal(out_point, true_value_point) - - -def test_fspace_tens_eval(func_tens): - """Test tensor-valued function evaluation.""" - intv = odl.IntervalProd([0, 0], [1, 1]) - fspace_tens = FunctionSpace(intv, out_dtype=(float, (2, 3))) - points = _points(fspace_tens.domain, 4) - mesh_shape = (4, 5) - mesh = _meshgrid(fspace_tens.domain, mesh_shape) - point = [0.5, 0.5] - values_points_shape = (2, 3, 4) - values_mesh_shape = (2, 3, 4, 5) - value_point_shape = (2, 3) - - func_ref, func = func_tens - - true_result_points = np.array(func_ref(points)) - true_result_mesh = np.array(func_ref(mesh)) - true_result_point = np.array(func_ref(np.array(point)[:, None])).squeeze() - - func_elem = fspace_tens.element(func) - - result_points = func_elem(points) - result_mesh = func_elem(mesh) - result_point = func_elem(point) - assert all_almost_equal(result_points, true_result_points) - assert all_almost_equal(result_mesh, true_result_mesh) - assert all_almost_equal(result_point, true_result_point) - assert result_points.flags.writeable - assert result_mesh.flags.writeable - assert result_point.flags.writeable - - out_points = np.empty(values_points_shape, dtype=float) - out_mesh = np.empty(values_mesh_shape, dtype=float) - out_point = np.empty(value_point_shape, dtype=float) - func_elem(points, out=out_points) - func_elem(mesh, out=out_mesh) - func_elem(point, out=out_point) - assert all_almost_equal(out_points, true_result_points) - assert all_almost_equal(out_mesh, true_result_mesh) - assert all_almost_equal(out_point, true_result_point) - - -def test_fspace_elem_eval_unusual_dtypes(): - """Check evaluation with unusual data types (int and string).""" - str3 = odl.Strings(3) - fspace = FunctionSpace(str3, out_dtype=int) - strings = np.array(['aa', 'b', 'cab', 'aba']) - out_vec = np.empty((4,), dtype=int) - - # Vectorized for arrays only - func_elem = fspace.element( - lambda s: np.array([str(si).count('a') for si in s])) - true_values = [2, 0, 1, 2] - - assert func_elem('abc') == 1 - assert all_equal(func_elem(strings), true_values) - func_elem(strings, out=out_vec) - assert all_equal(out_vec, true_values) - - -def test_fspace_elem_eval_vec_1d(func_vec_1d): - """Test evaluation in 1d since it's a corner case regarding shapes.""" - intv = odl.IntervalProd(0, 1) - fspace_vec = FunctionSpace(intv, out_dtype=(float, (2,))) - points = _points(fspace_vec.domain, 3) - mesh_shape = (4,) - mesh = _meshgrid(fspace_vec.domain, mesh_shape) - point1 = 0.5 - point2 = [0.5] - values_points_shape = (2, 3) - values_mesh_shape = (2, 4) - value_point_shape = (2,) - - func_ref, func = func_vec_1d - - true_result_points = np.array(func_ref(points)) - true_result_mesh = np.array(func_ref(mesh)) - true_result_point = np.array(func_ref(np.array([point1]))).squeeze() - - func_elem = fspace_vec.element(func) - - result_points = func_elem(points) - result_mesh = func_elem(mesh) - result_point1 = func_elem(point1) - result_point2 = func_elem(point2) - assert all_almost_equal(result_points, true_result_points) - assert all_almost_equal(result_mesh, true_result_mesh) - assert all_almost_equal(result_point1, true_result_point) - assert all_almost_equal(result_point2, true_result_point) - - out_points = np.empty(values_points_shape, dtype=float) - out_mesh = np.empty(values_mesh_shape, dtype=float) - out_point1 = np.empty(value_point_shape, dtype=float) - out_point2 = np.empty(value_point_shape, dtype=float) - func_elem(points, out=out_points) - func_elem(mesh, out=out_mesh) - func_elem(point1, out=out_point1) - func_elem(point2, out=out_point2) - assert all_almost_equal(out_points, true_result_points) - assert all_almost_equal(out_mesh, true_result_mesh) - assert all_almost_equal(out_point1, true_result_point) - assert all_almost_equal(out_point2, true_result_point) - - -def test_fspace_elem_equality(): - """Test equality check of fspace elements.""" - intv = odl.IntervalProd(0, 1) - fspace = FunctionSpace(intv) - - f_novec = fspace.element(func_nd_oop, vectorized=False) - - f_vec_oop = fspace.element(func_nd_oop, vectorized=True) - f_vec_oop_2 = fspace.element(func_nd_oop, vectorized=True) - - f_vec_ip = fspace.element(func_nd_ip, vectorized=True) - f_vec_ip_2 = fspace.element(func_nd_ip, vectorized=True) - - f_vec_dual = fspace.element(func_nd_dual, vectorized=True) - f_vec_dual_2 = fspace.element(func_nd_dual, vectorized=True) - - assert f_novec == f_novec - assert f_novec != f_vec_oop - assert f_novec != f_vec_ip - assert f_novec != f_vec_dual - - assert f_vec_oop == f_vec_oop - assert f_vec_oop == f_vec_oop_2 - assert f_vec_oop != f_vec_ip - assert f_vec_oop != f_vec_dual - - assert f_vec_ip == f_vec_ip - assert f_vec_ip == f_vec_ip_2 - assert f_vec_ip != f_vec_dual - - assert f_vec_dual == f_vec_dual - assert f_vec_dual == f_vec_dual_2 - - fspace_tens = FunctionSpace(intv, out_dtype=(float, (2, 3))) - - f_tens_oop = fspace_tens.element(func_tens_oop) - f_tens_oop2 = fspace_tens.element(func_tens_oop) - - f_tens_ip = fspace_tens.element(func_tens_ip) - f_tens_ip2 = fspace_tens.element(func_tens_ip) - - f_tens_seq = fspace_tens.element(func_tens_oop_seq) - f_tens_seq2 = fspace_tens.element(func_tens_oop_seq) - - assert f_tens_oop == f_tens_oop - assert f_tens_oop == f_tens_oop2 - assert f_tens_oop != f_tens_ip - assert f_tens_oop != f_tens_seq - - assert f_tens_ip == f_tens_ip - assert f_tens_ip == f_tens_ip2 - assert f_tens_ip != f_tens_seq - - # Sequences are wrapped, will compare to not equal - assert f_tens_seq == f_tens_seq - assert f_tens_seq != f_tens_seq2 - - -def test_fspace_elem_assign(out_shape): - """Check assignment of fspace elements.""" - fspace = FunctionSpace(odl.IntervalProd(0, 1), - out_dtype=(float, out_shape)) - - ndim = len(out_shape) - if ndim == 0: - f_oop = fspace.element(func_nd_oop) - f_ip = fspace.element(func_nd_ip) - f_dual = fspace.element(func_nd_dual) - elif ndim == 1: - f_oop = fspace.element(func_vec_nd_oop) - f_ip = fspace.element(func_vec_nd_ip) - f_dual = fspace.element(func_vec_nd_dual) - elif ndim == 2: - f_oop = fspace.element(func_tens_oop) - f_ip = fspace.element(func_tens_ip) - f_dual = fspace.element(func_tens_dual) - else: - assert False - - f_out = fspace.element() - f_out.assign(f_oop) - assert f_out == f_oop - - f_out = fspace.element() - f_out.assign(f_ip) - assert f_out == f_ip - - f_out = fspace.element() - f_out.assign(f_dual) - assert f_out == f_dual - - -def test_fspace_elem_copy(out_shape): - """Check copying of fspace elements.""" - fspace = FunctionSpace(odl.IntervalProd(0, 1), - out_dtype=(float, out_shape)) - - ndim = len(out_shape) - if ndim == 0: - f_oop = fspace.element(func_nd_oop) - f_ip = fspace.element(func_nd_ip) - f_dual = fspace.element(func_nd_dual) - elif ndim == 1: - f_oop = fspace.element(func_vec_nd_oop) - f_ip = fspace.element(func_vec_nd_ip) - f_dual = fspace.element(func_vec_nd_dual) - elif ndim == 2: - f_oop = fspace.element(func_tens_oop) - f_ip = fspace.element(func_tens_ip) - f_dual = fspace.element(func_tens_dual) - else: - assert False - - f_out = f_oop.copy() - assert f_out == f_oop - - f_out = f_ip.copy() - assert f_out == f_ip - - f_out = f_dual.copy() - assert f_out == f_dual - - -def test_fspace_elem_real_imag_conj(out_shape): - """Check taking real/imaginary parts of fspace elements.""" - fspace = FunctionSpace(odl.IntervalProd(0, 1), - out_dtype=(complex, out_shape)) - - ndim = len(out_shape) - if ndim == 0: - f_elem = fspace.element(func_complex_nd_oop) - elif ndim == 1: - f_elem = fspace.element(func_vec_complex_nd_oop) - elif ndim == 2: - f_elem = fspace.element(func_tens_complex_oop) - else: - assert False - - points = _points(fspace.domain, 4) - mesh_shape = (5,) - mesh = _meshgrid(fspace.domain, mesh_shape) - point = 0.5 - values_points_shape = out_shape + (4,) - values_mesh_shape = out_shape + mesh_shape - - result_points = f_elem(points) - result_point = f_elem(point) - result_mesh = f_elem(mesh) - - assert all_almost_equal(f_elem.real(points), result_points.real) - assert all_almost_equal(f_elem.real(point), result_point.real) - assert all_almost_equal(f_elem.real(mesh), result_mesh.real) - assert all_almost_equal(f_elem.imag(points), result_points.imag) - assert all_almost_equal(f_elem.imag(point), result_point.imag) - assert all_almost_equal(f_elem.imag(mesh), result_mesh.imag) - assert all_almost_equal(f_elem.conj()(points), result_points.conj()) - assert all_almost_equal(f_elem.conj()(point), np.conj(result_point)) - assert all_almost_equal(f_elem.conj()(mesh), result_mesh.conj()) - - out_points = np.empty(values_points_shape, dtype=float) - out_mesh = np.empty(values_mesh_shape, dtype=float) - - f_elem.real(points, out=out_points) - f_elem.real(mesh, out=out_mesh) - - assert all_almost_equal(out_points, result_points.real) - assert all_almost_equal(out_mesh, result_mesh.real) - - f_elem.imag(points, out=out_points) - f_elem.imag(mesh, out=out_mesh) - - assert all_almost_equal(out_points, result_points.imag) - assert all_almost_equal(out_mesh, result_mesh.imag) - - out_points = np.empty(values_points_shape, dtype=complex) - out_mesh = np.empty(values_mesh_shape, dtype=complex) - - f_elem.conj()(points, out=out_points) - f_elem.conj()(mesh, out=out_mesh) - - assert all_almost_equal(out_points, result_points.conj()) - assert all_almost_equal(out_mesh, result_mesh.conj()) - - -def test_fspace_zero(out_shape): - """Check zero element.""" - - fspace = FunctionSpace(odl.IntervalProd(0, 1), - out_dtype=(float, out_shape)) - - points = _points(fspace.domain, 4) - mesh_shape = (5,) - mesh = _meshgrid(fspace.domain, mesh_shape) - point = 0.5 - values_points_shape = out_shape + (4,) - values_point_shape = out_shape - values_mesh_shape = out_shape + mesh_shape - - f_zero = fspace.zero() - - assert all_equal(f_zero(points), np.zeros(values_points_shape)) - if not out_shape: - assert f_zero(point) == 0.0 - else: - assert all_equal(f_zero(point), np.zeros(values_point_shape)) - assert all_equal(f_zero(mesh), np.zeros(values_mesh_shape)) - - out_points = np.empty(values_points_shape) - out_mesh = np.empty(values_mesh_shape) - - f_zero(points, out=out_points) - f_zero(mesh, out=out_mesh) - - assert all_equal(out_points, np.zeros(values_points_shape)) - assert all_equal(out_mesh, np.zeros(values_mesh_shape)) - - -def test_fspace_one(out_shape): - """Check one element.""" - - fspace = FunctionSpace(odl.IntervalProd(0, 1), - out_dtype=(float, out_shape)) - - points = _points(fspace.domain, 4) - mesh_shape = (5,) - mesh = _meshgrid(fspace.domain, mesh_shape) - point = 0.5 - values_points_shape = out_shape + (4,) - values_point_shape = out_shape - values_mesh_shape = out_shape + mesh_shape - - f_one = fspace.one() - - assert all_equal(f_one(points), np.ones(values_points_shape)) - if not out_shape: - assert f_one(point) == 1.0 - else: - assert all_equal(f_one(point), np.ones(values_point_shape)) - assert all_equal(f_one(mesh), np.ones(values_mesh_shape)) - - out_points = np.empty(values_points_shape) - out_mesh = np.empty(values_mesh_shape) - - f_one(points, out=out_points) - f_one(mesh, out=out_mesh) - - assert all_equal(out_points, np.ones(values_points_shape)) - assert all_equal(out_mesh, np.ones(values_mesh_shape)) - - -def test_fspace_lincomb_scalar(a, b): - """Check linear combination in function spaces. - - Note: Special cases and more alignment options are tested later in the - special methods like ``__add__``. - """ - intv = odl.IntervalProd([0, 0], [1, 1]) - fspace = FunctionSpace(intv) - points = _points(fspace.domain, 4) - true_result = a * func_nd_oop(points) + b * func_nd_bcast_ref(points) - - # Non-vectorized evaluation, checking with `out` array and without. - f_elem1_novec = fspace.element(func_nd_oop, vectorized=False) - f_elem2_novec = fspace.element(func_nd_bcast_oop, vectorized=False) - out_novec = fspace.element(vectorized=False) - fspace.lincomb(a, f_elem1_novec, b, f_elem2_novec, out_novec) - - assert all_equal(out_novec(points), true_result) - out_arr = np.empty(4) - out_novec(points, out=out_arr) - assert all_equal(out_arr, true_result) - - # Vectorized evaluation with definition from out-of-place (oop), - # in-place (ip) and dual-use (dual) versions of Python functions. - # Checking evaluation with `out` array and without. - - # out-of-place - f_elem1_oop = fspace.element(func_nd_oop) - f_elem2_oop = fspace.element(func_nd_bcast_oop) - out_oop = fspace.element() - fspace.lincomb(a, f_elem1_oop, b, f_elem2_oop, out_oop) - - assert all_equal(out_oop(points), true_result) - out_arr = np.empty(4) - out_oop(points, out=out_arr) - assert all_equal(out_arr, true_result) - - # in-place - f_elem1_ip = fspace.element(func_nd_ip) - f_elem2_ip = fspace.element(func_nd_bcast_ip) - out_ip = fspace.element() - fspace.lincomb(a, f_elem1_ip, b, f_elem2_ip, out_ip) - - assert all_equal(out_ip(points), true_result) - out_arr = np.empty(4) - out_ip(points, out=out_arr) - assert all_equal(out_arr, true_result) - - # dual - f_elem1_dual = fspace.element(func_nd_dual) - f_elem2_dual = fspace.element(func_nd_bcast_dual) - out_dual = fspace.element() - fspace.lincomb(a, f_elem1_dual, b, f_elem2_dual, out_dual) - - assert all_equal(out_dual(points), true_result) - out_arr = np.empty(4) - out_dual(points, out=out_arr) - assert all_equal(out_arr, true_result) - - # Check mixing vectorized and non-vectorized functions - out = fspace.element() - fspace.lincomb(a, f_elem1_oop, b, f_elem2_novec, out) - assert all_equal(out(points), true_result) - out_arr = np.empty(4) - out(points, out=out_arr) - assert all_equal(out_arr, true_result) - - # Alignment options - # out = a * out + b * f2, out = f1.copy() -> same as before - out = f_elem1_oop.copy() - fspace.lincomb(a, out, b, f_elem2_oop, out) - true_result_aligned = true_result - assert all_equal(out(points), true_result_aligned) - - # out = a * f1 + b * out, out = f2.copy() -> same as before - out = f_elem2_oop.copy() - fspace.lincomb(a, f_elem1_oop, b, out, out) - true_result_aligned = true_result - assert all_equal(out(points), true_result_aligned) - - # out = a * out + b * out - out = f_elem1_oop.copy() - fspace.lincomb(a, out, b, out, out) - true_result_aligned = (a + b) * f_elem1_oop(points) - assert all_equal(out(points), true_result_aligned) - - # out = a * f1 + b * f1 - out = fspace.element() - fspace.lincomb(a, f_elem1_oop, b, f_elem1_oop, out) - true_result_aligned = (a + b) * f_elem1_oop(points) - assert all_equal(out(points), true_result_aligned) - - -def test_fspace_lincomb_vec_tens(a, b, out_shape): - """Check linear combination in function spaces.""" - if out_shape == (): - return - - intv = odl.IntervalProd([0, 0], [1, 1]) - fspace = FunctionSpace(intv, out_dtype=(float, out_shape)) - points = _points(fspace.domain, 4) - - ndim = len(out_shape) - if ndim == 1: - f_elem1 = fspace.element(func_vec_nd_oop) - f_elem2 = fspace.element(func_vec_nd_other) - true_result = (a * func_vec_nd_ref(points) + - b * func_vec_nd_other(points)) - elif ndim == 2: - f_elem1 = fspace.element(func_tens_oop) - f_elem2 = fspace.element(func_tens_other) - true_result = a * func_tens_ref(points) + b * func_tens_other(points) - else: - assert False - - out_func = fspace.element() - fspace.lincomb(a, f_elem1, b, f_elem2, out_func) - assert all_equal(out_func(points), true_result) - out_arr = np.empty(out_shape + (4,)) - out_func(points, out=out_arr) - assert all_equal(out_arr, true_result) - - -# NOTE: multiply and divide are tested via special methods - - -def test_fspace_elem_power(power, out_shape): - """Check taking powers of fspace elements.""" - # Make sure test functions don't take negative values - intv = odl.IntervalProd([1, 0], [2, 1]) - fspace = FunctionSpace(intv, out_dtype=(float, out_shape)) - points = _points(fspace.domain, 4) - - ndim = len(out_shape) - with np.errstate(all='ignore'): - if ndim == 0: - f_elem = fspace.element(func_nd_oop) - true_result = func_nd_ref(points) ** power - elif ndim == 1: - f_elem = fspace.element(func_vec_nd_oop) - true_result = func_vec_nd_ref(points) ** power - elif ndim == 2: - f_elem = fspace.element(func_tens_oop) - true_result = func_tens_ref(points) ** power - else: - assert False - - # Out-of-place power - f_elem_pow = f_elem ** power - assert all_almost_equal(f_elem_pow(points), true_result) - out_arr = np.empty(out_shape + (4,)) - f_elem_pow(points, out_arr) - assert all_almost_equal(out_arr, true_result) - - # In-place power - f_elem_pow = f_elem.copy() - f_elem_pow **= power - assert all_almost_equal(f_elem_pow(points), true_result) - out_arr = np.empty(out_shape + (4,)) - f_elem_pow(points, out_arr) - assert all_almost_equal(out_arr, true_result) - - -def test_fspace_elem_arithmetic(odl_arithmetic_op, out_shape): - """Test arithmetic of fspace elements.""" - op = odl_arithmetic_op - - intv = odl.IntervalProd([1, 0], [2, 1]) - fspace = FunctionSpace(intv, out_dtype=(float, out_shape)) - points = _points(fspace.domain, 4) - - ndim = len(out_shape) - if ndim == 0: - f_elem1 = fspace.element(func_nd_oop) - f_elem2 = fspace.element(func_nd_other) - elif ndim == 1: - f_elem1 = fspace.element(func_vec_nd_oop) - f_elem2 = fspace.element(func_vec_nd_other) - elif ndim == 2: - f_elem1 = fspace.element(func_tens_oop) - f_elem2 = fspace.element(func_tens_other) - else: - assert False - - result1 = f_elem1(points) - result1_cpy = result1.copy() - result2 = f_elem2(points) - true_result_func = op(result1, result2) - true_result_scal = op(result1_cpy, -2.0) - - f_elem1_cpy = f_elem1.copy() - func_arith_func = op(f_elem1, f_elem2) - func_arith_scal = op(f_elem1_cpy, -2.0) - assert all_almost_equal(func_arith_func(points), true_result_func) - assert all_almost_equal(func_arith_scal(points), true_result_scal) - out_arr_func = np.empty(out_shape + (4,)) - out_arr_scal = np.empty(out_shape + (4,)) - func_arith_func(points, out=out_arr_func) - func_arith_scal(points, out=out_arr_scal) - assert all_almost_equal(out_arr_func, true_result_func) - assert all_almost_equal(out_arr_scal, true_result_scal) - - -if __name__ == '__main__': - odl.util.test_file(__file__) diff --git a/odl/test/tomo/backends/astra_setup_test.py b/odl/test/tomo/backends/astra_setup_test.py index e6dac546d0f..d3502cf5bc2 100644 --- a/odl/test/tomo/backends/astra_setup_test.py +++ b/odl/test/tomo/backends/astra_setup_test.py @@ -28,7 +28,7 @@ def _discrete_domain(ndim): - """Create `DiscreteLp` space with isotropic grid stride. + """Create `DiscretizedSpace` space with isotropic grid stride. Parameters ---------- @@ -37,8 +37,8 @@ def _discrete_domain(ndim): Returns ------- - space : `DiscreteLp` - Returns a `DiscreteLp` instance + space : `DiscretizedSpace` + Returns a `DiscretizedSpace` instance """ max_pt = np.arange(1, ndim + 1) min_pt = -max_pt @@ -48,7 +48,7 @@ def _discrete_domain(ndim): def _discrete_domain_anisotropic(ndim): - """Create `DiscreteLp` space with anisotropic grid stride. + """Create `DiscretizedSpace` space with anisotropic grid stride. Parameters ---------- @@ -57,8 +57,8 @@ def _discrete_domain_anisotropic(ndim): Returns ------- - space : `DiscreteLp` - Returns a `DiscreteLp` instance + space : `DiscretizedSpace` + Returns a `DiscretizedSpace` instance """ min_pt = [-1] * ndim max_pt = [1] * ndim diff --git a/odl/test/trafos/fourier_test.py b/odl/test/trafos/fourier_test.py index 285e1609c59..bdf2b39ff4d 100644 --- a/odl/test/trafos/fourier_test.py +++ b/odl/test/trafos/fourier_test.py @@ -46,6 +46,20 @@ def _params_from_dtype(dtype): return halfcomplex, complex_dtype(dtype) +def _dft_space(shape, dtype='float64'): + try: + ndim = len(shape) + except TypeError: + ndim = 1 + return odl.uniform_discr( + [0] * ndim, + np.subtract(shape, 1), + shape, + dtype=dtype, + nodes_on_bdry=True, + ) + + def sinc(x): # numpy.sinc scales by pi, we don't want that return np.sinc(x / np.pi) @@ -57,12 +71,12 @@ def sinc(x): def test_dft_init(impl): # Just check if the code runs at all shape = (4, 5) - dom = odl.discr_sequence_space(shape) + dom = _dft_space(shape) dom_nonseq = odl.uniform_discr([0, 0], [1, 1], shape) - dom_f32 = odl.discr_sequence_space(shape, dtype='float32') - ran = odl.discr_sequence_space(shape, dtype='complex128') - ran_c64 = odl.discr_sequence_space(shape, dtype='complex64') - ran_hc = odl.discr_sequence_space((3, 5), dtype='complex128') + dom_f32 = dom.astype('float32') + ran = _dft_space(shape, dtype='complex128') + ran_c64 = ran.astype('complex64') + ran_hc = _dft_space((3, 5), dtype='complex128') # Implicit range DiscreteFourierTransform(dom, impl=impl) @@ -86,8 +100,8 @@ def test_dft_init(impl): def test_dft_init_raise(): # Test different error scenarios shape = (4, 5) - dom = odl.discr_sequence_space(shape) - dom_f32 = odl.discr_sequence_space(shape, dtype='float32') + dom = _dft_space(shape) + dom_f32 = _dft_space(shape, dtype='float32') # Bad types with pytest.raises(TypeError): @@ -107,36 +121,36 @@ def test_dft_init_raise(): DiscreteFourierTransform(dom, axes=(1, -3)) # Badly shaped range - bad_ran = odl.discr_sequence_space((3, 5), dtype='complex128') + bad_ran = _dft_space((3, 5), dtype='complex128') with pytest.raises(ValueError): DiscreteFourierTransform(dom, bad_ran) - bad_ran = odl.discr_sequence_space((10, 10), dtype='complex128') + bad_ran = _dft_space((10, 10), dtype='complex128') with pytest.raises(ValueError): DiscreteFourierTransform(dom, bad_ran) - bad_ran = odl.discr_sequence_space((4, 5), dtype='complex128') + bad_ran = _dft_space((4, 5), dtype='complex128') with pytest.raises(ValueError): DiscreteFourierTransform(dom, bad_ran, halfcomplex=True) - bad_ran = odl.discr_sequence_space((4, 3), dtype='complex128') + bad_ran = _dft_space((4, 3), dtype='complex128') with pytest.raises(ValueError): DiscreteFourierTransform(dom, bad_ran, halfcomplex=True, axes=(0,)) # Bad data types - bad_ran = odl.discr_sequence_space(shape, dtype='complex64') + bad_ran = _dft_space(shape, dtype='complex64') with pytest.raises(ValueError): DiscreteFourierTransform(dom, bad_ran) - bad_ran = odl.discr_sequence_space(shape, dtype='float64') + bad_ran = _dft_space(shape, dtype='float64') with pytest.raises(ValueError): DiscreteFourierTransform(dom, bad_ran) - bad_ran = odl.discr_sequence_space((4, 3), dtype='float64') + bad_ran = _dft_space((4, 3), dtype='float64') with pytest.raises(ValueError): DiscreteFourierTransform(dom, bad_ran, halfcomplex=True) - bad_ran = odl.discr_sequence_space((4, 3), dtype='complex128') + bad_ran = _dft_space((4, 3), dtype='complex128') with pytest.raises(ValueError): DiscreteFourierTransform(dom_f32, bad_ran, halfcomplex=True) @@ -148,25 +162,25 @@ def test_dft_init_raise(): def test_dft_range(): # 1d shape = 10 - dom = odl.discr_sequence_space(shape, dtype='complex128') + dom = _dft_space(shape, dtype='complex128') fft = DiscreteFourierTransform(dom) - true_ran = odl.discr_sequence_space(shape, dtype='complex128') + true_ran = _dft_space(shape, dtype='complex128') assert fft.range == true_ran # 3d shape = (3, 4, 5) - ran = odl.discr_sequence_space(shape, dtype='complex64') + ran = _dft_space(shape, dtype='complex64') fft = DiscreteFourierTransform(ran) - true_ran = odl.discr_sequence_space(shape, dtype='complex64') + true_ran = _dft_space(shape, dtype='complex64') assert fft.range == true_ran # 3d, with axes and halfcomplex shape = (3, 4, 5) axes = (-1, -2) ran_shape = (3, 3, 5) - dom = odl.discr_sequence_space(shape, dtype='float32') + dom = _dft_space(shape, dtype='float32') fft = DiscreteFourierTransform(dom, axes=axes, halfcomplex=True) - true_ran = odl.discr_sequence_space(ran_shape, dtype='complex64') + true_ran = _dft_space(ran_shape, dtype='complex64') assert fft.range == true_ran @@ -177,10 +191,10 @@ def test_idft_init(impl): # Just check if the code runs at all; this uses the init function of # DiscreteFourierTransform, so we don't need exhaustive tests here shape = (4, 5) - ran = odl.discr_sequence_space(shape, dtype='complex128') - ran_hc = odl.discr_sequence_space(shape, dtype='float64') - dom = odl.discr_sequence_space(shape, dtype='complex128') - dom_hc = odl.discr_sequence_space((3, 5), dtype='complex128') + ran = _dft_space(shape, dtype='complex128') + ran_hc = _dft_space(shape, dtype='float64') + dom = _dft_space(shape, dtype='complex128') + dom_hc = _dft_space((3, 5), dtype='complex128') # Implicit range DiscreteFourierTransformInverse(dom, impl=impl) @@ -195,7 +209,7 @@ def test_dft_call(impl): # 2d, complex, all ones and random back & forth shape = (4, 5) - dft_dom = odl.discr_sequence_space(shape, dtype='complex64') + dft_dom = _dft_space(shape, dtype='complex64') dft = DiscreteFourierTransform(domain=dft_dom, impl=impl) idft = DiscreteFourierTransformInverse(range=dft_dom, impl=impl) @@ -229,7 +243,7 @@ def test_dft_call(impl): # 2d, halfcomplex, first axis shape = (4, 5) axes = 0 - dft_dom = odl.discr_sequence_space(shape, dtype='float32') + dft_dom = _dft_space(shape, dtype='float32') dft = DiscreteFourierTransform(domain=dft_dom, impl=impl, halfcomplex=True, axes=axes) idft = DiscreteFourierTransformInverse(range=dft_dom, impl=impl, @@ -262,7 +276,7 @@ def test_dft_sign(impl): # 2d, complex, all ones and random back & forth shape = (4, 5) - dft_dom = odl.discr_sequence_space(shape, dtype='complex64') + dft_dom = _dft_space(shape, dtype='complex64') dft_minus = DiscreteFourierTransform(domain=dft_dom, impl=impl, sign='-') dft_plus = DiscreteFourierTransform(domain=dft_dom, impl=impl, sign='+') @@ -283,7 +297,7 @@ def test_dft_sign(impl): # 2d, halfcomplex, first axis shape = (4, 5) axes = (0,) - dft_dom = odl.discr_sequence_space(shape, dtype='float32') + dft_dom = _dft_space(shape, dtype='float32') arr = dft_dom.element([[0, 0, 0, 0, 0], [0, 0, 1, 1, 0], [0, 0, 1, 1, 0], @@ -307,7 +321,7 @@ def test_dft_init_plan(impl): # 2d, halfcomplex, first axis shape = (4, 5) axes = 0 - dft_dom = odl.discr_sequence_space(shape, dtype='float32') + dft_dom = _dft_space(shape, dtype='float32') dft = DiscreteFourierTransform(dft_dom, impl=impl, axes=axes, halfcomplex=True) @@ -516,13 +530,12 @@ def char_interval(x): def char_interval_ft(x): return np.exp(-1j * x / 2) * sinc(x / 2) / np.sqrt(2 * np.pi) - fspace = odl.FunctionSpace(odl.IntervalProd(-2, 2), out_dtype=complex) - discr = odl.uniform_discr_fromspace(fspace, 40, impl='numpy') + discr = odl.uniform_discr(-2, 2, 40, impl='numpy', dtype='complex128') dft = FourierTransform(discr) for factor in (2, 1j, -2.5j, 1 - 4j): func_true_ft = factor * dft.range.element(char_interval_ft) - func_dft = dft(factor * fspace.element(char_interval)) + func_dft = dft(factor * discr.element(char_interval)) assert (func_dft - func_true_ft).norm() < 1e-6 @@ -615,7 +628,6 @@ def hat_func_ft(x): return sinc(x / 2) ** 2 / np.sqrt(2 * np.pi) # Using a single-precision implementation, should be as good - # With linear interpolation in the discretization, should be better? discr = odl.uniform_discr(-2, 2, 101, impl='numpy', dtype='float32') dft = FourierTransform(discr) func_true_ft = dft.range.element(hat_func_ft) diff --git a/odl/tomo/backends/astra_cpu.py b/odl/tomo/backends/astra_cpu.py index 8e6a7d0abf1..7f1c70321eb 100644 --- a/odl/tomo/backends/astra_cpu.py +++ b/odl/tomo/backends/astra_cpu.py @@ -1,4 +1,4 @@ -# Copyright 2014-2019 The ODL contributors +# Copyright 2014-2020 The ODL contributors # # This file is part of ODL. # @@ -12,7 +12,7 @@ import numpy as np -from odl.discr import DiscreteLp, DiscreteLpElement +from odl.discr import DiscretizedSpace, DiscretizedSpaceElement from odl.tomo.backends.astra_setup import ( astra_algorithm, astra_data, astra_projection_geometry, astra_projector, astra_volume_geometry) @@ -61,8 +61,8 @@ def default_astra_proj_type(geom): return 'line_fanflat' if geom.ndim == 2 else 'linearcone' else: raise TypeError( - 'no default exists for {}, `astra_proj_type` must be given explicitly' - ''.format(type(geom)) + 'no default exists for {}, `astra_proj_type` must be given ' + 'explicitly'.format(type(geom)) ) @@ -72,11 +72,11 @@ def astra_cpu_forward_projector(vol_data, geometry, proj_space, out=None, Parameters ---------- - vol_data : `DiscreteLpElement` + vol_data : `DiscretizedSpaceElement` Volume data to which the forward projector is applied. geometry : `Geometry` Geometry defining the tomographic setup. - proj_space : `DiscreteLp` + proj_space : `DiscretizedSpace` Space to which the calling operator maps. out : ``proj_space`` element, optional Element of the projection space to which the result is written. If @@ -93,8 +93,8 @@ def astra_cpu_forward_projector(vol_data, geometry, proj_space, out=None, Projection data resulting from the application of the projector. If ``out`` was provided, the returned object is a reference to it. """ - if not isinstance(vol_data, DiscreteLpElement): - raise TypeError('volume data {!r} is not a `DiscreteLpElement` ' + if not isinstance(vol_data, DiscretizedSpaceElement): + raise TypeError('volume data {!r} is not a `DiscretizedSpaceElement` ' 'instance.'.format(vol_data)) if vol_data.space.impl != 'numpy': raise TypeError("`vol_data.space.impl` must be 'numpy', got {!r}" @@ -102,8 +102,8 @@ def astra_cpu_forward_projector(vol_data, geometry, proj_space, out=None, if not isinstance(geometry, Geometry): raise TypeError('geometry {!r} is not a Geometry instance' ''.format(geometry)) - if not isinstance(proj_space, DiscreteLp): - raise TypeError('`proj_space` {!r} is not a DiscreteLp ' + if not isinstance(proj_space, DiscretizedSpace): + raise TypeError('`proj_space` {!r} is not a DiscretizedSpace ' 'instance.'.format(proj_space)) if proj_space.impl != 'numpy': raise TypeError("`proj_space.impl` must be 'numpy', got {!r}" @@ -117,7 +117,7 @@ def astra_cpu_forward_projector(vol_data, geometry, proj_space, out=None, else: if out not in proj_space: raise TypeError('`out` {} is neither None nor a ' - 'DiscreteLpElement instance'.format(out)) + 'DiscretizedSpaceElement instance'.format(out)) ndim = vol_data.ndim @@ -160,11 +160,11 @@ def astra_cpu_back_projector(proj_data, geometry, vol_space, out=None, Parameters ---------- - proj_data : `DiscreteLpElement` + proj_data : `DiscretizedSpaceElement` Projection data to which the back-projector is applied. geometry : `Geometry` Geometry defining the tomographic setup. - vol_space : `DiscreteLp` + vol_space : `DiscretizedSpace` Space to which the calling operator maps. out : ``vol_space`` element, optional Element of the reconstruction space to which the result is written. @@ -182,9 +182,11 @@ def astra_cpu_back_projector(proj_data, geometry, vol_space, out=None, projector. If ``out`` was provided, the returned object is a reference to it. """ - if not isinstance(proj_data, DiscreteLpElement): - raise TypeError('projection data {!r} is not a DiscreteLpElement ' - 'instance'.format(proj_data)) + if not isinstance(proj_data, DiscretizedSpaceElement): + raise TypeError( + 'projection data {!r} is not a `DiscretizedSpaceElement` ' + 'instance'.format(proj_data) + ) if proj_data.space.impl != 'numpy': raise TypeError('`proj_data` must be a `numpy.ndarray` based, ' "container got `impl` {!r}" @@ -192,8 +194,8 @@ def astra_cpu_back_projector(proj_data, geometry, vol_space, out=None, if not isinstance(geometry, Geometry): raise TypeError('geometry {!r} is not a Geometry instance' ''.format(geometry)) - if not isinstance(vol_space, DiscreteLp): - raise TypeError('volume space {!r} is not a DiscreteLp ' + if not isinstance(vol_space, DiscretizedSpace): + raise TypeError('volume space {!r} is not a DiscretizedSpace ' 'instance'.format(vol_space)) if vol_space.impl != 'numpy': raise TypeError("`vol_space.impl` must be 'numpy', got {!r}" @@ -207,7 +209,7 @@ def astra_cpu_back_projector(proj_data, geometry, vol_space, out=None, else: if out not in vol_space: raise TypeError('`out` {} is neither None nor a ' - 'DiscreteLpElement instance'.format(out)) + 'DiscretizedSpaceElement instance'.format(out)) ndim = proj_data.ndim diff --git a/odl/tomo/backends/astra_cuda.py b/odl/tomo/backends/astra_cuda.py index 96cee858413..69627888b23 100644 --- a/odl/tomo/backends/astra_cuda.py +++ b/odl/tomo/backends/astra_cuda.py @@ -1,4 +1,4 @@ -# Copyright 2014-2019 The ODL contributors +# Copyright 2014-2020 The ODL contributors # # This file is part of ODL. # @@ -16,7 +16,7 @@ import numpy as np from packaging.version import parse as parse_version -from odl.discr import DiscreteLp +from odl.discr import DiscretizedSpace from odl.tomo.backends.astra_setup import ( ASTRA_VERSION, astra_algorithm, astra_data, astra_projection_geometry, astra_projector, astra_volume_geometry) @@ -53,15 +53,15 @@ def __init__(self, geometry, reco_space, proj_space): ---------- geometry : `Geometry` Geometry defining the tomographic setup. - reco_space : `DiscreteLp` + reco_space : `DiscretizedSpace` Reconstruction space, the space of the images to be forward projected. - proj_space : `DiscreteLp` + proj_space : `DiscretizedSpace` Projection space, the space of the result. """ assert isinstance(geometry, Geometry) - assert isinstance(reco_space, DiscreteLp) - assert isinstance(proj_space, DiscreteLp) + assert isinstance(reco_space, DiscretizedSpace) + assert isinstance(proj_space, DiscretizedSpace) self.geometry = geometry self.reco_space = reco_space @@ -210,14 +210,14 @@ def __init__(self, geometry, reco_space, proj_space): ---------- geometry : `Geometry` Geometry defining the tomographic setup. - reco_space : `DiscreteLp` + reco_space : `DiscretizedSpace` Reconstruction space, the space to which the backprojection maps. - proj_space : `DiscreteLp` + proj_space : `DiscretizedSpace` Projection space, the space from which the backprojection maps. """ assert isinstance(geometry, Geometry) - assert isinstance(reco_space, DiscreteLp) - assert isinstance(proj_space, DiscreteLp) + assert isinstance(reco_space, DiscretizedSpace) + assert isinstance(proj_space, DiscretizedSpace) self.geometry = geometry self.reco_space = reco_space diff --git a/odl/tomo/backends/astra_setup.py b/odl/tomo/backends/astra_setup.py index 658263e59f8..c847f1f2574 100644 --- a/odl/tomo/backends/astra_setup.py +++ b/odl/tomo/backends/astra_setup.py @@ -1,4 +1,4 @@ -# Copyright 2014-2019 The ODL contributors +# Copyright 2014-2020 The ODL contributors # # This file is part of ODL. # @@ -29,7 +29,7 @@ import numpy as np -from odl.discr import DiscreteLp, DiscreteLpElement +from odl.discr import DiscretizedSpace, DiscretizedSpaceElement from odl.tomo.geometry import ( DivergentBeamGeometry, Flat1dDetector, Flat2dDetector, Geometry, ParallelBeamGeometry) @@ -181,7 +181,7 @@ def astra_volume_geometry(vol_space): Parameters ---------- - vol_space : `DiscreteLp` + vol_space : `DiscretizedSpace` Discretized space where the reconstruction (volume) lives. It must be 2- or 3-dimensional and uniformly discretized. @@ -194,8 +194,8 @@ def astra_volume_geometry(vol_space): NotImplementedError If the cell sizes are not the same in each dimension. """ - if not isinstance(vol_space, DiscreteLp): - raise TypeError('`vol_space` {!r} is not a DiscreteLp instance' + if not isinstance(vol_space, DiscretizedSpace): + raise TypeError('`vol_space` {!r} is not a DiscretizedSpace instance' ''.format(vol_space)) if not vol_space.is_uniform: @@ -553,7 +553,7 @@ def astra_data(astra_geom, datatype, data=None, ndim=2, allow_copy=False): given ``datatype``. datatype : {'volume', 'projection'} Type of the data container. - data : `DiscreteLpElement` or `numpy.ndarray`, optional + data : `DiscretizedSpaceElement` or `numpy.ndarray`, optional Data for the initialization of the data object. If ``None``, an ASTRA data object filled with zeros is created. ndim : {2, 3}, optional @@ -570,10 +570,10 @@ def astra_data(astra_geom, datatype, data=None, ndim=2, allow_copy=False): Handle for the new ASTRA internal data object. """ if data is not None: - if isinstance(data, (DiscreteLpElement, np.ndarray)): + if isinstance(data, (DiscretizedSpaceElement, np.ndarray)): ndim = data.ndim else: - raise TypeError('`data` {!r} is neither DiscreteLpElement ' + raise TypeError('`data` {!r} is neither DiscretizedSpaceElement ' 'instance nor a `numpy.ndarray`'.format(data)) else: ndim = int(ndim) diff --git a/odl/tomo/backends/skimage_radon.py b/odl/tomo/backends/skimage_radon.py index d78b81e9907..86d5d64ac67 100644 --- a/odl/tomo/backends/skimage_radon.py +++ b/odl/tomo/backends/skimage_radon.py @@ -14,7 +14,7 @@ from odl.discr import uniform_discr_frompartition, uniform_partition from odl.discr.discr_utils import linear_interpolator, point_collocation -from odl.util import writable_array +from odl.util.utility import writable_array try: import skimage @@ -38,21 +38,18 @@ def skimage_proj_space(geometry, volume_space, proj_space): def clamped_interpolation(skimage_range, sinogram): - """Interpolate in a possibly smaller space. - - Clip all points to fit within the bounds of the given space. - """ + """Return interpolator that clamps points to min/max of the space.""" min_x = skimage_range.domain.min()[1] max_x = skimage_range.domain.max()[1] - def interpolator(x, out=None): + def _interpolator(x, out=None): x = (x[0], np.clip(x[1], min_x, max_x)) interpolator = linear_interpolator( sinogram, skimage_range.grid.coord_vectors ) return interpolator(x, out=out) - return interpolator + return _interpolator def skimage_radon_forward_projector(volume, geometry, proj_space, out=None): @@ -60,11 +57,11 @@ def skimage_radon_forward_projector(volume, geometry, proj_space, out=None): Parameters ---------- - volume : `DiscreteLpElement` + volume : `DiscretizedSpaceElement` The volume to project. geometry : `Geometry` The projection geometry to use. - proj_space : `DiscreteLp` + proj_space : `DiscretizedSpace` Space in which the projections (sinograms) live. out : ``proj_space`` element, optional Element to which the result should be written. @@ -111,11 +108,11 @@ def skimage_radon_back_projector(sinogram, geometry, vol_space, out=None): Parameters ---------- - sinogram : `DiscreteLpElement` + sinogram : `DiscretizedSpaceElement` Sinogram (projections) to backproject. geometry : `Geometry` The projection geometry to use. - vol_space : `DiscreteLp` + vol_space : `DiscretizedSpace` Space in which reconstructed volumes live. out : ``vol_space`` element, optional An element to which the result should be written. diff --git a/odl/tomo/geometry/conebeam.py b/odl/tomo/geometry/conebeam.py index e4da61f4e23..25e8bb78d63 100644 --- a/odl/tomo/geometry/conebeam.py +++ b/odl/tomo/geometry/conebeam.py @@ -1,4 +1,4 @@ -# Copyright 2014-2019 The ODL contributors +# Copyright 2014-2020 The ODL contributors # # This file is part of ODL. # @@ -8,19 +8,19 @@ """Cone beam geometries in 2 and 3 dimensions.""" -from __future__ import print_function, division, absolute_import +from __future__ import absolute_import, division, print_function + import numpy as np from odl.discr import uniform_partition from odl.tomo.geometry.detector import ( - Flat1dDetector, Flat2dDetector, - CircularDetector, CylindricalDetector, SphericalDetector) + CircularDetector, CylindricalDetector, Flat1dDetector, Flat2dDetector, + SphericalDetector) from odl.tomo.geometry.geometry import ( - DivergentBeamGeometry, AxisOrientedGeometry) + AxisOrientedGeometry, DivergentBeamGeometry) from odl.tomo.util.utility import ( - euler_matrix, transform_system, is_inside_bounds) -from odl.util import signature_string, indent, array_str - + euler_matrix, is_inside_bounds, transform_system) +from odl.util import array_str, indent, signature_string __all__ = ('FanBeamGeometry', 'ConeBeamGeometry', 'cone_beam_geometry', 'helical_geometry') @@ -1329,7 +1329,7 @@ def cone_beam_geometry(space, src_radius, det_radius, num_angles=None, Parameters ---------- - space : `DiscreteLp` + space : `DiscretizedSpace` Reconstruction space, the space of the volumetric data to be projected. Must be 2- or 3-dimensional. src_radius : nonnegative float @@ -1549,7 +1549,7 @@ def helical_geometry(space, src_radius, det_radius, num_turns, Parameters ---------- - space : `DiscreteLp` + space : `DiscretizedSpace` Reconstruction space, the space of the volumetric data to be projected. Must be 3-dimensional. src_radius : nonnegative float diff --git a/odl/tomo/geometry/parallel.py b/odl/tomo/geometry/parallel.py index f39e9c1db28..1d7d3dad6b8 100644 --- a/odl/tomo/geometry/parallel.py +++ b/odl/tomo/geometry/parallel.py @@ -1,4 +1,4 @@ -# Copyright 2014-2019 The ODL contributors +# Copyright 2014-2020 The ODL contributors # # This file is part of ODL. # @@ -8,15 +8,15 @@ """Parallel beam geometries in 2 or 3 dimensions.""" -from __future__ import print_function, division, absolute_import +from __future__ import absolute_import, division, print_function + import numpy as np from odl.discr import uniform_partition from odl.tomo.geometry.detector import Flat1dDetector, Flat2dDetector -from odl.tomo.geometry.geometry import Geometry, AxisOrientedGeometry -from odl.tomo.util import euler_matrix, transform_system, is_inside_bounds -from odl.util import signature_string, indent, array_str - +from odl.tomo.geometry.geometry import AxisOrientedGeometry, Geometry +from odl.tomo.util import euler_matrix, is_inside_bounds, transform_system +from odl.util import array_str, indent, signature_string __all__ = ('ParallelBeamGeometry', 'Parallel2dGeometry', @@ -1481,7 +1481,7 @@ def parallel_beam_geometry(space, num_angles=None, det_shape=None): Parameters ---------- - space : `DiscreteLp` + space : `DiscretizedSpace` Reconstruction space, the space of the volumetric data to be projected. Needs to be 2d or 3d. num_angles : int, optional diff --git a/odl/tomo/operators/ray_trafo.py b/odl/tomo/operators/ray_trafo.py index edceb0343b0..f5e3346ed09 100644 --- a/odl/tomo/operators/ray_trafo.py +++ b/odl/tomo/operators/ray_trafo.py @@ -14,9 +14,8 @@ import numpy as np -from odl.discr import DiscreteLp +from odl.discr import DiscretizedSpace from odl.operator import Operator -from odl.space import FunctionSpace from odl.space.weighting import ConstWeighting from odl.tomo.backends import ( ASTRA_AVAILABLE, ASTRA_CUDA_AVAILABLE, ASTRA_VERSION, SKIMAGE_AVAILABLE, @@ -50,7 +49,7 @@ def __init__(self, reco_space, geometry, variant, **kwargs): Parameters ---------- - reco_space : `DiscreteLp` + reco_space : `DiscretizedSpace` Discretized reconstruction space, the domain of the forward operator or the range of the adjoint (back-projection). geometry : `Geometry` @@ -73,7 +72,7 @@ def __init__(self, reco_space, geometry, variant, **kwargs): For the default ``None``, the fastest available back-end is used. - proj_space : `DiscreteLp`, optional + proj_space : `DiscretizedSpace`, optional Discretized projection (sinogram) space, the range of the forward operator or the domain of the adjoint (back-projection). Default: Inferred from parameters. @@ -104,8 +103,8 @@ def __init__(self, reco_space, geometry, variant, **kwargs): reco_name = 'range' proj_name = 'domain' - if not isinstance(reco_space, DiscreteLp): - raise TypeError('`{}` must be a `DiscreteLp` instance, got ' + if not isinstance(reco_space, DiscretizedSpace): + raise TypeError('`{}` must be a `DiscretizedSpace` instance, got ' '{!r}'.format(reco_name, reco_space)) if not isinstance(geometry, Geometry): @@ -216,7 +215,6 @@ def __init__(self, reco_space, geometry, variant, **kwargs): proj_space = kwargs.pop('proj_space', None) if proj_space is None: dtype = reco_space.dtype - proj_fspace = FunctionSpace(geometry.params, out_dtype=dtype) if not reco_space.is_weighted: weighting = None @@ -242,7 +240,7 @@ def __init__(self, reco_space, geometry, variant, **kwargs): if geometry.motion_partition.ndim == 0: angle_labels = [] - if geometry.motion_partition.ndim == 1: + elif geometry.motion_partition.ndim == 1: angle_labels = ['$\\varphi$'] elif geometry.motion_partition.ndim == 2: # TODO: check order @@ -266,14 +264,16 @@ def __init__(self, reco_space, geometry, variant, **kwargs): else: axis_labels = angle_labels + det_labels - proj_space = DiscreteLp( - proj_fspace, geometry.partition, proj_tspace, - axis_labels=axis_labels) + proj_space = DiscretizedSpace( + geometry.partition, + proj_tspace, + axis_labels=axis_labels + ) else: # proj_space was given, checking some stuff - if not isinstance(proj_space, DiscreteLp): - raise TypeError('`{}` must be a `DiscreteLp` instance, ' + if not isinstance(proj_space, DiscretizedSpace): + raise TypeError('`{}` must be a `DiscretizedSpace` instance, ' 'got {!r}'.format(proj_name, proj_space)) if proj_space.shape != geometry.partition.shape: raise ValueError('`{}.shape` not equal to `geometry.shape`: ' @@ -346,7 +346,7 @@ def __init__(self, domain, geometry, **kwargs): Parameters ---------- - domain : `DiscreteLp` + domain : `DiscretizedSpace` Discretized reconstruction space, the domain of the forward projector. geometry : `Geometry` @@ -365,7 +365,7 @@ def __init__(self, domain, geometry, **kwargs): For the default ``None``, the fastest available back-end is used, tried in the above order. - range : `DiscreteLp`, optional + range : `DiscretizedSpace`, optional Discretized projection (sinogram) space, the range of the forward projector. Default: Inferred from parameters. @@ -461,7 +461,7 @@ def __init__(self, range, geometry, **kwargs): Parameters ---------- - range : `DiscreteLp` + range : `DiscretizedSpace` Discretized reconstruction space, the range of the backprojection operator. geometry : `Geometry` @@ -481,7 +481,7 @@ def __init__(self, range, geometry, **kwargs): For the default ``None``, the fastest available back-end is used, tried in the above order. - domain : `DiscreteLp`, optional + domain : `DiscretizedSpace`, optional Discretized projection (sinogram) space, the domain of the backprojection operator. Default: Inferred from parameters. diff --git a/odl/trafos/backends/pyfftw_bindings.py b/odl/trafos/backends/pyfftw_bindings.py index f65cc840bf6..0150c754df8 100644 --- a/odl/trafos/backends/pyfftw_bindings.py +++ b/odl/trafos/backends/pyfftw_bindings.py @@ -1,4 +1,4 @@ -# Copyright 2014-2018 The ODL contributors +# Copyright 2014-2020 The ODL contributors # # This file is part of ODL. # @@ -142,10 +142,11 @@ def pyfftw_call(array_in, array_out, direction='forward', axes=None, axes = normalized_axes_tuple(axes, array_in.ndim) - direction = _pyfftw_to_local(direction) + direction = _flag_pyfftw_to_odl(direction) fftw_plan_in = kwargs.pop('fftw_plan', None) - planning_effort = _pyfftw_to_local(kwargs.pop('planning_effort', - 'estimate')) + planning_effort = _flag_pyfftw_to_odl( + kwargs.pop('planning_effort', 'estimate') + ) planning_timelimit = kwargs.pop('planning_timelimit', None) threads = kwargs.pop('threads', None) normalise_idft = kwargs.pop('normalise_idft', False) @@ -183,10 +184,10 @@ def pyfftw_call(array_in, array_out, direction='forward', axes=None, if must_copy_array_in and not array_in_copied: plan_arr_in = np.empty_like(array_in) - flags = [_local_to_pyfftw(planning_effort), 'FFTW_DESTROY_INPUT'] + flags = [_flag_odl_to_pyfftw(planning_effort), 'FFTW_DESTROY_INPUT'] else: plan_arr_in = array_in - flags = [_local_to_pyfftw(planning_effort)] + flags = [_flag_odl_to_pyfftw(planning_effort)] if fftw_plan_in is None: if threads is None: @@ -196,7 +197,7 @@ def pyfftw_call(array_in, array_out, direction='forward', axes=None, threads = cpu_count() fftw_plan = pyfftw.FFTW( - plan_arr_in, array_out, direction=_local_to_pyfftw(direction), + plan_arr_in, array_out, direction=_flag_odl_to_pyfftw(direction), flags=flags, planning_timelimit=planning_timelimit, threads=threads, axes=axes) else: @@ -214,17 +215,17 @@ def pyfftw_call(array_in, array_out, direction='forward', axes=None, return fftw_plan -def _pyfftw_to_local(flag): +def _flag_pyfftw_to_odl(flag): return flag.lstrip('FFTW_').lower() -def _local_to_pyfftw(flag): +def _flag_odl_to_pyfftw(flag): return 'FFTW_' + flag.upper() def _pyfftw_destroys_input(flags, direction, halfcomplex, ndim): """Return ``True`` if FFTW destroys an input array, ``False`` otherwise.""" - if any(flag in flags or _pyfftw_to_local(flag) in flags + if any(flag in flags or _flag_pyfftw_to_odl(flag) in flags for flag in ('FFTW_MEASURE', 'FFTW_PATIENT', 'FFTW_EXHAUSTIVE', 'FFTW_DESTROY_INPUT')): return True diff --git a/odl/trafos/fourier.py b/odl/trafos/fourier.py index 88c6ee46955..03af04f02ff 100644 --- a/odl/trafos/fourier.py +++ b/odl/trafos/fourier.py @@ -12,11 +12,11 @@ import numpy as np -from odl.discr import DiscreteLp, discr_sequence_space +from odl.discr import DiscretizedSpace, uniform_discr from odl.operator import Operator from odl.set import ComplexNumbers, RealNumbers from odl.trafos.backends.pyfftw_bindings import ( - PYFFTW_AVAILABLE, _pyfftw_to_local, pyfftw_call) + PYFFTW_AVAILABLE, _flag_pyfftw_to_odl, pyfftw_call) from odl.trafos.util import ( dft_postprocess_data, dft_preprocess_data, reciprocal_grid, reciprocal_space) @@ -52,15 +52,15 @@ def __init__(self, inverse, domain, range=None, axes=None, sign='-', inverse : bool If ``True``, the inverse transform is created, otherwise the forward transform. - domain : `DiscreteLp` + domain : `DiscretizedSpace` Domain of the Fourier transform. If its - `DiscreteLp.exponent` is equal to 2.0, this operator has + `DiscretizedSpace.exponent` is equal to 2.0, this operator has an adjoint which is equal to the inverse. - range : `DiscreteLp`, optional + range : `DiscretizedSpace`, optional Range of the Fourier transform. If not given, the range is determined from ``domain`` and the other parameters as - a `discr_sequence_space` with exponent ``p / (p - 1)`` - (read as 'inf' for p=1 and 1 for p='inf'). + a `uniform_discr` with exponent unit cell size and exponent + ``p / (p - 1)`` (read as 'inf' for p=1 and 1 for p='inf'). axes : int or sequence of ints, optional Dimensions in which a transform is to be calculated. ``None`` means all axes. @@ -79,11 +79,13 @@ def __init__(self, inverse, domain, range=None, axes=None, sign='-', is faster but requires the ``pyfftw`` package. ``None`` selects the fastest available backend. """ - if not isinstance(domain, DiscreteLp): - raise TypeError('`domain` {!r} is not a `DiscreteLp` instance' - ''.format(domain)) - if range is not None and not isinstance(range, DiscreteLp): - raise TypeError('`range` {!r} is not a `DiscreteLp` instance' + if not isinstance(domain, DiscretizedSpace): + raise TypeError( + '`domain` {!r} is not a `DiscretizedSpace` instance' + ''.format(domain) + ) + if range is not None and not isinstance(range, DiscretizedSpace): + raise TypeError('`range` {!r} is not a `DiscretizedSpace` instance' ''.format(range)) # Implementation @@ -123,9 +125,11 @@ def __init__(self, inverse, domain, range=None, axes=None, sign='-', if range is None: impl = domain.tspace.impl - range = discr_sequence_space( - ran_shape, ran_dtype, impl, - exponent=conj_exponent(domain.exponent)) + shape = np.atleast_1d(ran_shape) + range = uniform_discr( + [0] * len(shape), shape - 1, shape, ran_dtype, impl, + nodes_on_bdry=True, exponent=conj_exponent(domain.exponent)) + else: if range.shape != ran_shape: raise ValueError('expected range shape {}, got {}.' @@ -267,7 +271,7 @@ def _call_pyfftw(self, x, out, **kwargs): kwargs.pop('normalise_idft', None) # Using `False` here kwargs.pop('axes', None) kwargs.pop('halfcomplex', None) - flags = list(_pyfftw_to_local(flag) for flag in + flags = list(_flag_pyfftw_to_odl(flag) for flag in kwargs.pop('flags', ('FFTW_MEASURE',))) try: flags.remove('unaligned') @@ -385,15 +389,15 @@ def __init__(self, domain, range=None, axes=None, sign='-', Parameters ---------- - domain : `DiscreteLp` + domain : `DiscretizedSpace` Domain of the Fourier transform. If its - `DiscreteLp.exponent` is equal to 2.0, this operator has + `DiscretizedSpace.exponent` is equal to 2.0, this operator has an adjoint which is equal to the inverse. - range : `DiscreteLp`, optional + range : `DiscretizedSpace`, optional Range of the Fourier transform. If not given, the range is determined from ``domain`` and the other parameters as - a `discr_sequence_space` with exponent ``p / (p - 1)`` - (read as 'inf' for p=1 and 1 for p='inf'). + a `uniform_discr` with unit cell size and exponent + ``p / (p - 1)`` (read as 'inf' for p=1 and 1 for p='inf'). axes : int or sequence of ints, optional Dimensions in which a transform is to be calculated. ``None`` means all axes. @@ -417,7 +421,8 @@ def __init__(self, domain, range=None, axes=None, sign='-', Complex-to-complex (default) transforms have the same grids in domain and range: - >>> domain = discr_sequence_space((2, 4)) + >>> domain = odl.uniform_discr([0, 0], [2, 4], (2, 4), + ... nodes_on_bdry=True) >>> fft = DiscreteFourierTransform(domain) >>> fft.domain.shape (2, 4) @@ -427,7 +432,8 @@ def __init__(self, domain, range=None, axes=None, sign='-', Real-to-complex transforms have a range grid with shape ``n // 2 + 1`` in the last tranform axis: - >>> domain = discr_sequence_space((2, 3, 4), dtype='float') + >>> domain = odl.uniform_discr([0, 0, 0], [2, 3, 4], (2, 3, 4), + ... nodes_on_bdry=True) >>> axes = (0, 1) >>> fft = DiscreteFourierTransform( ... domain, halfcomplex=True, axes=axes) @@ -472,7 +478,7 @@ def _call_pyfftw(self, x, out, **kwargs): kwargs.pop('normalise_idft', None) # Using `False` here kwargs.pop('axes', None) kwargs.pop('halfcomplex', None) - flags = list(_pyfftw_to_local(flag) for flag in + flags = list(_flag_pyfftw_to_odl(flag) for flag in kwargs.pop('flags', ('FFTW_MEASURE',))) try: flags.remove('unaligned') @@ -536,15 +542,15 @@ def __init__(self, range, domain=None, axes=None, sign='+', Parameters ---------- - range : `DiscreteLp` + range : `DiscretizedSpace` Range of the inverse Fourier transform. If its - `DiscreteLp.exponent` is equal to 2.0, this operator has + `DiscretizedSpace.exponent` is equal to 2.0, this operator has an adjoint which is equal to the inverse. - domain : `DiscreteLp`, optional + domain : `DiscretizedSpace`, optional Domain of the inverse Fourier transform. If not given, the - domain is determined from ``range`` and the other parameters - as a `discr_sequence_space` with exponent ``p / (p - 1)`` - (read as 'inf' for p=1 and 1 for p='inf'). + domain is determined from ``range`` and the other parameters as + a `uniform_discr` with unit cell size and exponent + ``p / (p - 1)`` (read as 'inf' for p=1 and 1 for p='inf'). axes : sequence of ints, optional Dimensions in which a transform is to be calculated. `None` means all axes. @@ -568,8 +574,9 @@ def __init__(self, range, domain=None, axes=None, sign='+', Complex-to-complex (default) transforms have the same grids in domain and range: - >>> range_ = discr_sequence_space((2, 4)) - >>> ifft = DiscreteFourierTransformInverse(range_) + >>> range = odl.uniform_discr([0, 0], [2, 4], (2, 4), + ... nodes_on_bdry=True) + >>> ifft = DiscreteFourierTransformInverse(range) >>> ifft.domain.shape (2, 4) >>> ifft.range.shape @@ -578,10 +585,11 @@ def __init__(self, range, domain=None, axes=None, sign='+', Complex-to-real transforms have a domain grid with shape ``n // 2 + 1`` in the last tranform axis: - >>> range_ = discr_sequence_space((2, 3, 4), dtype='float') + >>> range = odl.uniform_discr([0, 0, 0], [2, 3, 4], (2, 3, 4), + ... nodes_on_bdry=True) >>> axes = (0, 1) >>> ifft = DiscreteFourierTransformInverse( - ... range_, halfcomplex=True, axes=axes) + ... range, halfcomplex=True, axes=axes) >>> ifft.domain.shape # shortened in the second axis (2, 2, 4) >>> ifft.range.shape @@ -649,7 +657,7 @@ def _call_pyfftw(self, x, out, **kwargs): kwargs.pop('normalise_idft', None) # Using `True` here kwargs.pop('axes', None) kwargs.pop('halfcomplex', None) - flags = list(_pyfftw_to_local(flag) for flag in + flags = list(_flag_pyfftw_to_odl(flag) for flag in kwargs.pop('flags', ('FFTW_MEASURE',))) try: flags.remove('unaligned') @@ -718,12 +726,12 @@ def __init__(self, inverse, domain, range=None, impl=None, **kwargs): inverse : bool If ``True``, create the inverse transform, otherwise the forward transform. - domain : `DiscreteLp` + domain : `DiscretizedSpace` Domain of the Fourier transform. If the - `DiscreteLp.exponent` of ``domain`` and ``range`` are equal + `DiscretizedSpace.exponent` of ``domain`` and ``range`` are equal to 2.0, this operator has an adjoint which is equal to its inverse. - range : `DiscreteLp`, optional + range : `DiscretizedSpace`, optional Range of the Fourier transform. If not given, the range is determined from ``domain`` and the other parameters. The exponent is chosen to be the conjugate ``p / (p - 1)``, @@ -754,13 +762,13 @@ def __init__(self, inverse, domain, range=None, impl=None, **kwargs): Other Parameters ---------------- - tmp_r : `DiscreteLpElement` or `numpy.ndarray`, optional + tmp_r : `DiscretizedSpaceElement` or `numpy.ndarray`, optional Temporary for calculations in the real space (domain of this transform). It is shared with the inverse. Variants using this: R2C, R2HC, C2R (inverse) - tmp_f : `DiscreteLpElement` or `numpy.ndarray`, optional + tmp_f : `DiscretizedSpaceElement` or `numpy.ndarray`, optional Temporary for calculations in the frequency (reciprocal) space. It is shared with the inverse. @@ -790,8 +798,8 @@ def __init__(self, inverse, domain, range=None, impl=None, **kwargs): `_ for details. """ - if not isinstance(domain, DiscreteLp): - raise TypeError('domain {!r} is not a `DiscreteLp` instance' + if not isinstance(domain, DiscretizedSpace): + raise TypeError('domain {!r} is not a `DiscretizedSpace` instance' ''.format(domain)) if domain.impl != 'numpy': raise NotImplementedError( @@ -1171,12 +1179,12 @@ def __init__(self, domain, range=None, impl=None, **kwargs): Parameters ---------- - domain : `DiscreteLp` + domain : `DiscretizedSpace` Domain of the Fourier transform. If the - `DiscreteLp.exponent` of ``domain`` and ``range`` are equal + `DiscretizedSpace.exponent` of ``domain`` and ``range`` are equal to 2.0, this operator has an adjoint which is equal to its inverse. - range : `DiscreteLp`, optional + range : `DiscretizedSpace`, optional Range of the Fourier transform. If not given, the range is determined from ``domain`` and the other parameters. The exponent is chosen to be the conjugate ``p / (p - 1)``, @@ -1207,13 +1215,13 @@ def __init__(self, domain, range=None, impl=None, **kwargs): Other Parameters ---------------- - tmp_r : `DiscreteLpElement` or `numpy.ndarray`, optional + tmp_r : `DiscretizedSpaceElement` or `numpy.ndarray`, optional Temporary for calculations in the real space (domain of this transform). It is shared with the inverse. Variants using this: R2C, R2HC, C2R (inverse) - tmp_f : `DiscreteLpElement` or `numpy.ndarray`, optional + tmp_f : `DiscretizedSpaceElement` or `numpy.ndarray`, optional Temporary for calculations in the frequency (reciprocal) space. It is shared with the inverse. @@ -1409,12 +1417,12 @@ def __init__(self, range, domain=None, impl=None, **kwargs): """ Parameters ---------- - range : `DiscreteLp` + range : `DiscretizedSpace` Range of the inverse Fourier transform. If the - `DiscreteLp.exponent` of ``domain`` and ``range`` are equal + `DiscretizedSpace.exponent` of ``domain`` and ``range`` are equal to 2.0, this operator has an adjoint which is equal to its inverse. - domain : `DiscreteLp`, optional + domain : `DiscretizedSpace`, optional Domain of the inverse Fourier transform. If not given, the domain is determined from ``range`` and the other parameters. The exponent is chosen to be the conjugate ``p / (p - 1)``, @@ -1445,13 +1453,13 @@ def __init__(self, range, domain=None, impl=None, **kwargs): Other Parameters ---------------- - tmp_r : `DiscreteLpElement` or `numpy.ndarray`, optional + tmp_r : `DiscretizedSpaceElement` or `numpy.ndarray`, optional Temporary for calculations in the real space (range of this transform). It is shared with the inverse. Variants using this: C2R, R2C (forward), R2HC (forward) - tmp_f : `DiscreteLpElement` or `numpy.ndarray`, optional + tmp_f : `DiscretizedSpaceElement` or `numpy.ndarray`, optional Temporary for calculations in the frequency (reciprocal) space. It is shared with the inverse. diff --git a/odl/trafos/util/ft_utils.py b/odl/trafos/util/ft_utils.py index 2e7bb31d8f5..2e2f3059773 100644 --- a/odl/trafos/util/ft_utils.py +++ b/odl/trafos/util/ft_utils.py @@ -1,4 +1,4 @@ -# Copyright 2014-2019 The ODL contributors +# Copyright 2014-2020 The ODL contributors # # This file is part of ODL. # @@ -8,20 +8,19 @@ """Utility functions for Fourier transforms on regularly sampled data.""" -from __future__ import print_function, division, absolute_import +from __future__ import absolute_import, division, print_function + import numpy as np from odl.discr import ( - uniform_grid, DiscreteLp, uniform_partition_fromgrid, - uniform_discr_frompartition) + DiscretizedSpace, uniform_discr_frompartition, uniform_grid, + uniform_partition_fromgrid) from odl.set import RealNumbers from odl.util import ( - fast_1d_tensor_mult, conj_exponent, - is_real_dtype, is_numeric_dtype, is_real_floating_dtype, - is_complex_floating_dtype, complex_dtype, dtype_repr, - is_string, - normalized_scalar_param_list, normalized_axes_tuple) - + complex_dtype, conj_exponent, dtype_repr, fast_1d_tensor_mult, + is_complex_floating_dtype, is_numeric_dtype, is_real_dtype, + is_real_floating_dtype, is_string, normalized_axes_tuple, + normalized_scalar_param_list) __all__ = ('reciprocal_grid', 'realspace_grid', 'reciprocal_space', @@ -553,7 +552,7 @@ def reciprocal_space(space, axes=None, halfcomplex=False, shift=True, Parameters ---------- - space : `DiscreteLp` + space : `DiscretizedSpace` Real space whose reciprocal is calculated. It must be uniformly discretized. axes : sequence of ints, optional @@ -585,13 +584,13 @@ def reciprocal_space(space, axes=None, halfcomplex=False, shift=True, Returns ------- - rspace : `DiscreteLp` + rspace : `DiscretizedSpace` Reciprocal of the input ``space``. If ``halfcomplex=True``, the upper end of the domain (where the half space ends) is chosen to coincide with the grid node. """ - if not isinstance(space, DiscreteLp): - raise TypeError('`space` {!r} is not a `DiscreteLp` instance' + if not isinstance(space, DiscretizedSpace): + raise TypeError('`space` {!r} is not a `DiscretizedSpace` instance' ''.format(space)) if axes is None: axes = tuple(range(space.ndim)) diff --git a/odl/trafos/wavelet.py b/odl/trafos/wavelet.py index cbb34fa56b7..97abcfdb92c 100644 --- a/odl/trafos/wavelet.py +++ b/odl/trafos/wavelet.py @@ -1,4 +1,4 @@ -# Copyright 2014-2018 The ODL contributors +# Copyright 2014-2020 The ODL contributors # # This file is part of ODL. # @@ -8,14 +8,14 @@ """Discrete wavelet transformation on L2 spaces.""" -from __future__ import print_function, division, absolute_import +from __future__ import absolute_import, division, print_function import numpy as np -from odl.discr import DiscreteLp + +from odl.discr import DiscretizedSpace from odl.operator import Operator from odl.trafos.backends.pywt_bindings import ( - PYWT_AVAILABLE, - pywt_pad_mode, pywt_wavelet, precompute_raveled_slices) + PYWT_AVAILABLE, precompute_raveled_slices, pywt_pad_mode, pywt_wavelet) __all__ = ('WaveletTransform', 'WaveletTransformInverse') @@ -40,7 +40,7 @@ def __init__(self, space, wavelet, nlevels, variant, pad_mode='constant', Parameters ---------- - space : `DiscreteLp` + space : `DiscretizedSpace` Domain of the forward wavelet transform (the "image domain"). In the case of ``variant in ('inverse', 'adjoint')``, this space is the range of the operator. @@ -139,9 +139,11 @@ def __init__(self, space, wavelet, nlevels, variant, pad_mode='constant', .. _signal extension modes: https://pywavelets.readthedocs.io/en/latest/ref/signal-extension-modes.html """ - if not isinstance(space, DiscreteLp): - raise TypeError('`space` {!r} is not a `DiscreteLp` instance.' - ''.format(space)) + if not isinstance(space, DiscretizedSpace): + raise TypeError( + '`space` {!r} is not a `DiscretizedSpace` instance' + ''.format(space) + ) self.__impl, impl_in = str(impl).lower(), impl if self.impl not in _SUPPORTED_WAVELET_IMPLS: @@ -152,7 +154,7 @@ def __init__(self, space, wavelet, nlevels, variant, pad_mode='constant', elif np.isscalar(axes): axes = (axes,) elif len(axes) > space.ndim: - raise ValueError("Too many axes.") + raise ValueError("too many axes") self.axes = tuple(axes) if nlevels is None: @@ -272,7 +274,7 @@ def __init__(self, domain, wavelet, nlevels=None, pad_mode='constant', Parameters ---------- - domain : `DiscreteLp` + domain : `DiscretizedSpace` Domain of the wavelet transform (the "image domain"). wavelet : string or `pywt.Wavelet` Specification of the wavelet to be used in the transform. @@ -485,7 +487,7 @@ def __init__(self, range, wavelet, nlevels=None, pad_mode='constant', Parameters ---------- - range : `DiscreteLp` + range : `DiscretizedSpace` Domain of the forward wavelet transform (the "image domain"), which is the range of this inverse transform. wavelet : string or `pywt.Wavelet` diff --git a/odl/util/__init__.py b/odl/util/__init__.py index 7d91fd757b3..27e8af1463d 100644 --- a/odl/util/__init__.py +++ b/odl/util/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2014-2019 The ODL contributors +# Copyright 2014-2020 The ODL contributors # # This file is part of ODL. # @@ -6,31 +6,26 @@ # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. -"""Utility library for ODL, mainly for internal use.""" +"""Utilities mainly for internal use.""" from __future__ import absolute_import __all__ = () -from .testutils import * -__all__ += testutils.__all__ - from .utility import * -__all__ += utility.__all__ - from .npy_compat import * -__all__ += npy_compat.__all__ - from .normalize import * -__all__ += normalize.__all__ - from .graphics import * -__all__ += graphics.__all__ - from .numerics import * -__all__ += numerics.__all__ - from .vectorization import * -__all__ += vectorization.__all__ +from .testutils import * from . import ufuncs + +__all__ += utility.__all__ +__all__ += npy_compat.__all__ +__all__ += normalize.__all__ +__all__ += graphics.__all__ +__all__ += numerics.__all__ +__all__ += vectorization.__all__ +__all__ += testutils.__all__ diff --git a/odl/util/utility.py b/odl/util/utility.py index 45297db4bc8..9b5732d91e8 100644 --- a/odl/util/utility.py +++ b/odl/util/utility.py @@ -1,4 +1,4 @@ -# Copyright 2014-2019 The ODL contributors +# Copyright 2014-2020 The ODL contributors # # This file is part of ODL. # @@ -11,11 +11,9 @@ from __future__ import absolute_import, division, print_function from future.moves.itertools import zip_longest -import inspect -import sys +import contextlib from collections import OrderedDict from contextlib import contextmanager -from functools import wraps from itertools import product import numpy as np @@ -40,7 +38,7 @@ 'is_string', 'nd_iterator', 'conj_exponent', - 'none_context', + 'nullcontext', 'writable_array', 'signature_string', 'signature_string_parts', @@ -61,11 +59,6 @@ for rdt, cdt in TYPE_MAP_R2C.items()} TYPE_MAP_C2R.update({k: k for k in TYPE_MAP_R2C.keys()}) -if sys.version_info.major < 3: - getargspec = inspect.getargspec -else: - getargspec = inspect.getfullargspec - def indent(string, indent_str=' '): """Return a copy of ``string`` indented by ``indent_str``. @@ -157,11 +150,12 @@ def dedent(string, indent_str=' ', max_levels=None): lines = string.splitlines() - # Determine common (minumum) number of indentation levels, capped at + # Determine common (minimum) number of indentation levels, capped at # `max_levels` if given def num_indents(line): max_num = int(np.ceil(len(line) / len(indent_str))) + i = 0 # set for the case the loop is not run (`max_num == 0`) for i in range(max_num): if line.startswith(indent_str): line = line[len(indent_str):] @@ -316,42 +310,6 @@ def dtype_str(dtype): return '{}'.format(dtype) -def with_metaclass(meta, *bases): - """ - Function from jinja2/_compat.py. License: BSD. - - Use it like this:: - - class BaseForm(object): - pass - - class FormType(type): - pass - - class Form(with_metaclass(FormType, BaseForm)): - pass - - This requires a bit of explanation: the basic idea is to make a - dummy metaclass for one level of class instantiation that replaces - itself with the actual metaclass. Because of internal type checks - we also need to make sure that we downgrade the custom metaclass - for one level to something closer to type (that's why __call__ and - __init__ comes back from type etc.). - - This has the advantage over six.with_metaclass of not introducing - dummy classes into the final MRO. - """ - class metaclass(meta): - __call__ = type.__call__ - __init__ = type.__init__ - - def __new__(cls, name, this_bases, d): - if this_bases is None: - return type.__new__(cls, name, (), d) - return meta(name, bases, d) - return metaclass('temporary_class', None, {}) - - def cache_arguments(function): """Decorate function to cache the result with given arguments. @@ -589,88 +547,24 @@ def conj_exponent(exp): return exp / (exp - 1.0) -def preload_first_arg(instance, mode): - """Decorator to preload the first argument of a call method. - - Parameters - ---------- - instance : - Class instance to preload the call with - mode : {'out-of-place', 'in-place'} - - 'out-of-place': call is out-of-place -- ``f(x, **kwargs)`` - - 'in-place': call is in-place -- ``f(x, out, **kwargs)`` - - Notes - ----- - The decorated function has the signature according to ``mode``. +@contextmanager +def nullcontext(enter_result=None): + """Backport of the Python >=3.7 trivial context manager. - Examples - -------- - Define two functions which need some instance to act on and decorate - them manually: - - >>> class A(object): - ... '''My name is A.''' - >>> a = A() - ... - >>> def f_oop(inst, x): - ... print(inst.__doc__) - ... - >>> def f_ip(inst, out, x): - ... print(inst.__doc__) - ... - >>> f_oop_new = preload_first_arg(a, 'out-of-place')(f_oop) - >>> f_ip_new = preload_first_arg(a, 'in-place')(f_ip) - ... - >>> f_oop_new(0) - My name is A. - >>> f_ip_new(0, out=1) - My name is A. - - Decorate upon definition: - - >>> @preload_first_arg(a, 'out-of-place') - ... def set_x(obj, x): - ... '''Function to set x in ``obj`` to a given value.''' - ... obj.x = x - >>> set_x(0) - >>> a.x - 0 - - The function's name and docstring are preserved: - - >>> set_x.__name__ - 'set_x' - >>> set_x.__doc__ - 'Function to set x in ``obj`` to a given value.' + See `the Python documentation + `_ + for details. """ - - def decorator(call): - - @wraps(call) - def oop_wrapper(x, **kwargs): - return call(instance, x, **kwargs) - - @wraps(call) - def ip_wrapper(x, out, **kwargs): - return call(instance, x, out, **kwargs) - - if mode == 'out-of-place': - return oop_wrapper - elif mode == 'in-place': - return ip_wrapper - else: - raise ValueError('bad mode {!r}'.format(mode)) - - return decorator + try: + yield enter_result + finally: + pass -@contextmanager -def none_context(*args, **kwargs): - """Trivial context manager, accepts arbitrary args and returns ``None``.""" - yield +try: + nullcontext = contextlib.nullcontext +except AttributeError: + pass @contextmanager @@ -723,11 +617,13 @@ def writable_array(obj, **kwargs): >>> print(lst) [2, 4, 6] """ + arr = None try: arr = np.asarray(obj, **kwargs) yield arr finally: - obj[:] = arr + if arr is not None: + obj[:] = arr def signature_string(posargs, optargs, sep=', ', mod='!r'): @@ -1084,7 +980,7 @@ def repr_string(outer_string, inner_strings, allow_mixed_seps=True): Parameters ---------- - outer_str : str + outer_string : str Name of the class or function that should be printed outside the parentheses. inner_strings : sequence of sequence of str @@ -1556,13 +1452,15 @@ def npy_random_seed(seed): True """ do_seed = seed is not None + orig_rng_state = None try: if do_seed: orig_rng_state = np.random.get_state() np.random.seed(seed) yield + finally: - if do_seed: + if do_seed and orig_rng_state is not None: np.random.set_state(orig_rng_state) @@ -1599,7 +1497,7 @@ def unique(seq): try: return list(OrderedDict.fromkeys(seq)) except TypeError: - # Unhashable, resort to O(n^2) + # Non-hashable, resort to O(n^2) unique_values = [] for i in seq: if i not in unique_values: