diff --git a/doc/source/getting_started/code/getting_started_convolution.py b/doc/source/getting_started/code/getting_started_convolution.py index fa0e4c78022..bee0e914dc4 100644 --- a/doc/source/getting_started/code/getting_started_convolution.py +++ b/doc/source/getting_started/code/getting_started_convolution.py @@ -10,17 +10,18 @@ class Convolution(odl.Operator): The operator inherits from ``odl.Operator`` to be able to be used with ODL. """ - def __init__(self, kernel): + def __init__(self, space, kernel): """Initialize a convolution operator with a known kernel.""" - # Store the kernel - self.kernel = kernel - # Initialize the Operator class by calling its __init__ method. # This sets properties such as domain and range and allows the other # operator convenience functions to work. super(Convolution, self).__init__( - domain=kernel.space, range=kernel.space, linear=True) + domain=space, range=space, linear=True + ) + + # Store the kernel + self.kernel = kernel def _call(self, x): """Implement calling the operator by calling scipy.""" @@ -39,7 +40,7 @@ def adjoint(self): kernel = odl.phantom.cuboid(space, [-0.05, -0.05], [0.05, 0.05]) # Create convolution operator -A = Convolution(kernel) +A = Convolution(space, kernel) # Create phantom (the "unknown" solution) phantom = odl.phantom.shepp_logan(space, modified=True) @@ -48,9 +49,9 @@ def adjoint(self): g = A(phantom) # Display the results using the show method -kernel.show('kernel') -phantom.show('phantom') -g.show('convolved phantom') +space.show(kernel, 'kernel') +space.show(phantom, 'phantom') +space.show(g, 'convolved phantom') # Landweber @@ -59,13 +60,13 @@ def adjoint(self): f = space.zero() odl.solvers.landweber(A, f, g, niter=100, omega=1 / opnorm ** 2) -f.show('landweber') +space.show(f, 'landweber') # Conjugate gradient f = space.zero() odl.solvers.conjugate_gradient_normal(A, f, g, niter=100) -f.show('conjugate gradient') +space.show(f, 'conjugate gradient') # Tikhonov with identity @@ -76,7 +77,7 @@ def adjoint(self): f = space.zero() odl.solvers.conjugate_gradient(T, f, b, niter=100) -f.show('Tikhonov identity conjugate gradient') +space.show(f, 'Tikhonov identity conjugate gradient') # Tikhonov with gradient @@ -87,7 +88,7 @@ def adjoint(self): f = space.zero() odl.solvers.conjugate_gradient(T, f, b, niter=100) -f.show('Tikhonov gradient conjugate gradient') +space.show(f, 'Tikhonov gradient conjugate gradient') # Douglas-Rachford @@ -114,4 +115,4 @@ def adjoint(self): x = space.zero() odl.solvers.douglas_rachford_pd(x, f, g_funcs, lin_ops, tau=tau, sigma=sigma, niter=100) -x.show('TV Douglas-Rachford', force_show=True) +space.show(x, 'TV Douglas-Rachford', force_show=True) diff --git a/doc/source/getting_started/first_steps.rst b/doc/source/getting_started/first_steps.rst index 796adf1444b..7988c05b0b0 100644 --- a/doc/source/getting_started/first_steps.rst +++ b/doc/source/getting_started/first_steps.rst @@ -45,17 +45,19 @@ and create a wrapping `Operator` for it in ODL. The operator inherits from ``odl.Operator`` to be able to be used with ODL. """ - def __init__(self, kernel): + def __init__(self, space, kernel): """Initialize a convolution operator with a known kernel.""" - # Store the kernel - self.kernel = kernel - # Initialize the Operator class by calling its __init__ method. # This sets properties such as domain and range and allows the other # operator convenience functions to work. super(Convolution, self).__init__( - domain=kernel.space, range=kernel.space, linear=True) + domain=space, range=space, linear=True + ) + + # Store the kernel + self.kernel = kernel + def _call(self, x): """Implement calling the operator by calling scipy.""" @@ -75,7 +77,7 @@ ODL also provides a nice range of standard phantoms such as the `cuboid` and `sh kernel = odl.phantom.cuboid(space, [-0.05, -0.05], [0.05, 0.05]) # Create convolution operator - A = Convolution(kernel) + A = Convolution(space, kernel) # Create phantom (the "unknown" solution) phantom = odl.phantom.shepp_logan(space, modified=True) @@ -84,9 +86,9 @@ ODL also provides a nice range of standard phantoms such as the `cuboid` and `sh g = A(phantom) # Display the results using the show method - kernel.show('kernel') - phantom.show('phantom') - g.show('convolved phantom') + space.show(kernel, title='kernel') + space.show(phantom, title='phantom') + space.show(g, title='convolved phantom') .. image:: figures/getting_started_kernel.png diff --git a/doc/source/guide/code/functional_indepth_example.py b/doc/source/guide/code/functional_indepth_example.py index 427f4b1481a..c4fa72b24ab 100644 --- a/doc/source/guide/code/functional_indepth_example.py +++ b/doc/source/guide/code/functional_indepth_example.py @@ -27,7 +27,7 @@ def __init__(self, space, y): # the functional and always needs to be implemented. def _call(self, x): """Evaluate the functional.""" - return x.norm() ** 2 + x.inner(self.y) + return self.domain.norm(x) ** + self.domain.inner(x, self.y) # Next we define the gradient. Note that this is a property. @property @@ -89,7 +89,7 @@ def __init__(self, space, y): def _call(self, x): """Evaluate the functional.""" - return (x - self.y).norm()**2 / 4.0 + return self.domain.norm(x - self.y) ** 2 / 4 # Create a functional diff --git a/doc/source/guide/faq.rst b/doc/source/guide/faq.rst index c9232ee5cbb..6cb965a6898 100644 --- a/doc/source/guide/faq.rst +++ b/doc/source/guide/faq.rst @@ -49,42 +49,6 @@ General errors This will yield a specific error message for an erroneous module that helps you debugging your changes. -#. **Q:** When adding two space elements, the following error is shown:: - - 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? - - **P:** The elements you are trying to add are not in the same space. - For example, the following code triggers the same error: - - >>> x = odl.uniform_discr(0, 1, 10).one() - >>> y = odl.uniform_discr(0, 1, 11).one() - >>> x - y - - In this case, the problem is that the elements have a different number of entries. - Other possible issues include that they are discretizations of different sets, - have different data types (:term:`dtype`), or implementation (for example CUDA/CPU). - - **S:** The elements need to somehow be cast to the same space. - How to do this depends on the problem at hand. - To find what the issue is, inspect the ``space`` properties of both elements. - For the above example, we see that the issue lies in the number of discretization points: - - >>> x.space - odl.uniform_discr(0, 1, 10) - >>> y.space - odl.uniform_discr(0, 1, 11) - - * In the case of spaces being discretizations of different underlying spaces, - a transformation of some kind has to be applied (for example by using an operator). - In general, errors like this indicates a conceptual issue with the code, - 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 `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 thing does not seem to work. What am I doing wrong? diff --git a/doc/source/guide/functional_guide.rst b/doc/source/guide/functional_guide.rst index 5208cef2147..18184b631bd 100644 --- a/doc/source/guide/functional_guide.rst +++ b/doc/source/guide/functional_guide.rst @@ -177,8 +177,7 @@ All available functional arithmetic, including which properties and methods that | ``(S * a)(x)`` | ``S(a * x)`` | `FunctionalRightScalarMult` | | | | - Retains all properties. | +---------------------+-----------------+--------------------------------------------------------------------------------+ -| ``(v * S)(x)`` | ``v * S(x)`` | `FunctionalLeftVectorMult` | -| | | - Results in an operator rather than a functional. | +| ``(v * S)(x)`` | ``v * S(x)`` | Not supported | +---------------------+-----------------+--------------------------------------------------------------------------------+ | ``(S * v)(x)`` | ``S(v * x)`` | `FunctionalRightVectorMult` | | | | - Retains gradient and convex conjugate. | diff --git a/doc/source/guide/numpy_guide.rst b/doc/source/guide/numpy_guide.rst index 6dce19fa383..3dc31918f83 100644 --- a/doc/source/guide/numpy_guide.rst +++ b/doc/source/guide/numpy_guide.rst @@ -123,14 +123,14 @@ The convolution operation, written as ODL operator, could look like this:: >>> class MyConvolution(odl.Operator): ... """Operator for convolving with a given kernel.""" ... - ... def __init__(self, kernel): + ... def __init__(self, space, kernel): ... """Initialize the convolution.""" - ... self.kernel = kernel - ... ... # Initialize operator base class. ... # This operator maps from the space of vector to the same space and is linear ... super(MyConvolution, self).__init__( - ... domain=kernel.space, range=kernel.space, linear=True) + ... domain=space, range=space, linear=True) + ... + ... self.kernel = kernel ... ... def _call(self, x): ... # The output of an Operator is automatically cast to an ODL object @@ -139,7 +139,7 @@ The convolution operation, written as ODL operator, could look like this:: This operator can then be called on its domain elements:: >>> kernel = odl.rn(3).element([1, 2, 1]) - >>> conv_op = MyConvolution(kernel) + >>> conv_op = MyConvolution(r3, kernel) >>> conv_op([1, 2, 3]) rn(3).element([ 4., 8., 8.]) @@ -149,7 +149,7 @@ It can be also be used with any of the ODL operator functionalities such as mult >>> scaled_op([1, 2, 3]) rn(3).element([ 8., 16., 16.]) >>> y = odl.rn(3).element([1, 1, 1]) - >>> inner_product_op = odl.InnerProductOperator(y) + >>> inner_product_op = odl.InnerProductOperator(r3, y) >>> # Create composition with inner product operator with [1, 1, 1]. >>> # When called on a vector, the result should be the sum of the >>> # convolved vector. diff --git a/doc/source/guide/operator_guide.rst b/doc/source/guide/operator_guide.rst index 158098641c9..df6e4e2fc2c 100644 --- a/doc/source/guide/operator_guide.rst +++ b/doc/source/guide/operator_guide.rst @@ -50,7 +50,7 @@ example:: class MatrixOperator(odl.Operator): ... def _call(self, x, out): - self.matrix.dot(x, out=out.asarray()) + self.matrix.dot(x, out=out) In-place evaluation is usually more efficient and should be used *whenever possible*. @@ -86,8 +86,7 @@ avoided*. # In-place evaluation operator(x, out=y) -This public calling interface is (duck-)type-checked, so the private methods -can safely assume that their input data is of the operator domain element type. +This public calling interface is (duck-)type-checked, so the private methods can safely assume that their input data is of the operator domain element type. Operator arithmetic ------------------- @@ -108,9 +107,7 @@ Another example is matrix multiplication, which corresponds to operator composit .. _functional: https://en.wikipedia.org/wiki/Functional_(mathematics) -All available operator arithmetic is shown below. ``A``, ``B`` represent arbitrary `Operator`'s, -``f`` is an `Operator` whose `Operator.range` is a `Field` (sometimes called a functional_), and -``a`` is a scalar. +All available operator arithmetic is shown below. ``A``, ``B`` represent arbitrary `Operator`'s, ``f`` is an `Operator` whose `Operator.range` is a `Field` (sometimes called a functional_), and ``a`` is a scalar. +------------------+-----------------+----------------------------+ | Code | Meaning | Class | @@ -123,7 +120,7 @@ All available operator arithmetic is shown below. ``A``, ``B`` represent arbitra +------------------+-----------------+----------------------------+ | ``(A * a)(x)`` | ``A(a * x)`` | `OperatorRightScalarMult` | +------------------+-----------------+----------------------------+ -| ``(v * f)(x)`` | ``v * f(x)`` | `FunctionalLeftVectorMult` | +| ``(v * f)(x)`` | ``v * f(x)`` | Not supported (*) | +------------------+-----------------+----------------------------+ | ``(v * A)(x)`` | ``v * A(x)`` | `OperatorLeftVectorMult` | +------------------+-----------------+----------------------------+ @@ -132,23 +129,24 @@ All available operator arithmetic is shown below. ``A``, ``B`` represent arbitra | not available | ``A(x) * B(x)`` | `OperatorPointwiseProduct` | +------------------+-----------------+----------------------------+ +(*) The range of such an expression, if interpreted as operator, cannot be inferred. + There are also a few derived expressions using the above: -+------------------+--------------------------------------+ -| Code | Meaning | -+==================+======================================+ -| ``(+A)(x)`` | ``A(x)`` | -+------------------+--------------------------------------+ -| ``(-A)(x)`` | ``(-1) * A(x)`` | -+------------------+--------------------------------------+ -| ``(A - B)(x)`` | ``A(x) + (-1) * B(x)`` | -+------------------+--------------------------------------+ -| ``A**n(x)`` | ``A(A**(n-1)(x))``, ``A^1(x) = A(x)``| -+------------------+--------------------------------------+ -| ``(A / a)(x)`` | ``A((1/a) * x)`` | -+------------------+--------------------------------------+ -| ``(A @ B)(x)`` | ``(A * B)(x)`` | -+------------------+--------------------------------------+ - -Except for composition, operator arithmetic is generally only defined when `Operator.domain` and -`Operator.range` are either instances of `LinearSpace` or `Field`. ++------------------+-------------------------------------------+ +| Code | Meaning | ++==================+===========================================+ +| ``(+A)(x)`` | ``A(x)`` | ++------------------+-------------------------------------------+ +| ``(-A)(x)`` | ``(-1) * A(x)`` | ++------------------+-------------------------------------------+ +| ``(A - B)(x)`` | ``A(x) + (-1) * B(x)`` | ++------------------+-------------------------------------------+ +| ``(A **n)(x)`` | ``A((A ** (n-1))(x))``, ``A^1(x) = A(x)`` | ++------------------+-------------------------------------------+ +| ``(A / a)(x)`` | ``A((1/a) * x)`` | ++------------------+-------------------------------------------+ +| ``(A @ B)(x)`` | ``(A * B)(x)`` | ++------------------+-------------------------------------------+ + +Except for composition, operator arithmetic is generally only defined when `Operator.domain` and `Operator.range` are either instances of `LinearSpace` or `Field`. diff --git a/examples/deform/linearized_fixed_displacement.py b/examples/deform/linearized_fixed_displacement.py index af32996ff04..c9a0ec7f0c4 100644 --- a/examples/deform/linearized_fixed_displacement.py +++ b/examples/deform/linearized_fixed_displacement.py @@ -44,15 +44,15 @@ disp_field = disp_field_space.element(disp_func) # Show template and displacement field -template.show('Template') -disp_field.show('Displacement field') +templ_space.show(template, 'Template') +disp_field_space.show(disp_field, 'Displacement field') # --- Apply LinDeformFixedDisp and its adjoint --- # # Initialize the deformation operator with fixed displacement -deform_op = odl.deform.LinDeformFixedDisp(disp_field) +deform_op = odl.deform.LinDeformFixedDisp(templ_space, disp_field) # Apply the deformation operator to get the deformed template. deformed_template = deform_op(template) @@ -61,5 +61,7 @@ adj_result = deform_op.adjoint(template) # Show results -deformed_template.show('Deformed template') -adj_result.show('Adjoint applied to the template', force_show=True) +templ_space.show(deformed_template, 'Deformed template') +templ_space.show( + adj_result, 'Adjoint applied to the template', force_show=True +) diff --git a/examples/deform/linearized_fixed_template.py b/examples/deform/linearized_fixed_template.py index eb75f7362d1..0ce56696bab 100644 --- a/examples/deform/linearized_fixed_template.py +++ b/examples/deform/linearized_fixed_template.py @@ -43,15 +43,15 @@ disp_field = disp_field_space.element(disp_func) # Show template and displacement field -template.show('Template') -disp_field.show('Displacement field') +templ_space.show(template, 'Template') +disp_field_space.show(disp_field, 'Displacement field') # --- Apply LinDeformFixedTempl, derivative and its adjoint --- # # Initialize the deformation operator with fixed template -deform_op = odl.deform.LinDeformFixedTempl(template) +deform_op = odl.deform.LinDeformFixedTempl(templ_space, template) # Apply the deformation operator to get the deformed template. deformed_template = deform_op(disp_field) @@ -68,7 +68,10 @@ deriv_adj_result = deform_op_deriv.adjoint(templ_space.one()) # Show results -deformed_template.show('Deformed template') -deriv_result.show('Operator derivative applied to one()') -deriv_adj_result.show('Adjoint of the derivative applied to one()', - force_show=True) +templ_space.show(deformed_template, 'Deformed template') +templ_space.show(deriv_result, 'Operator derivative applied to one()') +disp_field_space.show( + deriv_adj_result, + 'Adjoint of the derivative applied to one()', + force_show=True, +) diff --git a/examples/operator/convolution_operator.py b/examples/operator/convolution_operator.py index bf344a183e4..9d600109e93 100644 --- a/examples/operator/convolution_operator.py +++ b/examples/operator/convolution_operator.py @@ -10,7 +10,7 @@ class Convolution(odl.Operator): The operator inherits from ``odl.Operator`` to be able to be used with ODL. """ - def __init__(self, kernel): + def __init__(self, space, kernel): """Initialize a convolution operator with a known kernel.""" # Store the kernel @@ -20,7 +20,8 @@ def __init__(self, kernel): # This sets properties such as domain and range and allows the other # operator convenience functions to work. super(Convolution, self).__init__( - domain=kernel.space, range=kernel.space, linear=True) + domain=space, range=space, linear=True + ) def _call(self, x): """Implement calling the operator by calling scipy.""" @@ -45,7 +46,7 @@ def adjoint(self): kernel = odl.phantom.cuboid(space, [-0.05, -0.05], [0.05, 0.05]) # Create convolution operator -A = Convolution(kernel) +A = Convolution(space, kernel) # Create phantom (the "unknown" solution) phantom = odl.phantom.shepp_logan(space, modified=True) @@ -54,6 +55,6 @@ def adjoint(self): g = A(phantom) # Display the results using the show method -kernel.show('kernel') -phantom.show('phantom') -g.show('convolved phantom') +space.show(kernel, 'Convolution Kernel') +space.show(phantom, 'Phantom') +space.show(g, 'Convolved Phantom') diff --git a/examples/solvers/adupdates_tomography.py b/examples/solvers/adupdates_tomography.py index 5b61df4e1c9..72c850840c7 100644 --- a/examples/solvers/adupdates_tomography.py +++ b/examples/solvers/adupdates_tomography.py @@ -74,8 +74,8 @@ # Create the artificial data. data_spaces = [op.range for op in ray_trafos] noisefree_data = [op(phantom) for op in ray_trafos] -data = [proj + 0.10 * np.ptp(proj) * odl.phantom.white_noise(proj.space) - for proj in noisefree_data] +data = [proj + 0.10 * np.ptp(proj) * odl.phantom.white_noise(data_space) + for proj, data_space in zip(noisefree_data, data_spaces)] # Functionals and operators for the total variation. This is the l1 norm of the # (discretized) gradient of the reconstruction. For each of the dimensions diff --git a/examples/solvers/bregman_tv_tomography.py b/examples/solvers/bregman_tv_tomography.py index 2f63e0d1323..e85bfa3a9b2 100644 --- a/examples/solvers/bregman_tv_tomography.py +++ b/examples/solvers/bregman_tv_tomography.py @@ -54,8 +54,9 @@ # Create phantom, forward project to create sinograms, and add 10% noise discr_phantom = odl.phantom.shepp_logan(reco_space, modified=True) noise_free_data = ray_trafo(discr_phantom) +noise_free_data_norm = ray_trafo.range.norm(noise_free_data) noise = odl.phantom.white_noise(ray_trafo.range) -noise *= 0.10 / noise.norm() * noise_free_data.norm() +noise *= 0.10 / ray_trafo.range.norm(noise) * noise_free_data_norm data = noise_free_data + noise # Components for variational problem: l2-squared data matching and isotropic diff --git a/examples/solvers/deconvolution_1d.py b/examples/solvers/deconvolution_1d.py index b60d6d23d55..75f5151ee18 100644 --- a/examples/solvers/deconvolution_1d.py +++ b/examples/solvers/deconvolution_1d.py @@ -7,13 +7,16 @@ class Convolution(odl.Operator): - def __init__(self, kernel, adjkernel=None): - self.kernel = kernel - self.adjkernel = (adjkernel if adjkernel is not None - else kernel.space.element(kernel[::-1].copy())) - self.norm = float(np.sum(np.abs(self.kernel))) + def __init__(self, space, kernel, adjkernel=None): super(Convolution, self).__init__( - domain=kernel.space, range=kernel.space, linear=True) + domain=space, range=space, linear=True + ) + self.kernel = np.asarray(kernel) + if adjkernel is None: + self.adjkernel = kernel[::-1] + else: + self.adjkernel = np.asarray(adjkernel) + self._norm = float(np.sum(np.abs(self.kernel))) def _call(self, x): return scipy.signal.convolve(x, self.kernel, mode='same') @@ -22,8 +25,8 @@ def _call(self, x): def adjoint(self): return Convolution(self.adjkernel, self.kernel) - def opnorm(self): - return self.norm + def norm(self): + return self._norm # Discretization @@ -38,7 +41,7 @@ def opnorm(self): # Dampening parameter for landweber iterations = 100 -omega = 1 / conv.opnorm() ** 2 +omega = 1 / conv.norm() ** 2 # Display callback diff --git a/examples/solvers/douglas_rachford_pd_heron.py b/examples/solvers/douglas_rachford_pd_heron.py index 36a469c7831..78ccc1f8a27 100644 --- a/examples/solvers/douglas_rachford_pd_heron.py +++ b/examples/solvers/douglas_rachford_pd_heron.py @@ -53,7 +53,7 @@ def print_objective(x): value = 0 for minp, maxp in rectangles: x_proj = np.minimum(np.maximum(x, minp), maxp) - value += (x - x_proj).norm() + value += space.norm(x - x_proj) print('Point = [{:.4f}, {:.4f}], Value = {:.4f}'.format(x[0], x[1], value)) @@ -63,14 +63,15 @@ def print_objective(x): tau=tau, sigma=sigma, niter=20, lam=lam, callback=print_objective, l=l) -# plot the result +# Plot the result +fig, ax = plt.subplots() for minp, maxp in rectangles: xp = [minp[0], maxp[0], maxp[0], minp[0], minp[0]] yp = [minp[1], minp[1], maxp[1], maxp[1], minp[1]] - plt.plot(xp, yp) + ax.plot(xp, yp) -plt.scatter(x[0], x[1]) +ax.scatter(x[0], x[1]) -plt.xlim(-1, 4) -plt.ylim(-1, 4) -plt.show() +ax.set_xlim(-1, 4) +ax.set_ylim(-1, 4) +fig.show() diff --git a/examples/solvers/douglas_rachford_pd_mri.py b/examples/solvers/douglas_rachford_pd_mri.py index adf3f5448c0..460bde16e38 100644 --- a/examples/solvers/douglas_rachford_pd_mri.py +++ b/examples/solvers/douglas_rachford_pd_mri.py @@ -28,8 +28,8 @@ # Create noisy MRI data phantom = odl.phantom.shepp_logan(space, modified=True) noisy_data = mri_op(phantom) + odl.phantom.white_noise(mri_op.range) * 0.1 -phantom.show('Phantom') -noisy_data.show('Noisy MRI Data') +space.show(phantom, 'Phantom') +ft.range.show(noisy_data, 'Noisy MRI Data') # Gradient for TV regularization gradient = odl.Gradient(space) @@ -44,11 +44,11 @@ # Solve x = mri_op.domain.zero() -callback = (odl.solvers.CallbackShow(step=5, clim=[0, 1]) & +callback = (odl.solvers.CallbackShow(space, step=5, clim=[0, 1]) & odl.solvers.CallbackPrintIteration()) odl.solvers.douglas_rachford_pd(x, f, g, lin_ops, tau=2.0, sigma=[1.0, 0.1], niter=500, callback=callback) -x.show('Douglas-Rachford Result') -ft.inverse(noisy_data).show('Fourier Inversion Result', force_show=True) +space.show(x, 'TV-regularized Result (Douglas-Rachford)') +space.show(ft.inverse(noisy_data), 'Fourier Inversion Result', force_show=True) diff --git a/examples/solvers/douglas_rachford_pd_tomography_tv.py b/examples/solvers/douglas_rachford_pd_tomography_tv.py index c5613d0f16f..3e6ea85c0ea 100644 --- a/examples/solvers/douglas_rachford_pd_tomography_tv.py +++ b/examples/solvers/douglas_rachford_pd_tomography_tv.py @@ -85,7 +85,7 @@ # Add noise to data raw_noise = odl.phantom.white_noise(ray_trafo.range) - data += raw_noise * eps / raw_noise.norm() + data += raw_noise * eps / ray_trafo.range.norm(raw_noise) # Create indicator indicator_l2_ball = odl.solvers.IndicatorLpUnitBall(ray_trafo.range, 2) diff --git a/examples/solvers/forward_backward_pd_denoising.py b/examples/solvers/forward_backward_pd_denoising.py index 707b8968a60..00bb4666bd3 100755 --- a/examples/solvers/forward_backward_pd_denoising.py +++ b/examples/solvers/forward_backward_pd_denoising.py @@ -45,7 +45,7 @@ h = 0.5 * odl.solvers.L2NormSquared(space).translated(noisy_data) # Create initial guess for the solver. -x = noisy_data.copy() +x = space.copy(noisy_data) # Used to display intermediate results and print iteration number. callback = (odl.solvers.CallbackShow(step=20, clim=[0, 255]) & diff --git a/examples/solvers/functional_basic_example.py b/examples/solvers/functional_basic_example.py index c79b4e9c9a4..0490662e0d4 100644 --- a/examples/solvers/functional_basic_example.py +++ b/examples/solvers/functional_basic_example.py @@ -23,7 +23,7 @@ def __init__(self, space): def _call(self, x): # This is what is returned when calling my_func(x) - return x.norm()**2 + return self.domain.norm(x) ** 2 @property def gradient(self): @@ -42,26 +42,40 @@ def convex_conj(self): space = odl.rn(n) my_func = MyFunctional(space=space) -# The functional evaluates correctly +# Functional evaluation x = space.element(np.random.randn(n)) -print(my_func(x) == x.norm() ** 2) - -# The gradient works -my_gradient = my_func.gradient -print(my_gradient(x) == 2.0 * x) - -# The standard implementation of the directional derivative works +print( + 'f(x) == ||x||^2 ?', + my_func(x) == space.norm(x) ** 2, +) + +# Gradient +my_grad = my_func.gradient +print( + 'grad f(x) == 2 * x ?', + all(my_grad(x) == 2.0 * x), +) + +# Derivative (implemented via gradient) p = space.element(np.random.randn(n)) my_deriv = my_func.derivative(x) -print(my_deriv(p) == my_gradient(x).inner(p)) +print( + 'Df(p)(x) == ?', + my_deriv(p) == space.inner(my_grad(x), p), +) -# The conjugate functional works +# Convex conjugate my_func_conj = my_func.convex_conj -print(my_func_conj(x) == 1.0 / 4.0 * x.norm() ** 2) +print( + 'f*(x) == ||x||^2 / 4 ?', + my_func_conj(x) == space.norm(x) ** 2 / 4, +) -# As a final, a bit more advanced, test, this check that the a scaled and -# translated version of the functional evalutes the gradient correctly +# Scaling and translating a functional, checking the gradient scal = np.random.rand() transl = space.element(np.random.randn(n)) -scal_and_transl_func_gradient = (scal * my_func.translated(transl)).gradient -print(scal_and_transl_func_gradient(x) == scal * my_func.gradient(x - transl)) +scal_transl_grad = (scal * my_func.translated(transl)).gradient +print( + 'grad [s * f(. - t)](x) == s * grad f(x - t) ?', + all(scal_transl_grad(x) == scal * my_func.gradient(x - transl)), +) diff --git a/examples/solvers/pdhg_denoising.py b/examples/solvers/pdhg_denoising.py index ed2662d3cf9..04327705ccc 100644 --- a/examples/solvers/pdhg_denoising.py +++ b/examples/solvers/pdhg_denoising.py @@ -15,66 +15,57 @@ import odl # Read test image: use only every second pixel, convert integer to float, -# and rotate to get the image upright +# and rotate to get the image upright, and rescale to [0, 1] image = np.rot90(scipy.misc.ascent()[::2, ::2], 3).astype('float') shape = image.shape - -# Rescale max to 1 image /= image.max() -# Discretized spaces +# Reconstruction space with pixel size 1 space = odl.uniform_discr([0, 0], shape, shape) -# Original image -orig = space.element(image) - -# Add noise -image += 0.1 * odl.phantom.white_noise(orig.space) - -# Data of noisy image -noisy = space.element(image) - -# Gradient operator -gradient = odl.Gradient(space) +# Noisy version of the image +noisy = image + 0.1 * odl.phantom.white_noise(space) -# Matrix of operators -op = odl.BroadcastOperator(odl.IdentityOperator(space), gradient) +# Make operator x -> (x, grad(x)) +grad = odl.Gradient(space) +L = odl.BroadcastOperator(odl.IdentityOperator(space), grad) -# Set up the functionals +# --- Problem Definition --- # -# l2-squared data matching -l2_norm = odl.solvers.L2NormSquared(space).translated(noisy) +# Squared L2 norm as data fit +data_fit = odl.solvers.L2NormSquared(space).translated(noisy) -# Isotropic TV-regularization: l1-norm of grad(x) -l1_norm = 0.15 * odl.solvers.L1Norm(gradient.range) +# Anisotropic TV-regularization: L1 norm of grad(x) +regularizer = 0.15 * odl.solvers.L1Norm(grad.range) -# Make separable sum of functionals, order must correspond to the operator K -g = odl.solvers.SeparableSum(l2_norm, l1_norm) +# Separable sum of functionals, order corresponding to the operator L +g = odl.solvers.SeparableSum(data_fit, regularizer) # Non-negativity constraint -f = odl.solvers.IndicatorNonnegativity(op.domain) +f = odl.solvers.IndicatorNonnegativity(L.domain) # --- Select solver parameters and solve using PDHG --- # -# Estimated operator norm, add 10 percent to ensure ||K||_2^2 * sigma * tau < 1 -op_norm = 1.1 * odl.power_method_opnorm(op, xstart=noisy) +# Estimated operator norm, adding 10 percent safety margin +L_norm = 1.1 * odl.power_method_opnorm(L, xstart=noisy) niter = 200 # Number of iterations -tau = 1.0 / op_norm # Step size for the primal variable -sigma = 1.0 / op_norm # Step size for the dual variable +tau = 1.0 / L_norm # Step size for the primal variable +sigma = 1.0 / L_norm # Step size for the dual variable # Optional: pass callback objects to solver -callback = (odl.solvers.CallbackPrintIteration() & - odl.solvers.CallbackShow(step=5)) +callback = (odl.solvers.CallbackPrintIteration(step=5) & + odl.solvers.CallbackShow(space, step=5)) # Starting point -x = op.domain.zero() +x = L.domain.zero() # Run algorithm (and display intermediates) -odl.solvers.pdhg(x, f, g, op, niter=niter, tau=tau, sigma=sigma, - callback=callback) +odl.solvers.pdhg( + x, f, g, L, niter=niter, tau=tau, sigma=sigma, callback=callback +) # Display images -orig.show(title='Original Image') -noisy.show(title='Noisy Image') -x.show(title='Reconstruction', force_show=True) +space.show(image, title='Original Image') +space.show(noisy, title='Noisy Image') +space.show(x, title='Reconstruction', force_show=True) diff --git a/examples/solvers/pdhg_denoising_complex.py b/examples/solvers/pdhg_denoising_complex.py index 7b2fdb3148f..155c77be11d 100644 --- a/examples/solvers/pdhg_denoising_complex.py +++ b/examples/solvers/pdhg_denoising_complex.py @@ -30,7 +30,7 @@ orig = space.element(image) # Add noise -noisy = image + 0.05 * odl.phantom.white_noise(orig.space) +noisy = image + 0.05 * odl.phantom.white_noise(space) # Gradient operator gradient = odl.Gradient(space) diff --git a/examples/solvers/proximal_gradient_denoising.py b/examples/solvers/proximal_gradient_denoising.py index 668fe8f4920..959b83c0c86 100644 --- a/examples/solvers/proximal_gradient_denoising.py +++ b/examples/solvers/proximal_gradient_denoising.py @@ -1,4 +1,4 @@ -"""L1-regularized denoising using the proximal gradient solvers. +"""Removal of salt-and-pepper noise using the proximal gradient solvers. Solves the optimization problem @@ -6,35 +6,32 @@ Where ``grad`` is the spatial gradient operator and ``g`` is given noisy data. -The proximal gradient solvers are also known as ISTA and FISTA. +The proximal gradient solvers are also known as ISTA and FISTA, respectively. """ import odl - # --- Set up problem definition --- # - -# Define function space: discretized functions on the rectangle -# [-20, 20]^2 with 300 samples per dimension. +# Reconstruction space: discretized functions on the rectangle [-20, 20]^2 +# with 300 samples per dimension space = odl.uniform_discr( - min_pt=[-20, -20], max_pt=[20, 20], shape=[300, 300]) - -# Create phantom -data = odl.phantom.shepp_logan(space, modified=True) -data = odl.phantom.salt_pepper_noise(data) - -# Create gradient operator -grad = odl.Gradient(space) + min_pt=[-20, -20], max_pt=[20, 20], shape=[300, 300] +) +# Create noisy phantom +data = odl.phantom.salt_pepper_noise( + space, odl.phantom.shepp_logan(space, modified=True) +) # --- Set up the inverse problem --- # -# Create data discrepancy by translating the l1 norm +# Create data fit term by translating the L1 norm l1_norm = odl.solvers.L1Norm(space) -data_discrepancy = l1_norm.translated(data) +data_fit = l1_norm.translated(data) -# l2-squared norm of gradient +# Use squared L2 norm of the gradient for regularization +grad = odl.Gradient(space) regularizer = 0.05 * odl.solvers.L2NormSquared(grad.range) * grad # --- Select solver parameters and solve using proximal gradient --- # @@ -43,24 +40,23 @@ gamma = 0.01 # Optionally pass callback to the solver to display intermediate results -callback = (odl.solvers.CallbackPrintIteration() & - odl.solvers.CallbackShow()) +callback = (odl.solvers.CallbackPrintIteration(step=5) & + odl.solvers.CallbackShow(space, step=5)) # Run the algorithm (ISTA) x = space.zero() odl.solvers.proximal_gradient( - x, f=data_discrepancy, g=regularizer, niter=200, gamma=gamma, - callback=callback) + x, f=data_fit, g=regularizer, niter=200, gamma=gamma, callback=callback +) -# Compare to accelerated version (FISTA) which is much faster +# Compare to accelerated version (FISTA) which converges much faster callback.reset() x_acc = space.zero() odl.solvers.accelerated_proximal_gradient( - x_acc, f=data_discrepancy, g=regularizer, niter=50, gamma=gamma, - callback=callback) + x_acc, f=data_fit, g=regularizer, niter=50, gamma=gamma, callback=callback +) # Display images -data.show(title='Data') -x.show(title='L1 Regularized Reconstruction') -x_acc.show(title='L1 Regularized Reconstruction (Accelerated)', - force_show=True) +space.show(data, title='Noisy Image') +space.show(x, title='L1-denoised Image (ISTA)') +space.show(x_acc, title='L1-denoised Image (FISTA)', force_show=True) diff --git a/examples/solvers/rosenbrock_minimization.py b/examples/solvers/rosenbrock_minimization.py index d15d3176791..167ace3a99e 100644 --- a/examples/solvers/rosenbrock_minimization.py +++ b/examples/solvers/rosenbrock_minimization.py @@ -1,20 +1,3 @@ -# Copyright 2014-2016 The ODL development group -# -# This file is part of ODL. -# -# ODL is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# ODL is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with ODL. If not, see . - """Minimize the Rosenbrock functional. This example shows how this can be done using a variety of solution methods. @@ -23,51 +6,71 @@ import odl from matplotlib import pyplot as plt -# Create the solution space space = odl.rn(2) - -# Create objective functional f = odl.solvers.RosenbrockFunctional(space) - -# Define a line search method line_search = odl.solvers.BacktrackingLineSearch(f) +# --- Steepest Descent --- # -# Solve problem using steepest descent -callback = odl.solvers.CallbackShowConvergence(f, logx=True, logy=True, - color='b') +callback = odl.solvers.CallbackShowConvergence( + f, logx=True, logy=True, color='b' +) x = space.zero() -odl.solvers.steepest_descent(f, x, line_search=line_search, - callback=callback) -legend_artists = [callback.ax.collections[-1], ] -legend_labels = ['SD', ] +odl.solvers.steepest_descent( + f, x, line_search=line_search, callback=callback +) +legend_artists = [callback.ax.collections[-1]] +legend_labels = ['SD'] -# Solve problem using nonlinear conjugate gradient -callback = odl.solvers.CallbackShowConvergence(f, logx=True, logy=True, - color='g') +# --- Nonlinear CG --- # + +callback = odl.solvers.CallbackShowConvergence( + f, logx=True, logy=True, color='g' +) x = space.zero() -odl.solvers.conjugate_gradient_nonlinear(f, x, line_search=line_search, - callback=callback) +odl.solvers.conjugate_gradient_nonlinear( + f, x, line_search=line_search, callback=callback +) legend_artists.append(callback.ax.collections[-1]) legend_labels.append('CG') -# Solve problem using bfgs -callback = odl.solvers.CallbackShowConvergence(f, logx=True, logy=True, - color='r') +# --- Broyden's Method --- # + +callback = odl.solvers.CallbackShowConvergence( + f, logx=True, logy=True, color='m' +) x = space.zero() -odl.solvers.bfgs_method(f, x, line_search=line_search, - callback=callback) +odl.solvers.broydens_method( + f, x, line_search=line_search, callback=callback +) +legend_artists.append(callback.ax.collections[-1]) +legend_labels.append('Broyden') + +# --- BFGS --- # + +callback = odl.solvers.CallbackShowConvergence( + f, logx=True, logy=True, color='r' +) +x = space.zero() +odl.solvers.bfgs_method( + f, x, line_search=line_search, callback=callback +) legend_artists.append(callback.ax.collections[-1]) legend_labels.append('BFGS') -# Solve problem using newtons method -callback = odl.solvers.CallbackShowConvergence(f, logx=True, logy=True, - color='k') +# --- Newton's Method --- # + +callback = odl.solvers.CallbackShowConvergence( + f, logx=True, logy=True, color='k' +) x = space.zero() -odl.solvers.newtons_method(f, x, line_search=line_search, - callback=callback) +odl.solvers.newtons_method( + f, x, line_search=line_search, callback=callback +) legend_artists.append(callback.ax.collections[-1]) legend_labels.append('Newton') +# --- Add legend to plots and show it --- # + plt.legend(legend_artists, legend_labels) plt.show() diff --git a/examples/solvers/scipy_solvers.py b/examples/solvers/scipy_solvers.py index 6414ed63fd0..a8cc18fcc86 100644 --- a/examples/solvers/scipy_solvers.py +++ b/examples/solvers/scipy_solvers.py @@ -23,11 +23,8 @@ # Convert laplacian to scipy operator scipy_laplacian = odl.operator.oputils.as_scipy_operator(laplacian) -# Convert to array and flatten -rhs_arr = rhs.asarray().ravel() - # Solve using scipy -result, info = scipy_solvers.cg(scipy_laplacian, rhs_arr) +result, info = scipy_solvers.cg(scipy_laplacian, rhs.ravel()) # Other options include # result, info = scipy_solvers.cgs(scipy_laplacian, rhs_arr) @@ -38,5 +35,5 @@ # Convert back to odl and display result result_odl = space.element(result.reshape(space.shape)) # result is flat -result_odl.show('Result') -(rhs - laplacian(result_odl)).show('Residual', force_show=True) +space.show(result_odl, 'Result') +space.show(rhs - laplacian(result_odl), 'Residual', force_show=True) diff --git a/examples/space/simple_r.py b/examples/space/simple_r.py index f35766424bc..bb684a1c5d9 100644 --- a/examples/space/simple_r.py +++ b/examples/space/simple_r.py @@ -22,23 +22,7 @@ def __eq__(self, other): return isinstance(other, Reals) def element(self, value=0): - return RealNumber(self, value) - - -class RealNumber(odl.set.space.LinearSpaceElement): - """Real vectors are floats.""" - - __val__ = None - - def __init__(self, space, v): - super(RealNumber, self).__init__(space) - self.__val__ = v - - def __float__(self): - return self.__val__.__float__() - - def __str__(self): - return str(self.__val__) + return float(value) R = Reals() diff --git a/examples/space/simple_rn.py b/examples/space/simple_rn.py index f4fb5fb9635..77beeb347b9 100644 --- a/examples/space/simple_rn.py +++ b/examples/space/simple_rn.py @@ -5,7 +5,7 @@ import numpy as np import odl -from odl.space.base_tensors import TensorSpace, Tensor +from odl.space.base_tensors import TensorSpace from odl.util.testutils import timer @@ -13,55 +13,46 @@ class SimpleRn(TensorSpace): """The real space R^n, non-optimized implmentation.""" def __init__(self, size): - super(SimpleRn, self).__init__(size, dtype=float) + super(SimpleRn, self).__init__(size, dtype='float64') def zero(self): - return self.element(np.zeros(self.size)) + return np.zeros(self.size) def one(self): - return self.element(np.ones(self.size)) + return np.ones(self.size) def _lincomb(self, a, x1, b, x2, out): - out.data[:] = a * x1.data + b * x2.data + out[:] = a * x1 + b * x2 def _inner(self, x1, x2): - return float(np.vdot(x1.data, x2.data)) + return float(np.vdot(x1, x2)) def _multiply(self, x1, x2, out): - out.data[:] = x1.data * x2.data + out[:] = x1 * x2 def _divide(self, x1, x2, out): - out.data[:] = x1.data / x2.data + out[:] = x1 / x2 + + def __contains__(self, other): + return ( + isinstance(other, np.ndarray) + and other.shape == (self.size,) + and other.dtype == 'float64' + ) def element(self, *args, **kwargs): if not args and not kwargs: - return self.element(np.empty(self.size)) + return np.empty(self.size) if isinstance(args[0], np.ndarray): if args[0].shape == (self.size,): - return RnVector(self, args[0]) + return np.asarray(args[0]) else: - raise ValueError('input array {} is of shape {}, expected ' - 'shape ({},).'.format(args[0], args[0].shape, - self.dim,)) + raise ValueError( + 'input array has shape {}, expected shape ({},)' + ''.format(args[0].shape, self.dim) + ) else: - return self.element(np.array( - *args, **kwargs).astype(np.float64, copy=False)) - return self.element(np.empty(self.dim, dtype=np.float64)) - - -class RnVector(Tensor): - def __init__(self, space, data): - super(RnVector, self).__init__(space) - self.data = data - - def __getitem__(self, index): - return self.data.__getitem__(index) - - def __setitem__(self, index, value): - return self.data.__setitem__(index, value) - - def asarray(self, *args): - return self.data(*args) + return np.array(*args, **kwargs).astype('float64', copy=False) r5 = SimpleRn(5) @@ -72,68 +63,43 @@ def asarray(self, *args): iterations = 10 # Perform some benchmarks with rn -opt_spc = odl.rn(n) -simple_spc = SimpleRn(n) +opt_space = odl.rn(n) +simple_space = SimpleRn(n) x, y, z = np.random.rand(n), np.random.rand(n), np.random.rand(n) -ox, oy, oz = (opt_spc.element(x.copy()), opt_spc.element(y.copy()), - opt_spc.element(z.copy())) -sx, sy, sz = (simple_spc.element(x.copy()), simple_spc.element(y.copy()), - simple_spc.element(z.copy())) -if 'cuda' in odl.space.entry_points.tensor_space_impl_names(): - cu_spc = odl.rn(n, impl='cuda') - cx, cy, cz = (cu_spc.element(x.copy()), cu_spc.element(y.copy()), - cu_spc.element(z.copy())) +ox, oy, oz = (opt_space.copy(a) for a in (x, y, z)) +sx, sy, sz = (simple_space.copy(a) for a in (x, y, z)) print(" lincomb:") with timer("SimpleRn"): for _ in range(iterations): - simple_spc.lincomb(2.13, sx, 3.14, sy, out=sz) + simple_space.lincomb(2.13, sx, 3.14, sy, out=sz) print("result: {}".format(sz[1:5])) with timer("odl numpy"): for _ in range(iterations): - opt_spc.lincomb(2.13, ox, 3.14, oy, out=oz) + opt_space.lincomb(2.13, ox, 3.14, oy, out=oz) print("result: {}".format(oz[1:5])) -if 'cuda' in odl.space.entry_points.tensor_space_impl_names(): - with timer("odl cuda"): - for _ in range(iterations): - cu_spc.lincomb(2.13, cx, 3.14, cy, out=cz) - print("result: {}".format(cz[1:5])) - - print("\n Norm:") with timer("SimpleRn"): for _ in range(iterations): - result = sz.norm() + result = simple_space.norm(sz) print("result: {}".format(result)) with timer("odl numpy"): for _ in range(iterations): - result = oz.norm() + result = opt_space.norm(oz) print("result: {}".format(result)) -if 'cuda' in odl.space.entry_points.tensor_space_impl_names(): - with timer("odl cuda"): - for _ in range(iterations): - result = cz.norm() - print("result: {}".format(result)) - print("\n Inner:") with timer("SimpleRn"): for _ in range(iterations): - result = sz.inner(sx) + result = simple_space.inner(sx, sz) print("result: {}".format(result)) with timer("odl numpy"): for _ in range(iterations): - result = oz.inner(ox) + result = opt_space.inner(ox, oz) print("result: {}".format(result)) - -if 'cuda' in odl.space.entry_points.tensor_space_impl_names(): - with timer("odl cuda"): - for _ in range(iterations): - result = cz.inner(cx) - print("result: {}".format(result)) diff --git a/examples/tomo/backends/astra_performance_cpu_parallel_2d_cg.py b/examples/tomo/backends/astra_performance_cpu_parallel_2d_cg.py index 46d85362db1..fffc5432d81 100644 --- a/examples/tomo/backends/astra_performance_cpu_parallel_2d_cg.py +++ b/examples/tomo/backends/astra_performance_cpu_parallel_2d_cg.py @@ -94,7 +94,7 @@ plt.figure('ASTRA Reconstruction') plt.imshow(rec.T, origin='lower', cmap='bone') plt.figure('ODL Sinogram') -plt.imshow(data.asarray().T, origin='lower', cmap='bone') +plt.imshow(data.T, origin='lower', cmap='bone') plt.figure('ODL Reconstruction') -plt.imshow(x.asarray().T, origin='lower', cmap='bone') +plt.imshow(x.T, origin='lower', cmap='bone') plt.show() diff --git a/examples/tomo/backends/astra_performance_cuda_cone_3d_cg.py b/examples/tomo/backends/astra_performance_cuda_cone_3d_cg.py index bf40a724d97..340c0991fda 100644 --- a/examples/tomo/backends/astra_performance_cuda_cone_3d_cg.py +++ b/examples/tomo/backends/astra_performance_cuda_cone_3d_cg.py @@ -34,7 +34,7 @@ src_radius=500, det_radius=500) -phantom = odl.phantom.shepp_logan(reco_space, modified=True).asarray() +phantom = odl.phantom.shepp_logan(reco_space, modified=True) # --- ASTRA --- @@ -105,5 +105,5 @@ plt.figure('ASTRA Reconstruction') plt.imshow(rec.T[coords], origin='lower', cmap='bone') plt.figure('ODL Reconstruction') -plt.imshow(x.asarray().T[coords], origin='lower', cmap='bone') +plt.imshow(x.T[coords], origin='lower', cmap='bone') plt.show() diff --git a/examples/tomo/backends/astra_performance_cuda_parallel_2d_cg.py b/examples/tomo/backends/astra_performance_cuda_parallel_2d_cg.py index 4d110488c9b..1b6acad241e 100644 --- a/examples/tomo/backends/astra_performance_cuda_parallel_2d_cg.py +++ b/examples/tomo/backends/astra_performance_cuda_parallel_2d_cg.py @@ -94,7 +94,7 @@ plt.figure('ASTRA Reconstruction') plt.imshow(rec.T, origin='lower', cmap='bone') plt.figure('ODL Sinogram') -plt.imshow(data.asarray().T, origin='lower', cmap='bone') +plt.imshow(data.T, origin='lower', cmap='bone') plt.figure('ODL Reconstruction') -plt.imshow(x.asarray().T, origin='lower', cmap='bone') +plt.imshow(x.T, origin='lower', cmap='bone') plt.show() diff --git a/examples/trafos/fourier_trafo.py b/examples/trafos/fourier_trafo.py index 123bab1313d..9d812ec9882 100644 --- a/examples/trafos/fourier_trafo.py +++ b/examples/trafos/fourier_trafo.py @@ -13,18 +13,20 @@ # Create a phantom and its Fourier transfrom and display them. phantom = odl.phantom.shepp_logan(space, modified=True) -phantom.show(title='Shepp-Logan Phantom') +space.show(phantom, title='Shepp-Logan Phantom') phantom_ft = ft_op(phantom) -phantom_ft.show(title='Full Fourier Transform') +ft_op.range.show(phantom_ft, title='Full Fourier Transform') # Calculate the inverse transform. phantom_ft_inv = ft_op.inverse(phantom_ft) -phantom_ft_inv.show(title='Full Fourier Transform Inverted') +space.show(phantom_ft_inv, title='Full Fourier Transform Inverted') # Calculate the FT only along the first axis. ft_op_axis0 = odl.trafos.FourierTransform(space, axes=0) phantom_ft_axis0 = ft_op_axis0(phantom) -phantom_ft_axis0.show(title='Fourier transform Along Axis 0') +ft_op_axis0.range.show( + phantom_ft_axis0, title='Fourier Transform Along Axis 0' +) # If a real space is used, the Fourier transform can be calculated in the # "half-complex" mode. This means that along the last axis of the transform, @@ -33,14 +35,17 @@ real_space = space.real_space ft_op_halfc = odl.trafos.FourierTransform(real_space, halfcomplex=True) phantom_real = odl.phantom.shepp_logan(real_space, modified=True) -phantom_real.show(title='Shepp-Logan Phantom, Real Version') +space.show(phantom_real, title='Shepp-Logan Phantom, Real Version') phantom_real_ft = ft_op_halfc(phantom_real) -phantom_real_ft.show(title='Half-complex Fourier Transform') +ft_op_halfc.range.show( + phantom_real_ft, title='Half-complex Fourier Transform' +) # If the space is real, the inverse also gives a real result. phantom_real_ft_inv = ft_op_halfc.inverse(phantom_real_ft) -phantom_real_ft_inv.show(title='Half-complex Fourier Transform Inverted', - force_show=True) +space.show( + phantom_real_ft_inv, title='Half-complex Fourier Transform Inverted' +) # The FT operator itself has no option of (zero-)padding, but it can be # composed with a `ResizingOperator` which does exactly that. Note that the @@ -49,4 +54,6 @@ ft_op = odl.trafos.FourierTransform(padding_op.range) padded_ft_op = ft_op * padding_op phantom_ft_padded = padded_ft_op(phantom) -phantom_ft_padded.show('Padded FT of the Phantom', force_show=True) +padded_ft_op.range.show( + phantom_ft_padded, 'Padded FT of The Phantom', force_show=True +) diff --git a/examples/trafos/wavelet_trafo.py b/examples/trafos/wavelet_trafo.py index 7fb4e3d2aa9..eace38754dd 100644 --- a/examples/trafos/wavelet_trafo.py +++ b/examples/trafos/wavelet_trafo.py @@ -10,19 +10,19 @@ # automatically. The default backend is PyWavelets (pywt). wavelet_op = odl.trafos.WaveletTransform(space, wavelet='Haar', nlevels=2) -# Create a phantom and its wavelet transfrom and display them. +# Create a phantom and its wavelet transfrom and display them phantom = odl.phantom.shepp_logan(space, modified=True) -phantom.show(title='Shepp-Logan Phantom') +space.show(phantom, title='Shepp-Logan Phantom') -# Note that the wavelet transform is a vector in rn. +# Note that the wavelet transform is a vector in R^n phantom_wt = wavelet_op(phantom) -phantom_wt.show(title='Wavelet Transform') +wavelet_op.range.show(phantom_wt, title='Wavelet Transform') -# It may however (for some choices of wbasis) be interpreted as a vector in the -# domain of the transformation -phantom_wt_2d = space.element(phantom_wt.asarray().reshape(space.shape)) -phantom_wt_2d.show('Wavelet Transform in 2d') +# It may however (for some choices of wbasis) be interpreted as an element +# of the transformation domain +phantom_wt_2d = phantom_wt.reshape(space.shape) +space.show(phantom_wt_2d, 'Wavelet Transform in 2D') -# Calculate the inverse transform. +# Calculate the inverse transform phantom_wt_inv = wavelet_op.inverse(phantom_wt) -phantom_wt_inv.show(title='Wavelet Transform Inverted', force_show=True) +space.show(phantom_wt_inv, title='Wavelet Transform Inverted', force_show=True) diff --git a/examples/visualization/show_1d.py b/examples/visualization/show_1d.py index a8a13876a52..31342169f17 100644 --- a/examples/visualization/show_1d.py +++ b/examples/visualization/show_1d.py @@ -6,7 +6,6 @@ issues with this example. """ -import matplotlib.pyplot as plt import numpy as np import odl @@ -15,12 +14,9 @@ elem = space.element(np.sin) # Get figure object -fig = elem.show(title='Sine Functions') +fig = space.show(elem, title='Sine Functions') # Plot into the same figure -fig = (elem / 2).show(fig=fig) - -# Plotting is deferred until show() is called -plt.show() +fig = space.show(elem / 2, fig=fig) # "Instant" plotting can be forced -elem.show(force_show=True) +space.show(elem, force_show=True) diff --git a/examples/visualization/show_2d.py b/examples/visualization/show_2d.py index d02e65f7089..b65176c6860 100644 --- a/examples/visualization/show_2d.py +++ b/examples/visualization/show_2d.py @@ -12,13 +12,13 @@ phantom = odl.phantom.shepp_logan(space, modified=True) # Show all data -phantom.show() +space.show(phantom) # We can show subsets by index -phantom.show(indices=[None, 50]) +space.show(phantom, indices=[None, 50]) # Or we can show by coordinate -phantom.show(coords=[None, 0.5]) +space.show(phantom, coords=[None, 0.5]) # We can also show subsets -phantom.show(coords=[[None, 0.5], None], force_show=True) +space.show(phantom, coords=[[None, 0.5], None], force_show=True) diff --git a/examples/visualization/show_2d_complex.py b/examples/visualization/show_2d_complex.py index 705f2113b40..ac329117629 100644 --- a/examples/visualization/show_2d_complex.py +++ b/examples/visualization/show_2d_complex.py @@ -10,4 +10,4 @@ space = odl.uniform_discr([0, 0], [1, 1], [100, 100], dtype='complex') phantom = odl.phantom.shepp_logan(space, modified=True) * (1 + 0.5j) -phantom.show(force_show=True) +space.show(phantom, force_show=True) diff --git a/examples/visualization/show_callback.py b/examples/visualization/show_callback.py index ad0be9affe8..b989f3e9fcf 100644 --- a/examples/visualization/show_callback.py +++ b/examples/visualization/show_callback.py @@ -8,12 +8,12 @@ # Callback in 1d adds new lines to the figure space_1d = odl.uniform_discr(0, 1, 100) -callback = sleep & odl.solvers.CallbackShow() +callback = sleep & odl.solvers.CallbackShow(space_1d) for name, elem in space_1d.examples: callback(elem) # Callback in 2d replaces the figure in place space_2d = odl.uniform_discr([0, 0], [1, 1], [100, 100]) -callback = sleep & odl.solvers.CallbackShow() +callback = sleep & odl.solvers.CallbackShow(space_2d) for name, elem in space_2d.examples: callback(elem) diff --git a/examples/visualization/visualize_vector_examples.py b/examples/visualization/show_examples.py similarity index 83% rename from examples/visualization/visualize_vector_examples.py rename to examples/visualization/show_examples.py index 30cbacf1bf1..37193849162 100644 --- a/examples/visualization/visualize_vector_examples.py +++ b/examples/visualization/show_examples.py @@ -6,11 +6,11 @@ space_1d = odl.uniform_discr(0, 1, 100) for name, elem in space_1d.examples: - elem.show(name) + space_1d.show(elem, name) space_2d = odl.uniform_discr([0, 0], [1, 1], [100, 100]) for name, elem in space_2d.examples: - elem.show(name) + space_2d.show(elem, name) plt.show() diff --git a/examples/visualization/show_productspace.py b/examples/visualization/show_productspace.py index 8909c5541a0..581c4d6d52e 100644 --- a/examples/visualization/show_productspace.py +++ b/examples/visualization/show_productspace.py @@ -1,4 +1,4 @@ -"""Example on using `ProductSpaceElement.show`.""" +"""Example on using `ProductSpace.show`.""" import odl import numpy as np @@ -16,16 +16,20 @@ # By default 4 uniformly spaced elements are shown. Since there are 7 in # total, the shown components are 0, 2, 4 and 6 -elem.show('Default') +pspace.show(elem, 'Default') # One can also use indexing by a list of indices or a slice. -elem.show('The First 2 Elements', indices=[0, 1]) +pspace.show(elem, 'The First 2 Elements', indices=[0, 1]) -elem.show('Every Third Element', indices=np.s_[::3]) +pspace.show(elem, 'Every Third Element', indices=np.s_[::3]) # Slices propagate (as in numpy): the first index in the slice applies to # the product space components, the other dimensions are applied to each # component. Here we take the second component and slice in the # middle along the second axis. -elem.show('Element at Index 2, Sliced by [:, n // 2]', - indices=[2, None, n // 2], force_show=True) +pspace.show( + elem, + 'Element at Index 2, Sliced by [:, n // 2]', + indices=[2, None, n // 2], + force_show=True +) diff --git a/examples/visualization/show_update_1d.py b/examples/visualization/show_update_1d.py index 9501e63bc13..14e64d2941c 100644 --- a/examples/visualization/show_update_1d.py +++ b/examples/visualization/show_update_1d.py @@ -15,7 +15,7 @@ # Reuse the figure indefinitely for i in range(m): - fig = (elem * i).show(fig=fig) + fig = space.show(elem * i, fig=fig) plt.pause(0.1) plt.show() diff --git a/examples/visualization/show_update_2d.py b/examples/visualization/show_update_2d.py index 77e61b325b8..00f45a5e3c6 100644 --- a/examples/visualization/show_update_2d.py +++ b/examples/visualization/show_update_2d.py @@ -13,7 +13,7 @@ # Reuse the figure indefinitely, values are overwritten. for i in range(m): - fig = (phantom * i).show(fig=fig, clim=[0, m]) + fig = space.show(phantom * i, fig=fig, clim=[0, m]) plt.pause(0.1) plt.show() diff --git a/examples/visualization/show_update_in_place_2d.py b/examples/visualization/show_update_in_place_2d.py index 8a86a7c23e9..a033912e826 100644 --- a/examples/visualization/show_update_in_place_2d.py +++ b/examples/visualization/show_update_in_place_2d.py @@ -16,6 +16,6 @@ # Reuse the figure indefinitely, values are overwritten. for i in range(m): - fig = (phantom * i).show(fig=fig, clim=[0, m], update_in_place=True) + fig = space.show(phantom * i, fig=fig, clim=[0, m], update_in_place=True) plt.show() diff --git a/examples/visualization/show_vector.py b/examples/visualization/show_vector.py index a737a2e8c12..ea310130013 100644 --- a/examples/visualization/show_vector.py +++ b/examples/visualization/show_vector.py @@ -8,4 +8,4 @@ space = odl.rn(5) vector = space.element([1, 2, 3, 4, 5]) -vector.show(force_show=True) +space.show(vector, force_show=True) diff --git a/odl/__init__.py b/odl/__init__.py index 985ba26f79a..02b3b35ed4d 100644 --- a/odl/__init__.py +++ b/odl/__init__.py @@ -30,7 +30,6 @@ 'solvers', 'tomo', 'trafos', - 'ufunc_ops', 'util', ) @@ -68,10 +67,10 @@ from . import solvers from . import tomo from . import trafos -from . import ufunc_ops from . import util +from ._ufunc import ufunc_ops, ufunc_funcs -# Add `test` function to global namespace so users can run `odl.test()` +# Import `test` function to global namespace so users can run `odl.test()` from .util import test # Amend `__all__` @@ -80,3 +79,4 @@ __all__ += set.__all__ __all__ += space.__all__ __all__ += ('test',) +__all__ += ('ufunc_ops', 'ufunc_funcs') diff --git a/odl/ufunc_ops/__init__.py b/odl/_ufunc/__init__.py similarity index 81% rename from odl/ufunc_ops/__init__.py rename to odl/_ufunc/__init__.py index da9ca8f282a..3946d1862f7 100644 --- a/odl/ufunc_ops/__init__.py +++ b/odl/_ufunc/__init__.py @@ -10,7 +10,7 @@ from __future__ import absolute_import -from .ufunc_ops import * +from ._ufunc_ops import ufunc_ops, ufunc_funcs __all__ = () -__all__ = ufunc_ops.__all__ +__all__ += ('ufunc_ops', 'ufunc_funcs') diff --git a/odl/_ufunc/_ufunc_ops.py b/odl/_ufunc/_ufunc_ops.py new file mode 100644 index 00000000000..50e5a88c9de --- /dev/null +++ b/odl/_ufunc/_ufunc_ops.py @@ -0,0 +1,556 @@ +# 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/. + +"""Operators based on NumPy UFuncs.""" + +import warnings + +import numpy as np + +LINEAR_UFUNCS = { + 'negative', 'degrees', 'rad2deg', 'radians', 'deg2rad', 'add', 'subtract' +} + + +def _ufunc_op_range(domain, nin, nout, types_dict): + """Infer the range of a ufunc operator.""" + if nin == 1: + try: + dom_type = domain.dtype.char + dom_base = domain + except AttributeError: + # TODO(kohr-h): better error message + raise ValueError('bad `domain`') + elif nin == 2: + try: + dom_type = domain[0].dtype.char + domain[1].dtype.char + dom_base = domain[0] + except (TypeError, IndexError, AttributeError): + # TODO(kohr-h): better error message + raise ValueError('bad `domain`') + else: + raise NotImplementedError + + ran_type = types_dict[dom_type] + + if nout == 1: + return dom_base.astype(ran_type) + elif nout == 2: + return dom_base.astype(ran_type[0]) * dom_base.astype(ran_type[1]) + else: + raise NotImplementedError + + +def ufunc_op___init__(self, domain): + """Initialize a new instance. + + Parameters + ---------- + domain : `TensorSpace` or `ProductSpace` + Space of elements to which this ufunc operator can be applied. + """ + from odl import Operator + + range = _ufunc_op_range( + domain, self.ufunc.nin, self.ufunc.nout, self.types + ) + Operator.__init__( + self, domain, range, linear=self.ufunc.__name__ in LINEAR_UFUNCS + ) + + +def _ufunc_op_call_11(ufunc, domain, x, out=None): + from odl.space.pspace import ProductSpace + from odl.space.base_tensors import TensorSpace + + if isinstance(domain, TensorSpace): + return ufunc(x, out=out) + elif isinstance(domain, ProductSpace): + if out is None: + return [ufunc(xi) for xi in x] + else: + for xi, oi in zip(x, out): + ufunc(xi, out=oi) + return out + else: + raise RuntimeError + + +def _ufunc_op_call_12(ufunc, domain, x, out=None): + from odl import ProductSpace + from odl.space.base_tensors import TensorSpace + + if isinstance(domain, TensorSpace): + if out is None: + return ufunc(x) + else: + ufunc(x, out=(out[0], out[1])) + return out + elif isinstance(domain, ProductSpace): + if out is None: + return [ufunc(xi) for xi in x] + else: + for xi, oi in zip(x, out): + ufunc(xi, out=(oi[0], oi[1])) + return out + else: + raise RuntimeError + + +def _ufunc_op_call_21(ufunc, domain, x, out=None): + from odl import ProductSpace + from odl.space.base_tensors import TensorSpace + + if isinstance(domain[0], TensorSpace): + return ufunc(x[0], x[1], out=out) + elif isinstance(domain[0], ProductSpace): + if out is None: + return [ufunc(xi[0], xi[1]) for xi in x] + else: + for xi, oi in zip(x, out): + ufunc(xi[0], xi[1], out=oi) + return out + else: + raise RuntimeError + + +def _ufunc_op_call_22(ufunc, domain, x, out=None): + from odl import ProductSpace + from odl.space.base_tensors import TensorSpace + + if isinstance(domain[0], TensorSpace): + if out is None: + return ufunc(x[0], x[1]) + else: + ufunc(x[0], x[1], out=(out[0], out[1])) + return out + elif isinstance(domain[0], ProductSpace): + if out is None: + return [ufunc(xi[0], xi[1]) for xi in x] + else: + for xi, oi in zip(x, out): + ufunc(xi[0], xi[1], out=(oi[0], oi[1])) + return out + else: + raise RuntimeError + + +def ufunc_op_derivative(name): + from odl import MultiplyOperator, Operator + + if name == 'sin': + def derivative(self, point): + cos = ufunc_op_cls('cos')(self.domain) + return MultiplyOperator(self.domain, cos(point)) + elif name == 'cos': + def derivative(self, point): + sin = ufunc_op_cls('sin')(self.domain) + return MultiplyOperator(self.domain, -sin(point)) + elif name == 'tan': + def derivative(self, point): + tan = self + return MultiplyOperator(self.domain, 1 + tan(point) ** 2) + elif name == 'sqrt': + def derivative(self, point): + sqrt = self + return MultiplyOperator(self.domain, 0.5 / sqrt(point)) + elif name == 'square': + def derivative(self, point): + return MultiplyOperator(self.domain, 2.0 * point) + elif name == 'log': + def derivative(self, point): + return MultiplyOperator(self.domain, 1.0 / point) + elif name == 'exp': + def derivative(self, point): + exp = self + return MultiplyOperator(self.domain, exp(point)) + elif name == 'reciprocal': + def derivative(self, point): + reciprocal = self + return MultiplyOperator(self.domain, reciprocal(point) ** 2) + elif name == 'sinh': + def derivative(self, point): + cosh = ufunc_op_cls('cosh')(self.domain) + return MultiplyOperator(self.domain, cosh(point)) + elif name == 'cosh': + def derivative(self, point): + sinh = ufunc_op_cls('sinh')(self.domain) + return MultiplyOperator(self.domain, sinh(point)) + else: + # Fallback to default + derivative = Operator.derivative + + derivative.__doc__ = 'Return the derivative operator.' + return derivative + + +in1_default = [-1.0, 1.0, 2.0] +in2_default = [[-1.0, 1.0, 2.0], [0.5, -1.0, 2.0]] +UFUNC_INPUT_FOR_DOC = { + 'abs': {'type': 'd', 'input': in1_default}, # = absolute + 'absolute': {'type': 'd', 'input': in1_default}, + 'add': {'type': 'dd', 'input': in2_default}, + 'arccos': {'type': 'd', 'input': [-1.0, 0.0, 1.0]}, + 'arccosh': {'type': 'd', 'input': [1.0, 1.5, 2.0]}, + 'arcsin': {'type': 'd', 'input': [-1.0, 0.0, 1.0]}, + 'arcsinh': {'type': 'd', 'input': in1_default}, + 'arctan': {'type': 'd', 'input': in1_default}, + 'arctan2': {'type': 'dd', 'input': in2_default}, + 'arctanh': {'type': 'd', 'input': [-0.5, 0.0, 0.5]}, + 'bitwise_and': {'type': 'll', 'input': [[-1, 1, 0], [1, 0, 0]]}, + 'bitwise_not': {'type': 'l', 'input': [[-2, 0, 1]]}, # = invert + 'bitwise_or': {'type': 'll', 'input': [[-1, 1, 0], [1, 0, 0]]}, + 'bitwise_xor': {'type': 'll', 'input': [[-1, 1, 0], [1, 0, 0]]}, + 'cbrt': {'type': 'd', 'input': in1_default}, + 'ceil': {'type': 'd', 'input': [-0.5, 0.0, 0.5]}, + 'conj': {'type': 'D', + 'input': [-0.5, 0.0 + 1.0j, 0.5 - 0.5j]}, # = conjugate + 'conjugate': {'type': 'D', 'input': [-0.5, 0.0 + 1.0j, 0.5 - 0.5j]}, + 'copysign': {'type': 'dd', 'input': in2_default}, + 'cos': {'type': 'd', 'input': in1_default}, + 'cosh': {'type': 'd', 'input': in1_default}, + 'deg2rad': {'type': 'd', 'input': in1_default}, + 'degrees': {'type': 'd', 'input': in1_default}, + 'divide': {'type': 'dd', 'input': in2_default}, # = true_divide + 'divmod': {'type': 'll', 'input': [[-1, 2, 3], [2, 2, 2]]}, + 'equal': {'type': 'dd', 'input': in2_default}, + 'exp': {'type': 'd', 'input': in1_default}, + 'exp2': {'type': 'd', 'input': in1_default}, + 'expm1': {'type': 'd', 'input': in1_default}, + 'fabs': {'type': 'd', 'input': in1_default}, + 'float_power': {'type': 'dd', + 'input': [[2.0, 1.0, 2.0], [0.5, -1.0, 2.0]]}, + 'floor': {'type': 'd', 'input': in1_default}, + 'floor_divide': {'type': 'll', 'input': [[-1, 2, 3], [2, 2, 2]]}, + 'fmax': {'type': 'dd', 'input': in2_default}, + 'fmin': {'type': 'dd', 'input': in2_default}, + 'fmod': {'type': 'dd', 'input': [[0.5, -1.0, 2.0], [2.0, 1.0, 2.0]]}, + 'frexp': {'type': 'd', 'input': in1_default}, + 'gcd': {'type': 'll', 'input': [[-2, 6, 0], [2, 9, 0]]}, + 'greater': {'type': 'dd', 'input': in2_default}, + 'greater_equal': {'type': 'dd', 'input': in2_default}, + 'heaviside': {'type': 'dd', 'input': [[0.5, 0.0, 2.0], [2.0, 1.0, 2.0]]}, + 'hypot': {'type': 'dd', 'input': in2_default}, + 'invert': {'type': 'l', 'input': [-2, 0, 1]}, + 'isfinite': {'type': 'd', 'input': [1.0, float('inf'), float('nan')]}, + 'isinf': {'type': 'd', 'input': [1.0, float('inf'), float('nan')]}, + 'isnan': {'type': 'd', 'input': [1.0, float('inf'), float('nan')]}, + 'lcm': {'type': 'll', 'input': [[-2, 6, 0], [2, 9, 0]]}, + 'ldexp': {'type': 'dl', 'input': [[0.5, -1.0, 2.0], [2, 1, -2]]}, + 'left_shift': {'type': 'll', 'input': [[-2, 1, 2], [2, 1, 0]]}, + 'less': {'type': 'dd', 'input': in2_default}, + 'less_equal': {'type': 'dd', 'input': in2_default}, + 'log': {'type': 'd', 'input': [0.5, 1.0, 2.0]}, + 'log10': {'type': 'd', 'input': [0.5, 1.0, 2.0]}, + 'log1p': {'type': 'd', 'input': [0.0, 0.5, 1.0]}, + 'log2': {'type': 'd', 'input': [0.5, 1.0, 2.0]}, + 'logaddexp': {'type': 'dd', 'input': in2_default}, + 'logaddexp2': {'type': 'dd', 'input': in2_default}, + 'logical_and': {'type': '??', 'input': [[True, False, True, False], + [True, True, False, False]]}, + 'logical_not': {'type': '?', 'input': [True, False]}, + 'logical_or': {'type': '??', 'input': [[True, False, True, False], + [True, True, False, False]]}, + 'logical_xor': {'type': '??', 'input': [[True, False, True, False], + [True, True, False, False]]}, + 'maximum': {'type': 'dd', 'input': in2_default}, + 'minimum': {'type': 'dd', 'input': in2_default}, + 'mod': {'type': 'll', 'input': [[-1, 2, 3], [2, 2, 2]]}, # = remainder + 'modf': {'type': 'd', 'input': in1_default}, + 'multiply': {'type': 'dd', 'input': in2_default}, + 'negative': {'type': 'd', 'input': in1_default}, + 'nextafter': {'type': 'dd', 'input': in2_default}, + 'not_equal': {'type': 'dd', 'input': in2_default}, + 'positive': {'type': 'd', 'input': in1_default}, + 'power': {'type': 'dd', 'input': in2_default}, + 'rad2deg': {'type': 'd', 'input': in1_default}, + 'radians': {'type': 'd', 'input': in1_default}, + 'reciprocal': {'type': 'd', 'input': [-0.5, 1.0, 2.0]}, + 'remainder': {'type': 'll', 'input': [[-1, 2, 3], [2, 2, 2]]}, + 'right_shift': {'type': 'll', 'input': [[-2, 1, 2], [2, 1, 0]]}, + 'rint': {'type': 'd', 'input': in1_default}, + 'sign': {'type': 'd', 'input': in1_default}, + 'signbit': {'type': 'd', 'input': in1_default}, + 'sin': {'type': 'd', 'input': in1_default}, + 'sinh': {'type': 'd', 'input': in1_default}, + 'spacing': {'type': 'd', 'input': in1_default}, + 'sqrt': {'type': 'd', 'input': [0.0, 0.5, 1.0]}, + 'square': {'type': 'd', 'input': in1_default}, + 'subtract': {'type': 'dd', 'input': in2_default}, + 'tan': {'type': 'd', 'input': in1_default}, + 'tanh': {'type': 'd', 'input': in1_default}, + 'true_divide': {'type': 'dd', 'input': in2_default}, + 'trunc': {'type': 'd', 'input': [-0.5, 0.0, 1.5]}, +} + + +def ufunc_op_cls(name): + """Dynamically generate a ufunc operator class for a given ufunc name.""" + from odl import Operator, tensor_space + + # --- Get ufunc, map to impl --- # + + ufunc = getattr(np, name) + assert isinstance(ufunc, np.ufunc) + + if ufunc.nin == 1 and ufunc.nout == 1: + _call_impl = _ufunc_op_call_11 + elif ufunc.nin == 1 and ufunc.nout == 2: + _call_impl = _ufunc_op_call_12 + elif ufunc.nin == 2 and ufunc.nout == 1: + _call_impl = _ufunc_op_call_21 + elif ufunc.nin == 2 and ufunc.nout == 2: + _call_impl = _ufunc_op_call_22 + else: + raise NotImplementedError + + def _call(self, x, out=None): + return _call_impl(ufunc, self.domain, x, out) + + # --- Generate docstring --- # + + types = dict(t.split('->') for t in ufunc.types) + try: + ufunc_input = UFUNC_INPUT_FOR_DOC[name] + except KeyError: + # Unknown ufunc, try to find some appropriate type + if 'd' * ufunc.nin in types: + in_type = 'd' * ufunc.nin + out_type = types[in_type] + elif 'l' * ufunc.nin in types: + in_type = 'l' * ufunc.nin + out_type = types[in_type] + elif '?' * ufunc.nin in types: + in_type = '?' * ufunc.nin + out_type = types[in_type] + else: + in_type = 'd' * ufunc.nin + out_type = 'd' * ufunc.nout + + if ufunc.nin == 1: + inp = np.array(in1_default, dtype=in_type).tolist() + else: + inp = [ + np.array(in2_default[0], dtype=in_type[0]).tolist(), + np.array(in2_default[1], dtype=in_type[1]).tolist(), + ] + + warnings.warn( + 'ufunc {!r} not known, assuming default input type {!r}' + ''.format(name, in_type) + ) + + else: + in_type = ufunc_input['type'] + inp = ufunc_input['input'] + out_type = types[in_type] + + if ufunc.nin == 1: + space_in = tensor_space(len(inp), dtype=in_type) + space_str = 'odl.{!r}'.format(space_in) + result = ufunc(inp) + elif ufunc.nin == 2: + space_in = ( + tensor_space(len(inp[0]), dtype=in_type[0]) + * tensor_space(len(inp[1]), dtype=in_type[1]) + ) + space_str = 'odl.{!r} * odl.{!r}'.format( + space_in[0], space_in[1] + ) + result = ufunc(*inp) + + if ufunc.nout == 1: + outp = result.astype(out_type, copy=False) + elif ufunc.nout == 2: + outp = np.empty(2, dtype=object) + outp[0] = result[0] + outp[1] = result[1] + + summary = ufunc.__doc__.splitlines()[2] + docstring = """ + {summary} + + Examples + -------- + >>> space = {space} + >>> op = odl.ufunc_ops.{name}(space) + >>> op({arg}) + {result!r} + """.format( + summary=summary, space=space_str, name=name, arg=inp, result=outp + ) + + # --- Make class --- # + + attrs = { + 'ufunc': ufunc, + 'types': types, + '__init__': ufunc_op___init__, + '_call': _call, + '__doc__': docstring, + 'derivative': ufunc_op_derivative(name), + } + return type(name, (Operator,), attrs) + + +ufunc_ops = type( + 'ufunc_ops', (object,), {'__getattr__': staticmethod(ufunc_op_cls)} +)() + + +# --- Functionals --- # + + +def ufunc_func___init__(self, domain): + """Initialize a new instance. + + Parameters + ---------- + domain : `Field` + Scalar field to which this ufunc functional can be applied. + """ + from odl.solvers.functional import Functional + + Functional.__init__( + self, domain, linear=self.ufunc.__name__ in LINEAR_UFUNCS + ) + + +def ufunc_func_gradient(name): + from odl.solvers.functional import ( + Functional, ConstantFunctional, FunctionalQuotient, ScalingFunctional) + + if name == 'sin': + def gradient(self): + cos = ufunc_func_cls('cos')(self.domain) + return cos + elif name == 'cos': + def gradient(self): + sin = ufunc_func_cls('sin')(self.domain) + return -sin + elif name == 'tan': + def gradient(self): + tan = self + square = ufunc_func_cls('square')(self.domain) + return 1 + square * tan + elif name == 'sqrt': + def gradient(self): + sqrt = self + return FunctionalQuotient( + ConstantFunctional(self.domain, 0.5), sqrt + ) + elif name == 'square': + def gradient(self): + return ScalingFunctional(self.domain, 2.0) + elif name == 'log': + def gradient(self): + reciprocal = ufunc_func_cls('reciprocal')(self.domain) + return reciprocal + elif name == 'exp': + def gradient(self): + exp = self + return exp + elif name == 'reciprocal': + def gradient(self): + square = ufunc_func_cls('square')(self.domain) + return FunctionalQuotient( + ConstantFunctional(self.domain, -1.0), square + ) + elif name == 'sinh': + def gradient(self): + cosh = ufunc_func_cls('cosh')(self.domain) + return cosh + elif name == 'cosh': + def gradient(self): + sinh = ufunc_func_cls('sinh')(self.domain) + return sinh + else: + # Fallback to default + gradient = Functional.gradient + + gradient.__doc__ = 'Return the gradient operator.' + return gradient + + +def ufunc_func_cls(name): + """Dynamically generate a ufunc functional class for a given ufunc name.""" + from odl import RealNumbers, ComplexNumbers, Integers + from odl.solvers.functional import Functional + + # --- Get ufunc, map to impl --- # + + ufunc = getattr(np, name) + assert isinstance(ufunc, np.ufunc) + + if ufunc.nin != 1 or ufunc.nout !=1: + raise ValueError( + 'ufunc functionals only defined for ufuncs with 1 input and ' + '1 output' + ) + + def _call(self, x): + return ufunc(x) + + # --- Generate docstring --- # + + types = dict(t.split('->') for t in ufunc.types) + try: + ufunc_input = UFUNC_INPUT_FOR_DOC[name] + except KeyError: + raise ValueError('ufunc `{}` not supported'.format(name)) + + in_type = ufunc_input['type'] + inp = ufunc_input['input'][-1] + out_type = types[in_type] + + if in_type == 'd': + domain = RealNumbers() + elif in_type == 'D': + domain = ComplexNumbers() + elif in_type in {'l', '?'}: + domain = Integers() + else: + raise RuntimeError + + space_str = 'odl.{!r}'.format(domain) + + result = ufunc(inp) + outp = domain.astype(out_type).element(result) + + summary = ufunc.__doc__.splitlines()[2] + docstring = """ + {summary} + + Examples + -------- + >>> space = {space} + >>> func = odl.ufunc_funcs.{name}(space) + >>> func({arg}) + {result!r} + """.format( + summary=summary, space=space_str, name=name, arg=inp, result=outp + ) + + # --- Make class --- # + + attrs = { + 'ufunc': ufunc, + 'types': types, + '__init__': ufunc_func___init__, + '_call': _call, + '__doc__': docstring, + 'gradient': property(ufunc_func_gradient(name)), + } + return type(name, (Functional,), attrs) + + +ufunc_funcs = type( + 'ufunc_funcs', (object,), {'__getattr__': staticmethod(ufunc_func_cls)} +)() + +# TODO(kohr-h): doctest diff --git a/odl/contrib/tensorflow/examples/tensorflow_tomography.py b/odl/contrib/tensorflow/examples/tensorflow_tomography.py index 68d9fcd8114..0ae1c4d8d60 100644 --- a/odl/contrib/tensorflow/examples/tensorflow_tomography.py +++ b/odl/contrib/tensorflow/examples/tensorflow_tomography.py @@ -24,7 +24,7 @@ # Create data phantom = odl.phantom.shepp_logan(space, True) data = ray_transform(phantom) -noisy_data = data + odl.phantom.white_noise(data.space) +noisy_data = data + odl.phantom.white_noise(ray_transform.range) # Create tensorflow layers from odl operators ray_transform_layer = odl.contrib.tensorflow.as_tensorflow_layer( diff --git a/odl/contrib/tensorflow/space.py b/odl/contrib/tensorflow/space.py index ff152fadf04..a464b27ed71 100644 --- a/odl/contrib/tensorflow/space.py +++ b/odl/contrib/tensorflow/space.py @@ -12,7 +12,6 @@ import tensorflow as tf from odl.set import LinearSpace, RealNumbers -from odl.set.space import LinearSpaceElement from odl.operator import Operator @@ -89,6 +88,7 @@ def __repr__(self): return 'TensorflowSpace({})'.format(self.shape) +# TODO: fix (remove this and fix references) class TensorflowSpaceElement(LinearSpaceElement): """Elements in TensorflowSpace.""" diff --git a/odl/deform/linearized.py b/odl/deform/linearized.py index cd28fc8c5e5..21cccf46731 100644 --- a/odl/deform/linearized.py +++ b/odl/deform/linearized.py @@ -13,17 +13,14 @@ import numpy as np 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 -from odl.util import indent, signature_string +from odl.util import repr_string, signature_string_parts __all__ = ('LinDeformFixedTempl', 'LinDeformFixedDisp', 'linear_deform') -def linear_deform(template, displacement, interp='linear', out=None): +def linear_deform(space, template, displacement, interp='linear', out=None): """Linearized deformation of a template with a displacement field. The function maps a given template ``I`` and a given displacement @@ -31,12 +28,15 @@ def linear_deform(template, displacement, interp='linear', out=None): Parameters ---------- - 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 - template. - interp : str or sequence of str + space : `DiscretizedSpace` + Function space in which the deformation should be performed. + template : `array-like` or callable + Template to be deformed by a displacement field. Must be castable to + an element of ``space``. + displacement : `array-like` or callable + Vector field (displacement field) used to deform the template. + Must be castable to an element of ``space ** space.ndim``. + interp : str or sequence of str, optional Interpolation type that should be used to sample the template on the deformed grid. A single value applies to all axes, and a sequence gives the interpolation scheme per axis. @@ -56,35 +56,49 @@ def linear_deform(template, displacement, interp='linear', out=None): Examples -------- - Create a simple 1D template to initialize the operator and - apply it to a displacement field. Where the displacement is zero, - the output value is the same as the input value. - In the 4-th point, the value is taken from 0.2 (one cell) to the - left, i.e. 1.0. + A simple displacement of one point can be achieved with a displacement + field that is everywhere zero except in that point. For instance, take + the value of the 4th point from ``0.2`` (one cell) to the left, i.e., + ``1.0``: >>> space = odl.uniform_discr(0, 1, 5) >>> disp_field_space = space.tangent_bundle - >>> template = space.element([0, 0, 1, 0, 0]) - >>> displacement_field = disp_field_space.element([[0, 0, 0, -0.2, 0]]) - >>> linear_deform(template, displacement_field, interp='nearest') + >>> template = [0, 0, 1, 0, 0] + >>> displacement_field = [[0, 0, 0, -0.2, 0]] + >>> linear_deform(space, template, displacement_field, interp='nearest') array([ 0., 0., 1., 1., 0.]) - The result depends on the chosen interpolation. With 'linear' - interpolation and an offset of half the distance between two - points, 0.1, one gets the mean of the values. + The result depends on the chosen interpolation. With ``'linear'`` + interpolation and an offset of half the distance between two points, + ``0.1``, one gets the mean of the values: - >>> displacement_field = disp_field_space.element([[0, 0, 0, -0.1, 0]]) - >>> linear_deform(template, displacement_field, interp='linear') + >>> displacement_field = [[0, 0, 0, -0.1, 0]] + >>> linear_deform(space, template, displacement_field, interp='linear') array([ 0. , 0. , 1. , 0.5, 0. ]) + + We can also use callables directly, both as template and as deformation + field. For instance, we can flip a function by using the displacement + ``v(x) = -x + (1 - x)``: + + >>> space.element(lambda x: x) + array([ 0.1, 0.3, 0.5, 0.7, 0.9]) + >>> linear_deform(space, lambda x: x, [lambda x: 1 - 2 * x]) + array([ 0.9, 0.7, 0.5, 0.3, 0.1]) """ - points = template.space.points() + if not isinstance(space, DiscretizedSpace): + raise TypeError( + '`space` must be a `DiscretizedSpace`, got {!r}'.format(space) + ) + template = space.element(template) + displacement = space.tangent_bundle.element(displacement) + points = space.points() for i, vi in enumerate(displacement): - points[:, i] += vi.asarray().ravel() + points[:, i] += vi.ravel() templ_interpolator = per_axis_interpolator( - template, coord_vecs=template.space.grid.coord_vectors, interp=interp + template, coord_vecs=space.grid.coord_vectors, interp=interp ) values = templ_interpolator(points.T, out=out) - return values.reshape(template.space.shape) + return values.reshape(space.shape) class LinDeformFixedTempl(Operator): @@ -130,23 +144,16 @@ class LinDeformFixedTempl(Operator): i.e., :math:`W_I'(v)^*(J)(x) = J(x) \, \nabla I(x + v(x))`. """ - def __init__(self, template, domain=None, interp='linear'): + def __init__(self, range, template, interp='linear'): """Initialize a new instance. Parameters ---------- - template : `DiscretizedSpaceElement` - Fixed template that is to be deformed. - 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 - ``domain[0].partition == template.space.partition``, so - this option is useful mainly when using different interpolations - in displacement and template. - - Default: ``template.space.real_space.tangent_bundle`` - + range : `DiscretizedSpace` + Template space to which the operator maps. + template : `array-like` or callable + Fixed template that is to be deformed. Must be castable to an + element of ``range``. interp : str or sequence of str Interpolation type that should be used to sample the template on the deformed grid. A single value applies to all axes, and a @@ -163,59 +170,42 @@ def __init__(self, template, domain=None, interp='linear'): Examples -------- - Create a simple 1D template to initialize the operator and - apply it to a displacement field. Where the displacement is zero, - the output value is the same as the input value. - In the 4-th point, the value is taken from 0.2 (one cell) to the - left, i.e. 1.0. + A simple displacement of one point can be achieved with a displacement + field that is everywhere zero except in that point. For instance, take + the value of the 4th point from ``0.2`` (one cell) to the left, i.e., + ``1.0``: >>> space = odl.uniform_discr(0, 1, 5) - >>> template = space.element([0, 0, 1, 0, 0]) - >>> op = LinDeformFixedTempl(template, interp='nearest') + >>> template = [0, 0, 1, 0, 0] + >>> op = odl.deform.LinDeformFixedTempl( + ... space, template, interp='nearest' + ... ) >>> disp_field = [[0, 0, 0, -0.2, 0]] - >>> print(op(disp_field)) - [ 0., 0., 1., 1., 0.] + >>> op(disp_field) + array([ 0., 0., 1., 1., 0.]) - The result depends on the chosen interpolation. With 'linear' - interpolation and an offset of half the distance between two - points, 0.1, one gets the mean of the values. + The result depends on the chosen interpolation. With ``'linear'`` + interpolation and an offset of half the distance between two points, + ``0.1``, one gets the mean of the values: - >>> op = LinDeformFixedTempl(template, interp='linear') + >>> op = odl.deform.LinDeformFixedTempl( + ... space, template, interp='linear' + ... ) >>> disp_field = [[0, 0, 0, -0.1, 0]] - >>> print(op(disp_field)) - [ 0. , 0. , 1. , 0.5, 0. ] + >>> op(disp_field) + array([ 0. , 0. , 1. , 0.5, 0. ]) """ - if not isinstance(template, DiscretizedSpaceElement): + if not isinstance(range, DiscretizedSpace): raise TypeError( - '`template` must be a `DiscretizedSpaceElement, got {!r}`' - ''.format(template) + '`range` must be a `DiscretizedSpace`, got {!r}'.format(range) ) - self.__template = template - - if domain is None: - domain = self.template.space.real_space.tangent_bundle - else: - if not isinstance(domain, ProductSpace): - # TODO: allow non-product spaces in the 1D case - raise TypeError('`domain` must be a `ProductSpace` ' - 'instance, got {!r}'.format(domain)) - if not domain.is_power_space: - raise TypeError('`domain` must be a power space, ' - 'got {!r}'.format(domain)) - 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: - raise ValueError( - '`template.space.partition` not equal to `coord_space`s ' - 'partiton ({!r} != {!r})' - ''.format(template.space.partition, domain[0].partition)) super(LinDeformFixedTempl, self).__init__( - domain=domain, range=template.space, linear=False) + domain=range.tangent_bundle, range=range, linear=False + ) - self.__interp_byaxis = _normalize_interp(interp, template.space.ndim) + self.__template = range.element(template) + self.__interp_byaxis = _normalize_interp(interp, range.ndim) @property def template(self): @@ -240,7 +230,9 @@ def interp(self): def _call(self, displacement, out=None): """Implementation of ``self(displacement[, out])``.""" - return linear_deform(self.template, displacement, self.interp, out) + return linear_deform( + self.range, self.template, displacement, self.interp, out + ) def derivative(self, displacement): """Derivative of the operator at ``displacement``. @@ -258,30 +250,32 @@ def derivative(self, displacement): # To implement the complex case we need to be able to embed the real # vector field space into the range of the gradient. Issue #59. if not self.range.is_real: - raise NotImplementedError('derivative not implemented for complex ' - 'spaces.') + raise NotImplementedError( + 'derivative not implemented for complex spaces.' + ) - displacement = self.domain.element(displacement) + displ = self.domain.element(displacement) # TODO: allow users to select what method to use here. - grad = Gradient(domain=self.range, method='central', - pad_mode='symmetric') - grad_templ = grad(self.template) - def_grad = self.domain.element( - [linear_deform(gf, displacement, self.interp) for gf in grad_templ] + grad = Gradient( + domain=self.range, method='central', pad_mode='symmetric' ) + grad_templ = grad(self.template) + def_grad = [ + linear_deform(self.range, gf, displ, self.interp) + for gf in grad_templ + ] return PointwiseInner(self.domain, def_grad) def __repr__(self): """Return ``repr(self)``.""" - posargs = [self.template] - optargs = [ - ('domain', self.domain, self.template.space.tangent_bundle), - ('interp', self.interp, 'linear'), - ] - inner_str = signature_string(posargs, optargs, mod='!r', sep=',\n') - return '{}(\n{}\n)'.format(self.__class__.__name__, indent(inner_str)) + posargs = [self.range, self.template] + optargs = [('interp', self.interp, 'linear')] + inner_parts = signature_string_parts(posargs, optargs) + return repr_string( + self.__class__.__name__, inner_parts, allow_mixed_seps=False + ) class LinDeformFixedDisp(Operator): @@ -317,23 +311,16 @@ class LinDeformFixedDisp(Operator): i.e., :math:`W_v^*(I)(x) \approx \exp(-\mathrm{div}\,v(x))\, I(x - v(x))`. """ - def __init__(self, displacement, templ_space=None, interp='linear'): + def __init__(self, domain, displacement, interp='linear'): """Initialize a new instance. Parameters ---------- - displacement : element of a power space of `DiscretizedSpace` - Fixed displacement field used in the deformation. - 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 - this option is useful mainly for support of complex spaces and if - different interpolations should be used for displacement and - template. - - Default: ``displacement.space[0]`` - + domain : `DiscretizedSpace` + Template space from which the operator takes inputs. + displacement : `array-like` or callable + Fixed displacement field used in the deformation. Must be castable + to an element of ``domain.tangent_bundle``. interp : str or sequence of str Interpolation type that should be used to sample the template on the deformed grid. A single value applies to all axes, and a @@ -343,65 +330,51 @@ def __init__(self, displacement, templ_space=None, interp='linear'): Examples -------- - Create a simple 1D template to initialize the operator and - apply it to a displacement field. Where the displacement is zero, - the output value is the same as the input value. - In the 4-th point, the value is taken from 0.2 (one cell) to the - left, i.e. 1.0. + A simple displacement of one point can be achieved with a displacement + field that is everywhere zero except in that point. For instance, take + the value of the 4th point from ``0.2`` (one cell) to the left, i.e., + ``1.0``: >>> space = odl.uniform_discr(0, 1, 5) - >>> disp_field = space.tangent_bundle.element([[0, 0, 0, -0.2, 0]]) - >>> op = odl.deform.LinDeformFixedDisp(disp_field, interp='nearest') + >>> disp_field = [[0, 0, 0, -0.2, 0]] + >>> op = odl.deform.LinDeformFixedDisp( + ... space, disp_field, interp='nearest' + ... ) >>> template = [0, 0, 1, 0, 0] - >>> print(op([0, 0, 1, 0, 0])) - [ 0., 0., 1., 1., 0.] + >>> op(template) + array([ 0., 0., 1., 1., 0.]) - The result depends on the chosen interpolation. With 'linear' - interpolation and an offset of half the distance between two - points, 0.1, one gets the mean of the values. + The result depends on the chosen interpolation. With ``'linear'`` + interpolation and an offset of half the distance between two points, + ``0.1``, one gets the mean of the values: - >>> disp_field = space.tangent_bundle.element([[0, 0, 0, -0.1, 0]]) - >>> op = odl.deform.LinDeformFixedDisp(disp_field, interp='linear') + >>> space = odl.uniform_discr(0, 1, 5) + >>> disp_field = [[0, 0, 0, -0.1, 0]] + >>> op = odl.deform.LinDeformFixedDisp( + ... space, disp_field, interp='linear' + ... ) >>> template = [0, 0, 1, 0, 0] - >>> print(op(template)) - [ 0. , 0. , 1. , 0.5, 0. ] + >>> op(template) + array([ 0. , 0. , 1. , 0.5, 0. ]) """ - if not isinstance(displacement, ProductSpaceElement): + if not isinstance(domain, DiscretizedSpace): raise TypeError( - '`displacement` must be a `ProductSpaceElement`, got {!r}' - ''.format(displacement) - ) - - if not displacement.space.is_power_space: - raise ValueError( - '`displacement.space` must be a power space, got {!r}' - ''.format(displacement.space) + '`domain` must be a `DiscretizedSpace`, got {!r}'.format(domain) ) - if not isinstance(displacement.space[0], DiscretizedSpace): - raise ValueError( - '`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, 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( - '`templ_space.partition` not equal to `displacement`s ' - 'partiton ({!r} != {!r})' - ''.format(templ_space.partition, - displacement.space[0].partition) - ) super(LinDeformFixedDisp, self).__init__( - domain=templ_space, range=templ_space, linear=True) + domain=domain, range=domain, linear=True + ) - self.__interp_byaxis = _normalize_interp(interp, templ_space.ndim) + try: + self.__displacement = self.domain.tangent_bundle.element( + displacement + ) + except (ValueError, TypeError): + self.__displacement = self.domain.tangent_bundle.element( + [displacement] + ) + self.__interp_byaxis = _normalize_interp(interp, domain.ndim) @property def interp_byaxis(self): @@ -426,7 +399,9 @@ def displacement(self): def _call(self, template, out=None): """Implementation of ``self(template[, out])``.""" - return linear_deform(template, self.displacement, self.interp, out) + return linear_deform( + self.domain, template, self.displacement, self.interp, out + ) @property def inverse(self): @@ -436,7 +411,7 @@ def inverse(self): valid for small displacements. """ return LinDeformFixedDisp( - -self.displacement, templ_space=self.domain, interp=self.interp + self.domain, -self.displacement, interp=self.interp ) @property @@ -447,21 +422,23 @@ def adjoint(self): valid for small displacements. """ # TODO allow users to select what method to use here. - div_op = Divergence(domain=self.displacement.space, method='forward', - pad_mode='symmetric') + div_op = Divergence( + domain=self.domain.tangent_bundle, + method='forward', + pad_mode='symmetric', + ) jacobian_det = self.domain.element(np.exp(-div_op(self.displacement))) return jacobian_det * self.inverse def __repr__(self): """Return ``repr(self)``.""" - posargs = [self.displacement] - optargs = [ - ('templ_space', self.domain, self.displacement.space[0]), - ('interp', self.interp, 'linear'), - ] - inner_str = signature_string(posargs, optargs, mod='!r', sep=',\n') - return '{}(\n{}\n)'.format(self.__class__.__name__, indent(inner_str)) + posargs = [self.domain, self.displacement] + optargs = [('interp', self.interp, 'linear')] + inner_parts = signature_string_parts(posargs, optargs) + return repr_string( + self.__class__.__name__, inner_parts, allow_mixed_seps=False + ) if __name__ == '__main__': diff --git a/odl/diagnostics/operator.py b/odl/diagnostics/operator.py index 1134708fb6e..6b57de1d5b4 100644 --- a/odl/diagnostics/operator.py +++ b/odl/diagnostics/operator.py @@ -1,4 +1,4 @@ -# Copyright 2014-2019 The ODL contributors +# Copyright 2014-2020 The ODL contributors # # This file is part of ODL. # @@ -49,6 +49,8 @@ def __init__(self, operator, operator_norm=None, verbose=True, tol=1e-5): tolerance used in a test can be a factor times this number. """ self.operator = operator + self.opdomain = operator.domain + self.oprange = operator.range self.verbose = False if operator_norm is None: self.operator_norm = self.norm() @@ -64,30 +66,13 @@ def log(self, message): print(message) def norm(self): - """Estimate the operator norm of the operator. - - The norm is estimated by calculating - - ``A(x).norm() / x.norm()`` - - for some nonzero ``x`` - - Returns - ------- - norm : float - Estimate of operator norm - - References - ---------- - Wikipedia article on `Operator norm - `_. - """ + """Estimate the operator norm of the operator.""" self.log('\n== Calculating operator norm ==\n') - operator_norm = max(power_method_opnorm(self.operator, maxiter=2, - xstart=x) - for name, x in samples(self.operator.domain) - if name != 'Zero') + operator_norm = max( + power_method_opnorm(self.operator, maxiter=2, xstart=x) + for name, x in samples(self.opdomain) if name != 'Zero' + ) self.log('Norm is at least: {}'.format(operator_norm)) self.operator_norm = operator_norm @@ -104,13 +89,14 @@ def self_adjoint(self): logger=self.log ) as counter: - for [name_x, x], [name_y, y] in samples(self.operator.domain, - self.operator.range): - x_norm = x.norm() - y_norm = y.norm() + for [name_x, x], [name_y, y] in samples( + self.opdomain, self.oprange + ): + x_norm = self.opdomain.norm(x) + y_norm = self.oprange.norm(y) - l_inner = self.operator(x).inner(y) - r_inner = x.inner(self.operator(y)) + l_inner = self.oprange.inner(self.operator(x), y) + r_inner = self.opdomain.inner(x, self.operator(y)) denom = self.operator_norm * x_norm * y_norm error = 0 if denom == 0 else abs(l_inner - r_inner) / denom @@ -137,13 +123,14 @@ def _adjoint_definition(self): logger=self.log ) as counter: - for [name_x, x], [name_y, y] in samples(self.operator.domain, - self.operator.range): - x_norm = x.norm() - y_norm = y.norm() + for [name_x, x], [name_y, y] in samples( + self.opdomain, self.oprange + ): + x_norm = self.opdomain.norm(x) + y_norm = self.oprange.norm(y) - l_inner = self.operator(x).inner(y) - r_inner = x.inner(self.operator.adjoint(y)) + l_inner = self.oprange.inner(self.operator(x), y) + r_inner = self.opdomain.inner(x, self.operator.adjoint(y)) denom = self.operator_norm * x_norm * y_norm error = 0 if denom == 0 else abs(l_inner - r_inner) / denom @@ -176,15 +163,15 @@ def _adjoint_of_adjoint(self): err_msg='error = ||Ax - (A^*)^* x|| / ||A|| ||x||', logger=self.log ) as counter: - for [name_x, x] in self.operator.domain.examples: + for [name_x, x] in self.opdomain.examples: opx = self.operator(x) op_adj_adj_x = self.operator.adjoint.adjoint(x) - denom = self.operator_norm * x.norm() + denom = self.operator_norm * self.opdomain.norm(x) if denom == 0: error = 0 else: - error = (opx - op_adj_adj_x).norm() / denom + error = self.oprange.norm(opx - op_adj_adj_x) / denom if error > self.tol: counter.fail('x={:25s} : error={:6.5f}' @@ -207,11 +194,11 @@ def adjoint(self): self.log('\n== Verifying operator adjoint ==\n') domain_range_ok = True - if self.operator.domain != self.operator.adjoint.range: + if self.opdomain != self.operator.adjoint.range: print('*** ERROR: A.domain != A.adjoint.range ***') domain_range_ok = False - if self.operator.range != self.operator.adjoint.domain: + if self.oprange != self.operator.adjoint.domain: print('*** ERROR: A.range != A.adjoint.domain ***') domain_range_ok = False @@ -239,8 +226,9 @@ def _derivative_convergence(self): err_msg="error = inf_c ||A(x+c*p)-A(x)-A'(x)(c*p)|| / c", logger=self.log ) as counter: - for [name_x, x], [name_dx, dx] in samples(self.operator.domain, - self.operator.domain): + for [name_x, x], [name_dx, dx] in samples( + self.opdomain, self.opdomain + ): # Precompute some values deriv = self.operator.derivative(x) derivdx = deriv(dx) @@ -253,7 +241,7 @@ def _derivative_convergence(self): while c > 1e-14: exact_step = self.operator(x + dx * c) - opx expected_step = c * derivdx - err = (exact_step - expected_step).norm() / c + err = self.oprange.norm(exact_step - expected_step) / c # Need to be slightly more generous here due to possible # numerical instabilities. @@ -289,7 +277,7 @@ def derivative(self): self.log('\n== Verifying operator derivative ==') try: - deriv = self.operator.derivative(self.operator.domain.zero()) + deriv = self.operator.derivative(self.opdomain.zero()) if not deriv.is_linear: print('Derivative is not a linear operator') @@ -311,14 +299,17 @@ def _scale_invariance(self): err_msg='error = ||A(c*x)-c*A(x)|| / |c| ||A|| ||x||', logger=self.log ) as counter: - for [name_x, x], [_, scale] in samples(self.operator.domain, - self.operator.domain.field): + for [name_x, x], [_, scale] in samples( + self.opdomain, self.opdomain.field + ): opx = self.operator(x) scaled_opx = self.operator(scale * x) - denom = self.operator_norm * scale * x.norm() - error = (0 if denom == 0 - else (scaled_opx - opx * scale).norm() / denom) + denom = self.operator_norm * scale * self.opdomain.norm(x) + error = ( + 0 if denom == 0 + else self.oprange.norm(scaled_opx - opx * scale) / denom + ) if error > self.tol: counter.fail('x={:25s} scale={:7.2f} error={:6.5f}' @@ -332,15 +323,20 @@ def _addition_invariance(self): '||A||(||x|| + ||y||)', logger=self.log ) as counter: - for [name_x, x], [name_y, y] in samples(self.operator.domain, - self.operator.domain): + for [name_x, x], [name_y, y] in samples( + self.opdomain, self.opdomain + ): opx = self.operator(x) opy = self.operator(y) opxy = self.operator(x + y) - denom = self.operator_norm * (x.norm() + y.norm()) - error = (0 if denom == 0 - else (opxy - opx - opy).norm() / denom) + denom = self.operator_norm * ( + self.opdomain.norm(x) + self.opdomain.norm(y) + ) + error = ( + 0 if denom == 0 + else self.oprange.norm(opxy - opx - opy) / denom + ) if error > self.tol: counter.fail('x={:25s} y={:25s} error={:6.5f}' @@ -355,8 +351,8 @@ def linear(self): self.log('\n== Verifying operator linearity ==\n') # Test if zero gives zero - result = self.operator(self.operator.domain.zero()) - result_norm = result.norm() + result = self.operator(self.opdomain.zero()) + result_norm = self.oprange.norm(result) if result_norm != 0.0: print("||A(0)||={:6.5f}. Should be 0.0000".format(result_norm)) diff --git a/odl/diagnostics/space.py b/odl/diagnostics/space.py index d391fbb5956..b3a9bb02e11 100644 --- a/odl/diagnostics/space.py +++ b/odl/diagnostics/space.py @@ -1,4 +1,4 @@ -# Copyright 2014-2019 The ODL contributors +# Copyright 2014-2020 The ODL contributors # # This file is part of ODL. # @@ -20,23 +20,20 @@ __all__ = ('SpaceTest',) -def _approx_equal(x, y, eps): +def _approx_equal(space, x, y, eps): """Test if elements ``x`` and ``y`` are approximately equal. ``eps`` is a given absolute tolerance. """ - if x.space != y.space: - return False - if x is y: return True try: - return x.dist(y) <= eps + return space.dist(x, y) <= eps except NotImplementedError: try: - return x == y - except NotImplementedError: + return all(x == y) + except (NotImplementedError, ValueError): return False @@ -71,7 +68,7 @@ def log(self, message): if self.verbose: print(message) - def element_method(self): + def element(self): """Verify `LinearSpace.element`.""" with fail_counter( test_name='Verifying element method', logger=self.log @@ -144,10 +141,12 @@ def _associativity_of_addition(self): logger=self.log ) as counter: - for [n_x, x], [n_y, y], [n_z, z] in samples(self.space, - self.space, - self.space): - correct = _approx_equal(x + (y + z), (x + y) + z, self.tol) + for [n_x, x], [n_y, y], [n_z, z] in samples( + self.space, self.space, self.space + ): + correct = _approx_equal( + self.space, x + (y + z), (x + y) + z, self.tol + ) if not correct: counter.fail('failed with x={:25s} y={:25s} z={:25s}' ''.format(n_x, n_y, n_z)) @@ -161,7 +160,7 @@ def _commutativity_of_addition(self): ) as counter: for [n_x, x], [n_y, y] in samples(self.space, self.space): - correct = _approx_equal(x + y, y + x, self.tol) + correct = _approx_equal(self.space, x + y, y + x, self.tol) if not correct: counter.fail('failed with x={:25s} y={:25s}' ''.format(n_x, n_y)) @@ -181,7 +180,7 @@ def _identity_of_addition(self): ) as counter: for [n_x, x] in samples(self.space): - correct = _approx_equal(x + zero, x, self.tol) + correct = _approx_equal(self.space, x + zero, x, self.tol) if not correct: counter.fail('failed with x={:25s}'.format(n_x)) @@ -200,7 +199,7 @@ def _inverse_element_of_addition(self): ) as counter: for [n_x, x] in samples(self.space): - correct = _approx_equal(x + (-x), zero, self.tol) + correct = _approx_equal(self.space, x + (-x), zero, self.tol) if not correct: counter.fail('failed with x={:25s}'.format(n_x)) @@ -212,10 +211,12 @@ def _commutativity_of_scalar_mult(self): logger=self.log ) as counter: - for [n_x, x], [_, a], [_, b] in samples(self.space, - self.space.field, - self.space.field): - correct = _approx_equal(a * (b * x), (a * b) * x, self.tol) + for [n_x, x], [_, a], [_, b] in samples( + self.space, self.space.field, self.space.field + ): + correct = _approx_equal( + self.space, a * (b * x), (a * b) * x, self.tol + ) if not correct: counter.fail('failed with x={:25s}, a={}, b={}' ''.format(n_x, a, b)) @@ -229,7 +230,7 @@ def _identity_of_mult(self): ) as counter: for [n_x, x] in samples(self.space): - correct = _approx_equal(1 * x, x, self.tol) + correct = _approx_equal(self.space, 1 * x, x, self.tol) if not correct: counter.fail('failed with x={:25s}'.format(n_x)) @@ -242,10 +243,12 @@ def _distributivity_of_mult_vector(self): logger=self.log ) as counter: - for [n_x, x], [n_y, y], [_, a] in samples(self.space, - self.space, - self.space.field): - correct = _approx_equal(a * (x + y), a * x + a * y, self.tol) + for [n_x, x], [n_y, y], [_, a] in samples( + self.space, self.space, self.space.field + ): + correct = _approx_equal( + self.space, a * (x + y), a * x + a * y, self.tol + ) if not correct: counter.fail('failed with x={:25s}, y={:25s}, a={}' ''.format(n_x, n_y, a)) @@ -259,10 +262,12 @@ def _distributivity_of_mult_scalar(self): logger=self.log ) as counter: - for [n_x, x], [_, a], [_, b] in samples(self.space, - self.space.field, - self.space.field): - correct = _approx_equal((a + b) * x, a * x + b * x, self.tol) + for [n_x, x], [_, a], [_, b] in samples( + self.space, self.space.field, self.space.field + ): + correct = _approx_equal( + self.space, (a + b) * x, a * x + b * x, self.tol + ) if not correct: counter.fail('failed with x={:25s}, a={}, b={}' ''.format(n_x, a, b)) @@ -276,8 +281,10 @@ def _subtraction(self): ) as counter: for [n_x, x], [n_y, y] in samples(self.space, self.space): - correct = (_approx_equal(x - y, x + (-1 * y), self.tol) and - _approx_equal(x - y, x + (-y), self.tol)) + correct = ( + _approx_equal(self.space, x - y, x + (-1 * y), self.tol) + and _approx_equal(self.space, x - y, x + (-y), self.tol) + ) if not correct: counter.fail('failed with x={:25s}, y={:25s}' ''.format(n_x, n_y)) @@ -292,7 +299,9 @@ def _division(self): for [n_x, x], [_, a] in samples(self.space, self.space.field): if a != 0: - correct = _approx_equal(x / a, x * (1.0 / a), self.tol) + correct = _approx_equal( + self.space, x / a, x * (1.0 / a), self.tol + ) if not correct: counter.fail('failed with x={:25s}, a={}' ''.format(n_x, a)) @@ -306,19 +315,19 @@ def _lincomb_aliased(self): ) as counter: for [n_x, x_in], [n_y, y] in samples(self.space, self.space): - x = x_in.copy() - x.lincomb(1, x, 1, y) - correct = _approx_equal(x, x_in + y, self.tol) + x = self.space.copy(x_in) + self.space.lincomb(1, x, 1, y, x) + correct = _approx_equal(self.space, x, x_in + y, self.tol) if not correct: - counter.fail('failed with x.lincomb(1, x, 1, y),' + counter.fail('failed with lincomb(1, x, 1, y, x),' 'x={:25s} y={:25s} ' ''.format(n_x, n_y)) - x = x_in.copy() - x.lincomb(1, x, 1, x) - correct = _approx_equal(x, x_in + x_in, self.tol) + x = self.space.copy(x_in) + self.space.lincomb(1, x, 1, x, x) + correct = _approx_equal(self.space, x, x_in + x_in, self.tol) if not correct: - counter.fail('failed with x.lincomb(1, x, 1, x),' + counter.fail('failed with lincomb(1, x, 1, x, x),' 'x={:25s} ' ''.format(n_x)) @@ -369,7 +378,9 @@ def _inner_linear_scalar(self): for [n_x, x], [n_y, y], [_, a] in samples(self.space, self.space, self.space.field): - error = abs((a * x).inner(y) - a * x.inner(y)) + error = abs( + self.space.inner(a * x, y) - a * self.space.inner(x, y) + ) if error > self.tol: counter.fail('x={:25s}, y={:25s}, a={}: error={}' ''.format(n_x, n_y, a, error)) @@ -383,7 +394,9 @@ def _inner_conjugate_symmetry(self): ) as counter: for [n_x, x], [n_y, y] in samples(self.space, self.space): - error = abs((x).inner(y) - y.inner(x).conjugate()) + error = abs( + self.space.inner(x, y) - self.space.inner(y, x).conjugate() + ) if error > self.tol: counter.fail('x={:25s}, y={:25s}: error={}' ''.format(n_x, n_y, error)) @@ -400,7 +413,10 @@ def _inner_linear_sum(self): for [n_x, x], [n_y, y], [n_z, z] in samples(self.space, self.space, self.space): - error = abs((x + y).inner(z) - (x.inner(z) + y.inner(z))) + error = abs( + self.space.inner(x + y, z) + - (self.space.inner(x, z) + self.space.inner(y, z)) + ) if error > self.tol: counter.fail('x={:25s}, y={:25s}, z={:25s}: error={}' ''.format(n_x, n_y, n_z, error)) @@ -414,7 +430,7 @@ def _inner_positive(self): ) as counter: for [n_x, x] in samples(self.space): - inner = x.inner(x) + inner = self.space.inner(x, x) if abs(inner.imag) > self.tol: counter.fail('.imag != 0, x={:25s}, .imag = {}' @@ -457,7 +473,7 @@ def inner(self): try: zero = self.space.zero() - zero.inner(zero) + self.space.inner(zero, zero) except NotImplementedError: self.log('Space has no inner product') return @@ -475,7 +491,7 @@ def _norm_positive(self): ) as counter: for [n_x, x] in samples(self.space): - norm = x.norm() + norm = self.space.norm(x) if n_x == 'Zero' and norm != 0: counter.fail('||0|| != 0.0, x={:25s}: ||x||={}' @@ -494,9 +510,9 @@ def _norm_subadditive(self): ) as counter: for [n_x, x], [n_y, y] in samples(self.space, self.space): - norm_x = x.norm() - norm_y = y.norm() - norm_xy = (x + y).norm() + norm_x = self.space.norm(x) + norm_y = self.space.norm(y) + norm_xy = self.space.norm(x + y) error = norm_xy - norm_x - norm_y @@ -513,7 +529,9 @@ def _norm_homogeneity(self): ) as counter: for [n_x, x], [_, a] in samples(self.space, self.space.field): - error = abs((a * x).norm() - abs(a) * x.norm()) + error = abs( + self.space.norm(a * x) - abs(a) * self.space.norm(x) + ) if error > self.tol: counter.fail('x={:25s} a={}: error={}' ''.format(n_x, a, error)) @@ -521,10 +539,10 @@ def _norm_homogeneity(self): def _norm_inner_compatible(self): """Verify compatibility of norm and inner product.""" try: - zero = self.space.zero() - zero.inner(zero) + self.space.norm(self.space.zero()) + self.space.inner(self.space.zero(), self.space.zero()) except NotImplementedError: - self.log('Space has no inner product') + self.log('Space does not have norm and inner product') return with fail_counter( @@ -534,7 +552,9 @@ def _norm_inner_compatible(self): ) as counter: for [n_x, x] in samples(self.space): - error = abs(x.norm() ** 2 - x.inner(x)) + error = abs( + self.space.norm(x) ** 2 - self.space.inner(x, x) + ) if error > self.tol: counter.fail('x={:25s}: error={}' @@ -576,7 +596,7 @@ def norm(self): self.log('\n== Verifying norm ==\n') try: - self.space.zero().norm() + self.space.norm(self.space.zero()) except NotImplementedError: self.log('Space has no norm') return @@ -595,7 +615,7 @@ def _dist_positivity(self): ) as counter: for [n_x, x], [n_y, y] in samples(self.space, self.space): - dist = x.dist(y) + dist = self.space.dist(x, y) if n_x == n_y and dist != 0: counter.fail('d(x, x) != 0.0, x={:25s}: dist={}' @@ -613,8 +633,8 @@ def _dist_symmetric(self): ) as counter: for [n_x, x], [n_y, y] in samples(self.space, self.space): - dist_1 = x.dist(y) - dist_2 = y.dist(x) + dist_1 = self.space.dist(x, y) + dist_2 = self.space.dist(y, x) error = abs(dist_1 - dist_2) if error > self.tol: @@ -629,12 +649,12 @@ def _dist_subtransitive(self): logger=self.log ) as counter: - for [n_x, x], [n_y, y], [n_z, z] in samples(self.space, - self.space, - self.space): - dxz = x.dist(z) - dxy = x.dist(y) - dyz = y.dist(z) + for [n_x, x], [n_y, y], [n_z, z] in samples( + self.space, self.space, self.space + ): + dxz = self.space.dist(x, z) + dxy = self.space.dist(x, y) + dyz = self.space.dist(y, z) error = dxz - (dxy + dyz) if error > self.tol: @@ -644,9 +664,10 @@ def _dist_subtransitive(self): def _dist_norm_compatible(self): """Verify compatibility of distance and norm.""" try: - self.space.zero().norm() + self.space.norm(self.space.zero()) + self.space.dist(self.space.zero(), self.space.zero()) except NotImplementedError: - self.log('Space has no norm') + self.log('Space does not have norm and dist') return with fail_counter( @@ -655,9 +676,10 @@ def _dist_norm_compatible(self): logger=self.log ) as counter: - for [n_x, x], [n_y, y] in samples(self.space, - self.space): - error = abs(x.dist(y) - (x - y).norm()) + for [n_x, x], [n_y, y] in samples(self.space, self.space): + error = abs( + self.space.dist(x, y) - self.space.norm(x - y) + ) if error > self.tol: counter.fail('x={:25s}, y={:25s}: error={}' @@ -720,7 +742,7 @@ def _multiply_zero(self): logger=self.log ) as counter: for [n_x, x] in samples(self.space): - error = (zero * x).norm() + error = self.space.norm(zero * x) if error > self.tol: counter.fail('x={:25s},: error={}' @@ -734,10 +756,10 @@ def _multiply_commutative(self): logger=self.log ) as counter: - for [n_x, x], [n_y, y], _ in samples(self.space, - self.space, - self.space): - correct = _approx_equal(x * y, y * x, self.tol) + for [n_x, x], [n_y, y], _ in samples( + self.space, self.space, self.space + ): + correct = _approx_equal(self.space, x * y, y * x, self.tol) if not correct: counter.fail('failed with x={:25s} y={:25s}' ''.format(n_x, n_y)) @@ -750,10 +772,12 @@ def _multiply_associative(self): logger=self.log ) as counter: - for [n_x, x], [n_y, y], [n_z, z] in samples(self.space, - self.space, - self.space): - correct = _approx_equal(x * (y * z), (x * y) * z, self.tol) + for [n_x, x], [n_y, y], [n_z, z] in samples( + self.space, self.space, self.space + ): + correct = _approx_equal( + self.space, x * (y * z), (x * y) * z, self.tol + ) if not correct: counter.fail('failed with x={:25s} y={:25s} z={:25s}' ''.format(n_x, n_y, n_z)) @@ -767,10 +791,12 @@ def _multiply_distributive_scalar(self): logger=self.log ) as counter: - for [n_x, x], [n_y, y], [_, a] in samples(self.space, - self.space, - self.space.field): - correct = _approx_equal(a * (x + y), a * x + a * y, self.tol) + for [n_x, x], [n_y, y], [_, a] in samples( + self.space, self.space, self.space.field + ): + correct = _approx_equal( + self.space, a * (x + y), a * x + a * y, self.tol + ) if not correct: counter.fail('failed with x={:25s} y={:25s} a={}' ''.format(n_x, n_y, a)) @@ -784,10 +810,12 @@ def _multiply_distributive_vector(self): logger=self.log ) as counter: - for [n_x, x], [n_y, y], [n_z, z] in samples(self.space, - self.space, - self.space): - correct = _approx_equal(x * (y + z), x * y + x * z, self.tol) + for [n_x, x], [n_y, y], [n_z, z] in samples( + self.space, self.space, self.space + ): + correct = _approx_equal( + self.space, x * (y + z), x * y + x * z, self.tol + ) if not correct: counter.fail('failed with x={:25s} y={:25s} z={:25s}' ''.format(n_x, n_y, n_z)) @@ -892,128 +920,12 @@ def contains(self): counter.fail('not obj not in space, with obj={}' ''.format(obj)) - def element_assign(self): - """Verify `LinearSpaceElement.assign`.""" - with fail_counter( - test_name='Verify behavior of `LinearSpaceElement.assign`', - logger=self.log - ) as counter: - - for [n_x, x], [n_y, y] in samples(self.space, - self.space): - x.assign(y) - correct = _approx_equal(x, y, self.tol) - if not correct: - counter.fail('failed with x={:25s} y={:25s}' - ''.format(n_x, n_y)) - - def element_copy(self): - """Verify `LinearSpaceElement.copy`.""" - with fail_counter( - test_name='Verify behavior of `LinearSpaceElement.copy`', - logger=self.log - ) as counter: - - for [n_x, x] in samples(self.space): - # equal after copy - y = x.copy() - correct = _approx_equal(x, y, self.tol) - if not correct: - counter.fail('failed with x={:s5s}' - ''.format(n_x)) - - # modify y, x stays the same - y *= 2.0 - correct = n_x == 'Zero' or not _approx_equal(x, y, self.tol) - if not correct: - counter.fail('modified y, x changed with x={:25s}' - ''.format(n_x)) - - def element_set_zero(self): - """Verify `LinearSpaceElement.set_zero`.""" - try: - zero = self.space.zero() - except NotImplementedError: - print('*** SPACE HAS NO ZERO VECTOR ***') - return - - with fail_counter( - test_name='Verify behavior of `LinearSpaceElement.set_zero`', - logger=self.log - ) as counter: - - for [n_x, x] in samples(self.space): - x.set_zero() - correct = _approx_equal(x, zero, self.tol) - if not correct: - counter.fail('failed with x={:25s}' - ''.format(n_x)) - - def element_equals(self): - """Verify `LinearSpaceElement.__eq__`.""" - try: - zero = self.space.zero() - except NotImplementedError: - print('*** SPACE HAS NO ZERO VECTOR ***') - return - - try: - zero == zero - except NotImplementedError: - self.log('Vector has no __eq__') - return - - with fail_counter( - test_name='Verify behavior of `element1 == element2`', - logger=self.log - ) as counter: - - for [n_x, x], [n_y, y] in samples(self.space, - self.space): - if n_x == n_y: - if not x == y: - counter.fail('failed x == x with x={:25s}' - ''.format(n_x)) - - if x != y: - counter.fail('failed not x != x with x={:25s}' - ''.format(n_x)) - else: - if x == y: - counter.fail('failed not x == y with x={:25s}, ' - 'x={:25s}'.format(n_x, n_y)) - - if not x != y: - counter.fail('failed x != y with x={:25s}, x={:25s}' - ''.format(n_x, n_y)) - - def element_space(self): - """Verify `LinearSpaceElement.space`.""" - with fail_counter( - test_name='Verify `LinearSpaceElement.space`', - logger=self.log - ) as counter: - - for [n_x, x] in samples(self.space): - if x.space != self.space: - counter.fail('failed with x={:25s}'.format(n_x)) - - def element(self): - """Verify `LinearSpaceElement`.""" - - self.log('\n== Verifying element attributes ==\n') - self.element_assign() - self.element_copy() - self.element_set_zero() - self.element_equals() - self.element_space() - def run_tests(self): """Run all tests on this space.""" self.log('\n== RUNNING ALL TESTS ==\n') self.log('Space = {}'.format(self.space)) self.field() - self.element_method() + self.element() self.linearity() self.inner() self.norm() @@ -1021,7 +933,6 @@ def run_tests(self): self.multiply() self.equals() self.contains() - self.element() def __str__(self): """Return ``str(self)``.""" diff --git a/odl/discr/__init__.py b/odl/discr/__init__.py index be0ebb1b817..accd2a4563c 100644 --- a/odl/discr/__init__.py +++ b/odl/discr/__init__.py @@ -10,12 +10,12 @@ from __future__ import absolute_import -from . import discr_utils from .diff_ops import * from .discr_ops import * from .discr_space import * from .grid import * from .partition import * +from . import discr_utils __all__ = () __all__ += diff_ops.__all__ diff --git a/odl/discr/diff_ops.py b/odl/discr/diff_ops.py index e7ba9d7f168..d596af237e7 100644 --- a/odl/discr/diff_ops.py +++ b/odl/discr/diff_ops.py @@ -101,10 +101,8 @@ def __init__(self, domain, axis, range=None, method='forward', >>> discr = odl.uniform_discr([0, 0], [2, 1], f.shape) >>> par_deriv = PartialDerivative(discr, axis=0, pad_mode='order1') >>> par_deriv(f) - uniform_discr([ 0., 0.], [ 2., 1.], (2, 5)).element( - [[ 0., 1., 2., 3., 4.], - [ 0., 1., 2., 3., 4.]] - ) + array([[ 0., 1., 2., 3., 4.], + [ 0., 1., 2., 3., 4.]]) """ if not isinstance(domain, DiscretizedSpace): raise TypeError('`domain` {!r} is not a DiscretizedSpace instance' @@ -137,11 +135,9 @@ def _call(self, x, out=None): if out is None: out = self.range.element() - # TODO: this pipes CUDA arrays through NumPy. Write native operator. - with writable_array(out) as out_arr: - finite_diff(x.asarray(), axis=self.axis, dx=self.dx, - method=self.method, pad_mode=self.pad_mode, - pad_const=self.pad_const, out=out_arr) + finite_diff(x, axis=self.axis, dx=self.dx, + method=self.method, pad_mode=self.pad_mode, + pad_const=self.pad_const, out=out) return out def derivative(self, point=None): @@ -274,26 +270,20 @@ def __init__(self, domain=None, range=None, method='forward', >>> grad = Gradient(discr) >>> grad_f = grad(f) >>> grad_f[0] - uniform_discr([ 0., 0.], [ 2., 5.], (2, 5)).element( - [[ 0., 1., 2., 3., 4.], - [ 0., -2., -4., -6., -8.]] - ) + array([[ 0., 1., 2., 3., 4.], + [ 0., -2., -4., -6., -8.]]) >>> grad_f[1] - uniform_discr([ 0., 0.], [ 2., 5.], (2, 5)).element( - [[ 1., 1., 1., 1., -4.], - [ 2., 2., 2., 2., -8.]] - ) + array([[ 1., 1., 1., 1., -4.], + [ 2., 2., 2., 2., -8.]]) Verify adjoint: >>> g = grad.range.element((data, data ** 2)) >>> adj_g = grad.adjoint(g) >>> adj_g - uniform_discr([ 0., 0.], [ 2., 5.], (2, 5)).element( - [[ 0., -2., -5., -8., -11.], - [ 0., -5., -14., -23., -32.]] - ) - >>> g.inner(grad_f) / f.inner(adj_g) + array([[ -0., -2., -5., -8., -11.], + [ -0., -5., -14., -23., -32.]]) + >>> grad.range.inner(g, grad_f) / grad.domain.inner(f, adj_g) 1.0 """ if domain is None and range is None: @@ -347,16 +337,14 @@ def _call(self, x, out=None): if out is None: out = self.range.element() - x_arr = x.asarray() ndim = self.domain.ndim dx = self.domain.cell_sides for axis in range(ndim): - with writable_array(out[axis]) as out_arr: - finite_diff(x_arr, axis=axis, dx=dx[axis], method=self.method, - pad_mode=self.pad_mode, - pad_const=self.pad_const, - out=out_arr) + finite_diff(x, axis=axis, dx=dx[axis], method=self.method, + pad_mode=self.pad_mode, + pad_const=self.pad_const, + out=out[axis]) return out def derivative(self, point=None): @@ -494,16 +482,16 @@ def __init__(self, domain=None, range=None, method='forward', ... [2., 3., 4., 5., 6.]]) >>> f = div.domain.element([data, data]) >>> div_f = div(f) - >>> print(div_f) - [[ 2., 2., 2., 2., -3.], - [ 2., 2., 2., 2., -4.], - [ -1., -2., -3., -4., -12.]] + >>> div_f + array([[ 2., 2., 2., 2., -3.], + [ 2., 2., 2., 2., -4.], + [ -1., -2., -3., -4., -12.]]) Verify adjoint: >>> g = div.range.element(data ** 2) >>> adj_div_g = div.adjoint(g) - >>> g.inner(div_f) / f.inner(adj_div_g) + >>> div.range.inner(g, div_f) / div.domain.inner(f, adj_div_g) 1.0 """ if domain is None and range is None: @@ -560,7 +548,7 @@ def _call(self, x, out=None): ndim = self.range.ndim dx = self.range.cell_sides - tmp = np.empty(out.shape, out.dtype, order=out.space.default_order) + tmp = np.empty(out.shape, out.dtype, order=self.range.default_order) with writable_array(out) as out_arr: for axis in range(ndim): finite_diff(x[axis], axis=axis, dx=dx[axis], @@ -679,11 +667,9 @@ def __init__(self, domain, range=None, pad_mode='constant', pad_const=0): >>> f = space.element(data) >>> lap = Laplacian(space) >>> lap(f) - uniform_discr([ 0., 0.], [ 3., 3.], (3, 3)).element( - [[ 0., 1., 0.], - [ 1., -4., 1.], - [ 0., 1., 0.]] - ) + array([[ 0., 1., 0.], + [ 1., -4., 1.], + [ 0., 1., 0.]]) """ if not isinstance(domain, DiscretizedSpace): raise TypeError('`domain` {!r} is not a DiscretizedSpace instance' @@ -701,7 +687,7 @@ def __init__(self, domain, range=None, pad_mode='constant', pad_const=0): ''.format(pad_mode_in)) if pad_mode in ('order1', 'order1_adjoint', 'order2', 'order2_adjoint'): - # TODO: Add these pad modes + # TODO: add these pad modes raise ValueError('`pad_mode` {} not implemented for Laplacian.' ''.format(pad_mode_in)) @@ -712,31 +698,27 @@ def _call(self, x, out=None): if out is None: out = self.range.zero() else: - out.set_zero() - - x_arr = x.asarray() - out_arr = out.asarray() - tmp = np.empty(out.shape, out.dtype, order=out.space.default_order) + out[:] = 0 + tmp = np.empty(out.shape, out.dtype, order=self.range.default_order) ndim = self.domain.ndim dx = self.domain.cell_sides - with writable_array(out) as out_arr: - for axis in range(ndim): - # TODO: this can be optimized - finite_diff(x_arr, axis=axis, dx=dx[axis] ** 2, - method='forward', - pad_mode=self.pad_mode, - pad_const=self.pad_const, out=tmp) + for axis in range(ndim): + # TODO: this can be optimized + finite_diff(x, axis=axis, dx=dx[axis] ** 2, + method='forward', + pad_mode=self.pad_mode, + pad_const=self.pad_const, out=tmp) - out_arr += tmp + out += tmp - finite_diff(x_arr, axis=axis, dx=dx[axis] ** 2, - method='backward', - pad_mode=self.pad_mode, - pad_const=self.pad_const, out=tmp) + finite_diff(x, axis=axis, dx=dx[axis] ** 2, + method='backward', + pad_mode=self.pad_mode, + pad_const=self.pad_const, out=tmp) - out_arr -= tmp + out -= tmp return out @@ -880,7 +862,7 @@ def finite_diff(f, axis, dx=1.0, method='forward', out=None, In-place evaluation: - >>> out = f.copy() + >>> out = np.copy(f) >>> out is finite_diff(f, axis=0, out=out) True """ diff --git a/odl/discr/discr_ops.py b/odl/discr/discr_ops.py index bebaa195973..e886bb3d2ff 100644 --- a/odl/discr/discr_ops.py +++ b/odl/discr/discr_ops.py @@ -18,10 +18,8 @@ from odl.discr.partition import uniform_partition from odl.operator import Operator from odl.space import tensor_space -from odl.util import ( - normalized_scalar_param_list, resize_array, safe_int_conv, writable_array) +from odl.util import normalized_scalar_param_list, resize_array, safe_int_conv from odl.util.numerics import _SUPPORTED_RESIZE_PAD_MODES -from odl.util.utility import nullcontext __all__ = ('Resampling', 'ResizingOperator') @@ -64,14 +62,14 @@ def __init__(self, domain, range, interp): Apply the corresponding resampling operator to an element: - >>> print(resampling([0, 1, 0])) - [ 0., 0., 1., 1., 0., 0.] + >>> resampling([0, 1, 0]) + array([ 0., 0., 1., 1., 0., 0.]) With linear interpolation: >>> resampling = odl.Resampling(coarse_discr, fine_discr, 'linear') - >>> print(resampling([0, 1, 0])) - [ 0. , 0.25, 0.75, 0.75, 0.25, 0. ] + >>> resampling([0, 1, 0]) + array([ 0. , 0.25, 0.75, 0.75, 0.25, 0. ]) """ if domain.domain != range.domain: raise ValueError( @@ -110,11 +108,9 @@ def _call(self, x, out=None): x, self.domain.grid.coord_vectors, self.interp ) - 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 - ) + return point_collocation( + interpolator, self.range.meshgrid, out=out + ) @property def inverse(self): @@ -154,14 +150,14 @@ def adjoint(self): coarser to a finer sampling: >>> x = [0, 1, 0] - >>> print(resampling_inv(resampling(x))) - [ 0., 1., 0.] + >>> resampling_inv(resampling(x)) + array([ 0., 1., 0.]) However, it can fail in the other direction: >>> y = [0, 0, 0, 1, 0, 0] - >>> print(resampling(resampling_inv(y))) - [ 0., 0., 1., 1., 0., 0.] + >>> resampling(resampling_inv(y)) + array([ 0., 0., 1., 1., 0., 0.]) """ return self.inverse @@ -254,29 +250,29 @@ def __init__(self, domain, range=None, ran_shp=None, **kwargs): >>> x = [[1, 2, 3, 4], ... [5, 6, 7, 8]] >>> resize_op = odl.ResizingOperator(space, ran_shp=(4, 4)) - >>> print(resize_op(x)) - [[ 0., 0., 0., 0.], - [ 1., 2., 3., 4.], - [ 5., 6., 7., 8.], - [ 0., 0., 0., 0.]] + >>> resize_op(x) + array([[ 0., 0., 0., 0.], + [ 1., 2., 3., 4.], + [ 5., 6., 7., 8.], + [ 0., 0., 0., 0.]]) >>> >>> resize_op = odl.ResizingOperator(space, ran_shp=(4, 4), ... offset=(0, 0), ... pad_mode='periodic') - >>> print(resize_op(x)) - [[ 1., 2., 3., 4.], - [ 5., 6., 7., 8.], - [ 1., 2., 3., 4.], - [ 5., 6., 7., 8.]] + >>> resize_op(x) + array([[ 1., 2., 3., 4.], + [ 5., 6., 7., 8.], + [ 1., 2., 3., 4.], + [ 5., 6., 7., 8.]]) >>> >>> resize_op = odl.ResizingOperator(space, ran_shp=(4, 4), ... offset=(0, 0), ... pad_mode='order0') - >>> print(resize_op(x)) - [[ 1., 2., 3., 4.], - [ 5., 6., 7., 8.], - [ 5., 6., 7., 8.], - [ 5., 6., 7., 8.]] + >>> resize_op(x) + array([[ 1., 2., 3., 4.], + [ 5., 6., 7., 8.], + [ 5., 6., 7., 8.], + [ 5., 6., 7., 8.]]) Alternatively, the range of the operator can be provided directly. This requires that the partitions match, i.e. that the cell sizes @@ -286,11 +282,11 @@ def __init__(self, domain, range=None, ran_shp=None, **kwargs): >>> large_spc = odl.uniform_discr([-0.5, 0], [1.5, 1], (4, 4)) >>> resize_op = odl.ResizingOperator(space, large_spc, ... pad_mode='periodic') - >>> print(resize_op(x)) - [[ 5., 6., 7., 8.], - [ 1., 2., 3., 4.], - [ 5., 6., 7., 8.], - [ 1., 2., 3., 4.]] + >>> resize_op(x) + array([[ 5., 6., 7., 8.], + [ 1., 2., 3., 4.], + [ 5., 6., 7., 8.], + [ 1., 2., 3., 4.]]) """ # Swap names to be able to use the range iterator without worries import builtins @@ -374,10 +370,9 @@ def axes(self): 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, pad_const=self.pad_const, - direction='forward', out=out_arr) + resize_array(x, self.range.shape, offset=self.offset, + pad_mode=self.pad_mode, pad_const=self.pad_const, + direction='forward', out=out) def derivative(self, point): """Derivative of this operator at ``point``. @@ -414,11 +409,9 @@ class ResizingOperatorAdjoint(Operator): def _call(self, x, out): """Implement ``self(x, out)``.""" - with writable_array(out) as out_arr: - resize_array(x.asarray(), op.domain.shape, - offset=op.offset, pad_mode=op.pad_mode, - pad_const=0, direction='adjoint', - out=out_arr) + resize_array(x, op.domain.shape, + offset=op.offset, pad_mode=op.pad_mode, + pad_const=0, direction='adjoint', out=out) @property def adjoint(self): diff --git a/odl/discr/discr_space.py b/odl/discr/discr_space.py index 8a1443ae342..17c23b2c037 100644 --- a/odl/discr/discr_space.py +++ b/odl/discr/discr_space.py @@ -19,9 +19,8 @@ RectPartition, uniform_partition, uniform_partition_fromintv) from odl.set import IntervalProd, RealNumbers from odl.space import ProductSpace -from odl.space.base_tensors import Tensor, TensorSpace +from odl.space.base_tensors import 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_floating_dtype, is_numeric_dtype, normalized_nodes_on_bdry, normalized_scalar_param_list, @@ -29,7 +28,6 @@ __all__ = ( 'DiscretizedSpace', - 'DiscretizedSpaceElement', 'uniform_discr_frompartition', 'uniform_discr_fromintv', 'uniform_discr', @@ -94,13 +92,6 @@ def __init__(self, partition, tspace, **kwargs): raise ValueError('got unexpected keyword arguments {}' ''.format(kwargs)) - # --- Meta-info - - @property - def element_type(self): - """`DiscretizedSpaceElement`""" - return DiscretizedSpaceElement - # --- Constructor args @property @@ -129,11 +120,16 @@ def domain(self): @property def weighting(self): - """This space's weighting scheme.""" + """This space's weighting factor(s).""" # TODO(kohr-h): `weighting` is optional in `tspace`, how should we # handle that? return self.tspace.weighting + @property + def weighting_type(self): + """This space's weighting type.""" + return self.tspace.weighting_type + @property def is_weighted(self): """``True`` if the ``tspace`` is weighted.""" @@ -244,6 +240,16 @@ def available_dtypes(self): """ return self.tspace.available_dtypes() + @property + def ufuncs(self): + """Access to NumPy ufuncs.""" + return self.tspace.ufuncs + + @property + def reduce(self): + """Access to NumPy reductions.""" + return self.tspace.reduce + # --- Derived properties @property @@ -281,7 +287,7 @@ def is_uniformly_weighted(self): return is_uniformly_weighted - # --- Element creation + # --- Element handling def element(self, inp=None, order=None, **kwargs): """Create an element from ``inp`` or from scratch. @@ -321,7 +327,7 @@ def element(self, inp=None, order=None, **kwargs): Returns ------- - element : `DiscretizedSpaceElement` + element The discretized element, calculated as ``point_collocation(inp)`` or ``tspace.element(inp)``, tried in this order. @@ -332,16 +338,16 @@ def element(self, inp=None, order=None, **kwargs): >>> space = odl.uniform_discr(-1, 1, 4) >>> space.element([1, 2, 3, 4]) - uniform_discr(-1.0, 1.0, 4).element([ 1., 2., 3., 4.]) + array([ 1., 2., 3., 4.]) >>> vector = odl.rn(4).element([0, 1, 2, 3]) >>> space.element(vector) - uniform_discr(-1.0, 1.0, 4).element([ 0., 1., 2., 3.]) + array([ 0., 1., 2., 3.]) On the other hand, non-discretized objects like Python functions can be discretized "on the fly": >>> space.element(lambda x: x * 2) - uniform_discr(-1.0, 1.0, 4).element([-1.5, -0.5, 0.5, 1.5]) + array([-1.5, -0.5, 0.5, 1.5]) This works also with parameterized functions, however only through keyword arguments (not positional arguments with @@ -352,35 +358,37 @@ def element(self, inp=None, order=None, **kwargs): ... >>> space = odl.uniform_discr(-1, 1, 4) >>> space.element(f, c=0.5) - uniform_discr(-1.0, 1.0, 4).element([ 0.5 , 0.5 , 0.5 , 0.75]) + array([ 0.5 , 0.5 , 0.5 , 0.75]) + + See Also + -------- + sampling : create a discrete element from a non-discretized one """ if inp is None: - return self.element_type(self, self.tspace.element(order=order)) + return self.tspace.element(order=order) elif inp in self and order is None: return inp - elif inp in self.tspace and order is None: - return self.element_type(self, inp) elif callable(inp): 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) - ) + return self.tspace.element(sampled, order=order) else: # Sequence-type input - return self.element_type( - self, self.tspace.element(inp, order=order) - ) + return self.tspace.element(inp, order=order) def zero(self): """Return the element of all zeros.""" - return self.element_type(self, self.tspace.zero()) + return self.tspace.zero() def one(self): """Return the element of all ones.""" - return self.element_type(self, self.tspace.one()) + return self.tspace.one() + + def __contains__(self, other): + """Return ``other in self``.""" + return other in self.tspace # --- Casting @@ -392,7 +400,7 @@ def _astype(self, dtype): # --- Slicing - # TODO: add `byaxis`_out when discretized tensor-valued functions are + # TODO: add `byaxis_out` when discretized tensor-valued functions are # available @property @@ -438,7 +446,7 @@ def __getitem__(self, indices): """ part = space.partition.byaxis[indices] - if isinstance(space.weighting, ConstWeighting): + if space.weighting_type == 'const': # Need to manually construct `tspace` since it doesn't # know where its weighting factor comes from try: @@ -464,8 +472,9 @@ def __getitem__(self, indices): except TypeError: labels = space.axis_labels[indices] else: - labels = tuple(space.axis_labels[int(i)] - for i in indices) + labels = tuple( + space.axis_labels[int(i)] for i in indices + ) return DiscretizedSpace(part, tspace, axis_labels=labels) @@ -508,17 +517,17 @@ def __hash__(self): # --- 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 _lincomb(self, a, x, b, y, out): + """Linear combination ``out = a * x + b * y``.""" + self.tspace.lincomb(a, x, b, y, out) def _multiply(self, x1, x2, out): - """Raw pointwise multiplication of two elements.""" - self.tspace._multiply(x1.tensor, x2.tensor, out.tensor) + """Multiplication ``out = x1 * x2``.""" + self.tspace._multiply(x1, x2, out) def _divide(self, x1, x2, out): - """Raw pointwise multiplication of two elements.""" - self.tspace._divide(x1.tensor, x2.tensor, out.tensor) + """Division ``out = x1 / x2``.""" + self.tspace._divide(x1, x2, out) # The inherited methods by default use a weighting by a constant # (the grid cell size). In dimensions where the partitioned set contains @@ -534,7 +543,7 @@ def _inner(self, x, y): 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) + return self.tspace.inner(x, y) def _norm(self, x): """Return ``self.norm(x)``.""" @@ -545,7 +554,7 @@ def _norm(self, x): 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) + return self.tspace.norm(x) def _dist(self, x, y): """Return ``self.dist(x, y)``.""" @@ -560,783 +569,18 @@ def _dist(self, x, y): 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.partition.set, self.shape, nodes_on_bdry=False - ) == self.partition: - use_uniform = True - nodes_on_bdry = False - elif uniform_partition_fromintv( - 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' - if self.ndim == 1: - posargs = [self.min_pt[0], self.max_pt[0], self.shape[0]] - posmod = ['', '', ''] - else: - posargs = [self.min_pt, self.max_pt, self.shape] - posmod = [array_str, array_str, ''] - - default_dtype_s = dtype_str( - self.tspace.default_dtype(RealNumbers()) - ) - - dtype_s = dtype_str(self.dtype) - optargs = [ - ('impl', self.impl, 'numpy'), - ('nodes_on_bdry', nodes_on_bdry, False), - ('dtype', dtype_s, default_dtype_s) - ] - - # Add weighting stuff if not equal to default - if ( - self.exponent == float('inf') - or self.ndim == 0 - or not is_floating_dtype(self.dtype) - ): - # In these cases, weighting constant 1 is the default - if ( - not isinstance(self.weighting, ConstWeighting) - or not np.isclose(self.weighting.const, 1.0) - ): - optargs.append(('weighting', self.weighting.const, None)) - else: - if ( - not isinstance(self.weighting, ConstWeighting) - or not np.isclose(self.weighting.const, self.cell_volume) - ): - optargs.append(('weighting', self.weighting.const, None)) - - optmod = [''] * len(optargs) - if self.dtype in (float, complex, int, bool): - optmod[2] = '!s' - - inner_parts = signature_string_parts( - posargs, optargs, [posmod, optmod] - ) - return repr_string(ctor, inner_parts) - - else: - ctor = self.__class__.__name__ - posargs = [self.partition, self.tspace] - inner_parts = signature_string_parts(posargs, []) - return repr_string(ctor, inner_parts, allow_mixed_seps=False) - - def __str__(self): - """Return ``str(self)``.""" - return repr(self) - - -class DiscretizedSpaceElement(Tensor): - - """Representation of a `DiscretizedSpace` element.""" - - def __init__(self, space, tensor): - """Initialize a new instance.""" - super(DiscretizedSpaceElement, self).__init__(space) - self.__tensor = tensor - - # --- Constructor args - - @property - def tensor(self): - """Structure for data storage.""" - return self.__tensor - - # --- Pass-through `space` properties - - @property - def cell_sides(self): - """Side lengths of a cell in an underlying *uniform* partition.""" - return self.space.cell_sides - - @property - 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 : `DiscretizedSpaceElement` - - Examples - -------- - Get the real part: - - >>> 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.]) - - Set the real part: - - >>> x = discr.element([1 + 1j, 2, 3 - 3j]) - >>> zero = discr.real_space.zero() - >>> x.real = zero - >>> x.real - uniform_discr(0.0, 1.0, 3).element([ 0., 0., 0.]) - - Other array-like types and broadcasting: - - >>> x.real = 1.0 - >>> x.real - uniform_discr(0.0, 1.0, 3).element([ 1., 1., 1.]) - >>> x.real = [2, 3, 4] - >>> x.real - uniform_discr(0.0, 1.0, 3).element([ 2., 3., 4.]) - """ - return self.space.real_space.element(self.tensor.real) - - @real.setter - def real(self, newreal): - """Set the real part of this element to ``newreal``. - - This method is invoked by ``x.real = other``. - - Parameters - ---------- - newreal : array-like or scalar - Values to be assigned to the real part of this element. - """ - self.tensor.real = newreal - - @property - def imag(self): - """Imaginary part of this element. - - Returns - ------- - imag : `DiscretizedSpaceElement` - - Examples - -------- - Get the imaginary part: - - >>> discr = uniform_discr(0, 1, 3, dtype=complex) - >>> x = discr.element([5+1j, 3, 2-2j]) - >>> x.imag - uniform_discr(0.0, 1.0, 3).element([ 1., 0., -2.]) - - Set the imaginary part: - - >>> x = discr.element([1 + 1j, 2, 3 - 3j]) - >>> zero = discr.real_space.zero() - >>> x.imag = zero - >>> x.imag - uniform_discr(0.0, 1.0, 3).element([ 0., 0., 0.]) - - Other array-like types and broadcasting: + return self.tspace.dist(x, y) - >>> x.imag = 1.0 - >>> x.imag - uniform_discr(0.0, 1.0, 3).element([ 1., 1., 1.]) - >>> x.imag = [2, 3, 4] - >>> x.imag - uniform_discr(0.0, 1.0, 3).element([ 2., 3., 4.]) - """ - return self.space.real_space.element(self.tensor.imag) - - @imag.setter - def imag(self, newimag): - """Set the imaginary part of this element to ``newimag``. - - This method is invoked by ``x.imag = other``. - - Parameters - ---------- - newimag : array-like or scalar - Values to be assigned to the imaginary part of this element. - - Raises - ------ - ValueError - If the space is real, i.e., no imagninary part can be set. - """ - if self.space.is_real: - raise ValueError('cannot set imaginary part in real spaces') - self.tensor.imag = newimag - - def conj(self, out=None): - """Complex conjugate of this element. - - Parameters - ---------- - out : `DiscretizedSpaceElement`, optional - Element to which the complex conjugate is written. - Must be an element of this element's space. - - Returns - ------- - out : `DiscretizedSpaceElement` - The complex conjugate element. If ``out`` is provided, - the returned object is a reference to it. - - Examples - -------- - >>> discr = uniform_discr(0, 1, 4, dtype=complex) - >>> x = discr.element([5+1j, 3, 2-2j, 1j]) - >>> y = x.conj() - >>> print(y) - [ 5.-1.j, 3.-0.j, 2.+2.j, 0.-1.j] - - The out parameter allows you to avoid a copy: - - >>> z = discr.element() - >>> z_out = x.conj(out=z) - >>> print(z) - [ 5.-1.j, 3.-0.j, 2.+2.j, 0.-1.j] - >>> z_out is z - True - - It can also be used for in-place conjugation: - - >>> x_out = x.conj(out=x) - >>> print(x) - [ 5.-1.j, 3.-0.j, 2.+2.j, 0.-1.j] - >>> x_out is x - True - """ - if out is None: - return self.space.element(self.tensor.conj()) - else: - self.tensor.conj(out=out.tensor) - return out - - def __setitem__(self, indices, values): - """Set values of this element. - - Parameters - ---------- - indices : int or `slice` - The position(s) that should be set - values : scalar or `array-like` - Value(s) to be assigned. - If ``indices`` is an integer, ``values`` must be a scalar - value. - If ``indices`` is a slice, ``values`` must be - broadcastable to the size of the slice (same size, - shape ``(1,)`` or scalar). - For ``indices == slice(None)``, i.e. in the call - ``vec[:] = values``, a multi-dimensional array of correct - shape is allowed as ``values``. - """ - if values in self.space: - self.tensor[indices] = values.tensor - else: - 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. - - This method is called by Numpy version 1.13 and higher as a single - point for the ufunc dispatch logic. An object implementing - ``__array_ufunc__`` takes over control when a `numpy.ufunc` is - called on it, allowing it to use custom implementations and - output types. - - This includes handling of in-place arithmetic like - ``npy_array += custom_obj``. In this case, the custom object's - ``__array_ufunc__`` takes precedence over the baseline - `numpy.ndarray` implementation. It will be called with - ``npy_array`` as ``out`` argument, which ensures that the - returned object is a Numpy array. For this to work properly, - ``__array_ufunc__`` has to accept Numpy arrays as ``out`` arguments. - - See the `corresponding NEP`_ and the `interface documentation`_ - for further details. See also the `general documentation on - Numpy ufuncs`_. - - .. note:: - When using operations that alter the shape (like ``reduce``), - or the data type (can be any of the methods), - the resulting array is wrapped in a space of the same - type as ``self.space``, propagating all essential properties - like weighting, exponent etc. as closely as possible. - - Parameters - ---------- - ufunc : `numpy.ufunc` - Ufunc that should be called on ``self``. - method : str - Method on ``ufunc`` that should be called on ``self``. - Possible values: - - ``'__call__'``, ``'accumulate'``, ``'at'``, ``'outer'``, - ``'reduce'`` - - input1, ..., inputN : - Positional arguments to ``ufunc.method``. - kwargs : - Keyword arguments to ``ufunc.method``. - - Returns - ------- - ufunc_result : `DiscretizedSpaceElement`, `numpy.ndarray` or tuple - Result of the ufunc evaluation. If no ``out`` keyword argument - 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``. - - Examples - -------- - We apply `numpy.add` to elements of a one-dimensional space: - - >>> space = odl.uniform_discr(0, 1, 3) - >>> x = space.element([1, 2, 3]) - >>> y = space.element([-1, -2, -3]) - >>> x.__array_ufunc__(np.add, '__call__', x, y) - uniform_discr(0.0, 1.0, 3).element([ 0., 0., 0.]) - >>> np.add(x, y) # same mechanism for Numpy >= 1.13 - uniform_discr(0.0, 1.0, 3).element([ 0., 0., 0.]) - - 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): - - >>> out = space.element() - >>> res = x.__array_ufunc__(np.add, '__call__', x, y, out=(out,)) - >>> out - uniform_discr(0.0, 1.0, 3).element([ 0., 0., 0.]) - >>> res is out - True - >>> out_tens = odl.rn(3).element() - >>> res = x.__array_ufunc__(np.add, '__call__', x, y, out=(out_tens,)) - >>> out_tens - rn(3).element([ 0., 0., 0.]) - >>> res is out_tens - True - >>> out_arr = np.empty(3) - >>> res = x.__array_ufunc__(np.add, '__call__', x, y, out=(out_arr,)) - >>> out_arr - array([ 0., 0., 0.]) - >>> res is out_arr - True - - With multiple dimensions: - - >>> space_2d = odl.uniform_discr([0, 0], [1, 2], (2, 3)) - >>> x = y = space_2d.one() - >>> x.__array_ufunc__(np.add, '__call__', x, y) - uniform_discr([ 0., 0.], [ 1., 2.], (2, 3)).element( - [[ 2., 2., 2.], - [ 2., 2., 2.]] - ) - - The ``ufunc.accumulate`` method retains the original space: - - >>> x = space.element([1, 2, 3]) - >>> x.__array_ufunc__(np.add, 'accumulate', x) - uniform_discr(0.0, 1.0, 3).element([ 1., 3., 6.]) - >>> np.add.accumulate(x) # same mechanism for Numpy >= 1.13 - uniform_discr(0.0, 1.0, 3).element([ 1., 3., 6.]) - - For multi-dimensional space elements, an optional ``axis`` parameter - can be provided (default is 0): - - >>> z = space_2d.one() - >>> z.__array_ufunc__(np.add, 'accumulate', z, axis=1) - uniform_discr([ 0., 0.], [ 1., 2.], (2, 3)).element( - [[ 1., 2., 3.], - [ 1., 2., 3.]] - ) - - The method also takes a ``dtype`` parameter: - - >>> z.__array_ufunc__(np.add, 'accumulate', z, dtype=complex) - uniform_discr([ 0., 0.], [ 1., 2.], (2, 3), dtype=complex).element( - [[ 1.+0.j, 1.+0.j, 1.+0.j], - [ 2.+0.j, 2.+0.j, 2.+0.j]] - ) - - The ``ufunc.at`` method operates in-place. Here we add the second - operand ``[5, 10]`` to ``x`` at indices ``[0, 2]``: - - >>> x = space.element([1, 2, 3]) - >>> x.__array_ufunc__(np.add, 'at', x, [0, 2], [5, 10]) - >>> x - uniform_discr(0.0, 1.0, 3).element([ 6., 2., 13.]) - - For outer-product-type operations, i.e., operations where the result - shape is the sum of the individual shapes, the ``ufunc.outer`` - method can be used: - - >>> space1 = odl.uniform_discr(0, 1, 2) - >>> space2 = odl.uniform_discr(0, 2, 3) - >>> x = space1.element([0, 3]) - >>> y = space2.element([1, 2, 3]) - >>> x.__array_ufunc__(np.add, 'outer', x, y) - uniform_discr([ 0., 0.], [ 1., 2.], (2, 3)).element( - [[ 1., 2., 3.], - [ 4., 5., 6.]] - ) - >>> y.__array_ufunc__(np.add, 'outer', y, x) - uniform_discr([ 0., 0.], [ 2., 1.], (3, 2)).element( - [[ 1., 4.], - [ 2., 5.], - [ 3., 6.]] - ) - - Using ``ufunc.reduce`` in 1D produces a scalar: - - >>> x = space.element([1, 2, 3]) - >>> x.__array_ufunc__(np.add, 'reduce', x) - 6.0 - - In multiple dimensions, ``axis`` can be provided for reduction over - selected axes: - - >>> z = space_2d.element([[1, 2, 3], - ... [4, 5, 6]]) - >>> z.__array_ufunc__(np.add, 'reduce', z, axis=1) - uniform_discr(0.0, 1.0, 2).element([ 6., 15.]) - - References - ---------- - .. _corresponding NEP: - https://docs.scipy.org/doc/numpy/neps/ufunc-overrides.html - - .. _interface documentation: - https://docs.scipy.org/doc/numpy/reference/arrays.classes.html\ -#numpy.class.__array_ufunc__ - - .. _general documentation on Numpy ufuncs: - https://docs.scipy.org/doc/numpy/reference/ufuncs.html - - .. _reduceat documentation: - https://docs.scipy.org/doc/numpy/reference/generated/\ - """ - # --- Process `out` --- # - - # Unwrap out if provided. The output parameters are all wrapped - # in one tuple, even if there is only one. - out_tuple = kwargs.pop('out', ()) - - # Check number of `out` args, depending on `method` - if method == '__call__' and len(out_tuple) not in (0, ufunc.nout): - raise ValueError( - "need 0 or {} `out` arguments for `method='__call__'`, " - 'got {}'.format(ufunc.nout, len(out_tuple))) - elif method != '__call__' and len(out_tuple) not in (0, 1): - raise ValueError( - "need 0 or 1 `out` arguments for `method={!r}`, " - 'got {}'.format(method, len(out_tuple))) - - # We allow our own element type, tensors and their data containers - # as `out` - valid_out_types = (type(self), - type(self.tensor), - type(self.tensor.data)) - if not all(isinstance(o, valid_out_types) or o is None - for o in out_tuple): - return NotImplemented - - # Assign to `out` or `out1` and `out2`, respectively (using the - # `tensor` attribute if available) - out = out1 = out2 = None - if len(out_tuple) == 1: - out = getattr(out_tuple[0], 'tensor', out_tuple[0]) - elif len(out_tuple) == 2: - out1 = getattr(out_tuple[0], 'tensor', out_tuple[0]) - out2 = getattr(out_tuple[1], 'tensor', out_tuple[1]) - - # --- Process `inputs` --- # - - # 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 - for elem in inputs) - - # --- Get some parameters for later --- # - - # Need to filter for `keepdims` in case `method='reduce'` since it's - # invalid (happening below) - keepdims = kwargs.pop('keepdims', False) - - # Determine list of remaining axes from `axis` for `'reduce'` - axis = kwargs.get('axis', None) - if axis is None: - reduced_axes = list(range(1, self.ndim)) - else: - try: - iter(axis) - except TypeError: - axis = (int(axis),) - - reduced_axes = [i for i in range(self.ndim) if i not in axis] - - # --- Evaluate ufunc --- # - - if method == '__call__': - if ufunc.nout == 1: - kwargs['out'] = (out,) - res_tens = self.tensor.__array_ufunc__( - ufunc, '__call__', *input_tensors, **kwargs) - - if out is None: - # Wrap result tensor in appropriate DiscretizedSpace space. - res_space = DiscretizedSpace( - self.space.partition, - res_tens.space, - axis_labels=self.space.axis_labels - ) - result = res_space.element(res_tens) - else: - result = out_tuple[0] - - return result - - elif ufunc.nout == 2: - kwargs['out'] = (out1, out2) - res1_tens, res2_tens = self.tensor.__array_ufunc__( - ufunc, '__call__', *input_tensors, **kwargs) - - if out1 is None: - # Wrap as for nout = 1 - res_space = DiscretizedSpace( - self.space.partition, - res1_tens.space, - axis_labels=self.space.axis_labels - ) - result1 = res_space.element(res1_tens) - else: - result1 = out_tuple[0] - - if out2 is None: - # Wrap as for nout = 1 - res_space = DiscretizedSpace( - self.space.partition, - res2_tens.space, - axis_labels=self.space.axis_labels - ) - result2 = res_space.element(res2_tens) - else: - result2 = out_tuple[1] - - return result1, result2 - - else: - raise NotImplementedError('nout = {} not supported' - ''.format(ufunc.nout)) - - elif method == 'reduce' and keepdims: - raise ValueError( - '`keepdims=True` cannot be used in `reduce` since there is ' - 'no unique way to determine a function domain in collapsed ' - 'axes') - - elif method == 'reduceat': - # Makes no sense since there is no way to determine in which - # space the result should live, except in special cases when - # axes are being completely collapsed or don't change size. - raise ValueError('`reduceat` not supported') - - elif ( - method == 'outer' - and not all(isinstance(inp, type(self)) for inp in inputs) - ): - raise TypeError( - "inputs must be of type {} for `method='outer'`, " - 'got types {}' - ''.format(type(self), tuple(type(inp) for inp in inputs)) - ) - - else: # method != '__call__', and otherwise valid - - if method != 'at': - # No kwargs allowed for 'at' - kwargs['out'] = (out,) - - res_tens = self.tensor.__array_ufunc__( - ufunc, method, *input_tensors, **kwargs) - - # Shortcut for scalar or no return value - if np.isscalar(res_tens) or res_tens is None: - # The first occurs for `reduce` with all axes, - # the second for in-place stuff (`at` currently) - return res_tens - - if out is None: - # Wrap in appropriate DiscretizedSpace space depending - # on `method` - if method == 'accumulate': - res_space = DiscretizedSpace( - self.space.partition, - res_tens.space, - axis_labels=self.space.axis_labels - ) - result = res_space.element(res_tens) - - elif method == 'outer': - # Concatenate partitions and axis_labels, - # and determine `tspace` from the result tensor - inp1, inp2 = inputs - 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] - labels = labels1 + labels2 - - if all(isinstance(inp.space.weighting, ConstWeighting) - for inp in inputs): - # For constant weighting, use the product of the - # two weighting constants. The result tensor space - # cannot know about the "correct" way to combine the - # two constants, so we need to do it manually here. - weighting = (inp1.space.weighting.const * - inp2.space.weighting.const) - tspace = type(res_tens.space)( - res_tens.shape, res_tens.dtype, - exponent=res_tens.space.exponent, - weighting=weighting) - else: - # Otherwise `TensorSpace` knows how to handle this - tspace = res_tens.space - - res_space = DiscretizedSpace( - part, tspace, axis_labels=labels - ) - result = res_space.element(res_tens) - - elif method == 'reduce': - # Index space by axis using `reduced_axes` - res_space = self.space.byaxis_in[reduced_axes].astype( - res_tens.dtype) - result = res_space.element(res_tens) - - else: - raise RuntimeError('bad `method`') - - else: - # `out` may be `out_tuple[0].tensor`, but we want to return - # the original one - result = out_tuple[0] - - return result - - def show(self, title=None, method='', coords=None, indices=None, + def show(self, elem, title=None, method='', coords=None, indices=None, force_show=False, fig=None, **kwargs): """Display the function graphically. Parameters ---------- + elem : array-like + Element to display using the properties of this space. title : string, optional Set the title of the figure - method : string, optional 1d methods: @@ -1360,7 +604,6 @@ def show(self, title=None, method='', coords=None, indices=None, point to be shown, i.e. ``[None, [0, 1]]`` shows all of the first axis and values between 0 and 1 in the second. This option is mutually exclusive with ``indices``. - indices : int, slice, Ellipsis or sequence, optional Display a slice of the array instead of the full array. If a sequence is given, the i-th entry indexes the i-th axis, @@ -1388,25 +631,18 @@ def show(self, title=None, method='', coords=None, indices=None, position along the remaining axes is shown (semantically ``[:, :, shape[2:] // 2]``). This option is mutually exclusive with ``coords``. - force_show : bool, optional Whether the plot should be forced to be shown now or deferred until later. Note that some backends always displays the plot, regardless of this value. - fig : `matplotlib.figure.Figure`, optional The figure to show in. Expected to be of same "style", as the figure given by this function. The most common use case 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. - - kwargs : {'figsize', 'saveto', 'clim', ...}, optional - Extra keyword arguments passed on to the display method. + kwargs + Extra keyword arguments like ``figsize``, ``saveto``, ``clim``, + ..., passed on to the display method. See the Matplotlib functions for documentation of extra options. @@ -1421,8 +657,7 @@ def show(self, title=None, method='', coords=None, indices=None, """ from odl.util.graphics import show_discrete_data - if 'interp' not in kwargs: - kwargs['interp'] = 'linear' + elem = self.element(elem) if self.ndim == 0: raise ValueError('nothing to show for 0-dimensional vector') @@ -1431,7 +666,7 @@ def show(self, title=None, method='', coords=None, indices=None, if indices is not None: raise ValueError('cannot provide both coords and indices') - partition = self.space.partition + partition = self.partition shape = self.shape indices = [] for axis, (n, coord) in enumerate(zip(shape, coords)): @@ -1469,8 +704,9 @@ def show(self, title=None, method='', coords=None, indices=None, # Default to showing x-y slice "in the middle" if indices is None and self.ndim >= 3: - indices = ((slice(None),) * 2 + - tuple(n // 2 for n in self.space.shape[2:])) + indices = ( + (slice(None),) * 2 + tuple(n // 2 for n in self.shape[2:]) + ) # Normalize indices if isinstance(indices, (Integral, slice)): @@ -1488,17 +724,21 @@ def show(self, title=None, method='', coords=None, indices=None, elif Ellipsis in indices: # Replace Ellipsis with the correct number of `slice(None)` pos = indices.index(Ellipsis) - indices = (tuple(indices[:pos]) + - (slice(None),) * (self.ndim - len(indices) + 1) + - tuple(indices[pos + 1:])) + indices = ( + tuple(indices[:pos]) + + (slice(None),) * (self.ndim - len(indices) + 1) + + tuple(indices[pos + 1:]) + ) # Now indices should be exactly of length `ndim` if len(indices) < self.ndim: - raise ValueError('too few axes ({} < {})'.format(len(indices), - self.ndim)) + raise ValueError( + 'too few axes ({} < {})'.format(len(indices), self.ndim) + ) if len(indices) > self.ndim: - raise ValueError('too many axes ({} > {})'.format(len(indices), - self.ndim)) + raise ValueError( + 'too many axes ({} > {})'.format(len(indices), self.ndim) + ) # Map `None` to `slice(None)` in indices for syntax like `coords` indices = tuple(slice(None) if idx is None else idx @@ -1506,16 +746,92 @@ def show(self, title=None, method='', coords=None, indices=None, squeezed_axes = [axis for axis in range(self.ndim) if not isinstance(indices[axis], Integral)] - axis_labels = [self.space.axis_labels[axis] for axis in squeezed_axes] + axis_labels = [self.axis_labels[axis] for axis in squeezed_axes] # Squeeze grid and values according to the index expression - part = self.space.partition[indices].squeeze() - values = self.asarray()[indices].squeeze() + part = self.partition[indices].squeeze() + values = elem[indices].squeeze() return show_discrete_data(values, part, title=title, method=method, force_show=force_show, fig=fig, axis_labels=axis_labels, **kwargs) + def __repr__(self): + """Return ``repr(self)``.""" + # Clunky check if the factory repr can be used + if uniform_partition_fromintv( + self.partition.set, self.shape, nodes_on_bdry=False + ) == self.partition: + use_uniform = True + nodes_on_bdry = False + elif uniform_partition_fromintv( + 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' + if self.ndim == 1: + posargs = [self.min_pt[0], self.max_pt[0], self.shape[0]] + posmod = ['', '', ''] + else: + posargs = [self.min_pt, self.max_pt, self.shape] + posmod = [array_str, array_str, ''] + + default_dtype_s = dtype_str( + self.tspace.default_dtype(RealNumbers()) + ) + + dtype_s = dtype_str(self.dtype) + optargs = [ + ('impl', self.impl, 'numpy'), + ('nodes_on_bdry', nodes_on_bdry, False), + ('dtype', dtype_s, default_dtype_s) + ] + + # Add weighting stuff if not equal to default + if ( + self.exponent == float('inf') + or self.ndim == 0 + or not is_floating_dtype(self.dtype) + ): + # In these cases, weighting constant 1 is the default + if ( + self.weighting_type != 'const' + or not np.isclose(self.weighting, 1.0) + ): + # TODO(kohr-h): this is probably not great but will work + optargs.append(('weighting', self.weighting, None)) + else: + if ( + self.weighting_type != 'const' + or not np.isclose(self.weighting, self.cell_volume) + ): + optargs.append(('weighting', self.weighting, None)) + + optmod = [''] * len(optargs) + if self.dtype in (float, complex, int, bool): + optmod[2] = '!s' + + inner_parts = signature_string_parts( + posargs, optargs, [posmod, optmod] + ) + return repr_string(ctor, inner_parts) + + else: + ctor = self.__class__.__name__ + posargs = [self.partition, self.tspace] + inner_parts = signature_string_parts(posargs, []) + return repr_string(ctor, inner_parts, allow_mixed_seps=False) + + def __str__(self): + """Return ``str(self)``.""" + return repr(self) + def uniform_discr_frompartition(partition, dtype=None, impl='numpy', **kwargs): """Return a uniformly discretized L^p function space. @@ -1543,7 +859,7 @@ def uniform_discr_frompartition(partition, dtype=None, impl='numpy', **kwargs): Examples -------- >>> part = odl.uniform_partition(0, 1, 10) - >>> uniform_discr_frompartition(part) + >>> odl.uniform_discr_frompartition(part) uniform_discr(0.0, 1.0, 10) See Also @@ -1606,8 +922,8 @@ def uniform_discr_fromintv(intv_prod, shape, dtype=None, impl='numpy', Examples -------- - >>> intv = IntervalProd(0, 1) - >>> uniform_discr_fromintv(intv, 10) + >>> intv = odl.IntervalProd(0, 1) + >>> odl.uniform_discr_fromintv(intv, 10) uniform_discr(0.0, 1.0, 10) See Also @@ -1675,7 +991,7 @@ def uniform_discr(min_pt, max_pt, shape, dtype=None, impl='numpy', **kwargs): -------- Create real space: - >>> space = uniform_discr([0, 0], [1, 1], (10, 10)) + >>> space = odl.uniform_discr([0, 0], [1, 1], (10, 10)) >>> space uniform_discr([ 0., 0.], [ 1., 1.], (10, 10)) >>> space.cell_sides @@ -1687,7 +1003,7 @@ def uniform_discr(min_pt, max_pt, shape, dtype=None, impl='numpy', **kwargs): Create complex space by giving a dtype: - >>> space = uniform_discr([0, 0], [1, 1], (10, 10), dtype=complex) + >>> space = odl.uniform_discr([0, 0], [1, 1], (10, 10), dtype=complex) >>> space uniform_discr([ 0., 0.], [ 1., 1.], (10, 10), dtype=complex) >>> space.is_complex diff --git a/odl/discr/discr_utils.py b/odl/discr/discr_utils.py index 25eb08a8e38..5f4f770c192 100644 --- a/odl/discr/discr_utils.py +++ b/odl/discr/discr_utils.py @@ -1321,7 +1321,7 @@ def dual_use_func(x, out=None, **kwargs): # in which case we copy. out = np.broadcast_to(out, out_shape) if not out.flags.writeable: - out = out.copy() + out = np.copy(out) elif tensor_valued: # The out object can be any array-like of objects with shapes diff --git a/odl/discr/grid.py b/odl/discr/grid.py index ef0d9bd2c35..8d66ec6076c 100644 --- a/odl/discr/grid.py +++ b/odl/discr/grid.py @@ -434,7 +434,7 @@ def stride(self): strd.append(0.0) self.__stride = np.array(strd) - return self.__stride.copy() + return np.copy(self.__stride) @property def extent(self): diff --git a/odl/discr/partition.py b/odl/discr/partition.py index aabacca2b24..4ccec5b544a 100644 --- a/odl/discr/partition.py +++ b/odl/discr/partition.py @@ -1,4 +1,4 @@ -# Copyright 2014-2018 The ODL contributors +# Copyright 2014-2020 The ODL contributors # # This file is part of ODL. # @@ -14,17 +14,18 @@ of partitions of intervals. """ -from __future__ import print_function, division, absolute_import +from __future__ import absolute_import, division, print_function + from builtins import object + import numpy as np from odl.discr.grid import RectGrid, uniform_grid_fromintv from odl.set import IntervalProd from odl.util import ( - normalized_index_expression, normalized_nodes_on_bdry, - normalized_scalar_param_list, safe_int_conv, - signature_string, indent, array_str, npy_printoptions) - + array_str, indent, normalized_index_expression, normalized_nodes_on_bdry, + normalized_scalar_param_list, npy_printoptions, safe_int_conv, + signature_string) __all__ = ('RectPartition', 'uniform_partition_fromintv', 'uniform_partition_fromgrid', 'uniform_partition', @@ -1295,8 +1296,11 @@ def nonuniform_partition(*coord_vecs, **kwargs): Parameters ---------- - coord_vecs1, ... coord_vecsN : `array-like` + coord_vec1, ... coord_vecN : `array-like` Arrays of coordinates of the mid-points of the partition cells. + + Other Parameters + ---------------- min_pt, max_pt : float or sequence of floats, optional Vectors defining the lower/upper limits of the intervals in an `IntervalProd` (a rectangular box). ``None`` entries mean diff --git a/odl/operator/default_ops.py b/odl/operator/default_ops.py index 448da71f2c2..240225ede7d 100644 --- a/odl/operator/default_ops.py +++ b/odl/operator/default_ops.py @@ -18,7 +18,6 @@ from odl.operator.operator import Operator from odl.set import ComplexNumbers, Field, LinearSpace, RealNumbers -from odl.set.space import LinearSpaceElement from odl.space import ProductSpace __all__ = ('ScalingOperator', 'ZeroOperator', 'IdentityOperator', @@ -50,15 +49,16 @@ def __init__(self, domain, scalar): Examples -------- >>> r3 = odl.rn(3) - >>> vec = r3.element([1, 2, 3]) + >>> x = r3.element([1, 2, 3]) >>> out = r3.element() - >>> op = ScalingOperator(r3, 2.0) - >>> op(vec, out) # In-place, Returns out - rn(3).element([ 2., 4., 6.]) - >>> out - rn(3).element([ 2., 4., 6.]) - >>> op(vec) # Out-of-place - rn(3).element([ 2., 4., 6.]) + >>> op = odl.ScalingOperator(r3, 2.0) + >>> op(x) + array([ 2., 4., 6.]) + >>> result = op(x, out) + >>> result + array([ 2., 4., 6.]) + >>> result is out + True """ if not isinstance(domain, (LinearSpace, Field)): raise TypeError('`domain` {!r} not a `LinearSpace` or `Field` ' @@ -77,7 +77,7 @@ def _call(self, x, out=None): if out is None: out = self.scalar * x else: - out.lincomb(self.scalar, x) + self.range.lincomb(self.scalar, x, out=out) return out @property @@ -87,13 +87,13 @@ def inverse(self): Examples -------- >>> r3 = odl.rn(3) - >>> vec = r3.element([1, 2, 3]) - >>> op = ScalingOperator(r3, 2.0) + >>> x = r3.element([1, 2, 3]) + >>> op = odl.ScalingOperator(r3, 2.0) >>> inv = op.inverse - >>> inv(op(vec)) == vec - True - >>> op(inv(vec)) == vec - True + >>> inv(op(x)) == x + array([ True, True, True], dtype=bool) + >>> op(inv(x)) == x + array([ True, True, True], dtype=bool) """ if self.scalar == 0.0: raise ZeroDivisionError('scaling operator not invertible for ' @@ -110,22 +110,22 @@ def adjoint(self): >>> r3 = odl.rn(3) >>> x = r3.element([1, 2, 3]) - >>> op = ScalingOperator(r3, 2) + >>> op = odl.ScalingOperator(r3, 2) >>> op(x) - rn(3).element([ 2., 4., 6.]) + array([ 2., 4., 6.]) >>> op.adjoint(x) # The same - rn(3).element([ 2., 4., 6.]) + array([ 2., 4., 6.]) In the complex case, the scalar is conjugated: >>> c3 = odl.cn(3) - >>> x_complex = c3.element([1, 1j, 1-1j]) - >>> op = ScalingOperator(c3, 1+1j) - >>> expected_op = ScalingOperator(c3, 1-1j) + >>> x_complex = c3.element([1, 1j, 1 - 1j]) + >>> op = odl.ScalingOperator(c3, 1+1j) + >>> expected_op = odl.ScalingOperator(c3, 1 - 1j) >>> op.adjoint(x_complex) - cn(3).element([ 1.-1.j, 1.+1.j, 0.-2.j]) + array([ 1.-1.j, 1.+1.j, 0.-2.j]) >>> expected_op(x_complex) # The same - cn(3).element([ 1.-1.j, 1.+1.j, 0.-2.j]) + array([ 1.-1.j, 1.+1.j, 0.-2.j]) Returns ------- @@ -154,7 +154,7 @@ def norm(self, estimate=False, **kwargs): -------- >>> spc = odl.rn(3) >>> scaling = odl.ScalingOperator(spc, 3.0) - >>> scaling.norm(True) + >>> scaling.norm(estimate=True) 3.0 """ return np.abs(self.scalar) @@ -219,14 +219,14 @@ def __init__(self, space, a, b): Examples -------- >>> r3 = odl.rn(3) - >>> r3xr3 = odl.ProductSpace(r3, r3) - >>> xy = r3xr3.element([[1, 2, 3], [1, 2, 3]]) + >>> pspace = odl.ProductSpace(r3, r3) + >>> xy = pspace.element([[1, 2, 3], [1, 2, 3]]) >>> z = r3.element() - >>> op = LinCombOperator(r3, 1.0, 1.0) + >>> op = odl.LinCombOperator(r3, 1.0, 1.0) >>> op(xy, out=z) # Returns z - rn(3).element([ 2., 4., 6.]) + array([ 2., 4., 6.]) >>> z - rn(3).element([ 2., 4., 6.]) + array([ 2., 4., 6.]) """ domain = ProductSpace(space, space) super(LinCombOperator, self).__init__(domain, space, linear=True) @@ -237,7 +237,7 @@ def _call(self, x, out=None): """Linearly combine ``x`` and write to ``out`` if given.""" if out is None: out = self.range.element() - out.lincomb(self.a, x[0], self.b, x[1]) + self.range.lincomb(self.a, x[0], self.b, x[1], out) return out def __repr__(self): @@ -265,18 +265,17 @@ class MultiplyOperator(Operator): in the second the scalar multiplication. """ - def __init__(self, multiplicand, domain=None, range=None): + def __init__(self, domain, multiplicand, range=None): """Initialize a new instance. Parameters ---------- - multiplicand : `LinearSpaceElement` or scalar - Value to multiply by. - domain : `LinearSpace` or `Field`, optional + domain : `LinearSpace` or `Field` Set to which the operator can be applied. - Default: ``multiplicand.space``. + multiplicand : `array-like` or scalar + Value to multiply by. range : `LinearSpace` or `Field`, optional - Set to which the operator maps. Default: ``multiplicand.space``. + Set to which the operator maps. Default: ``domain``. Examples -------- @@ -285,27 +284,27 @@ def __init__(self, multiplicand, domain=None, range=None): Multiply by vector: - >>> op = MultiplyOperator(x) + >>> op = odl.MultiplyOperator(r3, x) >>> op(x) - rn(3).element([ 1., 4., 9.]) + array([ 1., 4., 9.]) >>> out = r3.element() - >>> op(x, out) - rn(3).element([ 1., 4., 9.]) + >>> result = op(x, out) + >>> result + array([ 1., 4., 9.]) + >>> result is out + True Multiply by scalar: - >>> op2 = MultiplyOperator(x, domain=r3.field) - >>> op2(3) - rn(3).element([ 3., 6., 9.]) + >>> op = odl.MultiplyOperator(r3.field, x, range=r3) + >>> op(3) + array([ 3., 6., 9.]) >>> out = r3.element() - >>> op2(3, out) - rn(3).element([ 3., 6., 9.]) + >>> op(3, out) + array([ 3., 6., 9.]) """ - if domain is None: - domain = multiplicand.space - if range is None: - range = multiplicand.space + range = domain super(MultiplyOperator, self).__init__(domain, range, linear=True) @@ -324,9 +323,9 @@ def _call(self, x, out=None): return x * self.multiplicand elif not self.__range_is_field: if self.__domain_is_field: - out.lincomb(x, self.multiplicand) + self.range.lincomb(x, self.multiplicand, out=out) else: - out.assign(self.multiplicand * x) + self.range.multiply(x, self.multiplicand, out=out) else: raise ValueError('can only use `out` with `LinearSpace` range') @@ -348,46 +347,50 @@ def adjoint(self): Multiply by a space element: - >>> op = MultiplyOperator(x) + >>> op = odl.MultiplyOperator(r3, x) >>> out = r3.element() >>> op.adjoint(x) - rn(3).element([ 1., 4., 9.]) + array([ 1., 4., 9.]) Multiply scalars with a fixed vector: - >>> op2 = MultiplyOperator(x, domain=r3.field) - >>> op2.adjoint(x) + >>> op = odl.MultiplyOperator(r3.field, x, range=r3) + >>> op.adjoint(x) 14.0 Multiply vectors with a fixed scalar: - >>> op2 = MultiplyOperator(3.0, domain=r3, range=r3) - >>> op2.adjoint(x) - rn(3).element([ 3., 6., 9.]) + >>> op = odl.MultiplyOperator(r3, 3.0, range=r3) + >>> op.adjoint(x) + array([ 3., 6., 9.]) - Multiplication operator with complex space: + Multiplication operator on complex space: >>> c3 = odl.cn(3) >>> x_complex = c3.element([1, 1j, 1-1j]) - >>> op3 = MultiplyOperator(x_complex) - >>> op3.adjoint.multiplicand - cn(3).element([ 1.-0.j, 0.-1.j, 1.+1.j]) + >>> op = odl.MultiplyOperator(c3, x_complex) + >>> op.adjoint.multiplicand + array([ 1.-0.j, 0.-1.j, 1.+1.j]) """ if self.__domain_is_field: if isinstance(self.domain, RealNumbers): - return InnerProductOperator(self.multiplicand) + return InnerProductOperator(self.range, self.multiplicand) elif isinstance(self.domain, ComplexNumbers): - return InnerProductOperator(self.multiplicand.conjugate()) + return InnerProductOperator( + self.range, self.multiplicand.conjugate() + ) else: raise NotImplementedError( 'adjoint not implemented for domain{!r}' ''.format(self.domain)) elif self.domain.is_complex: - return MultiplyOperator(np.conj(self.multiplicand), - domain=self.range, range=self.domain) + return MultiplyOperator( + self.range, self.multiplicand.conjugate(), range=self.domain + ) else: - return MultiplyOperator(self.multiplicand, - domain=self.range, range=self.domain) + return MultiplyOperator( + self.range, self.multiplicand, range=self.domain + ) def __repr__(self): """Return ``repr(self)``.""" @@ -423,15 +426,15 @@ def __init__(self, domain, exponent): Examples -------- - Use with vectors + Use with vectors: - >>> op = PowerOperator(odl.rn(3), exponent=2) + >>> op = odl.PowerOperator(odl.rn(3), exponent=2) >>> op([1, 2, 3]) - rn(3).element([ 1., 4., 9.]) + array([ 1., 4., 9.]) - or scalars + Use with scalars: - >>> op = PowerOperator(odl.RealNumbers(), exponent=2) + >>> op = odl.PowerOperator(odl.RealNumbers(), exponent=2) >>> op(2.0) 4.0 """ @@ -452,8 +455,9 @@ def _call(self, x, out=None): elif self.__domain_is_field: raise ValueError('cannot use `out` with field') else: - out.assign(x) + self.range.assign(out, x) out **= self.exponent + return out def derivative(self, point): """Derivative of this operator. @@ -474,21 +478,22 @@ def derivative(self, point): -------- Use on vector spaces: - >>> op = PowerOperator(odl.rn(3), exponent=2) + >>> op = odl.PowerOperator(odl.rn(3), exponent=2) >>> dop = op.derivative(op.domain.element([1, 2, 3])) >>> dop([1, 1, 1]) - rn(3).element([ 2., 4., 6.]) + array([ 2., 4., 6.]) Use with scalars: - >>> op = PowerOperator(odl.RealNumbers(), exponent=2) + >>> op = odl.PowerOperator(odl.RealNumbers(), exponent=2) >>> dop = op.derivative(2.0) >>> dop(2.0) 8.0 """ - return self.exponent * MultiplyOperator(point ** (self.exponent - 1), - domain=self.domain, - range=self.range) + point = self.domain.element(point) + return self.exponent * MultiplyOperator( + self.domain, point ** (self.exponent - 1), self.range + ) def __repr__(self): """Return ``repr(self)``.""" @@ -503,11 +508,9 @@ def __str__(self): class InnerProductOperator(Operator): """Operator taking the inner product with a fixed space element. - Implements:: - - InnerProductOperator(y)(x) <==> y.inner(x) + Implements :: - This is only applicable in inner product spaces. + InnerProductOperator(space, y)(x) <==> space.inner(x, y) See Also -------- @@ -515,24 +518,26 @@ class InnerProductOperator(Operator): NormOperator : Vector space norm as operator. """ - def __init__(self, vector): + def __init__(self, space, vector): """Initialize a new instance. Parameters ---------- - vector : `LinearSpaceElement` + domain : `LinearSpace` or `Field` + Set of elements on which the operator can be applied. + vector : `array-like` The element to take the inner product with. Examples -------- >>> r3 = odl.rn(3) >>> x = r3.element([1, 2, 3]) - >>> op = InnerProductOperator(x) - >>> op(r3.element([1, 2, 3])) + >>> op = odl.InnerProductOperator(r3, x) + >>> op([1, 2, 3]) 14.0 """ super(InnerProductOperator, self).__init__( - vector.space, vector.space.field, linear=True) + space, space.field, linear=True) self.__vector = vector @property @@ -542,7 +547,7 @@ def vector(self): def _call(self, x): """Return the inner product with ``x``.""" - return x.inner(self.vector) + return self.domain.inner(x, self.vector) @property def adjoint(self): @@ -557,31 +562,11 @@ def adjoint(self): -------- >>> r3 = odl.rn(3) >>> x = r3.element([1, 2, 3]) - >>> op = InnerProductOperator(x) + >>> op = odl.InnerProductOperator(r3, x) >>> op.adjoint(2.0) - rn(3).element([ 2., 4., 6.]) - """ - return MultiplyOperator(self.vector, self.vector.space.field) - - @property - def T(self): - """Fixed vector of this operator. - - Returns - ------- - vector : `LinearSpaceElement` - The fixed space element used in this inner product operator. - - Examples - -------- - >>> r3 = odl.rn(3) - >>> x = r3.element([1, 2, 3]) - >>> x.T - InnerProductOperator(rn(3).element([ 1., 2., 3.])) - >>> x.T.T - rn(3).element([ 1., 2., 3.]) + array([ 2., 4., 6.]) """ - return self.vector + return MultiplyOperator(self.range, self.vector, range=self.domain) def __repr__(self): """Return ``repr(self)``.""" @@ -596,12 +581,9 @@ class NormOperator(Operator): """Vector space norm as an operator. - Implements:: - - NormOperator()(x) <==> x.norm() + Implements :: - This is only applicable in normed spaces, i.e., spaces implementing - a ``norm`` method. + NormOperator(space)(x) <==> space.norm(x) See Also -------- @@ -620,7 +602,7 @@ def __init__(self, space): Examples -------- >>> r2 = odl.rn(2) - >>> op = NormOperator(r2) + >>> op = odl.NormOperator(r2) >>> op([3, 4]) 5.0 """ @@ -628,14 +610,15 @@ def __init__(self, space): def _call(self, x): """Return the norm of ``x``.""" - return x.norm() + return self.domain.norm(x) def derivative(self, point): r"""Derivative of this operator in ``point``. - ``NormOperator().derivative(y)(x) == (y / y.norm()).inner(x)`` + Implements :: - This is only applicable in inner product spaces. + NormOperator(space).derivative(y)(x) <==> + space.inner(x, y / space.norm(y)) Parameters ---------- @@ -649,8 +632,8 @@ def derivative(self, point): Raises ------ ValueError - If ``point.norm() == 0``, in which case the derivative is not well - defined in the Frechet sense. + If ``point`` has a norm of 0, in which case the derivative is not + well-defined. Notes ----- @@ -663,17 +646,17 @@ def derivative(self, point): Examples -------- >>> r3 = odl.rn(3) - >>> op = NormOperator(r3) + >>> op = odl.NormOperator(r3) >>> derivative = op.derivative([1, 0, 0]) >>> derivative([1, 0, 0]) 1.0 """ point = self.domain.element(point) - norm = point.norm() + norm = self.domain.norm(point) if norm == 0: raise ValueError('not differentiable in 0') - return InnerProductOperator(point / norm) + return InnerProductOperator(self.domain, point / norm) def __repr__(self): """Return ``repr(self)``.""" @@ -688,12 +671,9 @@ class DistOperator(Operator): """Operator taking the distance to a fixed space element. - Implements:: - - DistOperator(y)(x) == y.dist(x) + Implements :: - This is only applicable in metric spaces, i.e., spaces implementing - a ``dist`` method. + DistOperator(space, y)(x) <==> space.dist(x, y) See Also -------- @@ -701,11 +681,13 @@ class DistOperator(Operator): NormOperator : Vector space norm as an operator. """ - def __init__(self, vector): + def __init__(self, space, vector): """Initialize a new instance. Parameters ---------- + space : `LinearSpace` + Space to take the distance in. vector : `LinearSpaceElement` Point to calculate the distance to. @@ -713,13 +695,12 @@ def __init__(self, vector): -------- >>> r2 = odl.rn(2) >>> x = r2.element([1, 1]) - >>> op = DistOperator(x) + >>> op = odl.DistOperator(r2, x) >>> op([4, 5]) 5.0 """ - super(DistOperator, self).__init__( - vector.space, RealNumbers(), linear=False) - self.__vector = vector + super(DistOperator, self).__init__(space, RealNumbers(), linear=False) + self.__vector = space.element(vector) @property def vector(self): @@ -727,16 +708,16 @@ def vector(self): return self.__vector def _call(self, x): - """Return the distance from ``self.vector`` to ``x``.""" - return self.vector.dist(x) + """Return the distance from ``x`` to the fixed vector.""" + return self.domain.dist(x, self.vector) def derivative(self, point): r"""The derivative operator. - ``DistOperator(y).derivative(z)(x) == - ((y - z) / y.dist(z)).inner(x)`` + Implements :: - This is only applicable in inner product spaces. + DistOperator(space, y).derivative(p)(x) <==> + space.inner(x, (y - p) / space.dist(y, p)) Parameters ---------- @@ -765,19 +746,19 @@ def derivative(self, point): -------- >>> r2 = odl.rn(2) >>> x = r2.element([1, 1]) - >>> op = DistOperator(x) + >>> op = odl.DistOperator(r2, x) >>> derivative = op.derivative([2, 1]) >>> derivative([1, 0]) 1.0 """ point = self.domain.element(point) diff = point - self.vector - dist = self.vector.dist(point) + dist = self.domain.dist(point, self.vector) if dist == 0: raise ValueError('not differentiable at the reference vector {!r}' ''.format(self.vector)) - return InnerProductOperator(diff / dist) + return InnerProductOperator(self.domain, diff / dist) def __repr__(self): """Return ``repr(self)``.""" @@ -797,43 +778,35 @@ class ConstantOperator(Operator): ConstantOperator(y)(x) == y """ - def __init__(self, constant, domain=None, range=None): + def __init__(self, range, constant, domain=None): """Initialize a new instance. Parameters ---------- - constant : `LinearSpaceElement` or ``range`` `element-like` - The constant space element to be returned. If ``range`` is not - provided, ``constant`` must be a `LinearSpaceElement` since the - operator range is then inferred from it. - domain : `LinearSpace`, optional - Domain of the operator. Default: ``vector.space`` range : `LinearSpace`, optional - Range of the operator. Default: ``vector.space`` + Set to which the operator maps. + constant : ``range`` `element-like` + The constant space element to be returned. + domain : `LinearSpace`, optional + Domain of the operator. Default: ``range`` Examples -------- >>> r3 = odl.rn(3) >>> x = r3.element([1, 2, 3]) - >>> op = ConstantOperator(x) - >>> op(x, out=r3.element()) - rn(3).element([ 1., 2., 3.]) + >>> op = odl.ConstantOperator(r3, x) + >>> op(x) + array([ 1., 2., 3.]) """ - - if ((domain is None or range is None) and - not isinstance(constant, LinearSpaceElement)): - raise TypeError('If either domain or range is unspecified ' - '`constant` must be LinearSpaceVector, got ' - '{!r}.'.format(constant)) - + if not isinstance(range, LinearSpace): + raise TypeError( + '`range` must be a `LinearSpace`, got {!r}'.format(range) + ) if domain is None: - domain = constant.space - if range is None: - range = constant.space + domain = range + super(ConstantOperator, self).__init__(domain, range, linear=False) self.__constant = range.element(constant) - linear = self.constant.norm() == 0 - super(ConstantOperator, self).__init__(domain, range, linear=linear) @property def constant(self): @@ -845,14 +818,7 @@ def _call(self, x, out=None): if out is None: return self.range.element(copy(self.constant)) else: - out.assign(self.constant) - - @property - def adjoint(self): - """Adjoint of the operator. - - Only defined if the operator is the constant operator. - """ + self.range.assign(out, self.constant) def derivative(self, point): """Derivative of this operator, always zero. @@ -865,12 +831,12 @@ def derivative(self, point): -------- >>> r3 = odl.rn(3) >>> x = r3.element([1, 2, 3]) - >>> op = ConstantOperator(x) + >>> op = odl.ConstantOperator(r3, x) >>> deriv = op.derivative([1, 1, 1]) >>> deriv([2, 2, 2]) - rn(3).element([ 0., 0., 0.]) + array([ 0., 0., 0.]) """ - return ZeroOperator(domain=self.domain, range=self.range) + return ZeroOperator(self.domain, self.range) def __repr__(self): """Return ``repr(self)``.""" @@ -890,30 +856,31 @@ class ZeroOperator(Operator): ZeroOperator(space)(x) == space.zero() """ - def __init__(self, domain, range=None): + def __init__(self, range, domain=None): """Initialize a new instance. Parameters ---------- - domain : `LinearSpace` - Domain of the operator. range : `LinearSpace`, optional - Range of the operator. Default: ``domain`` + Range of the operator. + domain : `LinearSpace` + Domain of the operator. Default: ``range`` Examples -------- >>> op = odl.ZeroOperator(odl.rn(3)) >>> op([1, 2, 3]) - rn(3).element([ 0., 0., 0.]) + array([ 0., 0., 0.]) Also works with domain != range: - >>> op = odl.ZeroOperator(odl.rn(3), odl.cn(4)) - >>> op([1, 2, 3]) - cn(4).element([ 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j]) + >>> op = odl.ZeroOperator(odl.cn(4), domain=odl.rn(3)) + >>> out = op.range.element() + >>> op([1, 2, 3], out=out) + array([ 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j]) """ - if range is None: - range = domain + if domain is None: + domain = range super(ZeroOperator, self).__init__(domain, range, linear=True) @@ -923,13 +890,13 @@ def _call(self, x, out=None): if out is None: out = 0 * x else: - out.lincomb(0, x) + self.range.lincomb(0, x, out=out) else: - result = self.range.zero() + zero = self.range.zero() if out is None: - out = result + out = zero else: - out.assign(result) + self.range.assign(out, zero) return out @property @@ -939,11 +906,11 @@ def adjoint(self): If ``self.domain == self.range`` the zero operator is self-adjoint, otherwise it is the `ZeroOperator` from `range` to `domain`. """ - return ZeroOperator(domain=self.range, range=self.domain) + return ZeroOperator(self.domain, domain=self.range) def __repr__(self): """Return ``repr(self)``.""" - return '{}({!r})'.format(self.__class__.__name__, self.domain) + return '{}({!r})'.format(self.__class__.__name__, self.range) def __str__(self): """Return ``str(self)``.""" @@ -973,24 +940,24 @@ def __init__(self, space): Take the real part of complex vector: >>> c3 = odl.cn(3) - >>> op = RealPart(c3) + >>> op = odl.RealPart(c3) >>> op([1 + 2j, 2, 3 - 1j]) - rn(3).element([ 1., 2., 3.]) + array([ 1., 2., 3.]) The operator is the identity on real spaces: >>> r3 = odl.rn(3) - >>> op = RealPart(r3) + >>> op = odl.RealPart(r3) >>> op([1, 2, 3]) - rn(3).element([ 1., 2., 3.]) + array([ 1., 2., 3.]) The operator also works on other `TensorSpace` spaces such as `DiscretizedSpace` spaces: >>> r3 = odl.uniform_discr(0, 1, 3, dtype=complex) - >>> op = RealPart(r3) + >>> op = odl.RealPart(r3) >>> op([1, 2, 3]) - uniform_discr(0.0, 1.0, 3).element([ 1., 2., 3.]) + array([ 1., 2., 3.]) """ real_space = space.real_space self.space_is_real = (space == real_space) @@ -1023,17 +990,17 @@ def inverse(self): The inverse is its own inverse if its domain is real: >>> r3 = odl.rn(3) - >>> op = RealPart(r3) + >>> op = odl.RealPart(r3) >>> op.inverse(op([1, 2, 3])) - rn(3).element([ 1., 2., 3.]) + array([ 1., 2., 3.]) - This is not a true inverse, only a pseudoinverse, the complex part - will by necessity be lost. + This is not a true inverse, only a pseudoinverse, the imaginary part + will by necessity be lost: >>> c3 = odl.cn(3) - >>> op = RealPart(c3) + >>> op = odl.RealPart(c3) >>> op.inverse(op([1 + 2j, 2, 3 - 1j])) - cn(3).element([ 1.+0.j, 2.+0.j, 3.+0.j]) + array([ 1.+0.j, 2.+0.j, 3.+0.j]) """ if self.space_is_real: return self @@ -1062,21 +1029,21 @@ def adjoint(self): The adjoint satisfies the adjoint equation for real spaces: >>> r3 = odl.rn(3) - >>> op = RealPart(r3) + >>> op = odl.RealPart(r3) >>> x = op.domain.element([1, 2, 3]) >>> y = op.range.element([3, 2, 1]) - >>> x.inner(op.adjoint(y)) == op(x).inner(y) + >>> op.domain.inner(x, op.adjoint(y)) == op.range.inner(op(x), y) True If the domain is complex, it only satisfies the weaker definition: >>> c3 = odl.cn(3) - >>> op = RealPart(c3) + >>> op = odl.RealPart(c3) >>> x = op.range.element([1, 2, 3]) >>> y = op.range.element([3, 2, 1]) - >>> AtAxy = op(op.adjoint(x)).inner(y) - >>> AtxAty = op.adjoint(x).inner(op.adjoint(y)) - >>> AtAxy == AtxAty + >>> AAtxy = op.range.inner(op(op.adjoint(x)), y) + >>> AtxAty = op.domain.inner(op.adjoint(x), op.adjoint(y)) + >>> AAtxy == AtxAty True """ if self.space_is_real: @@ -1108,16 +1075,16 @@ def __init__(self, space): Take the imaginary part of complex vector: >>> c3 = odl.cn(3) - >>> op = ImagPart(c3) + >>> op = odl.ImagPart(c3) >>> op([1 + 2j, 2, 3 - 1j]) - rn(3).element([ 2., 0., -1.]) + array([ 2., 0., -1.]) The operator is the zero operator on real spaces: >>> r3 = odl.rn(3) - >>> op = ImagPart(r3) + >>> op = odl.ImagPart(r3) >>> op([1, 2, 3]) - rn(3).element([ 0., 0., 0.]) + array([ 0., 0., 0.]) """ real_space = space.real_space self.space_is_real = (space == real_space) @@ -1150,17 +1117,17 @@ def inverse(self): The inverse is the zero operator if the domain is real: >>> r3 = odl.rn(3) - >>> op = ImagPart(r3) + >>> op = odl.ImagPart(r3) >>> op.inverse(op([1, 2, 3])) - rn(3).element([ 0., 0., 0.]) + array([ 0., 0., 0.]) This is not a true inverse, only a pseudoinverse, the real part will by necessity be lost. >>> c3 = odl.cn(3) - >>> op = ImagPart(c3) + >>> op = odl.ImagPart(c3) >>> op.inverse(op([1 + 2j, 2, 3 - 1j])) - cn(3).element([ 0.+2.j, 0.+0.j, -0.-1.j]) + array([ 0.+2.j, 0.+0.j, -0.-1.j]) """ if self.space_is_real: return ZeroOperator(self.domain) @@ -1189,21 +1156,21 @@ def adjoint(self): The adjoint satisfies the adjoint equation for real spaces: >>> r3 = odl.rn(3) - >>> op = ImagPart(r3) + >>> op = odl.ImagPart(r3) >>> x = op.domain.element([1, 2, 3]) >>> y = op.range.element([3, 2, 1]) - >>> x.inner(op.adjoint(y)) == op(x).inner(y) + >>> op.domain.inner(x, op.adjoint(y)) == op.domain.inner(op(x), y) True If the domain is complex, it only satisfies the weaker definition: >>> c3 = odl.cn(3) - >>> op = ImagPart(c3) + >>> op = odl.ImagPart(c3) >>> x = op.range.element([1, 2, 3]) >>> y = op.range.element([3, 2, 1]) - >>> AtAxy = op(op.adjoint(x)).inner(y) - >>> AtxAty = op.adjoint(x).inner(op.adjoint(y)) - >>> AtAxy == AtxAty + >>> AAtxy = op.range.inner(op(op.adjoint(x)), y) + >>> AtxAty = op.domain.inner(op.adjoint(x), op.adjoint(y)) + >>> AAtxy == AtxAty True """ if self.space_is_real: @@ -1238,23 +1205,23 @@ def __init__(self, space, scalar=1.0): Embed real vector into complex space: >>> r3 = odl.rn(3) - >>> op = ComplexEmbedding(r3) + >>> op = odl.ComplexEmbedding(r3) >>> op([1, 2, 3]) - cn(3).element([ 1.+0.j, 2.+0.j, 3.+0.j]) + array([ 1.+0.j, 2.+0.j, 3.+0.j]) Embed real vector as imaginary part into complex space: - >>> op = ComplexEmbedding(r3, scalar=1j) + >>> op = odl.ComplexEmbedding(r3, scalar=1j) >>> op([1, 2, 3]) - cn(3).element([ 0.+1.j, 0.+2.j, 0.+3.j]) + array([ 0.+1.j, 0.+2.j, 0.+3.j]) On complex spaces the operator is the same as simple multiplication by scalar: >>> c3 = odl.cn(3) - >>> op = ComplexEmbedding(c3, scalar=1 + 2j) + >>> op = odl.ComplexEmbedding(c3, scalar=1 + 2j) >>> op([1 + 1j, 2 + 2j, 3 + 3j]) - cn(3).element([-1.+3.j, -2.+6.j, -3.+9.j]) + array([-1.+3.j, -2.+6.j, -3.+9.j]) """ complex_space = space.complex_space self.scalar = complex_space.field.element(scalar) @@ -1269,7 +1236,7 @@ def _call(self, x, out): out.imag = self.scalar.imag * x else: # Complex domain - out.lincomb(self.scalar, x) + self.range.lincomb(self.scalar, x, out=out) @property def inverse(self): @@ -1283,7 +1250,7 @@ def inverse(self): >>> r3 = odl.rn(3) >>> op = ComplexEmbedding(r3, scalar=1) >>> op.inverse(op([1, 2, 4])) - rn(3).element([ 1., 2., 4.]) + array([ 1., 2., 4.]) """ if self.domain.is_real: # Real domain @@ -1320,25 +1287,25 @@ def adjoint(self): Examples -------- - The adjoint satisfies the adjoint equation for complex spaces + The adjoint satisfies the adjoint equation for complex spaces: >>> c3 = odl.cn(3) - >>> op = ComplexEmbedding(c3, scalar=1j) + >>> op = odl.ComplexEmbedding(c3, scalar=1j) >>> x = c3.element([1 + 1j, 2 + 2j, 3 + 3j]) >>> y = c3.element([3 + 1j, 2 + 2j, 3 + 1j]) - >>> Axy = op(x).inner(y) - >>> xAty = x.inner(op.adjoint(y)) + >>> Axy = op.range.inner(op(x), y) + >>> xAty = op.domain.inner(x, op.adjoint(y)) >>> Axy == xAty True - For real domains, it only satisfies the (right) adjoint equation + For real domains, it only satisfies the (right) adjoint equation: >>> r3 = odl.rn(3) - >>> op = ComplexEmbedding(r3, scalar=1j) + >>> op = odl.ComplexEmbedding(r3, scalar=1j) >>> x = r3.element([1, 2, 3]) >>> y = r3.element([3, 2, 3]) - >>> AtAxy = op.adjoint(op(x)).inner(y) - >>> AxAy = op(x).inner(op(y)) + >>> AtAxy = op.domain.inner(op.adjoint(op(x)), y) + >>> AxAy = op.range.inner(op(x), op(y)) >>> AtAxy == AxAy True """ @@ -1378,14 +1345,14 @@ def __init__(self, space): >>> c2 = odl.cn(2) >>> op = odl.ComplexModulus(c2) >>> op([3 + 4j, 2]) - rn(2).element([ 5., 2.]) + array([ 5., 2.]) The operator is the absolute value on real spaces: >>> r2 = odl.rn(2) >>> op = odl.ComplexModulus(r2) >>> op([1, -2]) - rn(2).element([ 1., 2.]) + array([ 1., 2.]) The operator also works on other `TensorSpace`'s such as `DiscretizedSpace`: @@ -1393,14 +1360,15 @@ def __init__(self, space): >>> space = odl.uniform_discr(0, 1, 2, dtype=complex) >>> op = odl.ComplexModulus(space) >>> op([3 + 4j, 2]) - uniform_discr(0.0, 1.0, 2).element([ 5., 2.]) + array([ 5., 2.]) """ real_space = space.real_space super(ComplexModulus, self).__init__(space, real_space, linear=False) def _call(self, x): """Return ``self(x)``.""" - return (x.real ** 2 + x.imag ** 2).ufuncs.sqrt() + # TODO(kohr-h): generalize to other array types + return np.sqrt(x.real ** 2 + x.imag ** 2) def derivative(self, x): r"""Return the derivative operator in the "C = R^2" sense. @@ -1422,14 +1390,14 @@ def derivative(self, x): >>> c2 = odl.cn(2) >>> op = odl.ComplexModulus(c2) >>> op([3 + 4j, 2]) - rn(2).element([ 5., 2.]) + array([ 5., 2.]) >>> deriv = op.derivative([3 + 4j, 2]) >>> deriv.domain cn(2) >>> deriv.range rn(2) >>> deriv([2 + 1j, 4j]) # [(3*2 + 4*1) / 5, (2*0 + 0*4) / 2] - rn(2).element([ 2., 0.]) + array([ 2., 0.]) Notes ----- @@ -1474,7 +1442,7 @@ def adjoint(self): >>> c2 = odl.cn(2) >>> op = odl.ComplexModulus(c2) >>> op([3 + 4j, 2]) - rn(2).element([ 5., 2.]) + array([ 5., 2.]) >>> deriv = op.derivative([3 + 4j, 2]) >>> adj = deriv.adjoint >>> adj.domain @@ -1490,15 +1458,15 @@ def adjoint(self): >>> y1 = deriv.range.element([5, 5]) >>> y2 = deriv.range.element([1, 2]) - >>> adj(y1).inner(adj(y2)) # + >>> deriv.domain.inner(adj(y1), adj(y2)) # (15+0j) - >>> deriv(adj(y1)).inner(y2) # + >>> deriv.range.inner(deriv(adj(y1)), y2) # 15.0 >>> x1 = deriv.domain.element([6 + 3j, 2j]) >>> x2 = deriv.domain.element([5, 10 + 4j]) - >>> deriv(x1).inner(deriv(x2)) # + >>> deriv.range.inner(deriv(x1), deriv(x2)) # 18.0 - >>> adj(deriv(x1)).inner(x2) # + >>> deriv.domain.inner(adj(deriv(x1)), x2) # (18+24j) Notes @@ -1540,7 +1508,7 @@ class ComplexModulusDerivativeAdjoint(Operator): def _call(self, u, out): """Implement ``self(u, out)``.""" - out.assign(x) + self.range.assign(out, x) tmp = u / op(x) out.real *= tmp out.imag *= tmp @@ -1578,14 +1546,14 @@ def __init__(self, space): >>> c2 = odl.cn(2) >>> op = odl.ComplexModulusSquared(c2) >>> op([3 + 4j, 2]) - rn(2).element([ 25., 4.]) + array([ 25., 4.]) On a real space, this is the same as squaring: >>> r2 = odl.rn(2) >>> op = odl.ComplexModulusSquared(r2) >>> op([1, -2]) - rn(2).element([ 1., 4.]) + array([ 1., 4.]) The operator also works on other `TensorSpace`'s such as `DiscretizedSpace`: @@ -1593,7 +1561,7 @@ def __init__(self, space): >>> space = odl.uniform_discr(0, 1, 2, dtype=complex) >>> op = odl.ComplexModulusSquared(space) >>> op([3 + 4j, 2]) - uniform_discr(0.0, 1.0, 2).element([ 25., 4.]) + array([ 25., 4.]) """ real_space = space.real_space super(ComplexModulusSquared, self).__init__( @@ -1620,14 +1588,14 @@ def derivative(self, x): >>> c2 = odl.cn(2) >>> op = odl.ComplexModulusSquared(c2) >>> op([3 + 4j, 2]) - rn(2).element([ 25., 4.]) + array([ 25., 4.]) >>> deriv = op.derivative([3 + 4j, 2]) >>> deriv.domain cn(2) >>> deriv.range rn(2) >>> deriv([2 + 1j, 4j]) # [(3*2 + 4*1) * 2, (2*0 + 0*4) * 2] - rn(2).element([ 20., 0.]) + array([ 20., 0.]) Notes ----- @@ -1656,7 +1624,8 @@ class ComplexModulusSquaredDerivative(Operator): def _call(self, y, out): """Return ``self(y)``.""" - x.real.multiply(y.real, out=out) + # TODO(kohr-h): generalize to other array types + np.multiply(x.real, y.real, out=out) out += x.imag * y.imag out *= 2 return out @@ -1686,15 +1655,15 @@ def adjoint(self): >>> y1 = deriv.range.element([1, 1]) >>> y2 = deriv.range.element([1, -1]) - >>> adj(y1).inner(adj(y2)) # + >>> deriv.domain.inner(adj(y1), adj(y2)) # (84+0j) - >>> deriv(adj(y1)).inner(y2) # + >>> deriv.range.inner(deriv(adj(y1)), y2) # 84.0 >>> x1 = deriv.domain.element([1j, 1j]) >>> x2 = deriv.domain.element([1 + 1j, 1j]) - >>> deriv(x1).inner(deriv(x2)) # + >>> deriv.range.inner(deriv(x1), deriv(x2)) # 112.0 - >>> adj(deriv(x1)).inner(x2) # + >>> deriv.domain.inner(adj(deriv(x1)), x2) # (112+16j) Notes @@ -1736,7 +1705,7 @@ class ComplexModulusSquaredDerivAdj(Operator): def _call(self, u, out): """Implement ``self(u, out)``.""" - out.assign(x) + self.range.assign(out, x) out.real *= u out.imag *= u out *= 2 diff --git a/odl/operator/operator.py b/odl/operator/operator.py index 125ca0fccb8..0c239bed6cc 100644 --- a/odl/operator/operator.py +++ b/odl/operator/operator.py @@ -15,8 +15,9 @@ from builtins import object from numbers import Integral, Number +import numpy as np + from odl.set import Field, LinearSpace, Set -from odl.set.space import LinearSpaceElement from odl.util import cache_arguments __all__ = ( @@ -26,7 +27,6 @@ 'OperatorVectorSum', 'OperatorLeftScalarMult', 'OperatorRightScalarMult', - 'FunctionalLeftVectorMult', 'OperatorLeftVectorMult', 'OperatorRightVectorMult', 'OperatorPointwiseProduct', @@ -58,10 +58,11 @@ def _default_call_out_of_place(op, x, **kwargs): out = op.range.element() result = op._call_in_place(x, out, **kwargs) if result is not None and result is not out: - raise ValueError('`op` returned a different value than `out`.' - 'With in-place evaluation, the operator can ' - 'only return nothing (`None`) or the `out` ' - 'parameter.') + raise ValueError( + '`op` returned a different value than `out`;\n' + 'with in-place evaluation, the operator may only return nothing ' + '(`None`) or the original `out` parameter.' + ) return out @@ -80,7 +81,9 @@ def _default_call_in_place(op, x, out, **kwargs): kwargs: Optional arguments to the operator. """ - out.assign(op.range.element(op._call_out_of_place(x, **kwargs))) + op.range.lincomb( + 1, op.range.element(op._call_out_of_place(x, **kwargs)), out=out + ) def _function_signature(func): @@ -652,15 +655,15 @@ def __call__(self, x, out=None, **kwargs): Out-of-place evaluation: >>> op(x) - rn(3).element([ 2., 4., 6.]) + array([ 2., 4., 6.]) In-place evaluation: >>> y = rn.element() >>> op(x, out=y) - rn(3).element([ 2., 4., 6.]) + array([ 2., 4., 6.]) >>> y - rn(3).element([ 2., 4., 6.]) + array([ 2., 4., 6.]) See Also -------- @@ -854,10 +857,10 @@ def __mul__(self, other): >>> op = odl.IdentityOperator(rn) >>> x = rn.element([1, 2, 3]) >>> op(x) - rn(3).element([ 1., 2., 3.]) + array([ 1., 2., 3.]) >>> Scaled = op * 3 >>> Scaled(x) - rn(3).element([ 3., 6., 9.]) + array([ 3., 6., 9.]) """ if isinstance(other, Operator): return OperatorComp(self, other) @@ -868,8 +871,8 @@ def __mul__(self, other): return other * self else: return OperatorRightScalarMult(self, other) - elif isinstance(other, LinearSpaceElement) and other in self.domain: - return OperatorRightVectorMult(self, other.copy()) + elif other in self.domain: + return OperatorRightVectorMult(self, self.domain.copy(other)) else: return NotImplemented @@ -936,20 +939,17 @@ def __rmul__(self, other): >>> op = odl.IdentityOperator(rn) >>> x = rn.element([1, 2, 3]) >>> op(x) - rn(3).element([ 1., 2., 3.]) + array([ 1., 2., 3.]) >>> Scaled = 3 * op >>> Scaled(x) - rn(3).element([ 3., 6., 9.]) + array([ 3., 6., 9.]) """ if isinstance(other, Operator): return OperatorComp(other, self) elif isinstance(other, Number): return OperatorLeftScalarMult(self, other) elif other in self.range: - return OperatorLeftVectorMult(self, other.copy()) - elif (isinstance(other, LinearSpaceElement) and - other.space.field == self.range): - return FunctionalLeftVectorMult(self, other.copy()) + return OperatorLeftVectorMult(self, self.range.copy(other)) else: return NotImplemented @@ -987,13 +987,13 @@ def __pow__(self, n): >>> op = odl.ScalingOperator(rn, 3) >>> x = rn.element([1, 2, 3]) >>> op(x) - rn(3).element([ 3., 6., 9.]) + array([ 3., 6., 9.]) >>> squared = op ** 2 >>> squared(x) - rn(3).element([ 9., 18., 27.]) + array([ 9., 18., 27.]) >>> squared = op**3 >>> squared(x) - rn(3).element([ 27., 54., 81.]) + array([ 27., 54., 81.]) """ if isinstance(n, Integral) and n > 0: op = self @@ -1029,10 +1029,10 @@ def __truediv__(self, other): >>> op = odl.IdentityOperator(rn) >>> x = rn.element([3, 6, 9]) >>> op(x) - rn(3).element([ 3., 6., 9.]) + array([ 3., 6., 9.]) >>> Scaled = op / 3.0 >>> Scaled(x) - rn(3).element([ 1., 2., 3.]) + array([ 1., 2., 3.]) """ if isinstance(other, Number): return self * (1.0 / other) @@ -1113,11 +1113,11 @@ def __init__(self, left, right, tmp_ran=None, tmp_dom=None): >>> x = r3.element([1, 2, 3]) >>> out = r3.element() >>> OperatorSum(op, op)(x, out) # In-place, returns out - rn(3).element([ 2., 4., 6.]) + array([ 2., 4., 6.]) >>> out - rn(3).element([ 2., 4., 6.]) + array([ 2., 4., 6.]) >>> OperatorSum(op, op)(x) - rn(3).element([ 2., 4., 6.]) + array([ 2., 4., 6.]) """ if left.range != right.range: raise OpTypeError('operator ranges {!r} and {!r} do not match' @@ -1247,7 +1247,7 @@ def __init__(self, operator, vector): >>> sum_op = odl.OperatorVectorSum(ident_op, y) >>> x = r3.element([4, 5, 6]) >>> sum_op(x) - rn(3).element([ 5., 7., 9.]) + array([ 5., 7., 9.]) """ if not isinstance(operator, Operator): raise TypeError('`op` {!r} not a Operator instance' @@ -1301,7 +1301,7 @@ def derivative(self, point): >>> sum = odl.OperatorVectorSum(op, r3.element([1, 2, 3])) >>> x = r3.element([4, 5, 6]) >>> sum.derivative(x)(x) - rn(3).element([ 4., 5., 6.]) + array([ 4., 5., 6.]) """ return self.operator.derivative(point) @@ -1555,7 +1555,7 @@ def __init__(self, operator, scalar): >>> operator = odl.IdentityOperator(space) >>> left_mul_op = OperatorLeftScalarMult(operator, 3) >>> left_mul_op([1, 2, 3]) - rn(3).element([ 3., 6., 9.]) + array([ 3., 6., 9.]) """ if not isinstance(operator.range, (LinearSpace, Field)): raise OpTypeError('range {!r} not a `LinearSpace` or `Field` ' @@ -1613,7 +1613,7 @@ def inverse(self): >>> operator = odl.IdentityOperator(space) >>> left_mul_op = OperatorLeftScalarMult(operator, 3) >>> left_mul_op.inverse([3, 3, 3]) - rn(3).element([ 1., 1., 1.]) + array([ 1., 1., 1.]) """ if self.scalar == 0.0: raise ZeroDivisionError('{} not invertible'.format(self)) @@ -1644,7 +1644,7 @@ def derivative(self, x): >>> left_mul_op = OperatorLeftScalarMult(operator, 3) >>> derivative = left_mul_op.derivative([0, 0, 0]) >>> derivative([1, 1, 1]) - rn(3).element([ 3., 3., 3.]) + array([ 3., 3., 3.]) """ if self.is_linear: return self @@ -1672,7 +1672,7 @@ def adjoint(self): >>> operator = odl.IdentityOperator(space) >>> left_mul_op = OperatorLeftScalarMult(operator, 3) >>> left_mul_op.adjoint([1, 2, 3]) - rn(3).element([ 3., 6., 9.]) + array([ 3., 6., 9.]) """ if not self.is_linear: @@ -1721,7 +1721,7 @@ def __init__(self, operator, scalar, tmp=None): >>> operator = odl.IdentityOperator(space) >>> left_mul_op = OperatorRightScalarMult(operator, 3) >>> left_mul_op([1, 2, 3]) - rn(3).element([ 3., 6., 9.]) + array([ 3., 6., 9.]) """ if not isinstance(operator.domain, (LinearSpace, Field)): raise OpTypeError('domain {!r} not a `LinearSpace` or `Field` ' @@ -1768,7 +1768,7 @@ def _call(self, x, out=None): tmp = self.__tmp else: tmp = self.domain.element() - tmp.lincomb(self.scalar, x) + self.domain.lincomb(self.scalar, x, out=tmp) self.operator(tmp, out=out) def __mul__(self, other): @@ -1800,7 +1800,7 @@ def inverse(self): >>> operator = odl.IdentityOperator(space) >>> left_mul_op = OperatorRightScalarMult(operator, 3) >>> left_mul_op.inverse([3, 3, 3]) - rn(3).element([ 1., 1., 1.]) + array([ 1., 1., 1.]) """ if self.scalar == 0.0: raise ZeroDivisionError('{} not invertible'.format(self)) @@ -1828,7 +1828,7 @@ def derivative(self, x): >>> left_mul_op = OperatorRightScalarMult(operator, 3) >>> derivative = left_mul_op.derivative([0, 0, 0]) >>> derivative([1, 1, 1]) - rn(3).element([ 3., 3., 3.]) + array([ 3., 3., 3.]) """ return self.scalar * self.operator.derivative(self.scalar * x) @@ -1853,7 +1853,7 @@ def adjoint(self): >>> operator = odl.IdentityOperator(space) >>> left_mul_op = OperatorRightScalarMult(operator, 3) >>> left_mul_op.adjoint([1, 2, 3]) - rn(3).element([ 3., 6., 9.]) + array([ 3., 6., 9.]) """ if not self.is_linear: @@ -1871,122 +1871,6 @@ def __str__(self): return '{} * {}'.format(self.operator, self.scalar) -class FunctionalLeftVectorMult(Operator): - - """Expression type for the functional left vector multiplication. - - A functional is an `Operator` whose `Operator.range` is - a `Field`. It is multiplied from left with a `LinearSpaceElement`, - resulting in an operator mapping from the `Operator.domain` to the - element's `LinearSpaceElement.space`. - - ``FunctionalLeftVectorMult(op, y)(x) == y * op(x)`` - """ - - def __init__(self, functional, vector): - """Initialize a new instance. - - Parameters - ---------- - functional : `Operator` - Functional in the vector multiplication. Its `range` must - be a `Field`. - vector : ``functional.range`` `element-like` - The element to multiply with. Its space's `LinearSpace.field` - must be the same as ``functional.range``. - - Examples - -------- - Create the operator ``(y * y^T)(x) = y * `` - - >>> space = odl.rn(3) - >>> y = space.element([1, 2, 3]) - >>> functional = odl.InnerProductOperator(y) - >>> left_mul_op = FunctionalLeftVectorMult(functional, y) - >>> left_mul_op([1, 2, 3]) - rn(3).element([ 14., 28., 42.]) - """ - if not isinstance(vector, LinearSpaceElement): - raise TypeError('`vector` {!r} not is not a LinearSpaceElement' - ''.format(vector)) - - if functional.range != vector.space.field: - raise OpTypeError('range {!r} not is not vector.space.field {!r}' - ''.format(functional.range, vector.space.field)) - - super(FunctionalLeftVectorMult, self).__init__( - functional.domain, vector.space, linear=functional.is_linear) - self.__functional = functional - self.__vector = vector - - @property - def functional(self): - """The functional part of this multiplication.""" - return self.__functional - - @property - def vector(self): - """The element part of this multiplication.""" - return self.__vector - - def _call(self, x, out=None): - """Implement ``self(x[, out])``.""" - if out is None: - return self.vector * self.functional(x) - else: - scalar = self.functional(x) - out.lincomb(scalar, self.vector) - - def derivative(self, x): - """Return the derivative at ``x``. - - Left scalar multiplication and derivative are commutative: - - ``FunctionalLeftVectorMult(op, y).derivative(z) == - FunctionalLeftVectorMult(op.derivative(z), y)`` - - Returns - ------- - derivative : `FunctionalLeftVectorMult` - """ - if self.is_linear: - return self - else: - return FunctionalLeftVectorMult(self.functional.derivative(x), - self.vector) - - @property - def adjoint(self): - """Adjoint of this operator. - - ``FunctionalLeftVectorMult(op, y).adjoint == - OperatorComp(op.adjoint, y.T)`` - - Returns - ------- - adjoint : `OperatorComp` - - Raises - ------ - OpNotImplementedError - If the underlying operator is non-linear. - """ - - if not self.is_linear: - raise OpNotImplementedError('nonlinear operators have no adjoint') - - return OperatorComp(self.functional.adjoint, self.vector.T) - - def __repr__(self): - """Return ``repr(self)``.""" - return '{}({!r}, {!r})'.format(self.__class__.__name__, - self.functional, self.vector) - - def __str__(self): - """Return ``str(self)``.""" - return '{} * {}'.format(self.vector, self.functional) - - class OperatorLeftVectorMult(Operator): """Expression type for the operator left vector multiplication. @@ -2084,11 +1968,10 @@ def adjoint(self): if not self.is_linear: raise OpNotImplementedError('nonlinear operators have no adjoint') - if self.vector.space.is_real: - # The complex conjugate of a real vector is the vector itself. - return self.operator.adjoint * self.vector - else: + if np.iscomplexobj(self.vector): return self.operator.adjoint * self.vector.conj() + else: + return self.operator.adjoint * self.vector def __repr__(self): """Return ``repr(self)``.""" @@ -2149,7 +2032,7 @@ def _call(self, x, out=None): return self.operator(x * self.vector) else: tmp = self.domain.element() - x.multiply(self.vector, out=tmp) + self.domain.multiply(x, self.vector, out=tmp) self.operator(tmp, out=out) @property @@ -2204,11 +2087,10 @@ def adjoint(self): if not self.is_linear: raise OpNotImplementedError('nonlinear operators have no adjoint') - if self.vector.space.is_real: - # The complex conjugate of a real vector is the vector itself. - return self.vector * self.operator.adjoint - else: + if np.iscomplexobj(self.vector): return self.vector.conj() * self.operator.adjoint + else: + return self.vector * self.operator.adjoint def __repr__(self): """Return ``repr(self)``.""" diff --git a/odl/operator/oputils.py b/odl/operator/oputils.py index 3ac71f8124f..894dc63c266 100644 --- a/odl/operator/oputils.py +++ b/odl/operator/oputils.py @@ -27,8 +27,8 @@ def matrix_representation(op): Parameters ---------- op : `Operator` - The linear operator of which one wants a matrix representation. - If the domain or range is a `ProductSpace`, it must be a power-space. + Linear operator of which a matrix representation should be computed. + Its ``domain`` and ``range`` must be `TensorSpace`'s. Returns ------- @@ -45,40 +45,11 @@ def matrix_representation(op): ... [4, 5, 6], ... [7, 8, 9]]) >>> op = odl.MatrixOperator(mat) - >>> matrix_representation(op) + >>> odl.matrix_representation(op) array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) - It also works with `ProductSpace`'s and higher dimensional `TensorSpace`'s. - In this case, the returned "matrix" will also be higher dimensional: - - >>> space = odl.uniform_discr([0, 0], [2, 2], (2, 2)) - >>> grad = odl.Gradient(space) - >>> tensor = odl.matrix_representation(grad) - >>> tensor.shape == (2, 2, 2, 2, 2) - True - - Since the "matrix" is now higher dimensional, we need to use e.g. - `numpy.tensordot` if we want to compute with the matrix representation: - - >>> x = space.element(lambda x: x[0] ** 2 + 2 * x[1] ** 2) - >>> grad(x) - ProductSpace(uniform_discr([ 0., 0.], [ 2., 2.], (2, 2)), 2).element([ - - [[ 2. , 2. ], - [-2.75, -6.75]], - - [[ 4. , -4.75], - [ 4. , -6.75]] - ]) - >>> np.tensordot(tensor, x, axes=grad.domain.ndim) - array([[[ 2. , 2. ], - [-2.75, -6.75]], - - [[ 4. , -4.75], - [ 4. , -6.75]]]) - Notes ---------- The algorithm works by letting the operator act on all unit vectors, and @@ -88,21 +59,14 @@ def matrix_representation(op): if not op.is_linear: raise ValueError('the operator is not linear') - if not (isinstance(op.domain, TensorSpace) or - (isinstance(op.domain, ProductSpace) and - op.domain.is_power_space and - all(isinstance(spc, TensorSpace) for spc in op.domain))): - raise TypeError('operator domain {!r} is neither `TensorSpace` ' - 'nor `ProductSpace` with only equal `TensorSpace` ' - 'components'.format(op.domain)) - - if not (isinstance(op.range, TensorSpace) or - (isinstance(op.range, ProductSpace) and - op.range.is_power_space and - all(isinstance(spc, TensorSpace) for spc in op.range))): - raise TypeError('operator range {!r} is neither `TensorSpace` ' - 'nor `ProductSpace` with only equal `TensorSpace` ' - 'components'.format(op.range)) + if not isinstance(op.domain, TensorSpace): + raise ValueError( + '`op.domain` must be a `TensorSpace`, got {!r}'.format(op.domain) + ) + if not isinstance(op.range, TensorSpace): + raise ValueError( + '`op.range` must be a `TensorSpace`, got {!r}'.format(op.range) + ) # Generate the matrix dtype = np.promote_types(op.domain.dtype, op.range.dtype) @@ -114,7 +78,7 @@ def matrix_representation(op): tmp_dom[j] = 1.0 op(tmp_dom, out=tmp_ran) - matrix[(Ellipsis,) + j] = tmp_ran.asarray() + matrix[(Ellipsis,) + j] = tmp_ran tmp_dom[j] = 0.0 @@ -206,10 +170,10 @@ def power_method_opnorm(op, xstart=None, maxiter=100, rtol=1e-05, atol=1e-08, x = noise_element(op.domain) else: # copy to ensure xstart is not modified - x = op.domain.element(xstart).copy() + x = op.domain.copy(xstart) # Take first iteration step to normalize input - x_norm = x.norm() + x_norm = op.domain.norm(x) if x_norm == 0: raise ValueError('``xstart`` must be nonzero') x /= x_norm @@ -237,7 +201,7 @@ def calc_opnorm(x_norm): x, tmp = tmp, x # Calculate x norm and verify it is valid - x_norm = x.norm() + x_norm = op.domain.norm(x) if x_norm == 0: raise ValueError('reached ``x=0`` after {} iterations'.format(i)) if not np.isfinite(x_norm): @@ -277,14 +241,15 @@ def as_scipy_operator(op): Examples -------- - Wrap operator and solve simple problem (here toy problem ``Ix = b``) + Wrap operator and solve simple problem (here the toy problem + ``2 * x = b``): - >>> op = odl.IdentityOperator(odl.rn(3)) - >>> scipy_op = as_scipy_operator(op) - >>> import scipy.sparse.linalg as scipy_solvers - >>> result, status = scipy_solvers.cg(scipy_op, [0, 1, 0]) + >>> op = odl.ScalingOperator(odl.rn(3), 2.0) + >>> scipy_op = odl.as_scipy_operator(op) + >>> from scipy.sparse.linalg import cg as scipy_cg + >>> result, status = scipy_cg(A=scipy_op, b=[0, 1, 0], atol=1e-4) >>> result - array([ 0., 1., 0.]) + array([ 0. , 0.5, 0. ]) Notes ----- @@ -307,15 +272,14 @@ def as_scipy_operator(op): shape = (native(op.range.size), native(op.domain.size)) def matvec(v): - return (op(v.reshape(op.domain.shape))).asarray().ravel() + return (op(v.reshape(op.domain.shape))).ravel() def rmatvec(v): - return (op.adjoint(v.reshape(op.range.shape))).asarray().ravel() + return (op.adjoint(v.reshape(op.range.shape))).ravel() - return scipy.sparse.linalg.LinearOperator(shape=shape, - matvec=matvec, - rmatvec=rmatvec, - dtype=dtype) + return scipy.sparse.linalg.LinearOperator( + shape=shape, matvec=matvec, rmatvec=rmatvec, dtype=dtype + ) def as_scipy_functional(func, return_gradient=False): diff --git a/odl/operator/pspace_ops.py b/odl/operator/pspace_ops.py index e1ddcd7802a..468d69f4a52 100644 --- a/odl/operator/pspace_ops.py +++ b/odl/operator/pspace_ops.py @@ -116,9 +116,7 @@ def __init__(self, operators, domain=None, range=None): >>> prod_op = odl.ProductSpaceOperator([[I, I]]) >>> prod_op(x) - ProductSpace(rn(3), 1).element([ - [ 5., 7., 9.] - ]) + array([array([ 5., 7., 9.])], dtype=object) Diagonal operator -- 0 or ``None`` means ignore, or the implicit zero operator: @@ -126,10 +124,7 @@ def __init__(self, operators, domain=None, range=None): >>> prod_op = odl.ProductSpaceOperator([[I, 0], ... [0, I]]) >>> prod_op(x) - ProductSpace(rn(3), 2).element([ - [ 1., 2., 3.], - [ 4., 5., 6.] - ]) + array([array([ 1., 2., 3.]), array([ 4., 5., 6.])], dtype=object) If a column is empty, the operator domain must be specified. The same holds for an empty row and the range of the operator: @@ -137,17 +132,11 @@ def __init__(self, operators, domain=None, range=None): >>> prod_op = odl.ProductSpaceOperator([[I, 0], ... [I, 0]], domain=r3 ** 2) >>> prod_op(x) - ProductSpace(rn(3), 2).element([ - [ 1., 2., 3.], - [ 1., 2., 3.] - ]) + array([array([ 1., 2., 3.]), array([ 1., 2., 3.])], dtype=object) >>> prod_op = odl.ProductSpaceOperator([[I, I], ... [0, 0]], range=r3 ** 2) >>> prod_op(x) - ProductSpace(rn(3), 2).element([ - [ 5., 7., 9.], - [ 0., 0., 0.] - ]) + array([array([ 5., 7., 9.]), array([ 0., 0., 0.])], dtype=object) """ # Lazy import to improve `import odl` time import scipy.sparse @@ -263,7 +252,7 @@ def _convert_to_spmatrix(operators): '{}'.format(len(row), i, ncols)) for j, col in enumerate(row): - if col is None or col is 0: + if col is None or col == 0: pass elif isinstance(col, Operator): irow.append(i) @@ -308,7 +297,7 @@ def _call(self, x, out=None): for i, evaluated in enumerate(has_evaluated_row): if not evaluated: - out[i].set_zero() + self.ops[i].range.set_zero(out[i]) return out @@ -337,15 +326,9 @@ def derivative(self, x): >>> prod_op = ProductSpaceOperator([[0, I], [0, 0]], ... domain=pspace, range=pspace) >>> prod_op(x) - ProductSpace(rn(3), 2).element([ - [ 4., 5., 6.], - [ 0., 0., 0.] - ]) + array([array([ 4., 5., 6.]), array([ 0., 0., 0.])], dtype=object) >>> prod_op.derivative(x)(x) - ProductSpace(rn(3), 2).element([ - [ 4., 5., 6.], - [ 0., 0., 0.] - ]) + array([array([ 4., 5., 6.]), array([ 0., 0., 0.])], dtype=object) Example with affine operator @@ -356,18 +339,12 @@ def derivative(self, x): Calling operator gives offset by [1, 1, 1] >>> op(x) - ProductSpace(rn(3), 2).element([ - [ 3., 4., 5.], - [ 0., 0., 0.] - ]) + array([array([ 3., 4., 5.]), array([ 0., 0., 0.])], dtype=object) Derivative of affine operator does not have this offset >>> op.derivative(x)(x) - ProductSpace(rn(3), 2).element([ - [ 4., 5., 6.], - [ 0., 0., 0.] - ]) + array([array([ 4., 5., 6.]), array([ 0., 0., 0.])], dtype=object) """ # Lazy import to improve `import odl` time import scipy.sparse @@ -413,15 +390,9 @@ def adjoint(self): >>> prod_op = ProductSpaceOperator([[0, I], [0, 0]], ... domain=pspace, range=pspace) >>> prod_op(x) - ProductSpace(rn(3), 2).element([ - [ 4., 5., 6.], - [ 0., 0., 0.] - ]) + array([array([ 4., 5., 6.]), array([ 0., 0., 0.])], dtype=object) >>> prod_op.adjoint(x) - ProductSpace(rn(3), 2).element([ - [ 0., 0., 0.], - [ 1., 2., 3.] - ]) + array([array([ 0., 0., 0.]), array([ 1., 2., 3.])], dtype=object) """ # Lazy import to improve `import odl` time import scipy.sparse @@ -566,16 +537,13 @@ def __init__(self, space, index): ... [2, 3], ... [4, 5, 6]] >>> proj(x) - rn(1).element([ 1.]) + array([ 1.]) Projection on sub-space: >>> proj = odl.ComponentProjection(pspace, [0, 2]) >>> proj(x) - ProductSpace(rn(1), rn(3)).element([ - [ 1.], - [ 4., 5., 6.] - ]) + array([array([ 1.]), array([ 4., 5., 6.])], dtype=object) """ self.__index = index super(ComponentProjection, self).__init__( @@ -589,9 +557,9 @@ def index(self): def _call(self, x, out=None): """Project ``x`` onto the subspace.""" if out is None: - out = x[self.index].copy() + out = self.range.copy(x[self.index]) else: - out.assign(x[self.index]) + self.range.assign(out, x[self.index]) return out @property @@ -657,21 +625,13 @@ def __init__(self, space, index): >>> proj_adj = odl.ComponentProjectionAdjoint(pspace, 0) >>> proj_adj(x[0]) - ProductSpace(rn(1), rn(2), rn(3)).element([ - [ 1.], - [ 0., 0.], - [ 0., 0., 0.] - ]) + array([array([ 1.]), array([ 0., 0.]), array([ 0., 0., 0.])], dtype=object) Projection on a sub-space corresponding to indices 0 and 2: >>> proj_adj = odl.ComponentProjectionAdjoint(pspace, [0, 2]) >>> proj_adj(x[[0, 2]]) - ProductSpace(rn(1), rn(2), rn(3)).element([ - [ 1.], - [ 0., 0.], - [ 4., 5., 6.] - ]) + array([array([ 1.]), array([ 0., 0.]), array([ 4., 5., 6.])], dtype=object) """ self.__index = index super(ComponentProjectionAdjoint, self).__init__( @@ -687,7 +647,7 @@ def _call(self, x, out=None): if out is None: out = self.range.zero() else: - out.set_zero() + self.range.set_zero(out) out[self.index] = x return out @@ -746,7 +706,7 @@ def __init__(self, *operators): Initialize an operator: >>> I = odl.IdentityOperator(odl.rn(3)) - >>> op = BroadcastOperator(I, 2 * I) + >>> op = odl.BroadcastOperator(I, 2 * I) >>> op.domain rn(3) >>> op.range @@ -756,15 +716,12 @@ def __init__(self, *operators): >>> x = [1, 2, 3] >>> op(x) - ProductSpace(rn(3), 2).element([ - [ 1., 2., 3.], - [ 2., 4., 6.] - ]) + array([array([ 1., 2., 3.]), array([ 2., 4., 6.])], dtype=object) Can also initialize by calling an operator repeatedly: >>> I = odl.IdentityOperator(odl.rn(3)) - >>> op = BroadcastOperator(I, 2) + >>> op = odl.BroadcastOperator(I, 2) >>> op.operators (IdentityOperator(rn(3)), IdentityOperator(rn(3))) """ @@ -832,18 +789,12 @@ def derivative(self, x): >>> x = [1, 2, 3] >>> op(x) - ProductSpace(rn(3), 2).element([ - [ 0., 1., 2.], - [ 0., 2., 4.] - ]) + array([array([ 0., 1., 2.]), array([ 0., 2., 4.])], dtype=object) The derivative of this affine operator does not have an offset: >>> op.derivative(x)(x) - ProductSpace(rn(3), 2).element([ - [ 1., 2., 3.], - [ 2., 4., 6.] - ]) + array([array([ 1., 2., 3.]), array([ 2., 4., 6.])], dtype=object) """ return BroadcastOperator(*[op.derivative(x) for op in self.operators]) @@ -861,7 +812,7 @@ def adjoint(self): >>> I = odl.IdentityOperator(odl.rn(3)) >>> op = BroadcastOperator(I, 2 * I) >>> op.adjoint([[1, 2, 3], [2, 3, 4]]) - rn(3).element([ 5., 8., 11.]) + array([ 5., 8., 11.]) """ return ReductionOperator(*[op.adjoint for op in self.operators]) @@ -925,7 +876,7 @@ def __init__(self, *operators): >>> op([[1, 2, 3], ... [4, 6, 8]]) - rn(3).element([ 9., 14., 19.]) + array([ 9., 14., 19.]) An ``out`` argument can be given for in-place evaluation: @@ -933,7 +884,7 @@ def __init__(self, *operators): >>> result = op([[1, 2, 3], ... [4, 6, 8]], out=out) >>> out - rn(3).element([ 9., 14., 19.]) + array([ 9., 14., 19.]) >>> result is out True @@ -985,8 +936,8 @@ def _call(self, x, out=None): return self.prod_op(x)[0] else: wrapped_out = self.prod_op.range.element([out], cast=False) - pspace_result = self.prod_op(x, out=wrapped_out) - return pspace_result[0] + self.prod_op(x, out=wrapped_out) + return out def derivative(self, x): """Derivative of the reduction operator. @@ -1011,9 +962,9 @@ def derivative(self, x): >>> op = ReductionOperator(I, 2 * I) >>> op([x, y]) - rn(3).element([ 9., 14., 19.]) + array([ 9., 14., 19.]) >>> op.derivative([x, y])([x, y]) - rn(3).element([ 9., 14., 19.]) + array([ 9., 14., 19.]) Example with affine operator @@ -1023,12 +974,12 @@ def derivative(self, x): Calling operator gives offset by [3, 3, 3] >>> op([x, y]) - rn(3).element([ 6., 11., 16.]) + array([ 6., 11., 16.]) Derivative of affine operator does not have this offset >>> op.derivative([x, y])([x, y]) - rn(3).element([ 9., 14., 19.]) + array([ 9., 14., 19.]) """ return ReductionOperator(*[op.derivative(xi) for op, xi in zip(self.operators, x)]) @@ -1046,10 +997,7 @@ def adjoint(self): >>> I = odl.IdentityOperator(odl.rn(3)) >>> op = ReductionOperator(I, 2 * I) >>> op.adjoint([1, 2, 3]) - ProductSpace(rn(3), 2).element([ - [ 1., 2., 3.], - [ 2., 4., 6.] - ]) + array([array([ 1., 2., 3.]), array([ 2., 4., 6.])], dtype=object) """ return BroadcastOperator(*[op.adjoint for op in self.operators]) @@ -1121,10 +1069,7 @@ def __init__(self, *operators, **kwargs): >>> op([[1, 2, 3], ... [4, 5, 6]]) - ProductSpace(rn(3), 2).element([ - [ 1., 2., 3.], - [ 8., 10., 12.] - ]) + array([array([ 1., 2., 3.]), array([ 8., 10., 12.])], dtype=object) Can also be created using a multiple of a single operator diff --git a/odl/operator/tensor_ops.py b/odl/operator/tensor_ops.py index 1e589ceee18..de4288b6822 100644 --- a/odl/operator/tensor_ops.py +++ b/odl/operator/tensor_ops.py @@ -18,7 +18,6 @@ from odl.set import ComplexNumbers, RealNumbers from odl.space import ProductSpace, tensor_space from odl.space.base_tensors import TensorSpace -from odl.space.weighting import ArrayWeighting from odl.util import dtype_repr, indent, signature_string, writable_array __all__ = ('PointwiseNorm', 'PointwiseInner', 'PointwiseSum', 'MatrixOperator', @@ -147,20 +146,20 @@ def __init__(self, vfspace, exponent=None, weighting=None): >>> x = vfspace.element([[[1, -4]], ... [[0, 3]]]) - >>> print(pw_norm(x)) - [[ 1., 5.]] + >>> pw_norm(x) + array([[ 1., 5.]]) We can change the exponent either in the vector field space or in the operator directly: >>> vfspace = odl.ProductSpace(spc, 2, exponent=1) >>> pw_norm = PointwiseNorm(vfspace) - >>> print(pw_norm(x)) - [[ 1., 7.]] + >>> pw_norm(x) + array([[ 1., 7.]]) >>> vfspace = odl.ProductSpace(spc, 2) >>> pw_norm = PointwiseNorm(vfspace, exponent=1) - >>> print(pw_norm(x)) - [[ 1., 7.]] + >>> pw_norm(x) + array([[ 1., 7.]]) """ if not isinstance(vfspace, ProductSpace): raise TypeError('`vfspace` {!r} is not a ProductSpace ' @@ -184,27 +183,33 @@ def __init__(self, vfspace, exponent=None, weighting=None): # Handle weighting, including sanity checks if weighting is None: - # TODO: find a more robust way of getting the weights as an array - if hasattr(self.domain.weighting, 'array'): - self.__weights = self.domain.weighting.array - elif hasattr(self.domain.weighting, 'const'): - self.__weights = (self.domain.weighting.const * - np.ones(len(self.domain))) + if vfspace.weighting_type == 'array': + self.__weights = vfspace.weighting + elif vfspace.weighting_type == 'const': + self.__weights = vfspace.weighting * np.ones(len(vfspace)) else: - raise ValueError('weighting scheme {!r} of the domain does ' - 'not define a weighting array or constant' - ''.format(self.domain.weighting)) + raise RuntimeError( + 'invalid weighting scheme {} of `vfspace`' + ''.format(vfspace.weighting_type) + ) elif np.isscalar(weighting): if weighting <= 0: - raise ValueError('weighting constant must be positive, got ' - '{}'.format(weighting)) - self.__weights = float(weighting) * np.ones(len(self.domain)) + raise ValueError( + 'weighting constant must be positive, got {}' + ''.format(weighting) + ) + self.__weights = float(weighting) * np.ones(len(vfspace)) else: self.__weights = np.asarray(weighting, dtype='float64') - if (not np.all(self.weights > 0) or - not np.all(np.isfinite(self.weights))): - raise ValueError('weighting array {} contains invalid ' - 'entries'.format(weighting)) + if not ( + np.all(self.__weights > 0) + and np.all(np.isfinite(self.weights)) + ): + raise ValueError( + 'weighting array {} contains invalid entries' + ''.format(weighting) + ) + self.__is_weighted = not np.array_equiv(self.weights, 1.0) @property @@ -233,7 +238,7 @@ def _call(self, f, out): def _call_vecfield_1(self, vf, out): """Implement ``self(vf, out)`` for exponent 1.""" - vf[0].ufuncs.absolute(out=out) + np.absolute(vf[0], out=out) if self.is_weighted: out *= self.weights[0] @@ -242,14 +247,14 @@ def _call_vecfield_1(self, vf, out): tmp = self.range.element() for fi, wi in zip(vf[1:], self.weights[1:]): - fi.ufuncs.absolute(out=tmp) + np.absolute(fi, out=tmp) if self.is_weighted: tmp *= wi out += tmp def _call_vecfield_inf(self, vf, out): """Implement ``self(vf, out)`` for exponent ``inf``.""" - vf[0].ufuncs.absolute(out=out) + np.absolute(vf[0], out=out) if self.is_weighted: out *= self.weights[0] @@ -258,16 +263,16 @@ def _call_vecfield_inf(self, vf, out): tmp = self.range.element() for vfi, wi in zip(vf[1:], self.weights[1:]): - vfi.ufuncs.absolute(out=tmp) + np.absolute(vfi, out=tmp) if self.is_weighted: tmp *= wi - out.ufuncs.maximum(tmp, out=out) + np.maximum(out, tmp, out=out) def _call_vecfield_p(self, vf, out): """Implement ``self(vf, out)`` for exponent 1 < p < ``inf``.""" # Optimization for 1 component - just absolute value (maybe weighted) if len(self.domain) == 1: - vf[0].ufuncs.absolute(out=out) + np.absolute(vf[0], out=out) if self.is_weighted: out *= self.weights[0] ** (1 / self.exponent) return @@ -290,13 +295,16 @@ def _abs_pow_ufunc(self, fi, out, p): """Compute |F_i(x)|^p point-wise and write to ``out``.""" # Optimization for very common cases if p == 0.5: - fi.ufuncs.absolute(out=out) - out.ufuncs.sqrt(out=out) + np.absolute(fi, out=out) + np.sqrt(out, out=out) elif p == 2.0 and self.base_space.field == RealNumbers(): - fi.multiply(fi, out=out) + np.multiply(fi, fi, out=out) + elif p == 2.0 and self.base_space.field == ComplexNumbers(): + np.absolute(fi, out=out) + np.square(out, out=out) else: - fi.ufuncs.absolute(out=out) - out.ufuncs.power(p, out=out) + np.absolute(fi, out=out) + np.power(out, p, out=out) def derivative(self, vf): """Derivative of the point-wise norm operator at ``vf``. @@ -323,36 +331,55 @@ def derivative(self, vf): Raises ------ NotImplementedError - * if the vector field space is complex, since the derivative - is not linear in that case + * if the vector field space is complex * if the exponent is ``inf`` """ + # TODO(kohr-h): Derivative not linear in this case, but could be + # supported anyway, similar to `ComplexModulus` if self.domain.field == ComplexNumbers(): - raise NotImplementedError('operator not Frechet-differentiable ' - 'on a complex space') + raise NotImplementedError( + 'derivative on complex spaces not supported' + ) + # TODO(kohr-h): Not strictly differentiable, but a poor man's + # derivative could still be supported, like with `LpNorm` if self.exponent == float('inf'): - raise NotImplementedError('operator not Frechet-differentiable ' - 'for exponent = inf') + raise NotImplementedError( + 'derivative not implemented for `exponent=inf`' + ) + Fb = self.domain.base_space().ufuncs vf = self.domain.element(vf) + + # Compute `pw_norm(vf)^(p-1)` vf_pwnorm_fac = self(vf) - if self.exponent != 2: # optimize away most common case. - vf_pwnorm_fac **= (self.exponent - 1) + if self.exponent != 2: # Optimize away most common case + vf_pwnorm_fac **= self.exponent - 1 - inner_vf = vf.copy() + # Compute `vf * abs(vf)^(p-2)` + # NB: singularity for zeros unavoidable if `p < 2` + inner_vf = self.domain.copy(vf) - for gi in inner_vf: - gi *= gi.ufuncs.absolute().ufuncs.power(self.exponent - 2) - if self.exponent >= 2: - # Any component that is zero is not divided with - nz = (vf_pwnorm_fac.asarray() != 0) - gi[nz] /= vf_pwnorm_fac[nz] - else: - # For exponents < 2 there will be a singularity if any - # component is zero. This results in inf or nan. See the - # documentation for further details. - gi /= vf_pwnorm_fac + def times_abs_pow_pm2(v): + if self.exponent != 2: + v *= Fb.power(Fb.abs(v), self.exponent - 2) + + self.domain.apply(times_abs_pow_pm2, inner_vf) + + # Divide `vf * abs(vf)^(p-2)` by `pw_norm(vf)^(p-1)`, + # avoiding 0/0 if `p >= 2` + if self.exponent >= 2: + nz = (vf_pwnorm_fac != 0) + + def div_pwnorm_fac(v): + v[nz] /= vf_pwnorm_fac[nz] + + else: + + def div_pwnorm_fac(v): + v /= vf_pwnorm_fac + + self.domain.apply(div_pwnorm_fac, inner_vf) return PointwiseInner(self.domain, inner_vf, weighting=self.weights) @@ -401,32 +428,47 @@ def __init__(self, adjoint, vfspace, vecfield, weighting=None): domain=vfspace, range=vfspace[0], base_space=vfspace[0], linear=True) + self._vecfield = vfspace.element(vecfield) # Bail out if the space is complex but we cannot take the complex # conjugate. - if (vfspace.field == ComplexNumbers() and - not hasattr(self.base_space.element_type, 'conj')): + if ( + vfspace.field == ComplexNumbers() + and not hasattr(self._vecfield, 'conj') + ): raise NotImplementedError( 'base space element type {!r} does not implement conj() ' 'method required for complex inner products' ''.format(self.base_space.element_type)) - self._vecfield = vfspace.element(vecfield) - # Handle weighting, including sanity checks if weighting is None: - if hasattr(vfspace.weighting, 'array'): - self.__weights = vfspace.weighting.array - elif hasattr(vfspace.weighting, 'const'): - self.__weights = (vfspace.weighting.const * - np.ones(len(vfspace))) + if vfspace.weighting_type == 'array': + self.__weights = vfspace.weighting + elif vfspace.weighting_type == 'const': + self.__weights = vfspace.weighting * np.ones(len(vfspace)) else: - raise ValueError('weighting scheme {!r} of the domain does ' - 'not define a weighting array or constant' - ''.format(vfspace.weighting)) + raise RuntimeError( + 'invalid weighting scheme {} of `vfspace`' + ''.format(vfspace.weighting_type) + ) elif np.isscalar(weighting): + if weighting <= 0: + raise ValueError( + 'weighting constant must be positive, got {}' + ''.format(weighting) + ) self.__weights = float(weighting) * np.ones(len(vfspace)) else: self.__weights = np.asarray(weighting, dtype='float64') + if not ( + np.all(self.__weights > 0) + and np.all(np.isfinite(self.weights)) + ): + raise ValueError( + 'weighting array {} contains invalid entries' + ''.format(weighting) + ) + self.__is_weighted = not np.array_equiv(self.weights, 1.0) @property @@ -505,8 +547,8 @@ def __init__(self, vfspace, vecfield, weighting=None): >>> x = vfspace.element([[[1, -4]], ... [[0, 3]]]) - >>> print(pw_inner(x)) - [[ 0., -7.]] + >>> pw_inner(x) + array([[ 0., -7.]]) """ super(PointwiseInner, self).__init__( adjoint=False, vfspace=vfspace, vecfield=vecfield, @@ -520,9 +562,9 @@ def vecfield(self): def _call(self, vf, out): """Implement ``self(vf, out)``.""" if self.domain.field == ComplexNumbers(): - vf[0].multiply(self._vecfield[0].conj(), out=out) + np.multiply(vf[0], self._vecfield[0].conj(), out=out) else: - vf[0].multiply(self._vecfield[0], out=out) + np.multiply(vf[0], self._vecfield[0], out=out) if self.is_weighted: out *= self.weights[0] @@ -531,13 +573,12 @@ def _call(self, vf, out): return tmp = self.range.element() - for vfi, gi, wi in zip(vf[1:], self.vecfield[1:], - self.weights[1:]): + for vfi, gi, wi in zip(vf[1:], self.vecfield[1:], self.weights[1:]): if self.domain.field == ComplexNumbers(): - vfi.multiply(gi.conj(), out=tmp) + np.multiply(vfi, gi.conj(), out=tmp) else: - vfi.multiply(gi, out=tmp) + np.multiply(vfi, gi, out=tmp) if self.is_weighted: tmp *= wi @@ -614,21 +655,18 @@ def __init__(self, sspace, vecfield, vfspace=None, weighting=None): weighting=weighting) # Get weighting from range - if hasattr(self.range.weighting, 'array'): - self.__ran_weights = self.range.weighting.array - elif hasattr(self.range.weighting, 'const'): - self.__ran_weights = (self.range.weighting.const * - np.ones(len(self.range))) - else: - raise ValueError('weighting scheme {!r} of the range does ' - 'not define a weighting array or constant' - ''.format(self.range.weighting)) + if self.range.weighting_type == 'array': + self.__ran_weights = self.range.weighting + elif self.range.weighting_type == 'const': + self.__ran_weights = ( + self.range.weighting * np.ones(len(self.range)) + ) def _call(self, f, out): """Implement ``self(vf, out)``.""" for vfi, oi, ran_wi, dom_wi in zip(self.vecfield, out, self.__ran_weights, self.weights): - vfi.multiply(f, out=oi) + np.multiply(vfi, f, out=oi) if not np.isclose(ran_wi, dom_wi): oi *= dom_wi / ran_wi @@ -692,8 +730,8 @@ def __init__(self, vfspace, weighting=None): >>> x = vfspace.element([[[1, -4]], ... [[0, 3]]]) - >>> print(pw_sum(x)) - [[ 1., -1.]] + >>> pw_sum(x) + array([[ 1., -1.]]) """ if not isinstance(vfspace, ProductSpace): raise TypeError('`vfspace` {!r} is not a ProductSpace ' @@ -750,7 +788,7 @@ def __init__(self, matrix, domain=None, range=None, axis=0): >>> op.range rn(3) >>> op([1, 2, 3, 4]) - rn(3).element([ 10., 10., 10.]) + array([ 10., 10., 10.]) For multi-dimensional arrays (tensors), the summation (contraction) can be performed along a specific axis. In @@ -773,7 +811,7 @@ def __init__(self, matrix, domain=None, range=None, axis=0): >>> space = odl.uniform_discr(0, 1, 4) >>> op = MatrixOperator(m, domain=space) >>> op(space.one()) - rn(3, weighting=0.25).element([ 4., 4., 4.]) + array([ 4., 4., 4.]) >>> np.array_equal(op.adjoint.matrix, m.T) True @@ -831,8 +869,10 @@ def __init__(self, matrix, domain=None, range=None, axis=0): if range is None: # Infer range range_dtype = np.promote_types(self.matrix.dtype, domain.dtype) - if (range_shape != domain.shape and - isinstance(domain.weighting, ArrayWeighting)): + if ( + range_shape != domain.shape + and domain.weighting_type == 'array' + ): # Cannot propagate weighting due to size mismatch. weighting = None else: @@ -1070,10 +1110,10 @@ def __init__(self, domain, sampling_points, variant='point_eval'): >>> op = odl.SamplingOperator(space, sampling_points=1) >>> x = space.element([1, 2, 3, 4]) >>> op(x) - rn(1).element([ 2.]) + array([ 2.]) >>> op = odl.SamplingOperator(space, sampling_points=[1, 2, 1]) >>> op(x) - rn(3).element([ 2., 3., 2.]) + array([ 2., 3., 2.]) There are two variants ``'point_eval'`` (default) and ``'integrate'``, where the latter scales values by the cell @@ -1084,7 +1124,7 @@ def __init__(self, domain, sampling_points, variant='point_eval'): >>> space.cell_volume # the scaling constant 0.25 >>> op(x) - rn(3).element([ 0.5 , 0.75, 0.5 ]) + array([ 0.5 , 0.75, 0.5 ]) In higher dimensions, a sequence of index array-likes must be given, or a single sequence for a single point: @@ -1095,12 +1135,12 @@ def __init__(self, domain, sampling_points, variant='point_eval'): >>> x = space.element([[1, 2, 3], ... [4, 5, 6]]) >>> op(x) - rn(1).element([ 3.]) + array([ 3.]) >>> sampling_points = [[0, 1, 1], # indices (0, 2), (1, 1), (1, 0) ... [2, 1, 0]] >>> op = odl.SamplingOperator(space, sampling_points) >>> op(x) - rn(3).element([ 3., 5., 4.]) + array([ 3., 5., 4.]) """ if not isinstance(domain, TensorSpace): raise TypeError('`domain` must be a `TensorSpace` instance, got ' @@ -1134,7 +1174,7 @@ def sampling_points(self): def _call(self, x): """Return values at indices, possibly weighted.""" - out = x.asarray().ravel()[self._indices_flat] + out = x.ravel()[self._indices_flat] if self.variant == 'point_eval': weights = 1.0 @@ -1165,7 +1205,9 @@ def adjoint(self): >>> op = odl.SamplingOperator(space, sampling_points) >>> x = space.element([[1, 2, 3], ... [4, 5, 6]]) - >>> abs(op.adjoint(op(x)).inner(x) - op(x).inner(op(x))) < 1e-10 + >>> AtAxx = op.domain.inner(op.adjoint(op(x)), x) + >>> AxAx = op.range.inner(op(x), op(x)) + >>> abs(AtAxx - AxAx) < 1e-10 True The ``'integrate'`` variant adjoint puts ones at the indices in @@ -1174,11 +1216,11 @@ def adjoint(self): >>> op = odl.SamplingOperator(space, sampling_points, ... variant='integrate') >>> op.adjoint(op.range.one()) # (0, 0) occurs twice - uniform_discr([-1., -1.], [ 1., 1.], (2, 3)).element( - [[ 2., 0., 0.], - [ 0., 1., 1.]] - ) - >>> abs(op.adjoint(op(x)).inner(x) - op(x).inner(op(x))) < 1e-10 + array([[ 2., 0., 0.], + [ 0., 1., 1.]]) + >>> AtAxx = op.domain.inner(op.adjoint(op(x)), x) + >>> AxAx = op.range.inner(op(x), op(x)) + >>> abs(AtAxx - AxAx) < 1e-10 True """ if self.variant == 'point_eval': @@ -1255,15 +1297,16 @@ def __init__(self, range, sampling_points, variant='char_fun'): >>> x = op.domain.element([1]) >>> # Put value 1 at index 1 >>> op(x) - uniform_discr(0.0, 1.0, 4).element([ 0., 1., 0., 0.]) - >>> op = odl.WeightedSumSamplingOperator(space, - ... sampling_points=[1, 2, 1]) + array([ 0., 1., 0., 0.]) + >>> op = odl.WeightedSumSamplingOperator( + ... space, sampling_points=[1, 2, 1] + ... ) >>> op.domain rn(3) >>> x = op.domain.element([1, 0.5, 0.25]) >>> # Index 1 occurs twice and gets two contributions (1 and 0.25) >>> op(x) - uniform_discr(0.0, 1.0, 4).element([ 0. , 1.25, 0.5 , 0. ]) + array([ 0. , 1.25, 0.5 , 0. ]) The ``'dirac'`` variant scales the values by the reciprocal cell volume of the operator range: @@ -1274,7 +1317,7 @@ def __init__(self, range, sampling_points, variant='char_fun'): >>> 1 / op.range.cell_volume # the scaling constant 4.0 >>> op(x) - uniform_discr(0.0, 1.0, 4).element([ 0., 5., 2., 0.]) + array([ 0., 5., 2., 0.]) In higher dimensions, a sequence of index array-likes must be given, or a single sequence for a single point: @@ -1286,19 +1329,15 @@ def __init__(self, range, sampling_points, variant='char_fun'): >>> x = op.domain.element([1]) >>> # Insert the value 1 at index (0, 2) >>> op(x) - uniform_discr([ 0., 0.], [ 1., 1.], (2, 3)).element( - [[ 0., 0., 1.], - [ 0., 0., 0.]] - ) + array([[ 0., 0., 1.], + [ 0., 0., 0.]]) >>> sampling_points = [[0, 1], # indices (0, 2) and (1, 1) ... [2, 1]] >>> op = odl.WeightedSumSamplingOperator(space, sampling_points) >>> x = op.domain.element([1, 2]) >>> op(x) - uniform_discr([ 0., 0.], [ 1., 1.], (2, 3)).element( - [[ 0., 0., 1.], - [ 0., 2., 0.]] - ) + array([[ 0., 0., 1.], + [ 0., 2., 0.]]) """ if not isinstance(range, TensorSpace): raise TypeError('`range` must be a `TensorSpace` instance, got ' @@ -1370,13 +1409,17 @@ def adjoint(self): >>> y = op.range.element([[1, 2, 3], ... [4, 5, 6]]) >>> op.adjoint(y) - rn(4).element([ 1., 5., 6., 1.]) + array([ 1., 5., 6., 1.]) >>> x = op.domain.element([1, 2, 3, 4]) - >>> abs(op.adjoint(op(x)).inner(x) - op(x).inner(op(x))) < 1e-10 + >>> AtAxx = op.domain.inner(op.adjoint(op(x)), x) + >>> AxAx = op.range.inner(op(x), op(x)) + >>> abs(AtAxx - AxAx) < 1e-10 True >>> op = odl.WeightedSumSamplingOperator(space, sampling_points, ... variant='char_fun') - >>> abs(op.adjoint(op(x)).inner(x) - op(x).inner(op(x))) < 1e-10 + >>> AtAxx = op.domain.inner(op.adjoint(op(x)), x) + >>> AxAx = op.range.inner(op(x), op(x)) + >>> abs(AtAxx - AxAx) < 1e-10 True """ if self.variant == 'dirac': @@ -1435,10 +1478,10 @@ def __init__(self, domain, order='C'): >>> x = space.element([[1, 2, 3], ... [4, 5, 6]]) >>> op(x) - rn(6).element([ 1., 2., 3., 4., 5., 6.]) + array([ 1., 2., 3., 4., 5., 6.]) >>> op = odl.FlatteningOperator(space, order='F') >>> op(x) - rn(6).element([ 1., 4., 2., 5., 3., 6.]) + array([ 1., 4., 2., 5., 3., 6.]) """ if not isinstance(domain, TensorSpace): raise TypeError('`domain` must be a `TensorSpace` instance, got ' @@ -1472,13 +1515,13 @@ def adjoint(self): >>> 1 / space.cell_volume # the scaling factor 2.0 >>> op.adjoint(y) - uniform_discr([-1., -1.], [ 1., 1.], (2, 4)).element( - [[ 2., 4., 6., 8.], - [ 10., 12., 14., 16.]] - ) + array([[ 2., 4., 6., 8.], + [ 10., 12., 14., 16.]]) >>> x = space.element([[1, 2, 3, 4], ... [5, 6, 7, 8]]) - >>> abs(op.adjoint(op(x)).inner(x) - op(x).inner(op(x))) < 1e-10 + >>> AtAxx = op.domain.inner(op.adjoint(op(x)), x) + >>> AxAx = op.range.inner(op(x), op(x)) + >>> abs(AtAxx - AxAx) < 1e-10 True """ scaling = getattr(self.domain, 'cell_volume', 1.0) @@ -1494,17 +1537,13 @@ def inverse(self): >>> op = odl.FlatteningOperator(space) >>> y = op.range.element([1, 2, 3, 4, 5, 6, 7, 8]) >>> op.inverse(y) - uniform_discr([-1., -1.], [ 1., 1.], (2, 4)).element( - [[ 1., 2., 3., 4.], - [ 5., 6., 7., 8.]] - ) + array([[ 1., 2., 3., 4.], + [ 5., 6., 7., 8.]]) >>> op = odl.FlatteningOperator(space, order='F') >>> op.inverse(y) - uniform_discr([-1., -1.], [ 1., 1.], (2, 4)).element( - [[ 1., 3., 5., 7.], - [ 2., 4., 6., 8.]] - ) - >>> op(op.inverse(y)) == y + array([[ 1., 3., 5., 7.], + [ 2., 4., 6., 8.]]) + >>> all(op(op.inverse(y)) == y) True """ op = self @@ -1526,8 +1565,7 @@ def __init__(self): def _call(self, x): """Reshape ``x`` back to n-dim. shape.""" - return np.reshape(x.asarray(), self.range.shape, - order=op.order) + return np.reshape(x, self.range.shape, order=op.order) @property def adjoint(self): diff --git a/odl/phantom/emission.py b/odl/phantom/emission.py index 4052498e4cf..2827dfe0900 100644 --- a/odl/phantom/emission.py +++ b/odl/phantom/emission.py @@ -155,12 +155,12 @@ def derenzo_sources(space, min_pt=None, max_pt=None): n = 300 # 2D - discr = odl.uniform_discr([-1, -1], [1, 1], [n, n]) - derenzo_sources(discr).show('derenzo_sources 2d') + space = odl.uniform_discr([-1, -1], [1, 1], [n, n]) + space.show(derenzo_sources(space), 'derenzo_sources 2d') # 3D - discr = odl.uniform_discr([-1, -1, -1], [1, 1, 1], [300, 300, 300]) - derenzo_sources(discr).show('derenzo_sources 3d') + space = odl.uniform_discr([-1, -1, -1], [1, 1, 1], [300, 300, 300]) + space.show(derenzo_sources(space), 'derenzo_sources 3d') # Run also the doctests run_doctests() diff --git a/odl/phantom/geometric.py b/odl/phantom/geometric.py index 9f8e421c80b..cf27bdf96bc 100644 --- a/odl/phantom/geometric.py +++ b/odl/phantom/geometric.py @@ -51,23 +51,19 @@ def cuboid(space, min_pt=None, max_pt=None): >>> space = odl.uniform_discr([0, 0], [1, 1], [4, 6]) >>> odl.phantom.cuboid(space) - uniform_discr([ 0., 0.], [ 1., 1.], (4, 6)).element( - [[ 0., 0., 0., 0., 0., 0.], - [ 0., 1., 1., 1., 1., 0.], - [ 0., 1., 1., 1., 1., 0.], - [ 0., 0., 0., 0., 0., 0.]] - ) + array([[ 0., 0., 0., 0., 0., 0.], + [ 0., 1., 1., 1., 1., 0.], + [ 0., 1., 1., 1., 1., 0.], + [ 0., 0., 0., 0., 0., 0.]]) By specifying the corners, the cuboid can be arbitrarily placed and scaled: >>> odl.phantom.cuboid(space, [0.25, 0], [0.75, 0.5]) - uniform_discr([ 0., 0.], [ 1., 1.], (4, 6)).element( - [[ 0., 0., 0., 0., 0., 0.], - [ 1., 1., 1., 0., 0., 0.], - [ 1., 1., 1., 0., 0., 0.], - [ 0., 0., 0., 0., 0., 0.]] - ) + array([[ 0., 0., 0., 0., 0., 0.], + [ 1., 1., 1., 0., 0., 0.], + [ 1., 1., 1., 0., 0., 0.], + [ 0., 0., 0., 0., 0., 0.]]) """ dom_min_pt = np.asarray(space.domain.min()) dom_max_pt = np.asarray(space.domain.max()) @@ -223,49 +219,45 @@ def indicate_proj_axis(space, scale_structures=0.5): Phantom in 2D space: >>> space = odl.uniform_discr([0, 0], [1, 1], shape=(8, 8)) - >>> phantom = indicate_proj_axis(space).asarray() - >>> print(odl.util.array_str(phantom, nprint=10)) - [[ 0., 0., 0., 0., 0., 0., 0., 0.], - [ 0., 0., 0., 1., 1., 0., 0., 0.], - [ 0., 0., 0., 1., 1., 0., 0., 0.], - [ 0., 0., 0., 0., 0., 0., 0., 0.], - [ 0., 0., 0., 0., 0., 0., 0., 0.], - [ 0., 0., 0., 0., 1., 0., 0., 0.], - [ 0., 0., 0., 1., 0., 0., 0., 0.], - [ 0., 0., 0., 0., 0., 0., 0., 0.]] + >>> indicate_proj_axis(space) + array([[ 0., 0., 0., 0., 0., 0., 0., 0.], + [ 0., 0., 0., 1., 1., 0., 0., 0.], + [ 0., 0., 0., 1., 1., 0., 0., 0.], + [ 0., 0., 0., 0., 0., 0., 0., 0.], + [ 0., 0., 0., 0., 0., 0., 0., 0.], + [ 0., 0., 0., 0., 1., 0., 0., 0.], + [ 0., 0., 0., 1., 0., 0., 0., 0.], + [ 0., 0., 0., 0., 0., 0., 0., 0.]]) >>> space = odl.uniform_discr([0] * 3, [1] * 3, [8, 8, 8]) - >>> phantom = odl.phantom.indicate_proj_axis(space).asarray() - >>> axis_sum_0 = np.sum(phantom, axis=0) - >>> print(odl.util.array_str(axis_sum_0, nprint=10)) - [[ 0., 0., 0., 0., 0., 0., 0., 0.], - [ 0., 0., 0., 0., 0., 0., 0., 0.], - [ 0., 0., 0., 0., 0., 0., 0., 0.], - [ 0., 0., 0., 3., 3., 0., 0., 0.], - [ 0., 0., 0., 3., 3., 0., 0., 0.], - [ 0., 0., 0., 0., 0., 0., 0., 0.], - [ 0., 0., 0., 0., 0., 0., 0., 0.], - [ 0., 0., 0., 0., 0., 0., 0., 0.]] - >>> axis_sum_1 = np.sum(phantom, axis=1) - >>> print(odl.util.array_str(axis_sum_1, nprint=10)) - [[ 0., 0., 0., 0., 0., 0., 0., 0.], - [ 0., 0., 0., 2., 2., 0., 0., 0.], - [ 0., 0., 0., 2., 2., 0., 0., 0.], - [ 0., 0., 0., 0., 0., 0., 0., 0.], - [ 0., 0., 0., 0., 0., 0., 0., 0.], - [ 0., 0., 0., 1., 1., 0., 0., 0.], - [ 0., 0., 0., 1., 1., 0., 0., 0.], - [ 0., 0., 0., 0., 0., 0., 0., 0.]] - >>> axis_sum_2 = np.sum(phantom, axis=2) - >>> print(odl.util.array_str(axis_sum_2, nprint=10)) - [[ 0., 0., 0., 0., 0., 0., 0., 0.], - [ 0., 0., 0., 2., 2., 0., 0., 0.], - [ 0., 0., 0., 2., 2., 0., 0., 0.], - [ 0., 0., 0., 0., 0., 0., 0., 0.], - [ 0., 0., 0., 0., 0., 0., 0., 0.], - [ 0., 0., 0., 0., 2., 0., 0., 0.], - [ 0., 0., 0., 2., 0., 0., 0., 0.], - [ 0., 0., 0., 0., 0., 0., 0., 0.]] + >>> phantom = odl.phantom.indicate_proj_axis(space) + >>> np.sum(phantom, axis=0) + array([[ 0., 0., 0., 0., 0., 0., 0., 0.], + [ 0., 0., 0., 0., 0., 0., 0., 0.], + [ 0., 0., 0., 0., 0., 0., 0., 0.], + [ 0., 0., 0., 3., 3., 0., 0., 0.], + [ 0., 0., 0., 3., 3., 0., 0., 0.], + [ 0., 0., 0., 0., 0., 0., 0., 0.], + [ 0., 0., 0., 0., 0., 0., 0., 0.], + [ 0., 0., 0., 0., 0., 0., 0., 0.]]) + >>> np.sum(phantom, axis=1) + array([[ 0., 0., 0., 0., 0., 0., 0., 0.], + [ 0., 0., 0., 2., 2., 0., 0., 0.], + [ 0., 0., 0., 2., 2., 0., 0., 0.], + [ 0., 0., 0., 0., 0., 0., 0., 0.], + [ 0., 0., 0., 0., 0., 0., 0., 0.], + [ 0., 0., 0., 1., 1., 0., 0., 0.], + [ 0., 0., 0., 1., 1., 0., 0., 0.], + [ 0., 0., 0., 0., 0., 0., 0., 0.]]) + >>> np.sum(phantom, axis=2) + array([[ 0., 0., 0., 0., 0., 0., 0., 0.], + [ 0., 0., 0., 2., 2., 0., 0., 0.], + [ 0., 0., 0., 2., 2., 0., 0., 0.], + [ 0., 0., 0., 0., 0., 0., 0., 0.], + [ 0., 0., 0., 0., 0., 0., 0., 0.], + [ 0., 0., 0., 0., 2., 0., 0., 0.], + [ 0., 0., 0., 2., 0., 0., 0., 0.], + [ 0., 0., 0., 0., 0., 0., 0., 0.]]) """ if not 0 < scale_structures <= 1: raise ValueError('`scale_structures` ({}) is not in (0, 1]' @@ -649,12 +641,12 @@ def ellipsoid_phantom(space, ellipsoids, min_pt=None, max_pt=None): >>> space = odl.uniform_discr([-1, -1], [1, 1], [5, 5]) >>> ellipses = [[1.0, 1.0, 1.0, 0.0, 0.0, 0.0], ... [1.0, 0.6, 0.6, 0.0, 0.0, 0.0]] - >>> print(ellipsoid_phantom(space, ellipses)) - [[ 0., 0., 1., 0., 0.], - [ 0., 1., 2., 1., 0.], - [ 1., 2., 2., 2., 1.], - [ 0., 1., 2., 1., 0.], - [ 0., 0., 1., 0., 0.]] + >>> ellipsoid_phantom(space, ellipses) + array([[ 0., 0., 1., 0., 0.], + [ 0., 1., 2., 1., 0.], + [ 1., 2., 2., 2., 1.], + [ 0., 1., 2., 1., 0.], + [ 0., 0., 1., 0., 0.]]) See Also -------- @@ -864,45 +856,45 @@ def sigmoid(val): # cuboid 1D space = odl.uniform_discr(-1, 1, 300) - cuboid(space).show('cuboid 1d') + space.show(cuboid(space), 'cuboid 1d') # cuboid 2D space = odl.uniform_discr([-1, -1], [1, 1], [300, 300]) - cuboid(space).show('cuboid 2d') + space.show(cuboid(space), 'cuboid 2d') # smooth cuboid - smooth_cuboid(space).show('smooth_cuboid x 2d') - smooth_cuboid(space, axis=[0, 1]).show('smooth_cuboid x-y 2d') + space.show(smooth_cuboid(space), 'smooth_cuboid x 2d') + space.show(smooth_cuboid(space, axis=[0, 1]), 'smooth_cuboid x-y 2d') # TGV phantom - tgv_phantom(space).show('tgv_phantom') + space.show(tgv_phantom(space), 'tgv_phantom') # cuboid 3D space = odl.uniform_discr([-1, -1, -1], [1, 1, 1], [300, 300, 300]) - cuboid(space).show('cuboid 3d') + space.show(cuboid(space), 'cuboid 3d') # Indicate proj axis 3D - indicate_proj_axis(space).show('indicate_proj_axis 3d') + space.show(indicate_proj_axis(space), 'indicate_proj_axis 3d') # ellipsoid phantom 2D space = odl.uniform_discr([-1, -1], [1, 1], [300, 300]) ellipses = [[1.0, 1.0, 1.0, 0.0, 0.0, 0.0], [1.0, 0.6, 0.6, 0.0, 0.0, 0.0]] - ellipsoid_phantom(space, ellipses).show('ellipse phantom 2d') + space.show(ellipsoid_phantom(space, ellipses), 'ellipse phantom 2d') # ellipsoid phantom 3D space = odl.uniform_discr([-1, -1, -1], [1, 1, 1], [300, 300, 300]) ellipsoids = [[1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [1.0, 0.6, 0.6, 0.6, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]] - ellipsoid_phantom(space, ellipsoids).show('ellipsoid phantom 3d') + space.show(ellipsoid_phantom(space, ellipsoids), 'ellipsoid phantom 3d') # Defrise phantom 2D space = odl.uniform_discr([-1, -1], [1, 1], [300, 300]) - defrise(space).show('defrise 2D') + space.show(defrise(space), 'defrise 2D') # Defrise phantom 2D space = odl.uniform_discr([-1, -1, -1], [1, 1, 1], [300, 300, 300]) - defrise(space).show('defrise 3D', coords=[0, None, None]) + space.show(defrise(space), 'defrise 3D', coords=[0, None, None]) # Run also the doctests from odl.util.testutils import run_doctests diff --git a/odl/phantom/misc_phantoms.py b/odl/phantom/misc_phantoms.py index bd35aad0526..ab33b8ae293 100644 --- a/odl/phantom/misc_phantoms.py +++ b/odl/phantom/misc_phantoms.py @@ -97,7 +97,7 @@ def blurred_rect(x): out = space.element(blurred_ellipse) out += space.element(blurred_rect) - return out.ufuncs.minimum(1, out=out) + return np.minimum(out, 1, out=out) def _submarine_2d_nonsmooth(space): @@ -141,7 +141,7 @@ def rect(x): out = space.element(ellipse) out += space.element(rect) - return out.ufuncs.minimum(1, out=out) + return np.minimum(out, 1, out=out) def text(space, text, font=None, border=0.2, inverted=True): @@ -258,11 +258,11 @@ def text(space, text, font=None, border=0.2, inverted=True): from odl.util.testutils import run_doctests space = odl.uniform_discr([-1, -1], [1, 1], [300, 300]) - submarine(space, smooth=False).show('submarine smooth=False') - submarine(space, smooth=True).show('submarine smooth=True') - submarine(space, smooth=True, taper=50).show('submarine taper=50') + space.show(submarine(space, smooth=False), 'submarine smooth=False') + space.show(submarine(space, smooth=True), 'submarine smooth=True') + space.show(submarine(space, smooth=True, taper=50), 'submarine taper=50') - text(space, text='phantom').show('phantom') + space.show(text(space, text='phantom'), 'phantom') # Run also the doctests run_doctests() diff --git a/odl/phantom/noise.py b/odl/phantom/noise.py index 3866f207235..037e781777c 100644 --- a/odl/phantom/noise.py +++ b/odl/phantom/noise.py @@ -119,21 +119,23 @@ def uniform_noise(space, low=0, high=1, seed=None): return space.element(values) -def poisson_noise(intensity, seed=None): +def poisson_noise(space, intensity, seed=None): r"""Poisson distributed noise with given intensity. Parameters ---------- + space : `TensorSpace` or `ProductSpace` + The space in which the noise is created. intensity : `TensorSpace` or `ProductSpace` element The intensity (usually called lambda) parameter of the noise. + seed : int, optional + Random seed to use for generating the noise. + For ``None``, use the current seed. Returns ------- poisson_noise : ``intensity.space`` element Poisson distributed random variable. - seed : int, optional - Random seed to use for generating the noise. - For ``None``, use the current seed. Notes ----- @@ -144,7 +146,7 @@ def poisson_noise(intensity, seed=None): .. math:: \frac{\lambda^k e^{-\lambda}}{k!} - Note that the function only takes integer values. + Note that the function only takes on integer values. See Also -------- @@ -156,16 +158,18 @@ def poisson_noise(intensity, seed=None): from odl.space import ProductSpace with npy_random_seed(seed): - if isinstance(intensity.space, ProductSpace): - values = [poisson_noise(subintensity) - for subintensity in intensity] + if isinstance(space, ProductSpace): + values = [ + poisson_noise(spc, xi) + for spc, xi in zip(space.spaces, intensity) + ] else: - values = np.random.poisson(intensity.asarray()) + values = np.random.poisson(intensity) - return intensity.space.element(values) + return space.element(values) -def salt_pepper_noise(vector, fraction=0.05, salt_vs_pepper=0.5, +def salt_pepper_noise(space, vector, fraction=0.05, salt_vs_pepper=0.5, low_val=None, high_val=None, seed=None): """Add salt and pepper noise to vector. @@ -174,6 +178,8 @@ def salt_pepper_noise(vector, fraction=0.05, salt_vs_pepper=0.5, Parameters ---------- + space : `TensorSpace` or `ProductSpace` + The space in which the noise is created. vector : element of `TensorSpace` or `ProductSpace` The vector that noise should be added to. fraction : float, optional @@ -196,7 +202,7 @@ def salt_pepper_noise(vector, fraction=0.05, salt_vs_pepper=0.5, Returns ------- - salt_pepper_noise : ``vector.space`` element + salt_pepper_noise : ``space`` element ``vector`` with salt and pepper noise. See Also @@ -219,13 +225,15 @@ def salt_pepper_noise(vector, fraction=0.05, salt_vs_pepper=0.5, 'interval [0, 1]'.format(salt_vs_pepper_in)) with npy_random_seed(seed): - if isinstance(vector.space, ProductSpace): - values = [salt_pepper_noise(subintensity, fraction, salt_vs_pepper, - low_val, high_val) - for subintensity in vector] + if isinstance(space, ProductSpace): + values = [ + salt_pepper_noise(vi, fraction, salt_vs_pepper, low_val, + high_val) + for vi in vector + ] else: - # Extract vector of values - values = vector.asarray().flatten() + # Make flat copy + values = vector.flatten() # Determine fill-in values if not given if low_val is None: @@ -233,18 +241,18 @@ def salt_pepper_noise(vector, fraction=0.05, salt_vs_pepper=0.5, if high_val is None: high_val = np.max(values) - # Create randomly selected points as a subset of image. - a = np.arange(vector.size) + # Create randomly selected points as a subset of image + a = np.arange(values.size) np.random.shuffle(a) - salt_indices = a[:int(fraction * vector.size * salt_vs_pepper)] - pepper_indices = a[int(fraction * vector.size * salt_vs_pepper): - int(fraction * vector.size)] + salt_indices = a[:int(fraction * values.size * salt_vs_pepper)] + pepper_indices = a[int(fraction * values.size * salt_vs_pepper): + int(fraction * values.size)] values[salt_indices] = high_val values[pepper_indices] = -low_val - values = values.reshape(vector.space.shape) + values = values.reshape(space.shape) - return vector.space.element(values) + return space.element(values) if __name__ == '__main__': @@ -252,22 +260,22 @@ def salt_pepper_noise(vector, fraction=0.05, salt_vs_pepper=0.5, import odl from odl.util.testutils import run_doctests - r100 = odl.rn(100) - white_noise(r100).show('white_noise') - uniform_noise(r100).show('uniform_noise') - white_noise(r100, mean=5).show('white_noise with mean') + space = odl.rn(100) + space.show(white_noise(space), 'white_noise') + space.show(uniform_noise(space), 'uniform_noise') + space.show(white_noise(space, mean=5), 'white_noise with mean') - c100 = odl.cn(100) - white_noise(c100).show('complex white_noise') - uniform_noise(c100).show('complex uniform_noise') + space = odl.cn(100) + space.show(white_noise(space), 'complex white_noise') + space.show(uniform_noise(space), 'complex uniform_noise') - discr = odl.uniform_discr([-1, -1], [1, 1], [300, 300]) - white_noise(discr).show('white_noise 2d') - uniform_noise(discr).show('uniform_noise 2d') + space = odl.uniform_discr([-1, -1], [1, 1], [300, 300]) + space.show(white_noise(space), 'white_noise 2d') + space.show(uniform_noise(space), 'uniform_noise 2d') - vector = odl.phantom.shepp_logan(discr, modified=True) - poisson_noise(vector * 100).show('poisson_noise 2d') - salt_pepper_noise(vector).show('salt_pepper_noise 2d') + phantom = odl.phantom.shepp_logan(space, modified=True) + space.show(poisson_noise(space, phantom * 100), 'poisson_noise 2d') + space.show(salt_pepper_noise(space, phantom), 'salt_pepper_noise 2d') # Run also the doctests run_doctests() diff --git a/odl/phantom/transmission.py b/odl/phantom/transmission.py index 01ea73dd33c..759325f6782 100644 --- a/odl/phantom/transmission.py +++ b/odl/phantom/transmission.py @@ -408,16 +408,24 @@ def transposeravel(arr): from odl.util.testutils import run_doctests # 2D - discr = odl.uniform_discr([-1, -1], [1, 1], [1000, 1000]) - shepp_logan(discr, modified=True).show('shepp_logan 2d modified=True') - shepp_logan(discr, modified=False).show('shepp_logan 2d modified=False') - forbild(discr).show('FORBILD 2d', clim=[1.035, 1.065]) - forbild(discr, value_type='materials').show('FORBILD 2d materials') + space = odl.uniform_discr([-1, -1], [1, 1], [1000, 1000]) + space.show( + shepp_logan(space, modified=True), 'shepp_logan 2d modified=True' + ) + space.show( + shepp_logan(space, modified=False), 'shepp_logan 2d modified=False' + ) + space.show(forbild(space), 'FORBILD 2d', clim=[1.035, 1.065]) + space.show(forbild(space, value_type='materials'), 'FORBILD 2d materials') # 3D - discr = odl.uniform_discr([-1, -1, -1], [1, 1, 1], [300, 300, 300]) - shepp_logan(discr, modified=True).show('shepp_logan 3d modified=True') - shepp_logan(discr, modified=False).show('shepp_logan 3d modified=False') + space = odl.uniform_discr([-1, -1, -1], [1, 1, 1], [300, 300, 300]) + space.show( + shepp_logan(space, modified=True), 'shepp_logan 3d modified=True' + ) + space.show( + shepp_logan(space, modified=False), 'shepp_logan 3d modified=False' + ) # Run also the doctests run_doctests() diff --git a/odl/set/sets.py b/odl/set/sets.py index 4e9012fc846..a432a9507d4 100644 --- a/odl/set/sets.py +++ b/odl/set/sets.py @@ -1,4 +1,4 @@ -# Copyright 2014-2019 The ODL contributors +# Copyright 2014-2020 The ODL contributors # # This file is part of ODL. # @@ -18,9 +18,19 @@ from odl.util import is_int_dtype, is_numeric_dtype, is_real_dtype, unique -__all__ = ('Set', 'EmptySet', 'UniversalSet', 'Field', 'Integers', - 'RealNumbers', 'ComplexNumbers', 'Strings', 'CartesianProduct', - 'SetUnion', 'SetIntersection', 'FiniteSet') +__all__ = ( + 'Set', + 'EmptySet', + 'Strings', + 'Field', + 'ComplexNumbers', + 'RealNumbers', + 'Integers', + 'CartesianProduct', + 'SetUnion', + 'SetIntersection', + 'FiniteSet', +) class Set(object): @@ -186,34 +196,6 @@ def element(self, inp=None): return None -class UniversalSet(Set): - - """Set of all objects. - - Forget about set theory for a moment :-). - """ - - def __contains__(self, other): - """Return ``other in self``, always ``True``.""" - return True - - def contains_set(self, other): - """Return ``True`` for any set.""" - return isinstance(other, Set) - - def __eq__(self, other): - """Return ``self == other``.""" - return isinstance(other, UniversalSet) - - def __hash__(self): - """Return ``hash(self)``.""" - return hash(type(self)) - - def element(self, inp=None): - """Return ``inp`` in any case.""" - return inp - - class Strings(Set): """Set of fixed-length (unicode) strings.""" @@ -306,16 +288,37 @@ def field(self): Notes ----- - This is a hack to make fields to work via duck-typing with - `LinearSpace`'s. + This is a hack to make fields work similarly to `LinearSpace`'s. """ return self + def astype(self, dtype): + """Return field corresponding to given dtype. + + Notes + ----- + This is a hack to make fields work similarly to `LinearSpace`'s. + """ + dtype = np.dtype(dtype) + if dtype == int: + return Integers() + elif dtype == float: + return RealNumbers() + elif dtype == complex: + return ComplexNumbers() + else: + raise ValueError( + '`dtype` {} not supported'.format(dtype_repr(dtype)) + ) + + class ComplexNumbers(Field): """Set of complex numbers.""" + dtype = np.dtype(complex) + def __contains__(self, other): """Return ``other in self``.""" return isinstance(other, Complex) @@ -378,6 +381,8 @@ class RealNumbers(Field): """Set of real numbers.""" + dtype = np.dtype(float) + def __contains__(self, other): """Return ``other in self``.""" return isinstance(other, Real) @@ -439,6 +444,8 @@ class Integers(Set): """Set of integers.""" + dtype = np.dtype(int) + def __contains__(self, other): """Return ``other in self``.""" return isinstance(other, Integral) diff --git a/odl/set/space.py b/odl/set/space.py index a42a557416b..6524a05b91e 100644 --- a/odl/set/space.py +++ b/odl/set/space.py @@ -1,4 +1,4 @@ -# Copyright 2014-2018 The ODL contributors +# Copyright 2014-2019 The ODL contributors # # This file is part of ODL. # @@ -9,21 +9,17 @@ """Abstract linear vector spaces.""" from __future__ import print_function, division, absolute_import -from builtins import object import numpy as np -from odl.set.sets import Field, Set, UniversalSet +from odl.set.sets import Field, Set -__all__ = ('LinearSpace', 'UniversalSpace') +__all__ = ('LinearSpace',) class LinearSpace(Set): - """Abstract linear vector space. - Its elements are represented as instances of the - `LinearSpaceElement` class. - """ + """Abstract linear vector space.""" def __init__(self, field): """Initialize a new instance. @@ -111,7 +107,7 @@ def _inner(self, x1, x2): This method is intended to be private. Public callers should resort to `inner` which is type-checked. """ - raise LinearSpaceNotImplementedError( + raise NotImplementedError( 'inner product not implemented in space {!r}'.format(self)) def _multiply(self, x1, x2, out): @@ -120,12 +116,12 @@ def _multiply(self, x1, x2, out): This method is intended to be private. Public callers should resort to `multiply` which is type-checked. """ - raise LinearSpaceNotImplementedError( + raise NotImplementedError( 'multiplication not implemented in space {!r}'.format(self)) def one(self): """Return the one (multiplicative unit) element of this space.""" - raise LinearSpaceNotImplementedError( + raise NotImplementedError( '`one` element not implemented in space {!r}'.format(self)) # Default methods @@ -183,6 +179,11 @@ def lincomb(self, a, x1, b=None, x2=None, out=None): Result of the linear combination. If ``out`` was provided, the returned object is a reference to it. + Raises + ------ + TypeError + If ``out`` is given but not an element of this space. + Notes ----- The elements ``out``, ``x1`` and ``x2`` may be aligned, thus a call @@ -196,35 +197,52 @@ def lincomb(self, a, x1, b=None, x2=None, out=None): if out is None: out = self.element() elif out not in self: - raise LinearSpaceTypeError('`out` {!r} is not an element of {!r}' - ''.format(out, self)) - if self.field is not None and a not in self.field: - raise LinearSpaceTypeError('`a` {!r} not an element of the field ' - '{!r} of {!r}' - ''.format(a, self.field, self)) - if x1 not in self: - raise LinearSpaceTypeError('`x1` {!r} is not an element of {!r}' - ''.format(x1, self)) - - if b is None: # Single element + raise TypeError( + '`out` {!r} is not an element of {!r}'.format(out, self) + ) + + if self.field is not None: + a = self.field.element(a) + + x1 = self.element(x1) + + if b is None: if x2 is not None: raise ValueError('`x2` provided but not `b`') self._lincomb(a, x1, 0, x1, out) return out - else: # Two elements - if self.field is not None and b not in self.field: - raise LinearSpaceTypeError('`b` {!r} not an element of the ' - 'field {!r} of {!r}' - ''.format(b, self.field, self)) - if x2 not in self: - raise LinearSpaceTypeError('`x2` {!r} is not an element of ' - '{!r}'.format(x2, self)) + if self.field is not None: + b = self.field.element(b) + + x2 = self.element(x2) + self._lincomb(a, x1, b, x2, out) + return out + + def assign(self, out, x): + """Assign ``x`` to ``out``.""" + self.lincomb(1, x, out=out) - self._lincomb(a, x1, b, x2, out) + def copy(self, x): + """Return a copy of ``x``. + This default implementation is intended to work with any space that + supports `lincomb`. Subclasses may choose to implement an optimized + variant. + """ + out = self.element() + self.assign(out, x) return out + def set_zero(self, out): + """Set ``out`` to zero. + + This default implementation should be overridden for spaces where + elements can have nonsensical entries, where multiplication with + 0 does not yield the desired result. + """ + self.lincomb(0, out, out=out) + def dist(self, x1, x2): """Return the distance between ``x1`` and ``x2``. @@ -238,12 +256,8 @@ def dist(self, x1, x2): dist : float Distance between ``x1`` and ``x2``. """ - if x1 not in self: - raise LinearSpaceTypeError('`x1` {!r} is not an element of ' - '{!r}'.format(x1, self)) - if x2 not in self: - raise LinearSpaceTypeError('`x2` {!r} is not an element of ' - '{!r}'.format(x2, self)) + x1 = self.element(x1) + x2 = self.element(x2) return float(self._dist(x1, x2)) def norm(self, x): @@ -259,9 +273,7 @@ def norm(self, x): norm : float Norm of ``x``. """ - if x not in self: - raise LinearSpaceTypeError('`x` {!r} is not an element of ' - '{!r}'.format(x, self)) + x = self.element(x) return float(self._norm(x)) def inner(self, x1, x2): @@ -277,17 +289,13 @@ def inner(self, x1, x2): inner : `LinearSpace.field` element Inner product of ``x1`` and ``x2``. """ - if x1 not in self: - raise LinearSpaceTypeError('`x1` {!r} is not an element of ' - '{!r}'.format(x1, self)) - if x2 not in self: - raise LinearSpaceTypeError('`x2` {!r} is not an element of ' - '{!r}'.format(x2, self)) + x1 = self.element(x1) + x2 = self.element(x2) inner = self._inner(x1, x2) if self.field is None: return inner else: - return self.field.element(self._inner(x1, x2)) + return self.field.element(inner) def multiply(self, x1, x2, out=None): """Return the pointwise product of ``x1`` and ``x2``. @@ -307,16 +315,23 @@ def multiply(self, x1, x2, out=None): """ if out is None: out = self.element() + elif out not in self: + raise TypeError( + '`out` {!r} is not an element of {!r}'.format(out, self) + ) + + if np.isscalar(x1): + if self.field is not None: + x1 = self.field.element(x1) + else: + x1 = self.element(x1) + + if np.isscalar(x2): + if self.field is not None: + x2 = self.field.element(x2) + else: + x2 = self.element(x2) - if out not in self: - raise LinearSpaceTypeError('`out` {!r} is not an element of ' - '{!r}'.format(out, self)) - if x1 not in self: - raise LinearSpaceTypeError('`x1` {!r} is not an element of ' - '{!r}'.format(x1, self)) - if x2 not in self: - raise LinearSpaceTypeError('`x2` {!r} is not an element of ' - '{!r}'.format(x2, self)) self._multiply(x1, x2, out) return out @@ -340,24 +355,26 @@ def divide(self, x1, x2, out=None): """ if out is None: out = self.element() + elif out not in self: + raise TypeError( + '`out` {!r} is not an element of {!r}'.format(out, self) + ) + + if np.isscalar(x1): + if self.field is not None: + x1 = self.field.element(x1) + else: + x1 = self.element(x1) + + if np.isscalar(x2): + if self.field is not None: + x2 = self.field.element(x2) + else: + x2 = self.element(x2) - if out not in self: - raise LinearSpaceTypeError('`out` {!r} is not an element of ' - '{!r}'.format(out, self)) - if x1 not in self: - raise LinearSpaceTypeError('`x1` {!r} is not an element of ' - '{!r}'.format(x1, self)) - if x2 not in self: - raise LinearSpaceTypeError('`x2` {!r} is not an element of ' - '{!r}'.format(x2, self)) self._divide(x1, x2, out) return out - @property - def element_type(self): - """Type of elements of this space (`LinearSpaceElement`).""" - return LinearSpaceElement - def __pow__(self, shape): """Return ``self ** shape``. @@ -374,10 +391,10 @@ def __pow__(self, shape): >>> r2 ** 4 ProductSpace(rn(2), 4) - Multiple powers work as expected: + Multiple powers work "outside-in": >>> r2 ** (4, 2) - ProductSpace(ProductSpace(rn(2), 4), 2) + ProductSpace(ProductSpace(rn(2), 2), 4) """ from odl.space import ProductSpace @@ -387,7 +404,7 @@ def __pow__(self, shape): shape = tuple(shape) pspace = self - for n in shape: + for n in reversed(shape): pspace = ProductSpace(pspace, n) return pspace @@ -412,8 +429,9 @@ def __mul__(self, other): from odl.space import ProductSpace if not isinstance(other, LinearSpace): - raise TypeError('Can only multiply with `LinearSpace`, got {!r}' - ''.format(other)) + raise TypeError( + '`other` must be a `LinearSpace`, got {!r}'.format(other) + ) return ProductSpace(self, other) @@ -422,651 +440,6 @@ def __str__(self): return repr(self) -class LinearSpaceElement(object): - - """Abstract class for `LinearSpace` elements. - - Do not use this class directly -- to create an element of a vector - space, call the space's `LinearSpace.element` method instead. - """ - - def __init__(self, space): - """Initialize a new instance. - - All deriving classes must call this method to set the `space` - property. - """ - self.__space = space - - @property - def space(self): - """Space to which this element belongs.""" - return self.__space - - # Convenience functions - def assign(self, other): - """Assign the values of ``other`` to ``self``.""" - return self.space.lincomb(1, other, out=self) - - def copy(self): - """Create an identical (deep) copy of self.""" - result = self.space.element() - result.assign(self) - return result - - def lincomb(self, a, x1, b=None, x2=None): - """Implement ``self[:] = a * x1 + b * x2``. - - Parameters - ---------- - a : element of ``space.field`` - Scalar to multiply ``x1`` with. - x1 : `LinearSpaceElement` - First space element in the linear combination. - b : element of ``space.field``, optional - Scalar to multiply ``x2`` with. Required if ``x2`` is - provided. - x2 : `LinearSpaceElement`, optional - Second space element in the linear combination. - - See Also - -------- - LinearSpace.lincomb - """ - return self.space.lincomb(a, x1, b, x2, out=self) - - def set_zero(self): - """Set this element to zero. - - See Also - -------- - LinearSpace.zero - """ - return self.space.lincomb(0, self, 0, self, out=self) - - # Convenience methods - def __iadd__(self, other): - """Implement ``self += other``.""" - if self.space.field is None: - return NotImplemented - elif other in self.space: - return self.space.lincomb(1, self, 1, other, out=self) - elif isinstance(other, LinearSpaceElement): - # We do not `return NotImplemented` here since we don't want a - # fallback for in-place. Otherwise python attempts - # `self = self + other` which does not modify self. - raise TypeError('cannot add {!r} and {!r} in-place' - ''.format(self, other)) - elif other in self.space.field: - one = getattr(self.space, 'one', None) - if one is None: - raise TypeError('cannot add {!r} and {!r} in-place' - ''.format(self, other)) - else: - # other --> other * space.one() - return self.space.lincomb(1, self, other, one(), out=self) - else: - try: - other = self.space.element(other) - except (TypeError, ValueError): - raise TypeError('cannot add {!r} and {!r} in-place' - ''.format(self, other)) - else: - return self.__iadd__(other) - - def __add__(self, other): - """Return ``self + other``.""" - # Instead of using __iadd__ we duplicate code here for performance - if getattr(other, '__array_priority__', 0) > self.__array_priority__: - return other.__radd__(self) - elif self.space.field is None: - return NotImplemented - elif other in self.space: - tmp = self.space.element() - return self.space.lincomb(1, self, 1, other, out=tmp) - elif isinstance(other, LinearSpaceElement): - return NotImplemented - elif other in self.space.field: - one = getattr(self.space, 'one', None) - if one is None: - return NotImplemented - else: - tmp = one() - return self.space.lincomb(1, self, other, tmp, out=tmp) - else: - try: - other = self.space.element(other) - except (TypeError, ValueError): - return NotImplemented - else: - return self.__add__(other) - - def __radd__(self, other): - """Return ``other + self``.""" - if getattr(other, '__array_priority__', 0) > self.__array_priority__: - return other.__add__(self) - else: - return self.__add__(other) - - def __isub__(self, other): - """Implement ``self -= other``.""" - if self.space.field is None: - return NotImplemented - elif other in self.space: - return self.space.lincomb(1, self, -1, other, out=self) - elif isinstance(other, LinearSpaceElement): - # We do not `return NotImplemented` here since we don't want a - # fallback for in-place. Otherwise python attempts - # `self = self - other` which does not modify self. - raise TypeError('cannot subtract {!r} and {!r} in-place' - ''.format(self, other)) - elif self.space.field is None: - return NotImplemented - elif other in self.space.field: - one = getattr(self.space, 'one', None) - if one is None: - raise TypeError('cannot subtract {!r} and {!r} in-place' - ''.format(self, other)) - else: - return self.space.lincomb(1, self, -other, one(), out=self) - else: - try: - other = self.space.element(other) - except (TypeError, ValueError): - raise TypeError('cannot subtract {!r} and {!r} in-place' - ''.format(self, other)) - else: - return self.__isub__(other) - - def __sub__(self, other): - """Return ``self - other``.""" - # Instead of using __isub__ we duplicate code here for performance - if getattr(other, '__array_priority__', 0) > self.__array_priority__: - return other.__rsub__(self) - elif self.space.field is None: - return NotImplemented - elif other in self.space: - tmp = self.space.element() - return self.space.lincomb(1, self, -1, other, out=tmp) - elif isinstance(other, LinearSpaceElement): - return NotImplemented - elif other in self.space.field: - one = getattr(self.space, 'one', None) - if one is None: - return NotImplemented - else: - tmp = one() - return self.space.lincomb(1, self, -other, tmp, out=tmp) - else: - try: - other = self.space.element(other) - except (TypeError, ValueError): - return NotImplemented - else: - return self.__sub__(other) - - def __rsub__(self, other): - """Return ``other - self``.""" - if getattr(other, '__array_priority__', 0) > self.__array_priority__: - return other.__sub__(self) - elif self.space.field is None: - return NotImplemented - elif other in self.space: - tmp = self.space.element() - return self.space.lincomb(1, other, -1, self, out=tmp) - elif isinstance(other, LinearSpaceElement): - return NotImplemented - elif other in self.space.field: - one = getattr(self.space, 'one', None) - if one is None: - return NotImplemented - else: - # other --> other * space.one() - tmp = one() - self.space.lincomb(other, tmp, out=tmp) - return self.space.lincomb(1, tmp, -1, self, out=tmp) - else: - try: - other = self.space.element(other) - except (TypeError, ValueError): - return NotImplemented - else: - return self.__rsub__(other) - - def __imul__(self, other): - """Implement ``self *= other``.""" - if self.space.field is None: - return NotImplemented - elif other in self.space.field: - return self.space.lincomb(other, self, out=self) - elif other in self.space: - return self.space.multiply(other, self, out=self) - elif isinstance(other, LinearSpaceElement): - # We do not `return NotImplemented` here since we don't want a - # fallback for in-place. Otherwise python attempts - # `self = self * other` which does not modify self. - raise TypeError('cannot multiply {!r} and {!r} in-place' - ''.format(self, other)) - else: - try: - other = self.space.element(other) - except (TypeError, ValueError): - raise TypeError('cannot multiply {!r} and {!r} in-place' - ''.format(self, other)) - else: - return self.__imul__(other) - - def __mul__(self, other): - """Return ``self * other``.""" - # Instead of using __imul__ we duplicate code here for performance - if getattr(other, '__array_priority__', 0) > self.__array_priority__: - return other.__rmul__(self) - elif self.space.field is None: - return NotImplemented - elif other in self.space.field: - tmp = self.space.element() - return self.space.lincomb(other, self, out=tmp) - elif other in self.space: - tmp = self.space.element() - return self.space.multiply(other, self, out=tmp) - elif isinstance(other, LinearSpaceElement): - return NotImplemented - else: - try: - other = self.space.element(other) - except (TypeError, ValueError): - return NotImplemented - else: - return self.__mul__(other) - - def __rmul__(self, other): - """Return ``other * self``.""" - if getattr(other, '__array_priority__', 0) > self.__array_priority__: - return other.__mul__(self) - else: - return self.__mul__(other) - - def __itruediv__(self, other): - """Implement ``self /= other``.""" - if self.space.field is None: - return NotImplemented - if other in self.space.field: - return self.space.lincomb(1.0 / other, self, out=self) - elif other in self.space: - return self.space.divide(self, other, out=self) - elif isinstance(other, LinearSpaceElement): - # We do not `return NotImplemented` here since we don't want a - # fallback for in-place. Otherwise python attempts - # `self = self / other` which does not modify self. - raise TypeError('cannot divide {!r} and {!r} in-place' - ''.format(self, other)) - else: - try: - other = self.space.element(other) - except (TypeError, ValueError): - raise TypeError('cannot divide {!r} and {!r} in-place' - ''.format(self, other)) - else: - return self.__itruediv__(other) - - __idiv__ = __itruediv__ - - def __truediv__(self, other): - """Return ``self / other``.""" - if getattr(other, '__array_priority__', 0) > self.__array_priority__: - return other.__rtruediv__(self) - elif self.space.field is None: - return NotImplemented - elif other in self.space.field: - tmp = self.space.element() - return self.space.lincomb(1.0 / other, self, out=tmp) - elif other in self.space: - tmp = self.space.element() - return self.space.divide(self, other, out=tmp) - elif isinstance(other, LinearSpaceElement): - return NotImplemented - else: - try: - other = self.space.element(other) - except (TypeError, ValueError): - return NotImplemented - else: - return self.__truediv__(other) - - __div__ = __truediv__ - - def __rtruediv__(self, other): - """Return ``other / self``.""" - if getattr(other, '__array_priority__', 0) > self.__array_priority__: - return other.__truediv__(self) - elif self.space.field is None: - return NotImplemented - elif other in self.space.field: - one = getattr(self.space, 'one', None) - if one is None: - return NotImplemented - else: - # other --> other * space.one() - tmp = one() - self.space.lincomb(other, tmp, out=tmp) - return self.space.divide(tmp, self, out=tmp) - elif other in self.space: - tmp = self.space.element() - return self.space.divide(other, self, out=tmp) - elif isinstance(other, LinearSpaceElement): - return NotImplemented - else: - try: - other = self.space.element(other) - except (TypeError, ValueError): - return NotImplemented - else: - return self.__rtruediv__(other) - - __rdiv__ = __rtruediv__ - - def __ipow__(self, p): - """Implement ``self ** p``. - - This is only defined for integer ``p``.""" - if self.space.field is None: - return NotImplemented - p, p_in = int(p), p - if p != p_in: - raise ValueError('expected integer `p`, got {}'.format(p_in)) - if p < 0: - self **= -p - self.space.divide(self.space.one(), self, out=self) - return self - elif p == 0: - self.assign(self.space.one()) - return self - elif p == 1: - return self - elif p % 2 == 0: - self *= self - self **= p // 2 - return self - else: - tmp = self.copy() - for _ in range(p - 2): - tmp *= self - self *= tmp - return self - - def __pow__(self, p): - """Return ``self ** p``.""" - if self.space.field is None: - return NotImplemented - tmp = self.copy() - tmp.__ipow__(p) - return tmp - - def __neg__(self): - """Return ``-self``.""" - if self.space.field is None: - return NotImplemented - return (-1) * self - - def __pos__(self): - """Return ``+self``.""" - return self.copy() - - def __cmp__(self, other): - """Comparsion not implemented.""" - # Stops python 2 from allowing comparsion of arbitrary objects - raise TypeError('unorderable types: {}, {}' - ''.format(self.__class__.__name__, type(other))) - - # Metric space method - def __eq__(self, other): - """Return ``self == other``. - - Two elements are equal if their distance is zero. - - Parameters - ---------- - other : `LinearSpaceElement` - Element of this space. - - Returns - ------- - equals : bool - ``True`` if the elements are equal ``False`` otherwise. - - See Also - -------- - LinearSpace.dist - - Notes - ----- - Equality is very sensitive to numerical errors, thus any - arithmetic operations should be expected to break equality. - - Examples - -------- - >>> rn = odl.rn(1, norm=np.linalg.norm) - >>> x = rn.element([0.1]) - >>> x == x - True - >>> y = rn.element([0.1]) - >>> x == y - True - >>> z = rn.element([0.3]) - >>> x + x + x == z - False - """ - if other is self: - # Optimization for a common case - return True - elif (not isinstance(other, LinearSpaceElement) or - other.space != self.space): - # Cannot use (if other not in self.space) since this is not - # reflexive. - return False - else: - return self.space.dist(self, other) == 0 - - def __ne__(self, other): - """Return ``self != other``.""" - return not self.__eq__(other) - - # Disable hash since vectors are mutable - __hash__ = None - - def __str__(self): - """Return ``str(self)``.""" - return repr(self) - - def __copy__(self): - """Return a copy of this element. - - See Also - -------- - LinearSpace.copy - """ - return self.copy() - - def __deepcopy__(self, memo): - """Return a deep copy of this element. - - See Also - -------- - LinearSpace.copy - """ - return self.copy() - - def norm(self): - """Return the norm of this element. - - See Also - -------- - LinearSpace.norm - """ - return self.space.norm(self) - - def dist(self, other): - """Return the distance of ``self`` to ``other``. - - See Also - -------- - LinearSpace.dist - """ - return self.space.dist(self, other) - - def inner(self, other): - """Return the inner product of ``self`` and ``other``. - - See Also - -------- - LinearSpace.inner - """ - return self.space.inner(self, other) - - def multiply(self, other, out=None): - """Return ``out = self * other``. - - If ``out`` is provided, the result is written to it. - - See Also - -------- - LinearSpace.multiply - """ - return self.space.multiply(self, other, out=out) - - def divide(self, other, out=None): - """Return ``out = self / other``. - - If ``out`` is provided, the result is written to it. - - See Also - -------- - LinearSpace.divide - """ - return self.space.divide(self, other, out=out) - - @property - def T(self): - """This element's transpose, i.e. the functional ``<. , self>``. - - Returns - ------- - transpose : `InnerProductOperator` - - Notes - ----- - This function is only defined in inner product spaces. - - In a complex space, the conjugate transpose of is taken instead - of the transpose only. - - Examples - -------- - >>> rn = odl.rn(3) - >>> x = rn.element([1, 2, 3]) - >>> y = rn.element([2, 1, 3]) - >>> x.T(y) - 13.0 - """ - from odl.operator import InnerProductOperator - return InnerProductOperator(self.copy()) - - # Give an `Element` a higher priority than any NumPy array type. This - # forces the usage of `__op__` of `Element` if the other operand - # is a NumPy object (applies also to scalars!). - __array_priority__ = 1000000.0 - - -class UniversalSpace(LinearSpace): - - """A dummy linear space class. - - Mostly raising `LinearSpaceNotImplementedError`. - """ - - def __init__(self): - """Initialize a new instance.""" - super(UniversalSpace, self).__init__(field=UniversalSet()) - - def element(self, inp=None): - """Dummy element creation method. - - raises `LinearSpaceNotImplementedError`. - """ - raise LinearSpaceNotImplementedError - - def _lincomb(self, a, x1, b, x2, out): - """Dummy linear combination. - - raises `LinearSpaceNotImplementedError`. - """ - raise LinearSpaceNotImplementedError - - def _dist(self, x1, x2): - """Dummy distance method. - - raises `LinearSpaceNotImplementedError`. - """ - raise LinearSpaceNotImplementedError - - def _norm(self, x): - """Dummy norm method. - - raises `LinearSpaceNotImplementedError`. - """ - raise LinearSpaceNotImplementedError - - def _inner(self, x1, x2): - """Dummy inner product method. - - raises `LinearSpaceNotImplementedError`. - """ - raise LinearSpaceNotImplementedError - - def _multiply(self, x1, x2, out): - """Dummy multiplication method. - - raises `LinearSpaceNotImplementedError`.""" - raise LinearSpaceNotImplementedError - - def _divide(self, x1, x2, out): - """Dummy division method. - - raises `LinearSpaceNotImplementedError`. - """ - raise LinearSpaceNotImplementedError - - def __eq__(self, other): - """Return ``self == other``. - - Dummy check, ``True`` for any `LinearSpace`. - """ - return isinstance(other, LinearSpace) - - def __contains__(self, other): - """Return ``other in self``. - - Dummy membership check, ``True`` for any `LinearSpaceElement`. - """ - return isinstance(other, LinearSpaceElement) - - -class LinearSpaceTypeError(TypeError): - """Exception for type errors in `LinearSpace`'s. - - This exception is raised when the wrong type of element is fed to - `LinearSpace.lincomb` and related functions. - """ - - -class LinearSpaceNotImplementedError(NotImplementedError): - """Exception for unimplemented functionality in `LinearSpace`'s. - - This exception is raised when a method is called in `LinearSpace` - that has not been defined in a specific space. - """ - - if __name__ == '__main__': from odl.util.testutils import run_doctests run_doctests() diff --git a/odl/solvers/functional/default_functionals.py b/odl/solvers/functional/default_functionals.py index abc4a095f99..29907dd7c09 100644 --- a/odl/solvers/functional/default_functionals.py +++ b/odl/solvers/functional/default_functionals.py @@ -15,8 +15,9 @@ import numpy as np from odl.operator import ( - ConstantOperator, DiagonalOperator, Operator, PointwiseNorm, - ScalingOperator, ZeroOperator) + ConstantOperator, DiagonalOperator, IdentityOperator, Operator, + PointwiseNorm, ScalingOperator, ZeroOperator) +from odl.set import LinearSpace from odl.solvers.functional.functional import ( Functional, FunctionalQuadraticPerturb) from odl.solvers.nonsmooth.proximal_operators import ( @@ -79,20 +80,26 @@ def __init__(self, space, exponent): # TODO: update when integration operator is in place: issue #440 def _call(self, x): """Return the Lp-norm of ``x``.""" + F = self.domain.ufuncs + R = self.domain.reduce + if self.exponent == 0: - return self.domain.one().inner(np.not_equal(x, 0)) + return self.domain.inner(self.domain.one(), F.not_equal(x, 0)) elif self.exponent == 1: - return x.ufuncs.absolute().inner(self.domain.one()) + return self.domain.inner(self.domain.one(), F.absolute(x)) elif self.exponent == 2: - return np.sqrt(x.inner(x)) + return np.sqrt(self.domain.inner(x, x)) elif np.isfinite(self.exponent): - tmp = x.ufuncs.absolute() - tmp.ufuncs.power(self.exponent, out=tmp) - return np.power(tmp.inner(self.domain.one()), 1 / self.exponent) + integrand = F.absolute(x) + F.power(integrand, self.exponent, out=integrand) + return np.power( + self.domain.inner(self.domain.one(), integrand), + 1 / self.exponent, + ) elif self.exponent == np.inf: - return x.ufuncs.absolute().ufuncs.max() + return R.max(F.absolute(x)) elif self.exponent == -np.inf: - return x.ufuncs.absolute().ufuncs.min() + return R.min(F.absolute(x)) else: raise RuntimeError('unknown exponent') @@ -144,7 +151,7 @@ def __init__(self): def _call(self, x): """Apply the gradient operator to the given point.""" - return x.ufuncs.sign() + return self.domain.ufuncs.sign(x) def derivative(self, x): """Derivative is a.e. zero.""" @@ -167,11 +174,11 @@ def _call(self, x): The gradient is not defined in 0. """ - norm_of_x = x.norm() - if norm_of_x == 0: + x_norm = self.domain.norm(x) + if x_norm == 0: return self.domain.zero() else: - return x / norm_of_x + return x / x_norm return L2Gradient() @@ -233,14 +240,14 @@ def __init__(self, vfspace, exponent=None): -------- >>> space = odl.rn(2) >>> pspace = odl.ProductSpace(space, 2) - >>> op = GroupL1Norm(pspace) - >>> op([[3, 3], [4, 4]]) + >>> func = GroupL1Norm(pspace) + >>> func([[3, 3], [4, 4]]) 10.0 Set exponent of inner (p) norm: - >>> op2 = GroupL1Norm(pspace, exponent=1) - >>> op2([[3, 3], [4, 4]]) + >>> func_1 = GroupL1Norm(pspace, exponent=1) + >>> func_1([[3, 3], [4, 4]]) 14.0 """ if not isinstance(vfspace, ProductSpace): @@ -255,8 +262,10 @@ def __init__(self, vfspace, exponent=None): def _call(self, x): """Return the group L1-norm of ``x``.""" # TODO: update when integration operator is in place: issue #440 - pointwise_norm = self.pointwise_norm(x) - return pointwise_norm.inner(pointwise_norm.space.one()) + pw_norm = self.pointwise_norm(x) + return self.pointwise_norm.range.inner( + self.pointwise_norm.range.one(), pw_norm + ) @property def gradient(self): @@ -265,7 +274,7 @@ def gradient(self): The functional is not differentiable in ``x=0``. However, when evaluating the gradient operator in this point it will return 0. - Notes + Notes ----- The gradient is given by @@ -283,27 +292,40 @@ def gradient(self): \left[ \nabla || ||f||_p ||_1 \right]_i = \frac{| f_i |^{p-2} f_i}{||f||_p^{p-1}} """ - functional = self + func = self class GroupL1Gradient(Operator): - """The gradient operator of the `GroupL1Norm` functional.""" + r"""The gradient operator of the `GroupL1Norm` functional. - def __init__(self): - """Initialize a new instance.""" - super(GroupL1Gradient, self).__init__( - functional.domain, functional.domain, linear=False) + Notes + ----- + The gradient is given by + + .. math:: + \left[ \nabla \| \|f\|_1 \|_1 \right]_i = + \frac{f_i}{|f_i|} + + .. math:: + \left[ \nabla \| \|f\|_2 \|_1 \right]_i = + \frac{f_i}{\|f\|_2} + + else: + + .. math:: + \left[ \nabla || ||f||_p ||_1 \right]_i = + \frac{| f_i |^{p-2} f_i}{||f||_p^{p-1}} + """ def _call(self, x, out): """Return ``self(x)``.""" - pwnorm_x = functional.pointwise_norm(x) - pwnorm_x.ufuncs.sign(out=pwnorm_x) - functional.pointwise_norm.derivative(x).adjoint(pwnorm_x, - out=out) - + pw_norm = func.pointwise_norm + pw_norm_x = pw_norm(x) + pw_norm.range.ufuncs.sign(pw_norm_x, out=pw_norm_x) + pw_norm.derivative(x).adjoint(pw_norm_x, out=out) return out - return GroupL1Gradient() + return GroupL1Gradient(func.domain, func.domain, linear=False) @property def proximal(self): @@ -384,7 +406,7 @@ def __init__(self, vfspace, exponent=None): def _call(self, x): """Return ``self(x)``.""" - x_norm = self.pointwise_norm(x).ufuncs.max() + x_norm = self.pointwise_norm.range.reduce.max(self.pointwise_norm(x)) if x_norm > 1: return np.inf @@ -533,8 +555,9 @@ def proximal(self): elif self.exponent == 1: return proximal_convex_conj_linfty(space=self.domain) else: - raise NotImplementedError('`proximal` only implemented for p=1, ' - 'p=2 or p=inf') + raise NotImplementedError( + '`proximal` only implemented for p=2 and p=inf' + ) def __repr__(self): """Return ``repr(self)``.""" @@ -568,7 +591,7 @@ class L1Norm(LpNorm): >>> f = odl.solvers.L1Norm(space) >>> x = space.one() >>> f.proximal([0.5, 1.0, 1.5])(x) - rn(3).element([ 0.5, 0. , 0. ]) + array([ 0.5, 0. , 0. ]) """ def __init__(self, space): @@ -652,7 +675,7 @@ class L2NormSquared(Functional): >>> f = odl.solvers.L2NormSquared(space) >>> x = space.one() >>> f.proximal([0.5, 1.5, 2.0])(x) - rn(3).element([ 0.5 , 0.25, 0.2 ]) + array([ 0.5 , 0.25, 0.2 ]) """ def __init__(self, space): @@ -669,7 +692,7 @@ def __init__(self, space): # TODO: update when integration operator is in place: issue #440 def _call(self, x): """Return the squared L2-norm of ``x``.""" - return x.inner(x) + return self.domain.inner(x, x) @property def gradient(self): @@ -886,7 +909,7 @@ def _call(self, x): # Since the proximal projects onto our feasible set we can simply # check if it changes anything proj = self.proximal(1)(x) - return np.inf if x.dist(proj) > 0 else 0 + return np.inf if self.domain.dist(x, proj) > 0 else 0 @property def proximal(self): @@ -980,8 +1003,7 @@ def constant(self): def _call(self, x): """Apply the functional to the given point.""" - if x.norm() == 0: - # In this case x is the zero-element. + if self.domain.norm(x) == 0: return self.constant else: return np.inf @@ -1082,8 +1104,7 @@ def __init__(self, space, prior=None): Examples -------- - - Test that KullbackLeibler(x,x) = 0 + Test that KullbackLeibler(x, x) = 0 >>> space = odl.rn(3) >>> prior = 3 * space.one() @@ -1091,7 +1112,6 @@ def __init__(self, space, prior=None): >>> func(prior) 0.0 - Test that zeros in the prior are handled correctly >>> prior = space.zero() @@ -1126,10 +1146,26 @@ def _call(self, x): with np.errstate(invalid='ignore', divide='ignore'): if self.prior is None: - res = (x - 1 - np.log(x)).inner(self.domain.one()) + # < x - 1 - log(x), one > + tmp = self.domain.ufuncs.log(x) + tmp += 1 + tmp -= x + res = self.domain.inner(tmp, self.domain.one()) + if res != 0: + # Avoid -0.0 + res = -res else: - xlogy = scipy.special.xlogy(self.prior, self.prior / x) - res = (x - self.prior + xlogy).inner(self.domain.one()) + # < x - g + xlogy(g, g/x), one > + g = self.prior + if isinstance(self.domain, ProductSpace): + tmp = self.domain.apply2( + lambda xi, i: scipy.special.xlogy(g[i], g[i] / xi), x + ) + else: + tmp = scipy.special.xlogy(g, g / x) + tmp -= g + tmp += x + res = self.domain.inner(tmp, self.domain.one()) if not np.isfinite(res): # In this case, some element was less than or equal to zero @@ -1260,10 +1296,20 @@ def _call(self, x): with np.errstate(invalid='ignore'): if self.prior is None: - res = -(np.log(1 - x)).inner(self.domain.one()) + # - < log(1 - x), one > + tmp = 1 - x + self.domain.ufuncs.log(tmp, out=tmp) + res = -self.domain.inner(tmp, self.domain.one()) else: - xlogy = scipy.special.xlogy(self.prior, 1 - x) - res = -self.domain.element(xlogy).inner(self.domain.one()) + # - < xlogy(g, 1 - x), one > + g = self.prior + if isinstance(self.domain, ProductSpace): + tmp = self.domain.apply2( + lambda xi, i: scipy.special.xlogy(g[i], 1 - xi), x + ) + else: + tmp = scipy.special.xlogy(g, 1 - x) + res = -self.domain.inner(tmp, self.domain.one()) if not np.isfinite(res): # In this case, some element was larger than or equal to one @@ -1407,11 +1453,28 @@ def _call(self, x): with np.errstate(invalid='ignore', divide='ignore'): if self.prior is None: - xlogx = scipy.special.xlogy(x, x) - res = (1 - x + xlogx).inner(self.domain.one()) + # < 1 - x + xlogy(x, x), one > + if isinstance(self.domain, ProductSpace): + tmp = self.domain.apply( + lambda xi: scipy.special.xlogy(xi, xi), x + ) + else: + tmp = scipy.special.xlogy(x, x) + tmp -= x + tmp += 1 + res = self.domain.inner(tmp, self.domain.one()) else: - xlogy = scipy.special.xlogy(x, x / self.prior) - res = (self.prior - x + xlogy).inner(self.domain.one()) + # < g - x + xlogy(x, x/g), one > + g = self.prior + if isinstance(self.domain, ProductSpace): + tmp = self.domain.apply2( + lambda xi, i: scipy.special.xlogy(xi, xi / g[i]), x + ) + else: + tmp = scipy.special.xlogy(x, x / g) + tmp -= x + tmp += self.prior + res = self.domain.inner(tmp, self.domain.one()) if not np.isfinite(res): # In this case, some element was less than or equal to zero @@ -1426,38 +1489,27 @@ def gradient(self): The gradient is not defined in points where one or more components are less than or equal to 0. """ - functional = self + func = self class KLCrossEntropyGradient(Operator): """The gradient operator of this functional.""" - def __init__(self): - """Initialize a new instance.""" - super(KLCrossEntropyGradient, self).__init__( - functional.domain, functional.domain, linear=False) - def _call(self, x): """Apply the gradient operator to the given point. The gradient is not defined in for points with components less than or equal to zero. """ - if functional.prior is None: - tmp = np.log(x) - else: - tmp = np.log(x / functional.prior) + F = func.domain.ufuncs - if np.all(np.isfinite(tmp)): - return tmp + if func.prior is None: + return F.log(x) else: - # The derivative is not defined. - raise ValueError('The gradient of the Kullback-Leibler ' - 'Cross Entropy functional is not defined ' - 'for `x` with one or more components ' - 'less than or equal to zero.'.format(x)) + g = func.prior + return g - 1 + F.log(x / g) - return KLCrossEntropyGradient() + return KLCrossEntropyGradient(func.domain, func.domain, linear=False) @property def proximal(self): @@ -1529,35 +1581,24 @@ def prior(self): # TODO: update when integration operator is in place: issue #440 def _call(self, x): """Return the value in the point ``x``.""" + F = self.domain.ufuncs + if self.prior is None: - tmp = self.domain.element((np.exp(x) - 1)).inner(self.domain.one()) + return self.domain.inner(self.domain.one(), F.expm1(x)) else: - tmp = (self.prior * (np.exp(x) - 1)).inner(self.domain.one()) - return tmp + return self.domain.inner( + self.domain.one(), self.prior * F.expm1(x) + ) - # TODO: replace this when UFuncOperators is in place: PL #576 @property def gradient(self): """Gradient operator of the functional.""" - functional = self - - class KLCrossEntCCGradient(Operator): - - """The gradient operator of this functional.""" - - def __init__(self): - """Initialize a new instance.""" - super(KLCrossEntCCGradient, self).__init__( - functional.domain, functional.domain, linear=False) - - def _call(self, x): - """Apply the gradient operator to the given point.""" - if functional.prior is None: - return self.domain.element(np.exp(x)) - else: - return functional.prior * np.exp(x) + from odl import ufunc_ops - return KLCrossEntCCGradient() + if self.prior is None: + return ufunc_ops.exp(self.domain) + else: + return self.prior * ufunc_ops.exp(self.domain) @property def proximal(self): @@ -1656,10 +1697,7 @@ def __init__(self, *functionals): >>> x = f_sum.domain.one() >>> f_sum.proximal([0.5, 2.0])(x) - ProductSpace(rn(3), 2).element([ - [ 0.5, 0.5, 0.5], - [ 0., 0., 0.] - ]) + array([array([ 0.5, 0.5, 0.5]), array([ 0., 0., 0.])], dtype=object) Create functional ``f([x1, ... ,xn]) = \sum_i ||xi||_1``: @@ -1762,52 +1800,86 @@ def __repr__(self): class QuadraticForm(Functional): - """Functional for a general quadratic form ``x^T A x + b^T x + c``.""" + """The quadratic form functional `` + c``. - def __init__(self, operator=None, vector=None, constant=0): - """Initialize a new instance. - - All parameters are optional, but at least one of ``op`` and ``vector`` - have to be provided in order to infer the space. - - The computed value is:: + It is important to note that this functional is only (provably) convex if + the operator ``A`` is linear and positive semi-definite. The `convex_conj` + property relies on this to be true, and its computation requires the + inverse of the symmetric part ``(A^* + A) / 2``. + The `gradient` of the functional is also defined for operators ``A`` that + lack these properties. + """ - x.inner(operator(x)) + vector.inner(x) + constant + def __init__(self, space, operator=None, vector=None, constant=0, + **kwargs): + """Initialize a new instance. Parameters ---------- + space : `LinearSpace` + Space on which the functional is defined. operator : `Operator`, optional Operator for the quadratic part of the functional. ``None`` means that this part is ignored. vector : `LinearSpaceElement`, optional Vector for the linear part of the functional. ``None`` means that this part is ignored. - constant : `Operator`, optional + constant : float, optional Constant offset of the functional. + operator_sym_inv : `Operator`, optional + Inverse of the symmetric part ``(A + A^*) / 2`` of ``operator``, + assuming the latter is linear. This operator is used in the + computation of the convex conjugate. If not given, ``operator`` + is assumed to be symmetric. + operator_prox_inv_fact : callable, optional + Function that takes the proximal ``sigma`` parameter and returns + the operator ``(I + sigma * (A + A^*) / 2).inverse``. It is + required for `proximal` if ``operator`` is not ``None``. """ - if operator is None and vector is None: - raise ValueError('need to provide at least one of `operator` and ' - '`vector`') - if operator is not None: - domain = operator.domain - elif vector is not None: - domain = vector.space - - if (operator is not None and vector is not None and - vector not in operator.domain): - raise ValueError('domain of `operator` and space of `vector` need ' - 'to match') + if not isinstance(space, LinearSpace): + raise TypeError( + '`space` must be a `LinearSpace`, got {!r}'.format(space) + ) + if ( + operator is not None + and (operator.domain != space or operator.range != space) + ): + raise ValueError( + '`domain` and `range` of `operator` must be equal to ' + '`space`, but {!r}, {!r}, {!r} are not all equal' + ''.format(operator.domain, operator.range, space)) super(QuadraticForm, self).__init__( - space=domain, linear=(operator is None and constant == 0)) + space=space, linear=(operator is None and constant == 0)) + + operator_sym_inv = kwargs.pop('operator_sym_inv', None) + if ( + operator_sym_inv is not None + and (operator_sym_inv.domain != space + or operator_sym_inv.range != space) + ): + raise ValueError( + '`domain` and `range` of `operator_sym_inv` must be equal to ' + '`space`, but {!r}, {!r}, {!r} are not all equal' + ''.format(operator.domain, operator.range, space)) + + operator_prox_inv_fact = kwargs.pop('operator_prox_inv_fact', None) + if ( + operator_prox_inv_fact is not None + and not callable(operator_prox_inv_fact) + ): + raise TypeError('`operator_prox_inv_fact` must be callable') self.__operator = operator - self.__vector = vector - self.__constant = constant + self.__vector = None if vector is None else space.element(vector) + self.__constant = space.field.element(constant) + self.__operator_sym_inv = operator_sym_inv + self.__operator_prox_inv_fact = operator_prox_inv_fact - if self.constant not in self.range: - raise ValueError('`constant` must be an element in the range of ' - 'the functional') + if kwargs: + raise TypeError( + 'got unexpected keyword arguments {}'.format(kwargs) + ) @property def operator(self): @@ -1824,66 +1896,141 @@ def constant(self): """Constant offset of the functional.""" return self.__constant + @property + def operator_sym_inv(self): + """Inverse of the symmetric part of `operator`.""" + return self.__operator_sym_inv + + @property + def operator_prox_inv_fact(self): + """Factory for ``sigma --> (I + sigma * (A + A^*)/2).inverse``.""" + return self.__operator_prox_inv_fact + def _call(self, x): """Return ``self(x)``.""" if self.operator is None: - return self.vector.inner(x) + self.constant + return self.domain.inner(x, self.vector) + self.constant elif self.vector is None: - return x.inner(self.operator(x)) + self.constant + return self.domain.inner(x, self.operator(x)) + self.constant else: tmp = self.operator(x) tmp += self.vector - return x.inner(tmp) + self.constant + return self.domain.inner(x, tmp) + self.constant @property def gradient(self): - """Gradient operator of the functional.""" + r"""Gradient operator of the quadratic form functional. + + The gradient of :math:`Q(x) = \langle x, A(x) + b \rangle + c` is + given by + + .. math:: + \nabla Q(x) = A(x) + A'(x)^* x + b. + + If :math:`A` is linear, this expression simplifies to + + .. math:: + \nabla Q(x) = (A + A^*) x + b. + """ if self.operator is None: - return ConstantOperator(self.vector, self.domain) + return ConstantOperator(self.domain, self.vector) + + if not self.operator.is_linear: + # TODO(kohr-h): add this + raise NotImplementedError( + '`gradient` not implemented for nonlinear `operator`' + ) + + # Figure out whether operator is symmetric + # NB: this check is equivalent to `op is op.adjoint` since + # there are no semantics for operator comparison + op = self.operator + adj = self.operator.adjoint + grad = 2 * op if op == adj else op + adj + + if self.vector is None: + return grad else: - if not self.operator.is_linear: - # TODO: Acutally works otherwise, but needs more work - raise NotImplementedError('`operator` must be linear') + return grad + self.vector - # Figure out if operator is symmetric - opadjoint = self.operator.adjoint - if opadjoint == self.operator: - gradient = 2 * self.operator - else: - gradient = self.operator + opadjoint + @property + def proximal(self): + r"""Factory function for the proximal operator. + + The proximal operator of :math:`Q(x) = \langle x, Ax + b \rangle + c` + is given by - # Return gradient - if self.vector is None: - return gradient + .. math:: + \text{prox}_{\sigma Q}(x) = (I + 2\sigma \bar A)^{-1} + (x - \sigma b), + + with the symmetric part :math:`\bar A = (A + A^*) / 2`. + """ + if self.operator is not None: + if not self.operator.is_linear: + # TODO(kohr-h): add this + raise NotImplementedError( + '`proximal` not available for nonlinear `operator`' + ) + if self.operator_prox_inv_fact is None: + raise TypeError( + '`operator_prox_inv_fact` is required for proximal with ' + 'operator' + ) + + def prox_fact(sigma): + """Proximal factory function.""" + rhs_op = IdentityOperator(self.domain) + if self.vector is not None: + rhs_op = rhs_op - sigma * self.vector + + if self.operator_prox_inv_fact is None: + return rhs_op else: - return gradient + self.vector + return self.operator_prox_inv_fact(2 * sigma) * rhs_op + + return prox_fact @property def convex_conj(self): r"""The convex conjugate functional of the quadratic form. - Notes - ----- - The convex conjugate of the quadratic form :math:` + + c` - is given by + The convex conjugate of the quadratic form + :math:`Q(x) = \langle x, Ax + b \rangle + c` with linear operator + :math:`A` and its symmetric part :math:`\bar A = (A + A^*)/2` is + given by + + .. math:: + Q^*(x) + &= \frac{1}{4} \langle A \bar A^{-1}(x - b), \bar A^{-1}(x - b) + \rangle - c \\ + &= \frac{1}{4}\langle x, \bar A^{-1} x \rangle + - \frac{1}{2} \langle x, \bar A^{-1} b \rangle + + \frac{1}{4} \langle b, \bar A^{-1} b \rangle - c. + + Thus, :math:`Q^*` is a quadratic form with operator + :math:`\bar A^{-1} / 4`, + vector :math:`-\bar A^{-1} b / 2` and constant + :math:`\frac{1}{4} \langle b, \bar A^{-1} b \rangle - c`. + + For the proximal, the required inverse operator is .. math:: - ( + + c)^* (x) = - <(x - b), A^-1 (x - b)> - c = - - - + - c. + (I + 2 \sigma \bar A^{-1} / 4)^{-1} = + \frac{2}{\sigma} \bar A (I + 2/\sigma \bar A)^{-1}. - If the quadratic part of the functional is zero it is instead given - by a translated indicator function on zero, i.e., if + If the quadratic part of the functional is zero, i.e., .. math:: - f(x) = + c, + Q(x) = \langle x, b \rangle + c, - then + then :math:`Q^*` is an indicator function of the point set + :math:`\{b\}` with constant :math:`-c`: .. math:: - f^*(x^*) = + Q^*(x) = \begin{cases} - -c & \text{if } x^* = b \\ + -c & \text{if } x = b \\ \infty & \text{else.} \end{cases} @@ -1892,26 +2039,46 @@ def convex_conj(self): IndicatorZero """ if self.operator is None: - tmp = IndicatorZero(space=self.domain, constant=-self.constant) - if self.vector is None: - return tmp - else: - return tmp.translated(self.vector) + fn = IndicatorZero(self.domain, constant=-self.constant) + return fn if self.vector is None else fn.translated(self.vector) + if not self.operator.is_linear: + raise NotImplementedError( + '`convex_conj` not available for nonlinear `operator`' + ) + + if self.operator_sym_inv is None: + op_inv = self.operator.inverse + else: + op_inv = self.operator_sym_inv + + constant = -self.constant if self.vector is None: - # Handle trivial case separately - return QuadraticForm(operator=self.operator.inverse, - constant=-self.constant) + vector = None else: - # Compute the needed variables - opinv = self.operator.inverse - vector = -opinv.adjoint(self.vector) - opinv(self.vector) - constant = self.vector.inner(opinv(self.vector)) - self.constant + vector = -op_inv(self.vector) / 2 + constant -= self.domain.inner(self.vector, vector) / 2 + + op = self.operator + adj = self.operator.adjoint + + kwargs = {} + # Inverse of 1/4 * [(A + A^*) / 2]^(-1) --> 2 * (A + A^*) + sym_op = 4 * op if op == adj else 2 * (op + adj) + kwargs['operator_sym_inv'] = sym_op + + if self.operator_prox_inv_fact is not None: - # Create new quadratic form - return QuadraticForm(operator=opinv, - vector=vector, - constant=constant) + def prox_inv_fact(sigma): + tmp_op = (4 / sigma) * op if op == adj else (op + adj) / sigma + return tmp_op * self.operator_prox_inv_fact(4 / sigma) + + kwargs['operator_prox_inv_fact'] = prox_inv_fact + + return QuadraticForm( + self.domain, operator=op_inv / 4, vector=vector, constant=constant, + **kwargs + ) class NuclearNorm(Functional): @@ -1989,10 +2156,10 @@ def _asarray(self, vec): This is the inverse of `_asvector`. """ shape = self.domain[0, 0].shape + self.pshape - arr = np.empty(shape, dtype=self.domain.dtype) + arr = np.empty(shape, dtype=self.domain[0, 0].dtype) for i, xi in enumerate(vec): for j, xij in enumerate(xi): - arr[..., i, j] = xij.asarray() + arr[..., i, j] = xij return arr @@ -2075,7 +2242,7 @@ def _call(self, x): sprox = np.sign(s) * np.maximum(abss, 0) elif func.pwisenorm.exponent == 2: s_reordered = np.moveaxis(s, -1, 0) - snorm = func.pwisenorm(s_reordered).asarray() + snorm = func.pwisenorm(s_reordered) snorm = np.maximum(self.sigma, snorm, out=snorm) sprox = ((1 - eps) - self.sigma / snorm)[..., None] * s elif func.pwisenorm.exponent == np.inf: @@ -2261,7 +2428,7 @@ def __init__(self, space, diameter=1, sum_rtol=None): ... and one where it lies inside the unit simplex. - >>> x /= x.ufuncs.sum() + >>> x /= np.sum(x) >>> ind_simplex(x) 0 """ @@ -2278,12 +2445,10 @@ def __init__(self, space, diameter=1, sum_rtol=None): def _call(self, x): """Return ``self(x)``.""" + sum_constr = abs(np.sum(x) / self.diameter - 1) <= self.sum_rtol + nonneg_constr = np.all(np.greater_equal(x, 0)) - sum_constr = abs(x.ufuncs.sum() / self.diameter - 1) <= self.sum_rtol - - nonneq_constr = x.ufuncs.greater_equal(0).asarray().all() - - if sum_constr and nonneq_constr: + if sum_constr and nonneg_constr: return 0 else: return np.inf @@ -2379,7 +2544,7 @@ def __init__(self, space, sum_value=1, sum_rtol=None): ... and one where it does. - >>> x /= x.ufuncs.sum() + >>> x /= np.sum(x) >>> ind_sum(x) 0 """ @@ -2396,8 +2561,7 @@ def __init__(self, space, sum_value=1, sum_rtol=None): def _call(self, x): """Return ``self(x)``.""" - - if abs(x.ufuncs.sum() / self.sum_value - 1) <= self.sum_rtol: + if abs(np.sum(x) / self.sum_value - 1) <= self.sum_rtol: return 0 else: return np.inf @@ -2408,7 +2572,6 @@ def gradient(self): The indicator functional is not differentiable over the entire domain. """ - raise NotImplementedError('Not implemented') @property @@ -2426,9 +2589,8 @@ def __init__(self, sigma): domain=domain, range=domain, linear=False) def _call(self, x, out): - - offset = 1 / x.size * (self.sum_value - x.ufuncs.sum()) - out.assign(x) + offset = 1 / x.size * (self.sum_value - np.sum(x)) + out[:] = x out += offset return ProximalSum @@ -2489,8 +2651,8 @@ class MoreauEnvelope(Functional): References ---------- - .. _Proximal Algorithms: \ -https://web.stanford.edu/~boyd/papers/pdf/prox_algs.pdf + .. _Proximal Algorithms: + https://web.stanford.edu/~boyd/papers/pdf/prox_algs.pdf """ def __init__(self, functional, sigma=1.0): @@ -2585,7 +2747,7 @@ def __init__(self, space, gamma): >>> x = 2 * gamma * space.one() >>> tol = 1e-5 - >>> constant = gamma / 2 * space.one().inner(space.one()) + >>> constant = gamma / 2 * space.norm(space.one()) ** 2 >>> f = odl.solvers.L1Norm(space) - constant >>> abs(huber_norm(x) - f(x)) < tol True @@ -2634,20 +2796,25 @@ def gamma(self): def _call(self, x): """Return ``self(x)``.""" if isinstance(self.domain, ProductSpace): - norm = PointwiseNorm(self.domain, 2)(x) + norm_op = PointwiseNorm(self.domain, 2) + norm = norm_op(x) + base_space = norm_op.range else: - norm = x.ufuncs.absolute() + norm = self.domain.ufuncs.absolute(x) + base_space = self.domain + + Fb = base_space.ufuncs if self.gamma > 0: - tmp = norm.ufuncs.square() - tmp *= 1 / (2 * self.gamma) + integrand = Fb.square(norm) + integrand *= 1 / (2 * self.gamma) - index = norm.ufuncs.greater_equal(self.gamma) - tmp[index] = norm[index] - self.gamma / 2 + linear_part = Fb.greater_equal(norm, self.gamma) + integrand[linear_part] = norm[linear_part] - self.gamma / 2 else: - tmp = norm + integrand = norm - return tmp.inner(tmp.space.one()) + return base_space.inner(base_space.one(), integrand) @property def convex_conj(self): @@ -2689,57 +2856,56 @@ def gradient(self): Check that the gradient norm is less than the norm of the one element: >>> space = odl.uniform_discr(0, 1, 14) - >>> norm_one = space.one().norm() + >>> norm_one = space.norm(space.one()) >>> x = odl.phantom.white_noise(space) >>> huber_norm = odl.solvers.Huber(space, gamma=0.1) >>> grad = huber_norm.gradient(x) >>> tol = 1e-5 - >>> grad.norm() <= norm_one + tol + >>> space.norm(grad) <= norm_one + tol True Redo previous example for a product space in two dimensions: - >>> domain = odl.uniform_discr([0, 0], [1, 1], [5, 5]) - >>> space = odl.ProductSpace(domain, 2) - >>> norm_one = space.one().norm() + >>> space = odl.uniform_discr([0, 0], [1, 1], [5, 5]) ** 2 + >>> norm_one = space.norm(space.one()) >>> x = odl.phantom.white_noise(space) >>> huber_norm = odl.solvers.Huber(space, gamma=0.2) >>> grad = huber_norm.gradient(x) >>> tol = 1e-5 - >>> grad.norm() <= norm_one + tol + >>> space.norm(grad) <= norm_one + tol True """ - functional = self + func = self class HuberGradient(Operator): """The gradient operator of this functional.""" - def __init__(self): - """Initialize a new instance.""" - super(HuberGradient, self).__init__( - functional.domain, functional.domain, linear=False) - def _call(self, x): """Apply the gradient operator to the given point.""" if isinstance(self.domain, ProductSpace): - norm = PointwiseNorm(self.domain, 2)(x) + pw_norm = PointwiseNorm(self.domain, 2) + norm = pw_norm(x) + base_space = pw_norm.range else: - norm = x.ufuncs.absolute() + norm = self.domain.ufuncs.absolute(x) + base_space = self.domain + + Fb = base_space.ufuncs - grad = x / functional.gamma + grad = x / func.gamma + linear_part = Fb.greater_equal(norm, func.gamma) - index = norm.ufuncs.greater_equal(functional.gamma) if isinstance(self.domain, ProductSpace): for xi, gi in zip(x, grad): - gi[index] = xi[index] / norm[index] + gi[linear_part] = xi[linear_part] / norm[linear_part] else: - grad[index] = x[index] / norm[index] + grad[linear_part] = x[linear_part] / norm[linear_part] return grad - return HuberGradient() + return HuberGradient(func.domain, func.domain, linear=False) def __repr__(self): '''Return ``repr(self)``.''' diff --git a/odl/solvers/functional/derivatives.py b/odl/solvers/functional/derivatives.py index ce7c0c7628c..152e5de1e40 100644 --- a/odl/solvers/functional/derivatives.py +++ b/odl/solvers/functional/derivatives.py @@ -53,7 +53,7 @@ def __init__(self, operator, point, method='forward', step=None): >>> func = odl.solvers.L2NormSquared(space) >>> hess = NumericalDerivative(func.gradient, [1, 1, 1]) >>> hess([0, 0, 1]) - rn(3).element([ 0., 0., 2.]) + array([ 0., 0., 2.]) Find the Hessian matrix: @@ -122,8 +122,7 @@ def __init__(self, operator, point, method='forward', step=None): def _call(self, dx): """Return ``self(x)``.""" x = self.point - - dx_norm = dx.norm() + dx_norm = self.domain.norm(dx) if dx_norm == 0: return 0 @@ -171,24 +170,24 @@ def __init__(self, functional, method='forward', step=None): >>> func = odl.solvers.L2NormSquared(space) >>> grad = NumericalGradient(func) >>> grad([1, 1, 1]) - rn(3).element([ 2., 2., 2.]) + array([ 2., 2., 2.]) The gradient gives the correct value with sufficiently small step size: - >>> grad([1, 1, 1]) == func.gradient([1, 1, 1]) + >>> all(grad([1, 1, 1]) == func.gradient([1, 1, 1])) True If the step is too large the result is not correct: >>> grad = NumericalGradient(func, step=0.5) >>> grad([1, 1, 1]) - rn(3).element([ 2.5, 2.5, 2.5]) + array([ 2.5, 2.5, 2.5]) But it can be improved by using the more accurate ``method='central'``: >>> grad = NumericalGradient(func, method='central', step=0.5) >>> grad([1, 1, 1]) - rn(3).element([ 2., 2., 2.]) + array([ 2., 2., 2.]) Notes ----- @@ -294,7 +293,7 @@ def derivative(self, point): >>> grad = NumericalGradient(func) >>> hess = grad.derivative([1, 1, 1]) >>> hess([1, 0, 0]) - rn(3).element([ 2., 0., 0.]) + array([ 2., 0., 0.]) Find the Hessian matrix: diff --git a/odl/solvers/functional/functional.py b/odl/solvers/functional/functional.py index 25622c50e98..3c95481a5e3 100644 --- a/odl/solvers/functional/functional.py +++ b/odl/solvers/functional/functional.py @@ -8,18 +8,16 @@ # 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 print_function, division, absolute_import +from __future__ import absolute_import, division, print_function + import numpy as np +from odl.operator.default_ops import ( + ConstantOperator, IdentityOperator, InnerProductOperator) from odl.operator.operator import ( - Operator, OperatorComp, OperatorLeftScalarMult, OperatorRightScalarMult, - OperatorRightVectorMult, OperatorSum, OperatorPointwiseProduct) -from odl.operator.default_ops import (IdentityOperator, ConstantOperator) -from odl.solvers.nonsmooth import (proximal_arg_scaling, proximal_translation, - proximal_quadratic_perturbation, - proximal_const_func, proximal_convex_conj) -from odl.util import signature_string, indent - + Operator, OperatorComp, OperatorLeftScalarMult, OperatorPointwiseProduct, + OperatorRightScalarMult, OperatorRightVectorMult, OperatorSum) +from odl.util import indent, signature_string __all__ = ('Functional', 'FunctionalLeftScalarMult', 'FunctionalRightScalarMult', 'FunctionalComp', @@ -204,7 +202,7 @@ def derivative(self, point): ------- derivative : `Operator` """ - return self.gradient(point).T + return InnerProductOperator(self.domain, self.gradient(point)) def translated(self, shift): """Return a translation of the functional. @@ -505,6 +503,7 @@ def proximal(self): -------- odl.solvers.nonsmooth.proximal_operators.proximal_const_func """ + from odl.solvers.nonsmooth import proximal_const_func if self.scalar < 0: raise ValueError('proximal operator of functional scaled with a ' @@ -590,6 +589,8 @@ def proximal(self): -------- odl.solvers.nonsmooth.proximal_operators.proximal_arg_scaling """ + from odl.solvers.nonsmooth import proximal_arg_scaling + return proximal_arg_scaling(self.functional.proximal, self.scalar) @@ -853,8 +854,11 @@ def proximal(self): -------- odl.solvers.nonsmooth.proximal_operators.proximal_translation """ - return proximal_translation(self.functional.proximal, - self.translation) + from odl.solvers.nonsmooth import proximal_translation + + return proximal_translation( + self.functional.proximal, self.translation + ) @property def convex_conj(self): @@ -1009,7 +1013,9 @@ def __init__(self, func, quadratic_coeff=0, linear_term=None, if linear_term is None: grad_lipschitz = func.grad_lipschitz else: - grad_lipschitz = (func.grad_lipschitz + self.linear_term.norm()) + grad_lipschitz = ( + func.grad_lipschitz + func.domain.norm(linear_term) + ) constant = func.domain.field.element(constant) if constant.imag != 0: @@ -1044,27 +1050,36 @@ def constant(self): def _call(self, x): """Apply the functional to the given point.""" - return (self.functional(x) + - self.quadratic_coeff * x.inner(x) + - x.inner(self.linear_term) + self.constant) + return ( + self.functional(x) + + self.quadratic_coeff * self.domain.inner(x, x) + + self.domain.inner(x, self.linear_term) + + self.constant + ) @property def gradient(self): """Gradient operator of the functional.""" - return (self.functional.gradient + - (2 * self.quadratic_coeff) * IdentityOperator(self.domain) + - ConstantOperator(self.linear_term)) + return ( + self.functional.gradient + + (2 * self.quadratic_coeff) * IdentityOperator(self.domain) + + ConstantOperator(self.domain, self.linear_term) + ) @property def proximal(self): """Proximal factory of the quadratically perturbed functional.""" + from odl.solvers.nonsmooth import proximal_quadratic_perturbation + if self.quadratic_coeff < 0: raise TypeError('`quadratic_coeff` {} must be non-negative' ''.format(self.quadratic_coeff)) return proximal_quadratic_perturbation( self.functional.proximal, - a=self.quadratic_coeff, u=self.linear_term) + a=self.quadratic_coeff, + u=self.linear_term, + ) @property def convex_conj(self): @@ -1320,6 +1335,8 @@ def proximal(self): proximal : proximal_convex_conj Proximal computed using the Moreu identity """ + from odl.solvers.nonsmooth import proximal_convex_conj + return proximal_convex_conj(self.convex_conj.proximal) def __repr__(self): @@ -1394,28 +1411,18 @@ def __init__(self, functional, point, subgrad): raise TypeError('`functional` {} not an instance of ``Functional``' ''.format(functional)) self.__functional = functional - - if point not in functional.domain: - raise ValueError('`point` {} is not in `functional.domain` {}' - ''.format(point, functional.domain)) - self.__point = point - - if subgrad not in functional.domain: - raise TypeError( - '`subgrad` must be an element in `functional.domain`, got ' - '{}'.format(subgrad)) - self.__subgrad = subgrad - - self.__constant = -functional(point) + subgrad.inner(point) - + space = functional.domain + self.__point = space.element(point) + self.__subgrad = space.element(subgrad) + self.__constant = -functional(point) + space.inner(subgrad, point) self.__bregman_dist = FunctionalQuadraticPerturb( - functional, linear_term=-subgrad, constant=self.__constant) - - grad_lipschitz = functional.grad_lipschitz + subgrad.norm() + functional, linear_term=-subgrad, constant=self.__constant + ) + grad_lipschitz = functional.grad_lipschitz + space.norm(subgrad) super(BregmanDistance, self).__init__( - space=functional.domain, linear=False, - grad_lipschitz=grad_lipschitz) + space, linear=False, grad_lipschitz=grad_lipschitz + ) @property def functional(self): @@ -1449,15 +1456,10 @@ def proximal(self): @property def gradient(self): """Gradient operator of the functional.""" - try: - op_to_return = self.functional.gradient - except NotImplementedError: - raise NotImplementedError( - '`self.functional.gradient` is not implemented for ' - '`self.functional` {}'.format(self.functional)) - - op_to_return = op_to_return - ConstantOperator(self.subgrad) - return op_to_return + return ( + self.functional.gradient + - ConstantOperator(self.domain, self.subgrad) + ) def __repr__(self): '''Return ``repr(self)``.''' @@ -1515,7 +1517,7 @@ def simple_functional(space, fcall=None, grad=None, prox=None, grad_lip=np.nan, >>> func([1, 2, 3]) 14.0 >>> func.gradient([1, 2, 3]) - rn(3).element([ 2., 4., 6.]) + array([ 2., 4., 6.]) """ if grad is not None and not isinstance(grad, Operator): grad_in = grad diff --git a/odl/solvers/iterative/iterative.py b/odl/solvers/iterative/iterative.py index b9d7a996f47..674910fe3ff 100644 --- a/odl/solvers/iterative/iterative.py +++ b/odl/solvers/iterative/iterative.py @@ -95,10 +95,12 @@ def landweber(op, x, rhs, niter, omega=None, projection=None, callback=None): `_. """ # TODO: add a book reference + dom = op.domain - if x not in op.domain: - raise TypeError('`x` {!r} is not in the domain of `op` {!r}' - ''.format(x, op.domain)) + if x not in dom: + raise TypeError( + '`x` {!r} is not in the domain of `op` {!r}'.format(x, dom) + ) if omega is None: omega = 1 / op.norm(estimate=True) ** 2 @@ -111,7 +113,7 @@ def landweber(op, x, rhs, niter, omega=None, projection=None, callback=None): op(x, out=tmp_ran) tmp_ran -= rhs op.derivative(x).adjoint(tmp_ran, out=tmp_dom) - x.lincomb(1, x, -omega, tmp_dom) + dom.lincomb(1, x, -omega, tmp_dom, out=x) if projection is not None: projection(x) @@ -159,20 +161,25 @@ def conjugate_gradient(op, x, rhs, niter, callback=None): """ # TODO: add a book reference # TODO: update doc + dom = op.domain + ran = op.range - if op.domain != op.range: - raise ValueError('operator needs to be self-adjoint') + if dom != ran: + raise ValueError( + '`op.domain` and `op.range` must coincide, but {!r} != {!r}' + ''.format(dom, ran) + ) - if x not in op.domain: + if x not in dom: raise TypeError('`x` {!r} is not in the domain of `op` {!r}' - ''.format(x, op.domain)) + ''.format(x, dom)) r = op(x) - r.lincomb(1, rhs, -1, r) # r = rhs - A x - p = r.copy() - d = op.domain.element() # Extra storage for storing A x + dom.lincomb(1, rhs, -1, r, out=r) # r = rhs - A x + p = ran.copy(r) + d = dom.element() # Extra storage for storing A x - sqnorm_r_old = r.norm() ** 2 # Only recalculate norm after update + sqnorm_r_old = dom.norm(r) ** 2 # Only recalculate norm after update if sqnorm_r_old == 0: # Return if no step forward return @@ -180,22 +187,21 @@ def conjugate_gradient(op, x, rhs, niter, callback=None): for _ in range(niter): op(p, out=d) # d = A p - inner_p_d = p.inner(d) - + inner_p_d = dom.inner(p, d) if inner_p_d == 0.0: # Return if step is 0 return alpha = sqnorm_r_old / inner_p_d - x.lincomb(1, x, alpha, p) # x = x + alpha*p - r.lincomb(1, r, -alpha, d) # r = r - alpha*d + dom.lincomb(1, x, alpha, p, out=x) # x = x + alpha * p + dom.lincomb(1, r, -alpha, d, out=r) # r = r - alpha * d - sqnorm_r_new = r.norm() ** 2 + sqnorm_r_new = dom.norm(r) ** 2 beta = sqnorm_r_new / sqnorm_r_old sqnorm_r_old = sqnorm_r_new - p.lincomb(1, r, beta, p) # p = s + b * p + dom.lincomb(1, r, beta, p, out=p) # p = s + b * p if callback is not None: callback(x) @@ -246,34 +252,36 @@ def conjugate_gradient_normal(op, x, rhs, niter=1, callback=None): """ # TODO: add a book reference # TODO: update doc + dom = op.domain + ran = op.range - if x not in op.domain: + if x not in dom: raise TypeError('`x` {!r} is not in the domain of `op` {!r}' - ''.format(x, op.domain)) + ''.format(x, dom)) d = op(x) - d.lincomb(1, rhs, -1, d) # d = rhs - A x + ran.lincomb(1, rhs, -1, d, out=d) # d <- rhs - A(x) p = op.derivative(x).adjoint(d) - s = p.copy() + s = dom.copy(p) q = op.range.element() - sqnorm_s_old = s.norm() ** 2 # Only recalculate norm after update + sqnorm_s_old = dom.norm(s) ** 2 # Only recalculate norm after update for _ in range(niter): op(p, out=q) # q = A p - sqnorm_q = q.norm() ** 2 + sqnorm_q = ran.norm(q) ** 2 if sqnorm_q == 0.0: # Return if residual is 0 return a = sqnorm_s_old / sqnorm_q - x.lincomb(1, x, a, p) # x = x + a*p - d.lincomb(1, d, -a, q) # d = d - a*Ap + dom.lincomb(1, x, a, p, out=x) # x <- x + a * p + ran.lincomb(1, d, -a, q, out=d) # d <- d - a * A(p) op.derivative(p).adjoint(d, out=s) # s = A^T d - sqnorm_s_new = s.norm() ** 2 + sqnorm_s_new = dom.norm(s) ** 2 b = sqnorm_s_new / sqnorm_s_old sqnorm_s_old = sqnorm_s_new - p.lincomb(1, s, b, p) # p = s + b * p + dom.lincomb(1, s, b, p, out=p) # p = s + b * p if callback is not None: callback(x) @@ -347,18 +355,20 @@ def gauss_newton(op, x, rhs, niter, zero_seq=exp_zero_seq(2.0), callback : callable, optional Object executing code per iteration, e.g. plotting each iterate. """ - if x not in op.domain: + dom = op.domain + ran = op.range + if x not in dom: raise TypeError('`x` {!r} is not in the domain of `op` {!r}' - ''.format(x, op.domain)) + ''.format(x, dom)) - x0 = x.copy() - id_op = IdentityOperator(op.domain) - dx = op.domain.zero() + x0 = dom.copy(x) + id_op = IdentityOperator(dom) + dx = dom.zero() - tmp_dom = op.domain.element() - u = op.domain.element() - tmp_ran = op.range.element() - v = op.range.element() + tmp_dom = dom.element() + u = dom.element() + tmp_ran = ran.element() + v = ran.element() for _ in range(niter): tm = next(zero_seq) @@ -366,13 +376,13 @@ def gauss_newton(op, x, rhs, niter, zero_seq=exp_zero_seq(2.0), deriv_adjoint = deriv.adjoint # v = rhs - op(x) - deriv(x0-x) - # u = deriv.T(v) - op(x, out=tmp_ran) # eval op(x) - v.lincomb(1, rhs, -1, tmp_ran) # assign v = rhs - op(x) - tmp_dom.lincomb(1, x0, -1, x) # assign temp tmp_dom = x0 - x - deriv(tmp_dom, out=tmp_ran) # eval deriv(x0-x) - v -= tmp_ran # assign v = rhs-op(x)-deriv(x0-x) - deriv_adjoint(v, out=u) # eval/assign u = deriv.T(v) + # u = deriv.adjoint(v) + op(x, out=tmp_ran) + ran.lincomb(1, rhs, -1, tmp_ran, out=v) # v <- rhs - op(x) + dom.lincomb(1, x0, -1, x, out=tmp_dom) # tmp_dom <- x0 - x + deriv(tmp_dom, out=tmp_ran) + v -= tmp_ran + deriv_adjoint(v, out=u) # Solve equation Tikhonov regularized system # (deriv.T o deriv + tm * id_op)^-1 u = dx @@ -383,7 +393,7 @@ def gauss_newton(op, x, rhs, niter, zero_seq=exp_zero_seq(2.0), conjugate_gradient(tikh_op, dx, u, 3) # Update x - x.lincomb(1, x0, 1, dx) # x = x0 + dx + dom.lincomb(1, x0, 1, dx, out=x) # x = x0 + dx if callback is not None: callback(x) @@ -474,13 +484,13 @@ def kaczmarz(ops, x, rhs, niter, omega=1, projection=None, random=False, -------- landweber """ - domain = ops[0].domain - if any(domain != opi.domain for opi in ops): + dom = ops[0].domain + if any(opi.domain != dom for opi in ops): raise ValueError('domains of `ops` are not all equal') - if x not in domain: + if x not in dom: raise TypeError('`x` {!r} is not in the domain of `ops` {!r}' - ''.format(x, domain)) + ''.format(x, dom)) if len(ops) != len(rhs): raise ValueError('`number of `ops` {} does not match number of ' @@ -494,7 +504,7 @@ def kaczmarz(ops, x, rhs, niter, omega=1, projection=None, random=False, tmp_rans = {ran: ran.element() for ran in unique_ranges} # Single reusable element in the domain - tmp_dom = domain.element() + tmp_dom = dom.element() # Iteratively find solution for _ in range(niter): @@ -511,7 +521,7 @@ def kaczmarz(ops, x, rhs, niter, omega=1, projection=None, random=False, # Update x ops[i].derivative(x).adjoint(tmp_ran, out=tmp_dom) - x.lincomb(1, x, -omega[i], tmp_dom) + dom.lincomb(1, x, -omega[i], tmp_dom, out=x) if projection is not None: projection(x) diff --git a/odl/solvers/iterative/statistical.py b/odl/solvers/iterative/statistical.py index d7a987d93c0..edcebae2c1a 100644 --- a/odl/solvers/iterative/statistical.py +++ b/odl/solvers/iterative/statistical.py @@ -97,7 +97,7 @@ def osmlem(op, x, data, niter, callback=None, **kwargs): used as starting point of the iteration, and its values are updated in each iteration step. The initial value of ``x`` should be non-negative. - data : sequence of ``op.range`` `element-like` + data : sequence of ``op.range`` `element-like` Right-hand sides of the equation defining the inverse problem. niter : int Number of iterations. @@ -177,8 +177,8 @@ def osmlem(op, x, data, niter, callback=None, **kwargs): for _ in range(niter): for i in range(n_ops): op[i](x, out=tmp_ran[i]) - tmp_ran[i].ufuncs.maximum(eps, out=tmp_ran[i]) - data[i].divide(tmp_ran[i], out=tmp_ran[i]) + np.maximum(tmp_ran[i], eps, out=tmp_ran[i]) + op[i].range.divide(data[i], tmp_ran[i], out=tmp_ran[i]) op[i].adjoint(tmp_ran[i], out=tmp_dom) tmp_dom /= sensitivities[i] diff --git a/odl/solvers/nonsmooth/admm.py b/odl/solvers/nonsmooth/admm.py index 3a76428d016..5338237c5c2 100644 --- a/odl/solvers/nonsmooth/admm.py +++ b/odl/solvers/nonsmooth/admm.py @@ -93,10 +93,12 @@ def admm_linearized(x, f, g, L, tau, sigma, niter, **kwargs): if not isinstance(L, Operator): raise TypeError('`op` {!r} is not an `Operator` instance' ''.format(L)) + primal_space = L.domain + dual_space = L.range - if x not in L.domain: + if x not in primal_space: raise OpDomainError('`x` {!r} is not in the domain of `op` {!r}' - ''.format(x, L.domain)) + ''.format(x, primal_space)) tau, tau_in = float(tau), tau if tau <= 0: @@ -117,13 +119,13 @@ def admm_linearized(x, f, g, L, tau, sigma, niter, **kwargs): raise TypeError('`callback` {} is not callable'.format(callback)) # Initialize range variables - z = L.range.zero() - u = L.range.zero() + z = dual_space.zero() + u = dual_space.zero() # Temporary for Lx + u [- z] tmp_ran = L(x) # Temporary for L^*(Lx + u - z) - tmp_dom = L.domain.element() + tmp_dom = primal_space.element() # Store proximals since their initialization may involve computation prox_tau_f = f.proximal(tau) @@ -137,7 +139,7 @@ def admm_linearized(x, f, g, L, tau, sigma, niter, **kwargs): L.adjoint(tmp_ran, out=tmp_dom) # x <- x^k - (tau/sigma) L^*(Lx^k + u^k - z^k) - x.lincomb(1, x, -tau / sigma, tmp_dom) + primal_space.lincomb(1, x, -tau / sigma, tmp_dom, out=x) # x^(k+1) <- prox[tau*f](x) prox_tau_f(x, out=x) diff --git a/odl/solvers/nonsmooth/alternating_dual_updates.py b/odl/solvers/nonsmooth/alternating_dual_updates.py index 07befcced70..bad5564e305 100644 --- a/odl/solvers/nonsmooth/alternating_dual_updates.py +++ b/odl/solvers/nonsmooth/alternating_dual_updates.py @@ -190,7 +190,7 @@ def adupdates(x, g, L, stepsize, inner_stepsizes, niter, random=False, tmp_ran = tmp_rans[L[j].range] proxs[j](arg, out=tmp_ran) x -= 1.0 / stepsize * L[j].adjoint(tmp_ran - duals[j]) - duals[j].assign(tmp_ran) + ranges[j].assign(duals[j], tmp_ran) if callback is not None and callback_loop == 'inner': callback(x) @@ -229,4 +229,4 @@ def adupdates_simple(x, g, L, stepsize, inner_stepsizes, niter, else duals[j] + stepsize * np.asarray(inner_stepsizes[j]) * L[j](x))) x -= 1.0 / stepsize * L[j].adjoint(dual_tmp - duals[j]) - duals[j].assign(dual_tmp) + ranges[j].assign(duals[j], dual_tmp) diff --git a/odl/solvers/nonsmooth/difference_convex.py b/odl/solvers/nonsmooth/difference_convex.py index 535fbd2ca9e..af8345db265 100644 --- a/odl/solvers/nonsmooth/difference_convex.py +++ b/odl/solvers/nonsmooth/difference_convex.py @@ -160,7 +160,10 @@ def prox_dca(x, f, g, niter, gamma, callback=None): raise ValueError('`f.domain` and `g.domain` need to be equal, but ' '{} != {}'.format(space, g.domain)) for _ in range(niter): - f.proximal(gamma)(x.lincomb(1, x, gamma, g.gradient(x)), out=x) + f.proximal(gamma)( + space.lincomb(1, x, gamma, g.gradient(x), out=x), + out=x, + ) if callback is not None: callback(x) @@ -248,10 +251,15 @@ def doubleprox_dc(x, y, f, phi, g, K, niter, gamma, mu, callback=None): g_convex_conj = g.convex_conj for _ in range(niter): - f.proximal(gamma)(x.lincomb(1, x, - gamma, K.adjoint(y) - phi.gradient(x)), - out=x) - g_convex_conj.proximal(mu)(y.lincomb(1, y, mu, K(x)), out=y) + tmp_dom = K.adjoint(y) - phi.gradient(x) # TODO(kohr-h): optimize + f.proximal(gamma)( + primal_space.lincomb(1, x, gamma, tmp_dom, out=x), + out=x, + ) + g_convex_conj.proximal(mu)( + dual_space.lincomb(1, y, mu, K(x), out=y), + out=y, + ) if callback is not None: callback(x) diff --git a/odl/solvers/nonsmooth/douglas_rachford.py b/odl/solvers/nonsmooth/douglas_rachford.py index c512cc2b159..36eb33982c4 100644 --- a/odl/solvers/nonsmooth/douglas_rachford.py +++ b/odl/solvers/nonsmooth/douglas_rachford.py @@ -13,6 +13,7 @@ import numpy as np from odl.operator import Operator +from odl.solvers.functional.functional import Functional __all__ = ('douglas_rachford_pd', 'douglas_rachford_pd_stepsize') @@ -126,6 +127,8 @@ def douglas_rachford_pd(x, f, g, L, niter, tau=None, sigma=None, """ # Validate input m = len(L) + if not isinstance(f, Functional): + raise TypeError('`f` must be a `Functional` instance') if not all(isinstance(op, Operator) for op in L): raise ValueError('`L` not a sequence of operators') if not all(op.is_linear for op in L): @@ -160,15 +163,16 @@ def douglas_rachford_pd(x, f, g, L, niter, tau=None, sigma=None, raise TypeError('got unexpected keyword arguments: {}'.format(kwargs)) # Pre-allocate values + dom = f.domain v = [Li.range.zero() for Li in L] - p1 = x.space.zero() + p1 = dom.zero() p2 = [Li.range.zero() for Li in L] - z1 = x.space.zero() + z1 = dom.zero() # Save a bit of memory: z2 elements are local to the loop where they # are used rans = {Li.range for Li in L} z2 = {ran: ran.zero() for ran in rans} - w1 = x.space.zero() + w1 = dom.zero() w2 = [Li.range.zero() for Li in L] for k in range(niter): @@ -183,19 +187,19 @@ def douglas_rachford_pd(x, f, g, L, niter, tau=None, sigma=None, Li.adjoint(vi, out=p1) z1 += p1 - z1.lincomb(1, x, -tau / 2, z1) + dom.lincomb(1, x, -tau / 2, z1, out=z1) else: - z1.assign(x) + dom.assign(z1, x) f.proximal(tau)(z1, out=p1) # Now p1 = prox[tau*f](x - tau/2 * sum(Li^* vi)) # Temporary z1 is no longer needed # w1 = 2 * p1 - x - w1.lincomb(2, p1, -1, x) + dom.lincomb(2, p1, -1, x, out=w1) # Part 1 of x += lam(k) * (z1 - p1) - x.lincomb(1, x, -lam_k, p1) + dom.lincomb(1, x, -lam_k, p1, out=x) # Now p1 is free to use as temporary; however, since p1 holds the # current primal iterate (not x) we call the callback here already @@ -210,10 +214,10 @@ def douglas_rachford_pd(x, f, g, L, niter, tau=None, sigma=None, for i in range(m): # Compute p2[i] = prox[sigma * g^*](v[i] + sigma[i]/2 * L[i](w1)) L[i](w1, out=p2[i]) - p2[i].lincomb(1, v[i], sigma[i] / 2, p2[i]) + L[i].range.lincomb(1, v[i], sigma[i] / 2, p2[i], out=p2[i]) prox_cc_g[i](sigma[i])(p2[i], out=p2[i]) # w2[i] = 2 * p2[i] - v[i] - w2[i].lincomb(2, p2[i], -1, v[i]) + L[i].range.lincomb(2, p2[i], -1, v[i], out=w2[i]) if len(L) > 0: # Compute p1 = sum(Li.adjoint(w2i) for Li, w2i in zip(L, w2)) @@ -224,30 +228,100 @@ def douglas_rachford_pd(x, f, g, L, niter, tau=None, sigma=None, Li.adjoint(w2i, out=z1) p1 += z1 else: - p1.set_zero() + dom.set_zero(p1) # z1 = w2 - tau/2 * p1 - z1.lincomb(1, w1, -tau / 2, p1) + dom.lincomb(1, w1, -tau / 2, p1, out=z1) # Part 2 of x += lam(k) * (z1 - p1) - x.lincomb(1, x, lam_k, z1) + dom.lincomb(1, x, lam_k, z1, out=x) # p1 = 2 * z1 - w1 - p1.lincomb(2, z1, -1, w1) + dom.lincomb(2, z1, -1, w1, out=p1) for i in range(m): z2i = z2[L[i].range] # Compute # z2[i] = prox[sigma[i] * l[i]^*](w2[i] + sigma[i]/2 * L[i](p1)) L[i](p1, out=z2i) - z2i.lincomb(1, w2[i], sigma[i] / 2, L[i](p1)) + L[i].range.lincomb(1, w2[i], sigma[i] / 2, L[i](p1), out=z2i) # prox_cc_l is the identity if `l is None`, thus omitted in that # case if l is not None: prox_cc_l[i](sigma[i])(z2i, out=z2i) # Compute v[i] += lam(k) * (z2[i] - p2[i]) - v[i].lincomb(1, v[i], lam_k, z2i) - v[i].lincomb(1, v[i], -lam_k, p2[i]) + L[i].range.lincomb(1, v[i], lam_k, z2i, out=v[i]) + L[i].range.lincomb(1, v[i], -lam_k, p2[i], out=v[i]) + + # The final result is actually in p1 according to the algorithm, so we need + # to assign here + dom.assign(x, p1) + + +def douglas_rachford_pd_simple(x, f, g, L, niter, tau=None, sigma=None, + callback=None, **kwargs): + r"""Simple version of the Douglas-Rachford PD splitting algorithm. + + This is a non-optimized version mainly intended for testing. + """ + m = len(L) + tau, sigma = douglas_rachford_pd_stepsize(L, tau, sigma) + assert len(sigma) == m + + prox_cc_g = [gi.convex_conj.proximal for gi in g] + + l = kwargs.pop('l', None) + assert l is None or len(l) == m + if l is not None: + prox_cc_l = [li.convex_conj.proximal for li in l] + + lam_in = kwargs.pop('lam', 1.0) + lam = lam_in if callable(lam_in) else lambda _: lam_in + + dom = f.domain + v = [Li.range.zero() for Li in L] + p2 = [None] * m + w2 = [None] * m + + for k in range(niter): + lam_k = lam(k) + + if len(L) > 0: + tmp_dom = sum(Li.adjoint(vi) for Li, vi in zip(L, v)) + tmp_dom = x - tau / 2 * tmp_dom + else: + tmp_dom = dom.copy(x) + + p1 = f.proximal(tau)(tmp_dom) + w1 = 2 * p1 - x + + for i in range(m): + tmp = v[i] + (sigma[i] / 2.0) * L[i](w1) + p2[i] = prox_cc_g[i](sigma[i])(tmp) + w2[i] = 2 * p2[i] - v[i] + + if len(L) > 0: + tmp_dom = sum(Li.adjoint(w2i) for Li, w2i in zip(L, w2)) + else: + tmp_dom = dom.zero() + + z1 = w1 - tau / 2 * tmp_dom + x += lam_k * (z1 - p1) + tmp_dom = 2 * z1 - w1 + + for i in range(m): + if l is None: + z2 = w2[i] + sigma[i] / 2 * L[i](tmp_dom) + else: + tmp = w2[i] + (sigma[i] / 2.0) * L[i](tmp_dom) + z2 = prox_cc_l[i](sigma[i])(tmp) + + v[i] += lam_k * (z2 - p2[i]) + + if callback is not None: + callback(p1) + + dom.assign(x, p1) def _operator_norms(L): diff --git a/odl/solvers/nonsmooth/forward_backward.py b/odl/solvers/nonsmooth/forward_backward.py index 4f1f26376bb..d55d0fb5ef3 100644 --- a/odl/solvers/nonsmooth/forward_backward.py +++ b/odl/solvers/nonsmooth/forward_backward.py @@ -165,15 +165,16 @@ def forward_backward_pd(x, f, g, L, h, tau, sigma, niter, raise TypeError('unexpected keyword argument: {}'.format(kwargs)) # Pre-allocate values + dom = f.domain v = [Li.range.zero() for Li in L] - y = x.space.zero() + y = dom.zero() for k in range(niter): x_old = x tmp_1 = grad_h(x) + sum(Li.adjoint(vi) for Li, vi in zip(L, v)) prox_f(tau)(x - tau * tmp_1, out=x) - y.lincomb(2.0, x, -1, x_old) + dom.lincomb(2.0, x, -1, x_old, out=y) for i in range(m): if l is not None: diff --git a/odl/solvers/nonsmooth/primal_dual_hybrid_gradient.py b/odl/solvers/nonsmooth/primal_dual_hybrid_gradient.py index ae7aea3cdd9..25952e9151b 100644 --- a/odl/solvers/nonsmooth/primal_dual_hybrid_gradient.py +++ b/odl/solvers/nonsmooth/primal_dual_hybrid_gradient.py @@ -192,6 +192,9 @@ def pdhg(x, f, g, L, niter, tau=None, sigma=None, **kwargs): raise TypeError('`f.domain` {!r} must equal `op.domain` {!r}' ''.format(f.domain, L.domain)) + primal_space = f.domain + dual_space = g.domain + # Step size parameters tau, sigma = pdhg_stepsize(L, tau, sigma) @@ -234,18 +237,21 @@ def pdhg(x, f, g, L, niter, tau=None, sigma=None, **kwargs): # Initialize the relaxation variable x_relax = kwargs.pop('x_relax', None) if x_relax is None: - x_relax = x.copy() + x_relax = primal_space.copy(x) elif x_relax not in L.domain: - raise TypeError('`x_relax` {} is not in the domain of ' - '`L` {}'.format(x_relax.space, L.domain)) + raise TypeError( + '`x_relax` {} is not in the domain of `L` {}' + ''.format(x_relax, L.domain) + ) # Initialize the dual variable y = kwargs.pop('y', None) if y is None: y = L.range.zero() elif y not in L.range: - raise TypeError('`y` {} is not in the range of `L` ' - '{}'.format(y.space, L.range)) + raise TypeError( + '`y` {} is not in the range of `L` {}'.format(y, L.range) + ) # Get the proximals proximal_primal = f.proximal @@ -257,20 +263,20 @@ def pdhg(x, f, g, L, niter, tau=None, sigma=None, **kwargs): proximal_primal_tau = proximal_primal(tau) # Temporary copy to store previous iterate - x_old = x.space.element() + x_old = primal_space.element() # Temporaries - dual_tmp = L.range.element() - primal_tmp = L.domain.element() + dual_tmp = dual_space.element() + primal_tmp = primal_space.element() for _ in range(niter): # Copy required for relaxation - x_old.assign(x) + primal_space.assign(x_old, x) # Gradient ascent in the dual variable y - # Compute dual_tmp = y + sigma * L(x_relax) + # dual_tmp <- y + sigma * L(x_relax) L(x_relax, out=dual_tmp) - dual_tmp.lincomb(1, y, sigma, dual_tmp) + dual_space.lincomb(1, y, sigma, dual_tmp, out=dual_tmp) # Apply the dual proximal if not proximal_constant: @@ -278,9 +284,9 @@ def pdhg(x, f, g, L, niter, tau=None, sigma=None, **kwargs): proximal_dual_sigma(dual_tmp, out=y) # Gradient descent in the primal variable x - # Compute primal_tmp = x + (- tau) * L.derivative(x).adjoint(y) + # primal_tmp <- x - tau * L.derivative(x).adjoint(y) L.derivative(x).adjoint(y, out=primal_tmp) - primal_tmp.lincomb(1, x, -tau, primal_tmp) + primal_space.lincomb(1, x, -tau, primal_tmp, out=primal_tmp) # Apply the primal proximal if not proximal_constant: @@ -299,7 +305,8 @@ def pdhg(x, f, g, L, niter, tau=None, sigma=None, **kwargs): sigma *= theta # Over-relaxation in the primal variable x - x_relax.lincomb(1 + theta, x, -theta, x_old) + # x_relax <- x + theta * (x - x_old) + primal_space.lincomb(1 + theta, x, -theta, x_old, out=x_relax) if callback is not None: callback(x) diff --git a/odl/solvers/nonsmooth/proximal_gradient_solvers.py b/odl/solvers/nonsmooth/proximal_gradient_solvers.py index 88291196893..d3e5f100dda 100644 --- a/odl/solvers/nonsmooth/proximal_gradient_solvers.py +++ b/odl/solvers/nonsmooth/proximal_gradient_solvers.py @@ -87,6 +87,7 @@ def proximal_gradient(x, f, g, gamma, niter, callback=None, **kwargs): raise TypeError('`x` {!r} is not in the domain of `g` {!r}' ''.format(x, g.domain)) + space = f.domain gamma, gamma_in = float(gamma), gamma if gamma <= 0: raise ValueError('`gamma` must be positive, got {}'.format(gamma_in)) @@ -102,16 +103,16 @@ def proximal_gradient(x, f, g, gamma, niter, callback=None, **kwargs): g_grad = g.gradient # Create temporary - tmp = x.space.element() + tmp = space.element() for k in range(niter): lam_k = lam(k) - # x - gamma grad_g (x) - tmp.lincomb(1, x, -gamma, g_grad(x)) + # tmp <- x - gamma * grad_g(x) + space.lincomb(1, x, -gamma, g_grad(x), out=tmp) - # Update x - x.lincomb(1 - lam_k, x, lam_k, f_prox(tmp)) + # x <- (1 - lambda_k) * x + lambda_k * prox_f(tmp) + space.lincomb(1 - lam_k, x, lam_k, f_prox(tmp), out=x) if callback is not None: callback(x) @@ -176,6 +177,7 @@ def accelerated_proximal_gradient(x, f, g, gamma, niter, callback=None, raise TypeError('`x` {!r} is not in the domain of `g` {!r}' ''.format(x, g.domain)) + space = f.domain gamma, gamma_in = float(gamma), gamma if gamma <= 0: raise ValueError('`gamma` must be positive, got {}'.format(gamma_in)) @@ -188,8 +190,8 @@ def accelerated_proximal_gradient(x, f, g, gamma, niter, callback=None, g_grad = g.gradient # Create temporary - tmp = x.space.element() - y = x.copy() + tmp = space.element() + y = space.copy(x) t = 1 for k in range(niter): @@ -197,17 +199,17 @@ def accelerated_proximal_gradient(x, f, g, gamma, niter, callback=None, t, t_old = (1 + np.sqrt(1 + 4 * t ** 2)) / 2, t alpha = (t_old - 1) / t - # x - gamma grad_g (y) - tmp.lincomb(1, y, -gamma, g_grad(y)) + # tmp <- x - gamma * grad_g(y) + space.lincomb(1, y, -gamma, g_grad(y), out=tmp) - # Store old x value in y - y.assign(x) + # y <- x + space.assign(y, x) - # Update x + # x <- prox_f(tmp) f_prox(tmp, out=x) - # Update y - y.lincomb(1 + alpha, x, -alpha, y) + # y <- (1 + alpha) * x - alpha * y + space.lincomb(1 + alpha, x, -alpha, y, out=y) if callback is not None: callback(x) diff --git a/odl/solvers/nonsmooth/proximal_operators.py b/odl/solvers/nonsmooth/proximal_operators.py index 48310c2e68b..95672289033 100644 --- a/odl/solvers/nonsmooth/proximal_operators.py +++ b/odl/solvers/nonsmooth/proximal_operators.py @@ -21,28 +21,40 @@ Foundations and Trends in Optimization, 1 (2014), pp 127-239. """ -from __future__ import print_function, division, absolute_import +from __future__ import absolute_import, division, print_function + import numpy as np from odl.operator import ( - Operator, IdentityOperator, ConstantOperator, DiagonalOperator, - PointwiseNorm, MultiplyOperator) + ConstantOperator, DiagonalOperator, IdentityOperator, MultiplyOperator, + Operator, PointwiseNorm) from odl.space import ProductSpace -from odl.set.space import LinearSpaceElement - -__all__ = ('combine_proximals', 'proximal_convex_conj', 'proximal_translation', - 'proximal_arg_scaling', 'proximal_quadratic_perturbation', - 'proximal_composition', 'proximal_const_func', - 'proximal_box_constraint', 'proximal_nonnegativity', - 'proximal_l1', 'proximal_convex_conj_l1', - 'proximal_l2', 'proximal_convex_conj_l2', - 'proximal_linfty', 'proximal_convex_conj_linfty', - 'proj_simplex', 'proj_l1', - 'proximal_l2_squared', 'proximal_convex_conj_l2_squared', - 'proximal_l1_l2', 'proximal_convex_conj_l1_l2', - 'proximal_convex_conj_kl', 'proximal_convex_conj_kl_cross_entropy', - 'proximal_huber') +__all__ = ( + 'combine_proximals', + 'proximal_convex_conj', + 'proximal_translation', + 'proximal_arg_scaling', + 'proximal_quadratic_perturbation', + 'proximal_composition', + 'proximal_const_func', + 'proximal_box_constraint', + 'proximal_l1', + 'proximal_convex_conj_l1', + 'proximal_l2', + 'proximal_convex_conj_l2', + 'proximal_linfty', + 'proximal_convex_conj_linfty', + 'proj_simplex', + 'proj_l1', + 'proximal_l2_squared', + 'proximal_convex_conj_l2_squared', + 'proximal_l1_l2', + 'proximal_convex_conj_l1_l2', + 'proximal_convex_conj_kl', + 'proximal_convex_conj_kl_cross_entropy', + 'proximal_huber', +) def combine_proximals(*factory_list): @@ -157,10 +169,10 @@ def convex_conj_prox_factory(sigma): # prox_factory accepts stepsize objects of the type given by sigma. space = prox_factory(sigma).domain - mult_inner = MultiplyOperator(1.0 / sigma, domain=space, range=space) - mult_outer = MultiplyOperator(sigma, domain=space, range=space) + mult_inner = MultiplyOperator(space, 1 / sigma) + mult_outer = MultiplyOperator(space, sigma) result = (IdentityOperator(space) - - mult_outer * prox_factory(1.0 / sigma) * mult_inner) + mult_outer * prox_factory(1 / sigma) * mult_inner) return result return convex_conj_prox_factory @@ -216,8 +228,12 @@ def translation_prox_factory(sigma): The proximal operator of ``s * F( . - y)`` where ``s`` is the step size """ - return (ConstantOperator(y) + prox_factory(sigma) * - (IdentityOperator(y.space) - ConstantOperator(y))) + prox = prox_factory(sigma) + space = prox.domain + return ( + ConstantOperator(space, y) + + prox * (IdentityOperator(space) - ConstantOperator(space, y)) + ) return translation_prox_factory @@ -276,7 +292,7 @@ def proximal_arg_scaling(prox_factory, scaling): # unconditionally, but only if the scaling factor is a scalar: if np.isscalar(scaling): if scaling == 0: - return proximal_const_func(prox_factory(1.0).domain) + return proximal_const_func(prox_factory(1).domain) elif scaling.imag != 0: raise ValueError("Complex scaling not supported.") else: @@ -301,15 +317,17 @@ def arg_scaling_prox_factory(sigma): scaling_square = scaling * scaling prox = prox_factory(sigma * scaling_square) space = prox.domain - mult_inner = MultiplyOperator(scaling, domain=space, range=space) - mult_outer = MultiplyOperator(1 / scaling, domain=space, range=space) - return mult_outer * prox * mult_inner + return ( + MultiplyOperator(space, scaling) + * prox + * MultiplyOperator(space, 1 / scaling) + ) return arg_scaling_prox_factory def proximal_quadratic_perturbation(prox_factory, a, u=None): - r"""Calculate the proximal of function F(x) + a * \|x\|^2 + . + r"""Calculate the proximal of function F(x) + a * ||x||^2 + . Parameters ---------- @@ -359,12 +377,9 @@ def proximal_quadratic_perturbation(prox_factory, a, u=None): """ a = float(a) if a < 0: - raise ValueError('scaling parameter muts be non-negative, got {}' - ''.format(a)) - - if u is not None and not isinstance(u, LinearSpaceElement): - raise TypeError('`u` must be `None` or a `LinearSpaceElement` ' - 'instance, got {!r}.'.format(u)) + raise ValueError( + 'scaling parameter must be non-negative, got {}'.format(a) + ) def quadratic_perturbation_prox_factory(sigma): r"""Create proximal for the quadratic perturbation with a given sigma. @@ -385,17 +400,21 @@ def quadratic_perturbation_prox_factory(sigma): else: sigma = np.asarray(sigma) - const = 1.0 / np.sqrt(sigma * 2.0 * a + 1) + const = 1 / np.sqrt(2 * sigma * a + 1) prox = proximal_arg_scaling(prox_factory, const)(sigma) + space = prox.domain if u is not None: - return (MultiplyOperator(const, domain=u.space, range=u.space) * - prox * - (MultiplyOperator(const, domain=u.space, range=u.space) - - sigma * const * u)) + return ( + MultiplyOperator(space, const) + * prox + * (MultiplyOperator(space, const) - sigma * const * u) + ) else: - space = prox.domain - return (MultiplyOperator(const, domain=space, range=space) * - prox * MultiplyOperator(const, domain=space, range=space)) + return ( + MultiplyOperator(space, const) + * prox + * MultiplyOperator(space, const) + ) return quadratic_perturbation_prox_factory @@ -468,8 +487,7 @@ def proximal_composition_factory(sigma): Id = IdentityOperator(operator.domain) Ir = IdentityOperator(operator.range) prox_muf = proximal(mu * sigma) - return (Id + - (1.0 / mu) * operator.adjoint * ((prox_muf - Ir) * operator)) + return Id + (1 / mu) * operator.adjoint * ((prox_muf - Ir) * operator) return proximal_composition_factory @@ -607,42 +625,21 @@ def __init__(self, sigma): def _call(self, x, out): """Apply the operator to ``x`` and store the result in ``out``.""" + F = self.domain.ufuncs + if lower is not None and upper is None: - x.ufuncs.maximum(lower, out=out) + F.maximum(x, lower, out=out) elif lower is None and upper is not None: - x.ufuncs.minimum(upper, out=out) + F.minimum(x, upper, out=out) elif lower is not None and upper is not None: - x.ufuncs.maximum(lower, out=out) - out.ufuncs.minimum(upper, out=out) + F.maximum(x, lower, out=out) + F.minimum(out, upper, out=out) else: - out.assign(x) + space.assign(out, x) return ProxOpBoxConstraint -def proximal_nonnegativity(space): - """Function to create the proximal operator of ``G(x) = ind(x >= 0)``. - - Function for the proximal operator of the functional ``G(x)=ind(x >= 0)`` - to be initialized. - - Parameters - ---------- - space : `LinearSpace` - Domain of the functional G(x) - - Returns - ------- - prox_factory : function - Factory for the proximal operator to be initialized - - See Also - -------- - proximal_box_constraint - """ - return proximal_box_constraint(space, lower=0) - - def proximal_convex_conj_l2(space, lam=1, g=None): r"""Proximal operator factory of the convex conj of the l2-norm/distance. @@ -785,28 +782,28 @@ def _call(self, x, out): eps = np.finfo(dtype).resolution * 10 if g is None: - x_norm = x.norm() * (1 + eps) + x_norm = self.domain.norm(x) * (1 + eps) if x_norm > 0: step = self.sigma * lam / x_norm else: step = np.infty - if step < 1.0: - out.lincomb(1.0 - step, x) + if step < 1: + self.range.lincomb(1 - step, x, out=out) else: - out.set_zero() + self.range.lincomb(0, out, out=out) else: - x_norm = (x - g).norm() * (1 + eps) + x_norm = self.domain.norm(x - g) * (1 + eps) if x_norm > 0: step = self.sigma * lam / x_norm else: step = np.infty - if step < 1.0: - out.lincomb(1.0 - step, x, step, g) + if step < 1: + self.range.lincomb(1 - step, x, step, g, out=out) else: - out.assign(g) + self.range.assign(out, g) return ProximalL2 @@ -890,28 +887,35 @@ def _call(self, x, out): """Apply the operator to ``x`` and store the result in ``out``""" # (x - sig*g) / (1 + sig/(2 lam)) sig = self.sigma + F = space.ufuncs + if np.isscalar(sig): if g is None: - out.lincomb(1 / (1 + 0.5 * sig / lam), x) + space.lincomb(1 / (1 + 0.5 * sig / lam), x, out=out) else: - out.lincomb(1 / (1 + 0.5 * sig / lam), x, - -sig / (1 + 0.5 * sig / lam), g) + space.lincomb( + 1 / (1 + 0.5 * sig / lam), + x, + -sig / (1 + 0.5 * sig / lam), + g, + out=out, + ) + elif sig in space: if g is None: - x.divide(1 + 0.5 / lam * sig, out=out) + F.divide(x, 1 + 0.5 / lam * sig, out=out) else: if x is out: # Can't write to `out` since old `x` is still needed - tmp = sig.multiply(g) - out.lincomb(1, x, -1, tmp) + tmp = F.multiply(sig, g) + space.lincomb(1, x, -1, tmp, out=out) else: - sig.multiply(g, out=out) - out.lincomb(1, x, -1, out) - out.divide(1 + 0.5 / lam * sig, out=out) + F.multiply(sig, g, out=out) + space.lincomb(1, x, -1, out, out=out) + F.divide(out, 1 + 0.5 / lam * sig, out=out) + else: - raise RuntimeError( - '`sigma` is neither a scalar nor a space element.' - ) + raise RuntimeError('bad `sig` {!r}'.format(sig)) return ProximalConvexConjL2Squared @@ -984,24 +988,32 @@ def _call(self, x, out): """Apply the operator to ``x`` and store the result in ``out``""" # (x + 2*sig*lam*g) / (1 + 2*sig*lam)) sig = self.sigma + F = space.ufuncs + if np.isscalar(sig): if g is None: - out.lincomb(1 / (1 + 2 * sig * lam), x) + space.lincomb(1 / (1 + 2 * sig * lam), x, out=out) else: - out.lincomb(1 / (1 + 2 * sig * lam), x, - 2 * sig * lam / (1 + 2 * sig * lam), g) + space.lincomb( + 1 / (1 + 2 * sig * lam), + x, + 2 * sig * lam / (1 + 2 * sig * lam), + g, + out=out, + ) + else: # sig in space if g is None: - x.divide(1 + 2 * sig * lam, out=out) + F.divide(x, 1 + 2 * sig * lam, out=out) else: if x is out: # Can't write to `out` since old `x` is still needed - tmp = sig.multiply(2 * lam * g) - out.lincomb(1, x, 1, tmp) + tmp = F.multiply(sig, 2 * lam * g) + space.lincomb(1, x, 1, tmp, out=out) else: - sig.multiply(2 * lam * g, out=out) - out.lincomb(1, x, 1, out) - out.divide(1 + 2 * sig * lam, out=out) + F.multiply(sig, 2 * lam * g, out=out) + space.lincomb(1, x, 1, out, out=out) + F.divide(out, 1 + 2 * sig * lam, out=out) return ProximalL2Squared @@ -1108,28 +1120,29 @@ def __init__(self, sigma): def _call(self, x, out): """Return ``self(x, out=out)``.""" - # lam * (x - sig * g) / max(lam, |x - sig * g|) + F = space.ufuncs + + # Compute lam * (x - sig * g) / max(lam, |x - sig * g|) # diff = x - sig * g - if g is not None: - diff = self.domain.element() - diff.lincomb(1, x, -self.sigma, g) - else: + if g is None: + # Handle aliased `x` and `out` + # This is necessary since we write to both `diff` and `out` if x is out: - # Handle aliased `x` and `out` - # This is necessary since we write to both `diff` and - # `out`. - diff = x.copy() + diff = space.copy(x) else: diff = x + else: + diff = space.element() + space.lincomb(1, x, -self.sigma, g, out=diff) # out = max( |x-sig*g|, lam ) / lam - diff.ufuncs.absolute(out=out) - out.ufuncs.maximum(lam, out=out) + F.absolute(diff, out=out) + F.maximum(out, lam, out=out) out /= lam # out = diff / ... - diff.divide(out, out=out) + F.divide(diff, out, out=out) return ProximalConvexConjL1 @@ -1216,24 +1229,26 @@ def __init__(self, sigma): def _call(self, x, out): """Return ``self(x, out=out)``.""" - # lam * (x - sig * g) / max(lam, |x - sig * g|) + pwnorm = PointwiseNorm(space, exponent=2) + Fb = pwnorm.range.ufuncs + + # Compute lam * (x - sig * g) / max(lam, |x - sig * g|) # diff = x - sig * g if g is not None: diff = self.domain.element() - diff.lincomb(1, x, -self.sigma, g) + space.lincomb(1, x, -self.sigma, g, out=diff) else: diff = x # denom = max( |x-sig*g|_2, lam ) / lam (|.|_2 pointwise) - pwnorm = PointwiseNorm(self.domain, exponent=2) denom = pwnorm(diff) - denom.ufuncs.maximum(lam, out=denom) + Fb.maximum(denom, lam, out=denom) denom /= lam # Pointwise division for out_i, diff_i in zip(out, diff): - diff_i.divide(denom, out=out_i) + Fb.divide(diff_i, denom, out=out_i) return ProximalConvexConjL1L2 @@ -1324,27 +1339,31 @@ def __init__(self, sigma): def _call(self, x, out): """Return ``self(x, out=out)``.""" + F = space.ufuncs + # diff = x - g - if g is not None: - diff = x - g - else: + # Handle aliased `x` and `out` (original `x` needed later) + if g is None: if x is out: - # Handle aliased `x` and `out` (original `x` needed later) - diff = x.copy() - else: + x_old = space.copy(x) diff = x + else: + diff = x_old = x + else: + x_old = x + diff = x - g # We write the operator as # x - (x - g) / max(|x - g| / sig*lam, 1) - denom = diff.ufuncs.absolute() + denom = F.absolute(diff) denom /= self.sigma * lam - denom.ufuncs.maximum(1, out=denom) + F.maximum(denom, 1, out=denom) # out = (x - g) / denom - diff.ufuncs.divide(denom, out=out) + F.divide(diff, denom, out=out) # out = x - ... - out.lincomb(1, x, -1, out) + space.lincomb(1, x_old, -1, out, out=out) return ProximalL1 @@ -1421,29 +1440,31 @@ def __init__(self, sigma): def _call(self, x, out): """Return ``self(x, out=out)``.""" + pwnorm = PointwiseNorm(self.domain, exponent=2) + Fb = pwnorm.range.ufuncs + # diff = x - g if g is not None: diff = x - g else: if x is out: # Handle aliased `x` and `out` (original `x` needed later) - diff = x.copy() + diff = space.copy(x) else: diff = x # We write the operator as # x - (x - g) / max(|x - g|_2 / sig*lam, 1) - pwnorm = PointwiseNorm(self.domain, exponent=2) denom = pwnorm(diff) denom /= self.sigma * lam - denom.ufuncs.maximum(1, out=denom) + Fb.maximum(denom, 1, out=denom) # out = (x - g) / denom for out_i, diff_i in zip(out, diff): - diff_i.divide(denom, out=out_i) + Fb.divide(diff_i, denom, out=out_i) # out = x - ... - out.lincomb(1, x, -1, out) + space.lincomb(1, x, -1, out, out=out) return ProximalL1L2 @@ -1497,10 +1518,10 @@ def _call(self, x, out): radius = 1 if x is out: - x = x.copy() + x = space.copy(x) - proj_l1(x, radius, out) - out.lincomb(-1, out, 1, x) + proj_l1(self.domain, x, radius, out=out) + self.range.lincomb(-1, out, 1, x, out=out) return ProximalLInfty @@ -1511,7 +1532,7 @@ def proximal_convex_conj_linfty(space): Implements the proximal operator of the convex conjugate of the functional :: - F(x) = \|x\|_\infty + F(x) = ||x||_inf with ``x`` in ``space``. @@ -1566,14 +1587,14 @@ def _call(self, x, out): return ProximalConvexConjLinfty -def proj_l1(x, radius=1, out=None): +def proj_l1(space, x, radius=1, out=None): r"""Projection onto l1-ball. - Projection onto:: + Projection onto - ``{ x \in X | ||x||_1 \leq r}`` + \Big\{ x \in X\ \Big|\ \|x\|_1 \leq r \Big\} - with ``r`` being the radius. + with :math:`r` being the radius. Parameters ---------- @@ -1590,40 +1611,39 @@ def proj_l1(x, radius=1, out=None): Notes ----- The projection onto an l1-ball can be computed by projection onto a - simplex, see [D+2008] for details. + simplex, see `[D+2008] `_ for + details. References ---------- - [D+2008] Duchi, J., Shalev-Shwartz, S., Singer, Y., and Chandra, T. + [D+2008] Duchi, J, Shalev-Shwartz, S, Singer, Y, and Chandra, T. *Efficient Projections onto the L1-ball for Learning in High dimensions*. - ICML 2008, pp. 272-279. http://doi.org/10.1145/1390156.1390191 + ICML 2008, pp. 272-279. See Also -------- proximal_linfty : proximal for l-infinity norm proj_simplex : projection onto simplex """ - - if out is None: - out = x.space.element() - - u = x.ufuncs.absolute() - v = x.ufuncs.sign() - proj_simplex(u, radius, out) - out *= v - + F = space.ufuncs + sign_x = F.sign(x) + F.absolute(x, out=out) + proj_simplex(space, out, radius, out=out) + out *= sign_x return out -def proj_simplex(x, diameter=1, out=None): +def proj_simplex(space, x, diameter=1, out=None): r"""Projection onto simplex. - Projection onto:: + Projection onto + + .. math:: - ``{ x \in X | x_i \geq 0, \sum_i x_i = r}`` + \Big\{ x \in X\ \Big|\ x_i \geq 0,\ \sum_i x_i = r\Big\} with :math:`r` being the diameter. It is computed by the formula proposed - in [D+2008]. + in `[D+2008] `_. Parameters ---------- @@ -1640,35 +1660,34 @@ def proj_simplex(x, diameter=1, out=None): Notes ----- The projection onto a simplex is not of closed-form but can be solved by a - non-iterative algorithm, see [D+2008] for details. + non-iterative algorithm, see `[D+2008] + `_ for details. References ---------- - [D+2008] Duchi, J., Shalev-Shwartz, S., Singer, Y., and Chandra, T. + [D+2008] Duchi, J, Shalev-Shwartz, S, Singer, Y, and Chandra, T. *Efficient Projections onto the L1-ball for Learning in High dimensions*. - ICML 2008, pp. 272-279. http://doi.org/10.1145/1390156.1390191 + ICML 2008, pp. 272-279. See Also -------- proj_l1 : projection onto l1-norm ball """ - if out is None: - out = x.space.element() + if isinstance(space, ProductSpace): + raise NotImplementedError('product spaces not supported') - # sort values in descending order - x_sor = x.asarray().flatten() - x_sor.sort() - x_sor = x_sor[::-1] + # Sort flattened array in descending order + x_flat = x.ravel() + x_sorted = np.sort(x_flat)[::-1] - # find critical index - j = np.arange(1, x.size + 1) - x_avrg = (1 / j) * (np.cumsum(x_sor) - diameter) - crit = x_sor - x_avrg - i = np.argwhere(crit >= 0).flatten().max() - - # output is a shifted and thresholded version of the input - out[:] = np.maximum(x - x_avrg[i], 0) + # Find critical index + j = np.arange(1, x_sorted.size + 1) + x_avg = (1 / j) * (np.cumsum(x_sorted) - diameter) + crit = x_sorted - x_avg + i = np.max(np.argwhere(crit >= 0).squeeze()) + # Output is a shifted and thresholded version of the input + out = np.maximum(x - x_avg[i], 0, out=out) return out @@ -1775,27 +1794,29 @@ def __init__(self, sigma): def _call(self, x, out): """Return ``self(x, out=out)``.""" - # (x + lam - sqrt((x - lam)^2 + 4*lam*sig*g)) / 2 + F = space.ufuncs + + # Compute (x + lam - sqrt((x - lam)^2 + 4*lam*sig*g)) / 2 # out = (x - lam)^2 if x is out: # Handle aliased `x` and `out` (need original `x` later on) - x = x.copy() + x = space.copy(x) else: - out.assign(x) + space.assign(out, x) out -= lam - out.ufuncs.square(out=out) + F.square(out, out=out) # out = ... + 4*lam*sigma*g # If g is None, it is taken as the one element if g is None: - out += 4.0 * lam * self.sigma + out += 4 * lam * self.sigma else: - out.lincomb(1, out, 4.0 * lam * self.sigma, g) + space.lincomb(1, out, 4 * lam * self.sigma, g, out=out) # out = x - sqrt(...) + lam - out.ufuncs.sqrt(out=out) - out.lincomb(1, x, -1, out) + F.sqrt(out, out=out) + space.lincomb(1, x, -1, out, out=out) out += lam # out = 1/2 * ... @@ -1911,22 +1932,28 @@ def _call(self, x, out): # Lazy import to improve `import odl` time import scipy.special + F = space.ufuncs + if g is None: # If g is None, it is taken as the one element # Different branches of lambertw is not an issue, see Notes - lambw = scipy.special.lambertw( - (self.sigma / lam) * np.exp(x / lam)) + arg = (self.sigma / lam) * F.exp(x / lam) else: - # Different branches of lambertw is not an issue, see Notes - lambw = scipy.special.lambertw( - (self.sigma / lam) * g * np.exp(x / lam)) + arg = (self.sigma / lam) * g * F.exp(x / lam) - if not np.issubsctype(self.domain.dtype, np.complexfloating): - lambw = lambw.real - - lambw = x.space.element(lambw) + if isinstance(space, ProductSpace): + if space.is_real: + lambw = space.apply( + lambda v: scipy.special.lambertw(v).real, arg + ) + else: + lambw = space.apply(scipy.special.lambertw, arg) + else: + lambw = scipy.special.lambertw(arg) + if space.dtype.kind != 'c': + lambw = lambw.real - out.lincomb(1, x, -lam, lambw) + space.lincomb(1, x, -lam, lambw, out=out) return ProximalConvexConjKLCrossEntropy @@ -1977,16 +2004,35 @@ def __init__(self, sigma): def _call(self, x, out): """Return ``self(x, out=out)``.""" if isinstance(self.domain, ProductSpace): - norm = PointwiseNorm(self.domain, 2)(x) + norm_op = PointwiseNorm(self.domain, 2) + Fb = norm_op.range.ufuncs + norm = norm_op(x) + + # Piecewise definition, threshold at ||x|| == gamma + sigma + mask = Fb.less_equal(norm, gamma + self.sigma) + + def branch1(oi, i): + oi[mask] = (gamma / (gamma + self.sigma)) * x[i][mask] + + space.apply2(branch1, out) + + Fb.logical_not(mask, out=mask) + sign_x = space.ufuncs.sign(x) + + def branch2(oi, i): + oi[mask] = x[i][mask] - self.sigma * sign_x[i][mask] + + space.apply2(branch2, out) + else: - norm = x.ufuncs.absolute() + F = space.ufuncs - mask = norm.ufuncs.less_equal(gamma + self.sigma) - out[mask] = gamma / (gamma + self.sigma) * x[mask] + norm = F.absolute(x) + mask = F.less_equal(norm, gamma + self.sigma) + out[mask] = gamma / (gamma + self.sigma) * x[mask] - mask.ufuncs.logical_not(out=mask) - sign_x = x.ufuncs.sign() - out[mask] = x[mask] - self.sigma * sign_x[mask] + F.logical_not(mask, out=mask) + out[mask] = x[mask] - self.sigma * F.sign(x)[mask] return out diff --git a/odl/solvers/smooth/gradient.py b/odl/solvers/smooth/gradient.py index 79e5c4504c0..c9f65bcc927 100644 --- a/odl/solvers/smooth/gradient.py +++ b/odl/solvers/smooth/gradient.py @@ -79,10 +79,13 @@ def steepest_descent(f, x, line_search=1.0, maxiter=1000, tol=1e-16, [GNS2009] Griva, I, Nash, S G, and Sofer, A. *Linear and nonlinear optimization*. Siam, 2009. """ + space = f.domain grad = f.gradient - if x not in grad.domain: - raise TypeError('`x` {!r} is not in the domain of `grad` {!r}' - ''.format(x, grad.domain)) + if x not in space: + raise TypeError( + 'expected `x in f.domain`, but {!r} is not in {!r}' + ''.format(x, space) + ) if not callable(line_search): line_search = ConstantLineSearch(line_search) @@ -91,12 +94,12 @@ def steepest_descent(f, x, line_search=1.0, maxiter=1000, tol=1e-16, for _ in range(maxiter): grad(x, out=grad_x) - dir_derivative = -grad_x.norm() ** 2 + dir_derivative = -grad.range.norm(grad_x) ** 2 if np.abs(dir_derivative) < tol: return # we have converged - step = line_search(x, -grad_x, dir_derivative) - x.lincomb(1, x, -step, grad_x) + step = line_search(x, -grad_x, dir_derivative) + space.lincomb(1, x, -step, grad_x, out=x) # x <- x - step * grad if projection is not None: projection(x) @@ -155,26 +158,30 @@ def adam(f, x, learning_rate=1e-3, beta1=0.9, beta2=0.999, eps=1e-8, *Adam: A Method for Stochastic Optimization*, ICLR 2015. """ grad = f.gradient - if x not in grad.domain: - raise TypeError('`x` {!r} is not in the domain of `grad` {!r}' - ''.format(x, grad.domain)) + space = f.domain - m = grad.domain.zero() - v = grad.domain.zero() + if x not in space: + raise TypeError( + '`x` {!r} is not in the domain {!r} of `f`'.format(x, space) + ) - grad_x = grad.range.element() + m = space.zero() + v = space.zero() + + gx = space.element() for _ in range(maxiter): - grad(x, out=grad_x) + grad(x, out=gx) - if grad_x.norm() < tol: + if space.norm(gx) < tol: return - m.lincomb(beta1, m, 1 - beta1, grad_x) - v.lincomb(beta2, v, 1 - beta2, grad_x ** 2) - + # m = beta1 * m + (1 - beta1) * grad(x) + space.lincomb(beta1, m, 1 - beta1, gx, out=m) + # v = beta2 * v + (1 - beta2) * grad(x) ** 2 + space.lincomb(beta2, v, 1 - beta2, gx ** 2, out=v) step = learning_rate * np.sqrt(1 - beta2) / (1 - beta1) - - x.lincomb(1, x, -step, m / (np.sqrt(v) + eps)) + # x = x - step * m / (sqrt(v) + eps) + space.lincomb(1, x, -step, m / (np.sqrt(v) + eps), out=x) if callback is not None: callback(x) diff --git a/odl/solvers/smooth/newton.py b/odl/solvers/smooth/newton.py index e5149d8e116..5e3ed33d309 100644 --- a/odl/solvers/smooth/newton.py +++ b/odl/solvers/smooth/newton.py @@ -18,11 +18,13 @@ __all__ = ('newtons_method', 'bfgs_method', 'broydens_method') -def _bfgs_direction(s, y, x, hessinv_estimate=None): +def _bfgs_direction(space, s, y, x, hessinv_estimate=None): r"""Compute ``Hn^-1(x)`` for the L-BFGS method. Parameters ---------- + space : `LinearSpace` + Space in which the direction should be determined. s : sequence of `LinearSpaceElement` The ``s`` coefficients in the BFGS update, see Notes. y : sequence of `LinearSpaceElement` @@ -52,30 +54,32 @@ def _bfgs_direction(s, y, x, hessinv_estimate=None): """ assert len(s) == len(y) - r = x.copy() + r = space.copy(x) alphas = np.zeros(len(s)) rhos = np.zeros(len(s)) for i in reversed(range(len(s))): - rhos[i] = 1.0 / y[i].inner(s[i]) - alphas[i] = rhos[i] * (s[i].inner(r)) - r.lincomb(1, r, -alphas[i], y[i]) + rhos[i] = 1.0 / space.inner(y[i], s[i]) + alphas[i] = rhos[i] * space.inner(s[i], r) + space.lincomb(1, r, -alphas[i], y[i], out=r) if hessinv_estimate is not None: r = hessinv_estimate(r) for i in range(len(s)): - beta = rhos[i] * (y[i].inner(r)) - r.lincomb(1, r, alphas[i] - beta, s[i]) + beta = rhos[i] * space.inner(y[i], r) + space.lincomb(1, r, alphas[i] - beta, s[i], out=r) return r -def _broydens_direction(s, y, x, hessinv_estimate=None, impl='first'): +def _broydens_direction(space, s, y, x, hessinv_estimate=None, impl='first'): r"""Compute ``Hn^-1(x)`` for Broydens method. Parameters ---------- + space : `LinearSpace` + Space in which the direction should be determined. s : sequence of `LinearSpaceElement`'s' The ``s`` coefficients in the Broydens update, see Notes. y : sequence of `LinearSpaceElement`'s' @@ -111,13 +115,13 @@ def _broydens_direction(s, y, x, hessinv_estimate=None, impl='first'): if hessinv_estimate is not None: r = hessinv_estimate(x) else: - r = x.copy() + r = space.copy(x) for i in range(len(s)): if impl == 'first': - r.lincomb(1, r, y[i].inner(r), s[i]) + space.lincomb(1, r, space.inner(y[i], r), s[i], out=r) elif impl == 'second': - r.lincomb(1, r, y[i].inner(x), s[i]) + space.lincomb(1, r, space.inner(y[i], x), s[i], out=r) else: raise RuntimeError('unknown `impl`') @@ -196,24 +200,27 @@ def newtons_method(f, x, line_search=1.0, maxiter=1000, tol=1e-16, optimization*. Siam, 2009. """ # TODO: update doc + space = f.domain grad = f.gradient - if x not in grad.domain: - raise TypeError('`x` {!r} is not in the domain of `f` {!r}' - ''.format(x, grad.domain)) + if x not in space: + raise TypeError( + 'expected `x in f.domain`, but {!r} is not in {!r}' + ''.format(x, space) + ) if not callable(line_search): line_search = ConstantLineSearch(line_search) if cg_iter is None: - # Motivated by that if it is Ax = b, x and b in Rn, it takes at most n - # iterations to solve with cg - cg_iter = grad.domain.size + # For the problem Ax = b, x and b in Rn, it takes at most n + # iterations to solve with CG, thus this default + cg_iter = space.size - # TODO: optimize by using lincomb and avoiding to create copies + # TODO: optimize by avoiding to create copies for _ in range(maxiter): # Initialize the search direction to 0 - search_direction = x.space.zero() + search_direction = space.zero() # Compute hessian (as operator) and gradient in the current point hessian = grad.derivative(x) @@ -224,20 +231,21 @@ def newtons_method(f, x, line_search=1.0, maxiter=1000, tol=1e-16, try: hessian_inverse = hessian.inverse except NotImplementedError: - conjugate_gradient(hessian, search_direction, - -deriv_in_point, cg_iter) + conjugate_gradient( + hessian, search_direction, -deriv_in_point, cg_iter + ) else: hessian_inverse(-deriv_in_point, out=search_direction) # Computing step length - dir_deriv = search_direction.inner(deriv_in_point) + dir_deriv = space.inner(search_direction, deriv_in_point) if np.abs(dir_deriv) <= tol: return step_length = line_search(x, search_direction, dir_deriv) - # Updating - x += step_length * search_direction + # x <- x + step_length * search_direction + space.lincomb(1, x, step_length, search_direction, out=x) if callback is not None: callback(x) @@ -304,10 +312,13 @@ def bfgs_method(f, x, line_search=1.0, maxiter=1000, tol=1e-15, num_store=None, [GNS2009] Griva, I, Nash, S G, and Sofer, A. *Linear and nonlinear optimization*. Siam, 2009. """ + space = f.domain grad = f.gradient - if x not in grad.domain: - raise TypeError('`x` {!r} is not in the domain of `grad` {!r}' - ''.format(x, grad.domain)) + if x not in space: + raise TypeError( + 'expected `x in f.domain`, but {!r} is not in {!r}' + ''.format(x, space) + ) if not callable(line_search): line_search = ConstantLineSearch(line_search) @@ -318,26 +329,26 @@ def bfgs_method(f, x, line_search=1.0, maxiter=1000, tol=1e-15, num_store=None, grad_x = grad(x) for i in range(maxiter): # Determine a stepsize using line search - search_dir = -_bfgs_direction(ss, ys, grad_x, hessinv_estimate) - dir_deriv = search_dir.inner(grad_x) + search_dir = -_bfgs_direction(space, ss, ys, grad_x, hessinv_estimate) + dir_deriv = space.inner(search_dir, grad_x) if np.abs(dir_deriv) == 0: return # we found an optimum step = line_search(x, direction=search_dir, dir_derivative=dir_deriv) - # Update x + # Update x (not with `lincomb`, since the update is used again) x_update = search_dir x_update *= step x += x_update grad_x, grad_diff = grad(x), grad_x - # grad_diff = grad(x) - grad(x_old) - grad_diff.lincomb(-1, grad_diff, 1, grad_x) + # grad_diff <- grad(x) - grad(x_old) + space.lincomb(-1, grad_diff, 1, grad_x, out=grad_diff) - y_inner_s = grad_diff.inner(x_update) + y_inner_s = space.inner(grad_diff, x_update) # Test for convergence if np.abs(y_inner_s) < tol: - if grad_x.norm() < tol: + if space.norm(grad_x) < tol: return else: # Reset if needed @@ -417,10 +428,13 @@ def broydens_method(f, x, line_search=1.0, impl='first', maxiter=1000, [Kva1991] Kvaalen, E. *A faster Broyden method*. BIT Numerical Mathematics 31 (1991), pp 369--372. """ + space = f.domain grad = f.gradient - if x not in grad.domain: - raise TypeError('`x` {!r} is not in the domain of `grad` {!r}' - ''.format(x, grad.domain)) + if x not in space: + raise TypeError( + 'expected `x in f.domain`, but {!r} is not in {!r}' + ''.format(x, space) + ) if not callable(line_search): line_search = ConstantLineSearch(line_search) @@ -435,55 +449,58 @@ def broydens_method(f, x, line_search=1.0, impl='first', maxiter=1000, grad_x = grad(x) for i in range(maxiter): - # find step size - search_dir = -_broydens_direction(ss, ys, grad_x, - hessinv_estimate, impl) - dir_deriv = search_dir.inner(grad_x) + # Find step size + search_dir = -_broydens_direction( + space, ss, ys, grad_x, hessinv_estimate, impl + ) + dir_deriv = space.inner(search_dir, grad_x) if np.abs(dir_deriv) == 0: - return # we found an optimum + # Critical point found + return step = line_search(x, search_dir, dir_deriv) - # update x + # Update x x_update = step * search_dir x += x_update - # compute new gradient + # Compute new gradient grad_x, grad_x_old = grad(x), grad_x delta_grad = grad_x - grad_x_old - # update hessian. + # Update Hessian. # TODO: reuse from above - v = _broydens_direction(ss, ys, delta_grad, hessinv_estimate, - impl) + v = _broydens_direction( + space, ss, ys, delta_grad, hessinv_estimate, impl + ) if impl == 'first': - divisor = x_update.inner(v) + denom = space.inner(x_update, v) # Test for convergence - if np.abs(divisor) < tol: - if grad_x.norm() < tol: + if np.abs(denom) < tol: + if space.norm(grad_x) < tol: return else: # Reset if needed ys = [] ss = [] continue - u = (x_update - v) / divisor + u = (x_update - v) / denom ss.append(u) ys.append(x_update) elif impl == 'second': - divisor = delta_grad.inner(delta_grad) + denom = space.inner(delta_grad, delta_grad) # Test for convergence - if np.abs(divisor) < tol: - if grad_x.norm() < tol: + if np.abs(denom) < tol: + if space.norm(grad_x) < tol: return else: # Reset if needed ys = [] ss = [] continue - u = (x_update - v) / divisor + u = (x_update - v) / denom ss.append(u) ys.append(delta_grad) diff --git a/odl/solvers/smooth/nonlinear_cg.py b/odl/solvers/smooth/nonlinear_cg.py index 1dfc5d92cba..ea6bd977bb4 100644 --- a/odl/solvers/smooth/nonlinear_cg.py +++ b/odl/solvers/smooth/nonlinear_cg.py @@ -78,9 +78,12 @@ def conjugate_gradient_nonlinear(f, x, line_search=1.0, maxiter=1000, nreset=0, odl.solvers.iterative.iterative.conjugate_gradient_normal : Equivalent solver but for least-squares problem with linear operator """ - if x not in f.domain: - raise TypeError('`x` {!r} is not in the domain of `f` {!r}' - ''.format(x, f.domain)) + space = f.domain + if x not in space: + raise TypeError( + 'expected `x in f.domain`, but {!r} is not in {!r}' + ''.format(x, space) + ) if not callable(line_search): line_search = ConstantLineSearch(line_search) @@ -91,11 +94,11 @@ def conjugate_gradient_nonlinear(f, x, line_search=1.0, maxiter=1000, nreset=0, for _ in range(nreset + 1): # First iteration is done without beta dx = -f.gradient(x) - dir_derivative = -dx.inner(dx) + dir_derivative = -space.inner(dx, dx) if abs(dir_derivative) < tol: return a = line_search(x, dx, dir_derivative) - x.lincomb(1, x, a, dx) # x = x + a * dx + space.lincomb(1, x, a, dx, out=x) # x <- x + a * dx s = dx # for 'HS' and 'DY' beta methods @@ -105,13 +108,19 @@ def conjugate_gradient_nonlinear(f, x, line_search=1.0, maxiter=1000, nreset=0, # Calculate "beta" if beta_method == 'FR': - beta = dx.inner(dx) / dx_old.inner(dx_old) + beta = space.inner(dx, dx) / space.inner(dx_old, dx_old) elif beta_method == 'PR': - beta = dx.inner(dx - dx_old) / dx_old.inner(dx_old) + beta = ( + space.inner(dx, dx - dx_old) + / space.inner(dx_old, dx_old) + ) elif beta_method == 'HS': - beta = - dx.inner(dx - dx_old) / s.inner(dx - dx_old) + beta = ( + -space.inner(dx, dx - dx_old) + / space.inner(s, dx - dx_old) + ) elif beta_method == 'DY': - beta = - dx.inner(dx) / s.inner(dx - dx_old) + beta = -space.inner(dx, dx) / space.inner(s, dx - dx_old) else: raise RuntimeError('unknown ``beta_method``') @@ -119,17 +128,17 @@ def conjugate_gradient_nonlinear(f, x, line_search=1.0, maxiter=1000, nreset=0, beta = max(0, beta) # Update search direction - s.lincomb(1, dx, beta, s) # s = dx + beta * s + space.lincomb(1, dx, beta, s, out=s) # s <- dx + beta * s # Find optimal step along s - dir_derivative = -dx.inner(s) + dir_derivative = -space.inner(dx, s) if abs(dir_derivative) <= tol: return a = line_search(x, s, dir_derivative) # Update position - x.lincomb(1, x, a, s) # x = x + a * s + space.lincomb(1, x, a, s, out=x) # x <- x + a * s if callback is not None: callback(x) diff --git a/odl/solvers/util/callback.py b/odl/solvers/util/callback.py index 8d1fba4a8ff..924bb00cb94 100644 --- a/odl/solvers/util/callback.py +++ b/odl/solvers/util/callback.py @@ -19,10 +19,11 @@ import numpy as np +from odl.set import LinearSpace from odl.util import signature_string __all__ = ('Callback', 'CallbackStore', 'CallbackApply', 'CallbackPrintTiming', - 'CallbackPrintIteration', 'CallbackPrint', 'CallbackPrintNorm', + 'CallbackPrintIteration', 'CallbackPrint', 'CallbackShow', 'CallbackSaveToDisk', 'CallbackSleep', 'CallbackShowConvergence', 'CallbackPrintHardwareUsage', 'CallbackProgressBar', 'save_animation') @@ -94,7 +95,7 @@ def __mul__(self, other): >>> operator = odl.ScalingOperator(r3, 2.0) >>> composed_callback = callback * operator >>> composed_callback([1, 2, 3]) - rn(3).element([ 2., 4., 6.]) + array([ 2., 4., 6.]) """ return _CallbackCompose(self, other) @@ -219,7 +220,7 @@ def __init__(self, results=None, function=None, step=1): Store the norm of the results: - >>> norm_function = lambda x: x.norm() + >>> norm_function = lambda x: odl.rn(3).norm(x) >>> callback = CallbackStore() * norm_function """ self.results = [] if results is None else results @@ -552,19 +553,6 @@ def __repr__(self): return '{}({})'.format(self.__class__.__name__, inner_str) -class CallbackPrintNorm(Callback): - - """Callback for printing the current norm.""" - - def __call__(self, result): - """Print the current norm.""" - print("norm = {}".format(result.norm())) - - def __repr__(self): - """Return ``repr(self)``.""" - return '{}()'.format(self.__class__.__name__) - - class CallbackShow(Callback): """Callback for showing iterates. @@ -575,13 +563,15 @@ class CallbackShow(Callback): odl.space.base_tensors.Tensor.show """ - def __init__(self, title=None, step=1, saveto=None, **kwargs): + def __init__(self, space, title=None, step=1, saveto=None, **kwargs): """Initialize a new instance. Additional parameters are passed through to the ``show`` method. Parameters ---------- + space : `LinearSpace` + Space that implements the ``show`` method for displaying elements. title : str, optional Format string for the title of the displayed figure. The title name is generated as :: @@ -619,24 +609,36 @@ def __init__(self, title=None, step=1, saveto=None, **kwargs): -------- Show the result of each iterate: - >>> callback = CallbackShow() + >>> space = odl.uniform_discr([-1, -1], [1, 1], (100, 100)) + >>> callback = CallbackShow(space) Show and save every fifth iterate in ``png`` format, overwriting the previous one: - >>> callback = CallbackShow(step=5, - ... saveto='my_path/my_iterate.png') + >>> callback = CallbackShow( + ... space, step=5, saveto='my_path/my_iterate.png' + ... ) Show and save each fifth iterate in ``png`` format, indexing the files with the iteration number: - >>> callback = CallbackShow(step=5, - ... saveto='my_path/my_iterate_{}.png') + >>> callback = CallbackShow( + ... space, step=5, saveto='my_path/my_iterate_{}.png' + ... ) Pass additional arguments to ``show``: - >>> callback = CallbackShow(step=5, clim=[0, 1]) + >>> callback = CallbackShow(space, step=5, clim=[0, 1]) """ + if not isinstance(space, LinearSpace): + raise TypeError( + '`space` must be a `LinearSpace`, got {!r}'.format(space) + ) + if not hasattr(space, 'show'): + raise ValueError('`space` must have a `show` method') + + self.space = space + if title is None: self.title = 'Iterate {}' else: @@ -649,29 +651,24 @@ def __init__(self, title=None, step=1, saveto=None, **kwargs): self.step = step self.fig = kwargs.pop('fig', None) self.iter = 0 - self.space_of_last_x = None self.kwargs = kwargs def __call__(self, x): """Show the current iterate.""" # Check if we should update the figure in-place - x_space = x.space - update_in_place = (self.space_of_last_x == x_space) - self.space_of_last_x = x_space - if self.iter % self.step == 0: title = self.title_formatter(self.iter) if self.saveto is None: - self.fig = x.show(title, fig=self.fig, - update_in_place=update_in_place, - **self.kwargs) - + self.fig = self.space.show( + x, title, fig=self.fig, update_in_place=True, **self.kwargs + ) else: saveto = self.saveto_formatter(self.iter) - self.fig = x.show(title, fig=self.fig, - update_in_place=update_in_place, - saveto=saveto, **self.kwargs) + self.fig = self.space.show( + x, title, fig=self.fig, update_in_place=True, + saveto=saveto, **self.kwargs + ) self.iter += 1 @@ -679,7 +676,6 @@ def reset(self): """Set `iter` to 0 and create a new figure.""" self.iter = 0 self.fig = None - self.space_of_last_x = None def __repr__(self): """Return ``repr(self)``.""" @@ -825,7 +821,7 @@ class CallbackShowConvergence(Callback): """Displays a convergence plot.""" - def __init__(self, functional, title='convergence', logx=False, logy=False, + def __init__(self, functional, title='Convergence', logx=False, logy=False, **kwargs): """Initialize a new instance. diff --git a/odl/solvers/util/steplen.py b/odl/solvers/util/steplen.py index 95e87d0d748..a384387b4c1 100644 --- a/odl/solvers/util/steplen.py +++ b/odl/solvers/util/steplen.py @@ -64,7 +64,7 @@ class BacktrackingLineSearch(LineSearch): optimization*. Siam, 2009. """ - def __init__(self, function, tau=0.5, discount=0.01, alpha=1.0, + def __init__(self, function, space=None, tau=0.5, discount=0.01, alpha=1.0, max_num_iter=None, estimate_step=False): """Initialize a new instance. @@ -74,6 +74,10 @@ def __init__(self, function, tau=0.5, discount=0.01, alpha=1.0, The cost function of the optimization problem to be solved. If ``function`` is not a `Functional`, calling this class later requires a value for the ``dir_derivative`` argument. + space : `LinearSpace`, optional + Space in which the line search should be performed. Required if + ``function`` is not a `Functional`, otherwise defaults to + ``function.domain``. tau : float, optional The amount the step length is decreased in each iteration, as long as it does not fulfill the decrease condition. @@ -93,7 +97,7 @@ def __init__(self, function, tau=0.5, discount=0.01, alpha=1.0, Examples -------- - Create line search + Create line search for a `Functional`: >>> r3 = odl.rn(3) >>> func = odl.solvers.L2NormSquared(r3) @@ -109,12 +113,13 @@ def __init__(self, function, tau=0.5, discount=0.01, alpha=1.0, >>> func(x + step_len * d) < func(x) True - Also works with non-functionals as arguments, but then the - dir_derivative argument is mandatory + The class also works with non-functionals as arguments, but then the + ``space`` must be provided to the class constructor, and the + ``dir_derivative`` argument to the call: >>> r3 = odl.rn(3) >>> func = lambda x: x[0] ** 2 + x[1] ** 2 + x[2] ** 2 - >>> line_search = BacktrackingLineSearch(func) + >>> line_search = BacktrackingLineSearch(func, space=r3) >>> x = r3.element([1, 2, 3]) >>> d = r3.element([-1, -1, -1]) >>> dir_derivative = -12 @@ -124,7 +129,17 @@ def __init__(self, function, tau=0.5, discount=0.01, alpha=1.0, >>> func(x + step_len * d) < func(x) True """ + from odl.solvers import Functional + self.function = function + if space is None: + if isinstance(function, Functional): + space = function.domain + else: + raise TypeError( + '`space` is required if `function` is not a `Functional`' + ) + self.space = space self.tau = float(tau) self.discount = float(discount) self.estimate_step = bool(estimate_step) @@ -169,7 +184,7 @@ def __call__(self, x, direction, dir_derivative=None): raise ValueError('`dir_derivative` only optional if ' '`function.gradient exists') else: - dir_derivative = gradient(x).inner(direction) + dir_derivative = self.space.inner(gradient(x), direction) else: dir_derivative = float(dir_derivative) @@ -191,7 +206,7 @@ def __call__(self, x, direction, dir_derivative=None): 'point ({})'.format(fx, x)) # Create temporary - point = x.copy() + point = self.space.copy(x) num_iter = 0 while True: @@ -201,7 +216,7 @@ def __call__(self, x, direction, dir_derivative=None): 'sufficient decrease' ''.format(self.max_num_iter, alpha)) - point.lincomb(1, x, alpha, direction) # pt = x + alpha * direction + self.space.lincomb(1, x, alpha, direction, out=point) fval = self.function(point) if np.isnan(fval): diff --git a/odl/space/__init__.py b/odl/space/__init__.py index 59368edebf7..a5ad1878694 100644 --- a/odl/space/__init__.py +++ b/odl/space/__init__.py @@ -10,7 +10,7 @@ from __future__ import absolute_import -from . import base_tensors, entry_points, weighting +from . import base_tensors, entry_points from .npy_tensors import * from .pspace import * from .space_utils import * diff --git a/odl/space/base_tensors.py b/odl/space/base_tensors.py index 1b65a440de4..1a8e009fdf9 100644 --- a/odl/space/base_tensors.py +++ b/odl/space/base_tensors.py @@ -15,13 +15,12 @@ import numpy as np from odl.set.sets import ComplexNumbers, RealNumbers -from odl.set.space import LinearSpace, LinearSpaceElement +from odl.set.space import LinearSpace from odl.util import ( - array_str, dtype_str, indent, is_complex_floating_dtype, is_floating_dtype, + dtype_str, is_complex_floating_dtype, is_floating_dtype, 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, nullcontext + signature_string) +from odl.util.utility import TYPE_MAP_C2R, TYPE_MAP_R2C __all__ = ('TensorSpace',) @@ -291,70 +290,6 @@ def nbytes(self): """Total number of bytes in memory used by an element of this space.""" return self.size * self.itemsize - def __contains__(self, other): - """Return ``other in self``. - - Returns - ------- - contains : bool - ``True`` if ``other`` has a ``space`` attribute that is equal - to this space, ``False`` otherwise. - - Examples - -------- - Elements created with the `TensorSpace.element` method are - guaranteed to be contained in the same space: - - >>> spc = odl.tensor_space((2, 3), dtype='uint64') - >>> spc.element() in spc - True - >>> x = spc.element([[0, 1, 2], - ... [3, 4, 5]]) - >>> x in spc - True - - Sizes, data types and other essential properties characterize - spaces and decide about membership: - - >>> smaller_spc = odl.tensor_space((2, 2), dtype='uint64') - >>> y = smaller_spc.element([[0, 1], - ... [2, 3]]) - >>> y in spc - False - >>> x in smaller_spc - False - >>> other_dtype_spc = odl.tensor_space((2, 3), dtype='uint32') - >>> z = other_dtype_spc.element([[0, 1, 2], - ... [3, 4, 5]]) - >>> z in spc - False - >>> x in other_dtype_spc - False - - On the other hand, spaces are not unique: - - >>> spc2 = odl.tensor_space((2, 3), dtype='uint64') - >>> spc2 == spc - True - >>> x2 = spc2.element([[5, 4, 3], - ... [2, 1, 0]]) - >>> x2 in spc - True - >>> x in spc2 - True - - Of course, random garbage is not in the space: - - >>> spc = odl.tensor_space((2, 3), dtype='uint64') - >>> None in spc - False - >>> object in spc - False - >>> False in spc - False - """ - return getattr(other, 'space', None) == self - def __eq__(self, other): """Return ``self == other``. @@ -457,6 +392,14 @@ def one(self): """ raise NotImplementedError('abstract method') + def assign(self, out, x): + """Assign ``x`` to ``out``.""" + out[:] = x + + def set_zero(self, out): + """Set ``out`` to 0.""" + out[:] = 0 + def _multiply(self, x1, x2, out): """The entry-wise product of two tensors, assigned to ``out``. @@ -497,421 +440,29 @@ def available_dtypes(): """ raise NotImplementedError('abstract method') - @property - def element_type(self): - """Type of elements in this space: `Tensor`.""" - return Tensor - - -class Tensor(LinearSpaceElement): - - """Abstract class for representation of `TensorSpace` elements.""" - - def asarray(self, out=None): - """Extract the data of this tensor as a Numpy array. - - This method should be overridden by subclasses. - - Parameters - ---------- - out : `numpy.ndarray`, optional - Array to write the result to. - - Returns - ------- - asarray : `numpy.ndarray` - Numpy array of the same data type and shape as the space. - If ``out`` was given, the returned object is a reference - to it. - """ - raise NotImplementedError('abstract method') - - def __getitem__(self, indices): - """Return ``self[indices]``. - - This method should be overridden by subclasses. - - Parameters - ---------- - indices : index expression - Integer, slice or sequence of these, defining the positions - of the data array which should be accessed. - - Returns - ------- - values : `TensorSpace.dtype` or `Tensor` - The value(s) at the given indices. Note that depending on - the implementation, the returned object may be a (writable) - view into the original array. - """ - raise NotImplementedError('abstract method') - - def __setitem__(self, indices, values): - """Implement ``self[indices] = values``. - - This method should be overridden by subclasses. - - Parameters - ---------- - indices : index expression - Integer, slice or sequence of these, defining the positions - of the data array which should be written to. - values : scalar, `array-like` or `Tensor` - The value(s) that are to be assigned. - - If ``index`` is an integer, ``value`` must be a scalar. - - If ``index`` is a slice or a sequence of slices, ``value`` - must be broadcastable to the shape of the slice. - """ - raise NotImplementedError('abstract method') - - @property - def impl(self): - """Name of the implementation back-end of this tensor.""" - return self.space.impl - - @property - def shape(self): - """Number of elements per axis.""" - return self.space.shape - - @property - def dtype(self): - """Data type of each entry.""" - return self.space.dtype - - @property - def size(self): - """Total number of entries.""" - return self.space.size - - @property - def ndim(self): - """Number of axes (=dimensions) of this tensor.""" - return self.space.ndim - - def __len__(self): - """Return ``len(self)``. - - The length is equal to the number of entries along axis 0. - """ - return len(self.space) - - @property - def itemsize(self): - """Size in bytes of one tensor entry.""" - return self.space.itemsize - - @property - def nbytes(self): - """Total number of bytes in memory occupied by this tensor.""" - return self.space.nbytes - - 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 : `Tensor` - Version of this element with given data type. - """ - raise NotImplementedError('abstract method') - - def __repr__(self): - """Return ``repr(self)``.""" - maxsize_full_print = 2 * np.get_printoptions()['edgeitems'] - self_str = array_str(self, nprint=maxsize_full_print) - if self.ndim == 1 and self.size <= maxsize_full_print: - return '{!r}.element({})'.format(self.space, self_str) - else: - return '{!r}.element(\n{}\n)'.format(self.space, indent(self_str)) - - def __str__(self): - """Return ``str(self)``.""" - return array_str(self) - - def __bool__(self): - """Return ``bool(self)``.""" - if self.size > 1: - raise ValueError('The truth value of an array with more than one ' - 'element is ambiguous. ' - 'Use np.any(a) or np.all(a)') - else: - return bool(self.asarray()) - - def __array__(self, dtype=None): - """Return a Numpy array from this tensor. - - Parameters - ---------- - dtype : - Specifier for the data type of the output array. - - Returns - ------- - array : `numpy.ndarray` - """ - if dtype is None: - return self.asarray() - else: - return self.asarray().astype(dtype, copy=False) - - def __array_wrap__(self, array): - """Return a new tensor wrapping the ``array``. - - Parameters - ---------- - array : `numpy.ndarray` - Array to be wrapped. - - Returns - ------- - wrapper : `Tensor` - Tensor wrapping ``array``. - """ - if array.ndim == 0: - return self.space.field.element(array) - else: - return self.space.element(array) - - def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): - """Interface to Numpy's ufunc machinery. - - This method is called by Numpy version 1.13 and higher as a single - point for the ufunc dispatch logic. An object implementing - ``__array_ufunc__`` takes over control when a `numpy.ufunc` is - called on it, allowing it to use custom implementations and - output types. - - This includes handling of in-place arithmetic like - ``npy_array += custom_obj``. In this case, the custom object's - ``__array_ufunc__`` takes precedence over the baseline - `numpy.ndarray` implementation. It will be called with - ``npy_array`` as ``out`` argument, which ensures that the - returned object is a Numpy array. For this to work properly, - ``__array_ufunc__`` has to accept Numpy arrays as ``out`` arguments. - - See the `corresponding NEP`_ and the `interface documentation`_ - for further details. See also the `general documentation on - Numpy ufuncs`_. - - .. note:: - This basic implementation casts inputs and - outputs to Numpy arrays and evaluates ``ufunc`` on those. - For `numpy.ndarray` based data storage, this incurs no - significant overhead compared to direct usage of Numpy arrays. - - For other (in particular non-local) implementations, e.g., - GPU arrays or distributed memory, overhead is significant due - to copies to CPU main memory. In those classes, the - ``__array_ufunc__`` mechanism should be overridden in favor of - a native implementations if possible. - - .. note:: - If no ``out`` parameter is provided, this implementation - just returns the raw array and does not attempt to wrap the - result in any kind of space. - - Parameters - ---------- - ufunc : `numpy.ufunc` - Ufunc that should be called on ``self``. - method : str - Method on ``ufunc`` that should be called on ``self``. - Possible values: - - ``'__call__'``, ``'accumulate'``, ``'at'``, ``'outer'``, - ``'reduce'``, ``'reduceat'`` - - input1, ..., inputN: - Positional arguments to ``ufunc.method``. - kwargs: - Keyword arguments to ``ufunc.method``. - - Returns - ------- - ufunc_result : `Tensor`, `numpy.ndarray` or tuple - Result of the ufunc evaluation. If no ``out`` keyword argument - was given, the result is a `Tensor` or a tuple - of such, depending on the number of outputs of ``ufunc``. - If ``out`` was provided, the returned object or tuple entries - refer(s) to ``out``. - - References - ---------- - .. _corresponding NEP: - https://docs.scipy.org/doc/numpy/neps/ufunc-overrides.html - - .. _interface documentation: - https://docs.scipy.org/doc/numpy/reference/arrays.classes.html\ -#numpy.class.__array_ufunc__ - - .. _general documentation on Numpy ufuncs: - https://docs.scipy.org/doc/numpy/reference/ufuncs.html - - .. _reduceat documentation: - https://docs.scipy.org/doc/numpy/reference/generated/\ -numpy.ufunc.reduceat.html - """ - # --- Process `out` --- # - - # Unwrap out if provided. The output parameters are all wrapped - # in one tuple, even if there is only one. - out_tuple = kwargs.pop('out', ()) - - # Check number of `out` args, depending on `method` - if method == '__call__' and len(out_tuple) not in (0, ufunc.nout): - raise ValueError( - "ufunc {}: need 0 or {} `out` arguments for " - "`method='__call__'`, got {}" - ''.format(ufunc.__name__, ufunc.nout, len(out_tuple))) - elif method != '__call__' and len(out_tuple) not in (0, 1): - raise ValueError( - 'ufunc {}: need 0 or 1 `out` arguments for `method={!r}`, ' - 'got {}'.format(ufunc.__name__, method, len(out_tuple))) - - # We allow our own tensors, the data container type and - # `numpy.ndarray` objects as `out` (see docs for reason for the - # latter) - valid_types = (type(self), type(self.data), np.ndarray) - if not all(isinstance(o, valid_types) or o is None - for o in out_tuple): - return NotImplemented - - # Assign to `out` or `out1` and `out2`, respectively - out = out1 = out2 = None - if len(out_tuple) == 1: - out = out_tuple[0] - elif len(out_tuple) == 2: - out1 = out_tuple[0] - out2 = out_tuple[1] - - # --- Process `inputs` --- # - - # Convert inputs that are ODL tensors or their data containers to - # Numpy arrays so that the native Numpy ufunc is called later - inputs = tuple( - np.asarray(inp) if isinstance(inp, (type(self), type(self.data))) - else inp - for inp in inputs) - - # --- Get some parameters for later --- # - - # Arguments for `writable_array` and/or space constructors - out_dtype = kwargs.get('dtype', None) - if out_dtype is None: - array_kwargs = {} - else: - array_kwargs = {'dtype': out_dtype} - - # --- Evaluate ufunc --- # - - if method == '__call__': - if ufunc.nout == 1: - # Make context for output (trivial one returns `None`) - if out is None: - out_ctx = nullcontext() - else: - out_ctx = writable_array(out, **array_kwargs) - - # Evaluate ufunc - with out_ctx as out_arr: - kwargs['out'] = out_arr - res = ufunc(*inputs, **kwargs) - - # Return result (may be a raw array or a space element) - return res - - elif ufunc.nout == 2: - # Make contexts for outputs (trivial ones return `None`) - if out1 is not None: - out1_ctx = writable_array(out1, **array_kwargs) - else: - out1_ctx = nullcontext() - if out2 is not None: - out2_ctx = writable_array(out2, **array_kwargs) - else: - out2_ctx = nullcontext() - - # Evaluate ufunc - with out1_ctx as out1_arr, out2_ctx as out2_arr: - kwargs['out'] = (out1_arr, out2_arr) - res1, res2 = ufunc(*inputs, **kwargs) - - # Return results (may be raw arrays or space elements) - return res1, res2 - - else: - raise NotImplementedError('nout = {} not supported' - ''.format(ufunc.nout)) - - else: # method != '__call__' - # Make context for output (trivial one returns `None`) - if out is None: - out_ctx = nullcontext() - else: - out_ctx = writable_array(out, **array_kwargs) - - # Evaluate ufunc method - if method == 'at': - with writable_array(inputs[0]) as inp_arr: - res = ufunc.at(inp_arr, *inputs[1:], **kwargs) - else: - with out_ctx as out_arr: - kwargs['out'] = out_arr - res = getattr(ufunc, method)(*inputs, **kwargs) - - # Return result (may be scalar, raw array or space element) - return res - - # Old ufuncs interface, will be deprecated when Numpy 1.13 becomes minimum - - @property - def ufuncs(self): - """Access to Numpy style universal functions. - - These default ufuncs are always available, but may or may not be - optimized for the specific space in use. - - .. note:: - This interface is will be deprecated when Numpy 1.13 becomes - the minimum required version. Use Numpy ufuncs directly, e.g., - ``np.sqrt(x)`` instead of ``x.ufuncs.sqrt()``. - """ - return TensorSpaceUfuncs(self) - - def show(self, title=None, method='', indices=None, force_show=False, + def show(self, elem, title=None, method='', indices=None, force_show=False, fig=None, **kwargs): """Display the function graphically. Parameters ---------- + elem : array-like + Element to display using the properties of this space. title : string, optional Set the title of the figure - method : string, optional 1d methods: - ``'plot'`` : graph plot + - ``'plot'`` : graph plot - ``'scatter'`` : scattered 2d points (2nd axis <-> value) + - ``'scatter'`` : scattered 2d points (2nd axis <-> value) 2d methods: - ``'imshow'`` : image plot with coloring according to - value, including a colorbar. + - ``'imshow'`` : image plot with coloring according to value, + including a colorbar. - ``'scatter'`` : cloud of scattered 3d points - (3rd axis <-> value) + - ``'scatter'`` : cloud of scattered 3d points (3rd axis <-> value) indices : index expression, optional Display a slice of the array instead of the full array. The @@ -922,18 +473,15 @@ def show(self, title=None, method='', indices=None, force_show=False, two axes at the "middle" along the remaining axes is shown (semantically ``[:, :, shape[2:] // 2]``). This option is mutually exclusive to ``coords``. - force_show : bool, optional Whether the plot should be forced to be shown now or deferred until later. Note that some backends always displays the plot, regardless of this value. - fig : `matplotlib.figure.Figure`, optional The figure to show in. Expected to be of same "style", as the figure given by this function. The most common use case is that ``fig`` is the return value of an earlier call to this function. - kwargs : {'figsize', 'saveto', 'clim', ...}, optional Extra keyword arguments passed on to the display method. See the Matplotlib functions for documentation of extra @@ -951,10 +499,12 @@ def show(self, title=None, method='', indices=None, force_show=False, from odl.discr import uniform_grid from odl.util.graphics import show_discrete_data + elem = self.element(elem) + # Default to showing x-y slice "in the middle" if indices is None and self.ndim >= 3: indices = tuple( - [slice(None)] * 2 + [n // 2 for n in self.space.shape[2:]] + [slice(None)] * 2 + [n // 2 for n in self.shape[2:]] ) if isinstance(indices, (Integral, slice)): @@ -975,17 +525,20 @@ def show(self, title=None, method='', indices=None, force_show=False, indices[pos + 1:]) if len(indices) < self.ndim: - raise ValueError('too few axes ({} < {})'.format(len(indices), - self.ndim)) + raise ValueError( + 'too few axes ({} < {})'.format(len(indices), self.ndim) + ) if len(indices) > self.ndim: - raise ValueError('too many axes ({} > {})'.format(len(indices), - self.ndim)) + raise ValueError( + 'too many axes ({} > {})'.format(len(indices), self.ndim) + ) # Squeeze grid and values according to the index expression - full_grid = uniform_grid([0] * self.ndim, np.array(self.shape) - 1, - self.shape) + full_grid = uniform_grid( + [0] * self.ndim, np.array(self.shape) - 1, self.shape + ) grid = full_grid[indices].squeeze() - values = self.asarray()[indices].squeeze() + values = elem[indices].squeeze() return show_discrete_data(values, grid, title=title, method=method, force_show=force_show, fig=fig, **kwargs) diff --git a/odl/space/npy_tensors.py b/odl/space/npy_tensors.py index d041a0e8f34..271e89ad712 100644 --- a/odl/space/npy_tensors.py +++ b/odl/space/npy_tensors.py @@ -12,24 +12,20 @@ from future.utils import native import ctypes +import inspect from builtins import object from functools import partial import numpy as np from odl.set.sets import ComplexNumbers, RealNumbers -from odl.set.space import LinearSpaceTypeError -from odl.space.base_tensors import Tensor, TensorSpace -from odl.space.weighting import ( - ArrayWeighting, ConstWeighting, CustomDist, CustomInner, CustomNorm, - Weighting) +from odl.space.base_tensors import TensorSpace from odl.util import ( - dtype_str, is_floating_dtype, is_numeric_dtype, is_real_dtype, nullcontext, - signature_string, writable_array) + dtype_str, is_numeric_dtype, is_real_dtype, signature_string) __all__ = ('NumpyTensorSpace',) - +getargspec = getattr(inspect, "getfullargspec", inspect.getargspec) _BLAS_DTYPES = (np.dtype('float32'), np.dtype('float64'), np.dtype('complex64'), np.dtype('complex128')) @@ -90,10 +86,6 @@ def __init__(self, shape, dtype=None, **kwargs): exponent : positive float, optional Exponent of the norm. For values other than 2.0, no inner product is defined. - - This option has no impact if either ``dist``, ``norm`` or - ``inner`` is given, or if ``dtype`` is non-numeric. - Default: 2.0 Other Parameters @@ -102,55 +94,9 @@ def __init__(self, shape, dtype=None, **kwargs): Use weighted inner product, norm, and dist. The following types are supported as ``weighting``: - ``None``: no weighting, i.e. weighting with ``1.0`` (default). - - `Weighting`: Use this weighting as-is. Compatibility - with this space's elements is not checked during init. - - ``float``: Weighting by a constant. - - array-like: Pointwise weighting by an array. - - This option cannot be combined with ``dist``, - ``norm`` or ``inner``. It also cannot be used in case of - non-numeric ``dtype``. - - dist : callable, optional - Distance function defining a metric on the space. - It must accept two `NumpyTensor` arguments and return - a non-negative real number. See ``Notes`` for - mathematical requirements. - - By default, ``dist(x, y)`` is calculated as ``norm(x - y)``. - - This option cannot be combined with ``weight``, - ``norm`` or ``inner``. It also cannot be used in case of - non-numeric ``dtype``. - - norm : callable, optional - The norm implementation. It must accept a - `NumpyTensor` argument, return a non-negative real number. - See ``Notes`` for mathematical requirements. - - By default, ``norm(x)`` is calculated as ``inner(x, x)``. - - This option cannot be combined with ``weight``, - ``dist`` or ``inner``. It also cannot be used in case of - non-numeric ``dtype``. - - inner : callable, optional - The inner product implementation. It must accept two - `NumpyTensor` arguments and return an element of the field - of the space (usually real or complex number). - See ``Notes`` for mathematical requirements. - - This option cannot be combined with ``weight``, - ``dist`` or ``norm``. It also cannot be used in case of - non-numeric ``dtype``. - - kwargs : - Further keyword arguments are passed to the weighting - classes. + - ``None``: no weighting, i.e. weighting with ``1.0`` (default). + - ``float``: Weighting by a constant. + - `array-like`: Pointwise weighting by an array. See Also -------- @@ -159,44 +105,6 @@ def __init__(self, shape, dtype=None, **kwargs): odl.space.space_utils.tensor_space : constructor for tensor spaces of arbitrary scalar data type - Notes - ----- - - A distance function or metric on a space :math:`\mathcal{X}` - is a mapping - :math:`d:\mathcal{X} \times \mathcal{X} \to \mathbb{R}` - satisfying the following conditions for all space elements - :math:`x, y, z`: - - * :math:`d(x, y) \geq 0`, - * :math:`d(x, y) = 0 \Leftrightarrow x = y`, - * :math:`d(x, y) = d(y, x)`, - * :math:`d(x, y) \leq d(x, z) + d(z, y)`. - - - A norm on a space :math:`\mathcal{X}` is a mapping - :math:`\| \cdot \|:\mathcal{X} \to \mathbb{R}` - satisfying the following conditions for all - space elements :math:`x, y`: and scalars :math:`s`: - - * :math:`\| x\| \geq 0`, - * :math:`\| x\| = 0 \Leftrightarrow x = 0`, - * :math:`\| sx\| = |s| \cdot \| x \|`, - * :math:`\| x+y\| \leq \| x\| + - \| y\|`. - - - An inner product on a space :math:`\mathcal{X}` over a field - :math:`\mathbb{F} = \mathbb{R}` or :math:`\mathbb{C}` is a - mapping - :math:`\langle\cdot, \cdot\rangle: \mathcal{X} \times - \mathcal{X} \to \mathbb{F}` - satisfying the following conditions for all - space elements :math:`x, y, z`: and scalars :math:`s`: - - * :math:`\langle x, y\rangle = - \overline{\langle y, x\rangle}`, - * :math:`\langle sx + y, z\rangle = s \langle x, z\rangle + - \langle y, z\rangle`, - * :math:`\langle x, x\rangle = 0 \Leftrightarrow x = 0`. - Examples -------- Explicit initialization with the class constructor: @@ -223,67 +131,47 @@ def __init__(self, shape, dtype=None, **kwargs): raise ValueError('`dtype` {!r} not supported' ''.format(dtype_str(dtype))) - dist = kwargs.pop('dist', None) - norm = kwargs.pop('norm', None) - inner = kwargs.pop('inner', None) weighting = kwargs.pop('weighting', None) - exponent = kwargs.pop('exponent', getattr(weighting, 'exponent', 2.0)) - - if (not is_numeric_dtype(self.dtype) and - any(x is not None for x in (dist, norm, inner, weighting))): - raise ValueError('cannot use any of `weighting`, `dist`, `norm` ' - 'or `inner` for non-numeric `dtype` {}' - ''.format(dtype)) - if exponent != 2.0 and any(x is not None for x in (dist, norm, inner)): - raise ValueError('cannot use any of `dist`, `norm` or `inner` ' - 'for exponent != 2') - # Check validity of option combination (0 or 1 may be provided) - num_extra_args = sum(a is not None - for a in (dist, norm, inner, weighting)) - if num_extra_args > 1: - raise ValueError('invalid combination of options `weighting`, ' - '`dist`, `norm` and `inner`') - - # Set the weighting - if weighting is not None: - if isinstance(weighting, Weighting): - if weighting.impl != 'numpy': - raise ValueError("`weighting.impl` must be 'numpy', " - '`got {!r}'.format(weighting.impl)) - if weighting.exponent != exponent: - raise ValueError('`weighting.exponent` conflicts with ' - '`exponent`: {} != {}' - ''.format(weighting.exponent, exponent)) - self.__weighting = weighting - else: - self.__weighting = _weighting(weighting, exponent) - - # Check (afterwards) that the weighting input was sane - if isinstance(self.weighting, NumpyTensorSpaceArrayWeighting): - if self.weighting.array.dtype == object: - raise ValueError('invalid `weighting` argument: {}' - ''.format(weighting)) - elif not np.can_cast(self.weighting.array.dtype, self.dtype): - raise ValueError( - 'cannot cast from `weighting` data type {} to ' - 'the space `dtype` {}' - ''.format(dtype_str(self.weighting.array.dtype), - dtype_str(self.dtype))) - if self.weighting.array.shape != self.shape: - raise ValueError('array-like weights must have same ' - 'shape {} as this space, got {}' - ''.format(self.shape, - self.weighting.array.shape)) - - elif dist is not None: - self.__weighting = NumpyTensorSpaceCustomDist(dist) - elif norm is not None: - self.__weighting = NumpyTensorSpaceCustomNorm(norm) - elif inner is not None: - self.__weighting = NumpyTensorSpaceCustomInner(inner) + if weighting is not None and not is_numeric_dtype(self.dtype): + raise TypeError( + 'cannot use `weighting` with non-numeric `dtype` {}' + ''.format(self.dtype) + ) + exponent = kwargs.pop('exponent', 2.0) + + # Exponent and weighting + self.__exponent = float(exponent) + + if weighting is None: + weighting = 1.0 + + if np.isscalar(weighting): + if weighting <= 0: + raise ValueError( + 'scalar `weighting` must be positive, got {}' + ''.format(weighting) + ) + self.__weighting = float(weighting) + self.__weighting_type = 'const' else: - # No weighting, i.e., weighting with constant 1.0 - self.__weighting = NumpyTensorSpaceConstWeighting(1.0, exponent) + weighting = np.atleast_1d(weighting) + if weighting.shape != self.shape: + raise ValueError( + '`weighting` array must have the same shape as this ' + 'space, but {} != {}' + ''.format(weighting.shape, self.shape) + ) + if not is_real_dtype(weighting.dtype): + raise ValueError( + '`weighting.dtype` must be real, got array with dtype {}' + ''.format(dtype_str(weighting.dtype)) + ) + self.__weighting = weighting + self.__weighting_type = 'array' + + # Caching + self.__ufuncs = None + self.__reduce = None # Make sure there are no leftover kwargs if kwargs: @@ -301,20 +189,23 @@ def default_order(self): @property def weighting(self): - """This space's weighting scheme.""" + """This space's weighting factor(s).""" return self.__weighting + @property + def weighting_type(self): + """This space's type of weighting.""" + return self.__weighting_type + @property def is_weighted(self): """Return ``True`` if the space is not weighted by constant 1.0.""" - return not ( - isinstance(self.weighting, NumpyTensorSpaceConstWeighting) and - self.weighting.const == 1.0) + return not (self.weighting_type == 'const' and self.weighting == 1.0) @property def exponent(self): """Exponent of the norm and the distance.""" - return self.weighting.exponent + return self.__exponent def element(self, inp=None, data_ptr=None, order=None): """Create a new element. @@ -347,7 +238,7 @@ def element(self, inp=None, data_ptr=None, order=None): Returns ------- - element : `NumpyTensor` + element : `numpy.ndarray` The new element, created from ``inp`` or from scratch. Examples @@ -359,21 +250,21 @@ def element(self, inp=None, data_ptr=None, order=None): >>> empty = space.element() >>> empty.shape (3,) - >>> empty.space - rn(3) + >>> empty in space + True >>> x = space.element([1, 2, 3]) >>> x - rn(3).element([ 1., 2., 3.]) + array([ 1., 2., 3.]) - If the input already is a `numpy.ndarray` of correct `dtype`, it - will merely be wrapped, i.e., both array and space element access - the same memory, such that mutations will affect both: + If the input already is a `numpy.ndarray` of correct `shape` and + `dtype`, a view will be created that shares memory with the original + array. Mutations will affect both: >>> arr = np.array([1, 2, 3], dtype=float) >>> elem = odl.rn(3).element(arr) >>> elem[0] = 0 >>> elem - rn(3).element([ 0., 2., 3.]) + array([ 0., 2., 3.]) >>> arr array([ 0., 2., 3.]) @@ -386,10 +277,8 @@ def element(self, inp=None, data_ptr=None, order=None): >>> ptr = arr.ctypes.data >>> y = int_space.element(data_ptr=ptr, order='F') >>> y - tensor_space((2, 3), dtype=int).element( - [[1, 2, 3], - [4, 5, 6]] - ) + array([[1, 2, 3], + [4, 5, 6]]) >>> y[0, 1] = -1 >>> arr array([[ 1, -1, 3], @@ -405,7 +294,7 @@ def element(self, inp=None, data_ptr=None, order=None): else: arr = np.empty(self.shape, dtype=self.dtype, order=order) - return self.element_type(self, arr) + return arr elif inp is None and data_ptr is not None: if order is None: @@ -417,7 +306,7 @@ def element(self, inp=None, data_ptr=None, order=None): as_numpy_array = np.ctypeslib.as_array(as_ctype_array) arr = as_numpy_array.view(dtype=self.dtype) arr = arr.reshape(self.shape, order=order) - return self.element_type(self, arr) + return arr elif inp is not None and data_ptr is None: if inp in self and order is None: @@ -431,11 +320,11 @@ def element(self, inp=None, data_ptr=None, order=None): # Make sure the result is writeable, if not make copy. # This happens for e.g. results of `np.broadcast_to()`. if not arr.flags.writeable: - arr = arr.copy() + arr = np.copy(arr.copy) if arr.shape != self.shape: raise ValueError('shape of `inp` not equal to space shape: ' '{} != {}'.format(arr.shape, self.shape)) - return self.element_type(self, arr) + return arr else: raise TypeError('cannot provide both `inp` and `data_ptr`') @@ -448,7 +337,7 @@ def zero(self): >>> space = odl.rn(3) >>> x = space.zero() >>> x - rn(3).element([ 0., 0., 0.]) + array([ 0., 0., 0.]) """ return self.element(np.zeros(self.shape, dtype=self.dtype, order=self.default_order)) @@ -461,7 +350,7 @@ def one(self): >>> space = odl.rn(3) >>> x = space.one() >>> x - rn(3).element([ 1., 1., 1.]) + array([ 1., 1., 1.]) """ return self.element(np.ones(self.shape, dtype=self.dtype, order=self.default_order)) @@ -541,7 +430,7 @@ def _lincomb(self, a, x1, b, x2, out): >>> out = space.element() >>> result = space.lincomb(1, x, 2, y, out) >>> result - rn(3).element([ 0., 1., 3.]) + array([ 0., 1., 3.]) >>> result is out True """ @@ -587,7 +476,7 @@ def _dist(self, x1, x2): >>> space_1_w.dist(x, y) 7.0 """ - return self.weighting.dist(x1, x2) + return _weighted_dist(x1, x2, self.exponent, self.weighting) def _norm(self, x): """Return the norm of ``x``. @@ -625,7 +514,7 @@ def _norm(self, x): >>> space_1_w.norm(x) 10.0 """ - return self.weighting.norm(x) + return _weighted_norm(x, self.exponent, self.weighting) def _inner(self, x1, x2): """Return the inner product of ``x1`` and ``x2``. @@ -659,7 +548,7 @@ def _inner(self, x1, x2): >>> space_w.inner(x, y) 5.0 """ - return self.weighting.inner(x1, x2) + return self.field.element(_weighted_inner(x1, x2, self.weighting)) def _multiply(self, x1, x2, out): """Compute the entry-wise product ``out = x1 * x2``. @@ -680,15 +569,15 @@ def _multiply(self, x1, x2, out): >>> x = space.element([1, 0, 3]) >>> y = space.element([-1, 1, -1]) >>> space.multiply(x, y) - rn(3).element([-1., 0., -3.]) + array([-1., 0., -3.]) >>> out = space.element() >>> result = space.multiply(x, y, out=out) >>> result - rn(3).element([-1., 0., -3.]) + array([-1., 0., -3.]) >>> result is out True """ - np.multiply(x1.data, x2.data, out=out.data) + np.multiply(x1, x2, out=out) def _divide(self, x1, x2, out): """Compute the entry-wise quotient ``x1 / x2``. @@ -709,15 +598,121 @@ def _divide(self, x1, x2, out): >>> x = space.element([2, 0, 4]) >>> y = space.element([1, 1, 2]) >>> space.divide(x, y) - rn(3).element([ 2., 0., 2.]) + array([ 2., 0., 2.]) >>> out = space.element() >>> result = space.divide(x, y, out=out) >>> result - rn(3).element([ 2., 0., 2.]) + array([ 2., 0., 2.]) >>> result is out True """ - np.divide(x1.data, x2.data, out=out.data) + np.divide(x1, x2, out=out) + + @property + def ufuncs(self): + """Access to NumPy ufuncs.""" + if self.__ufuncs is not None: + return self.__ufuncs + + class NumpyTensorSpaceUfuncs(object): + + """Accessor class for Ufuncs on tensor spaces.""" + + def __getattr__(self, name): + """Return ``self.name``.""" + attr = getattr(np, name, None) + if not isinstance(attr, np.ufunc): + raise ValueError('{!r} is not a ufunc'.format(name)) + return attr + + self.__ufuncs = NumpyTensorSpaceUfuncs() + return self.__ufuncs + + @property + def reduce(self): + """Access to NumPy reductions.""" + if self.__reduce is not None: + return self.__reduce + + class NumpyTensorSpaceReduce(object): + + """Accessor class for reductions on tensor spaces.""" + + def __getattr__(self, name): + """Return ``self.name``.""" + attr = getattr(np, name, None) + attr = getattr(attr, "_implementation", attr) # for numpy >= 1.16 + try: + spec = getargspec(attr) + except (ValueError, TypeError): + raise ValueError( + '{!r} is not a valid reduction'.format(name) + ) + if 'keepdims' not in spec.args: + raise ValueError( + '{!r} is not a valid reduction'.format(name) + ) + return attr + + self.__reduce = NumpyTensorSpaceReduce() + return self.__reduce + + def __contains__(self, other): + """Return ``other in self``. + + Returns + ------- + contains : bool + ``True`` if ``other`` has a ``space`` attribute that is equal + to this space, ``False`` otherwise. + + Examples + -------- + Elements created with the `TensorSpace.element` method are + guaranteed to be contained in the same space: + + >>> spc = odl.tensor_space((2, 3), dtype='uint64') + >>> spc.element() in spc + True + >>> x = spc.element([[0, 1, 2], + ... [3, 4, 5]]) + >>> x in spc + True + + Sizes, data types and other essential properties characterize + spaces and decide about membership: + + >>> smaller_spc = odl.tensor_space((2, 2), dtype='uint64') + >>> y = smaller_spc.element([[0, 1], + ... [2, 3]]) + >>> y in spc + False + >>> x in smaller_spc + False + >>> other_dtype_spc = odl.tensor_space((2, 3), dtype='uint32') + >>> z = other_dtype_spc.element([[0, 1, 2], + ... [3, 4, 5]]) + >>> z in spc + False + >>> x in other_dtype_spc + False + + Of course, random garbage is not in the space: + + >>> spc = odl.tensor_space((2, 3), dtype='uint64') + >>> None in spc + False + >>> object in spc + False + >>> False in spc + False + """ + # TODO: may need adaption + return ( + isinstance(other, np.ndarray) + and other.shape == self.shape + and other.dtype == self.dtype + ) def __eq__(self, other): """Return ``self == other``. @@ -754,13 +749,41 @@ def __eq__(self, other): if other is self: return True - return (super(NumpyTensorSpace, self).__eq__(other) and - self.weighting == other.weighting) + if self.weighting_type != getattr(other, 'weighting_type', None): + return False + + weighting_equal = ( + ( + self.weighting_type == 'const' + and self.weighting == other.weighting + ) or ( + self.weighting_type == 'array' + and self.weighting is other.weighting + ) + ) + + return ( + super(NumpyTensorSpace, self).__eq__(other) + and self.exponent == other.exponent + and weighting_equal + ) def __hash__(self): """Return ``hash(self)``.""" - return hash((super(NumpyTensorSpace, self).__hash__(), - self.weighting)) + if self.weighting_type == 'const': + weighting_hash = hash(self.weighting) + elif self.weighting_type == 'array': + weighting_hash = hash(self.weighting.tobytes()) + else: + raise RuntimeError + + return hash( + ( + super(NumpyTensorSpace, self).__hash__(), + self.exponent, + weighting_hash + ) + ) @property def byaxis(self): @@ -796,12 +819,12 @@ def __getitem__(self, indices): else: newshape = tuple(space.shape[i] for i in indices) - if isinstance(space.weighting, ArrayWeighting): - new_array = np.asarray(space.weighting.array[indices]) - weighting = NumpyTensorSpaceArrayWeighting( - new_array, space.weighting.exponent) - else: + if space.weighting_type == 'const': weighting = space.weighting + else: + # Can't preserve pointwise weighting, no idea how to + # remove axes + weighting = 1.0 return type(space)(newshape, space.dtype, weighting=weighting) @@ -813,6 +836,16 @@ def __repr__(self): def __repr__(self): """Return ``repr(self)``.""" + if self.weighting_type == 'const': + if self.weighting == 1.0: + weight_str = '' + else: + weight_str = 'weighting=' + str(self.weighting) + else: + weight_str = 'weighting=' + np.array2string( + self.weighting, separator=', ' + ) + if self.ndim == 1: posargs = [self.size] else: @@ -838,919 +871,11 @@ def __repr__(self): optmod = '' inner_str = signature_string(posargs, optargs, mod=['', optmod]) - weight_str = self.weighting.repr_part if weight_str: inner_str += ', ' + weight_str return '{}({})'.format(ctor_name, inner_str) - @property - def element_type(self): - """Type of elements in this space: `NumpyTensor`.""" - return NumpyTensor - - -class NumpyTensor(Tensor): - - """Representation of a `NumpyTensorSpace` element.""" - - def __init__(self, space, data): - """Initialize a new instance.""" - Tensor.__init__(self, space) - self.__data = data - - @property - def data(self): - """The `numpy.ndarray` representing the data of ``self``.""" - return self.__data - - def asarray(self, out=None): - """Extract the data of this array as a ``numpy.ndarray``. - - This method is invoked when calling `numpy.asarray` on this - tensor. - - Parameters - ---------- - out : `numpy.ndarray`, optional - Array in which the result should be written in-place. - Has to be contiguous and of the correct dtype. - - Returns - ------- - asarray : `numpy.ndarray` - Numpy array with the same data type as ``self``. If - ``out`` was given, the returned object is a reference - to it. - - Examples - -------- - >>> space = odl.rn(3, dtype='float32') - >>> x = space.element([1, 2, 3]) - >>> x.asarray() - array([ 1., 2., 3.], dtype=float32) - >>> np.asarray(x) is x.asarray() - True - >>> out = np.empty(3, dtype='float32') - >>> result = x.asarray(out=out) - >>> out - array([ 1., 2., 3.], dtype=float32) - >>> result is out - True - >>> space = odl.rn((2, 3)) - >>> space.one().asarray() - array([[ 1., 1., 1.], - [ 1., 1., 1.]]) - """ - if out is None: - return self.data - else: - out[:] = self.data - return 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 : `NumpyTensor` - Version of this element with given data type. - """ - return self.space.astype(dtype).element(self.data.astype(dtype)) - - @property - def data_ptr(self): - """A raw pointer to the data container of ``self``. - - Examples - -------- - >>> import ctypes - >>> space = odl.tensor_space(3, dtype='uint16') - >>> x = space.element([1, 2, 3]) - >>> arr_type = ctypes.c_uint16 * 3 # C type "array of 3 uint16" - >>> buffer = arr_type.from_address(x.data_ptr) - >>> arr = np.frombuffer(buffer, dtype='uint16') - >>> arr - array([1, 2, 3], dtype=uint16) - - In-place modification via pointer: - - >>> arr[0] = 42 - >>> x - tensor_space(3, dtype='uint16').element([42, 2, 3]) - """ - return self.data.ctypes.data - - def __eq__(self, other): - """Return ``self == other``. - - Returns - ------- - equals : bool - True if all entries of ``other`` are equal to this - the entries of ``self``, False otherwise. - - Examples - -------- - >>> space = odl.rn(3) - >>> x = space.element([1, 2, 3]) - >>> y = space.element([1, 2, 3]) - >>> x == y - True - - >>> y = space.element([-1, 2, 3]) - >>> x == y - False - >>> x == object - False - - Space membership matters: - - >>> space2 = odl.tensor_space(3, dtype='int64') - >>> y = space2.element([1, 2, 3]) - >>> x == y or y == x - False - """ - if other is self: - return True - elif other not in self.space: - return False - else: - return np.array_equal(self.data, other.data) - - def copy(self): - """Return an identical (deep) copy of this tensor. - - Parameters - ---------- - None - - Returns - ------- - copy : `NumpyTensor` - The deep copy - - Examples - -------- - >>> space = odl.rn(3) - >>> x = space.element([1, 2, 3]) - >>> y = x.copy() - >>> y == x - True - >>> y is x - False - """ - return self.space.element(self.data.copy()) - - def __copy__(self): - """Return ``copy(self)``. - - This implements the (shallow) copy interface of the ``copy`` - module of the Python standard library. - - See Also - -------- - copy - - Examples - -------- - >>> from copy import copy - >>> space = odl.rn(3) - >>> x = space.element([1, 2, 3]) - >>> y = copy(x) - >>> y == x - True - >>> y is x - False - """ - return self.copy() - - def __getitem__(self, indices): - """Return ``self[indices]``. - - Parameters - ---------- - indices : index expression - Integer, slice or sequence of these, defining the positions - of the data array which should be accessed. - - Returns - ------- - values : `NumpyTensorSpace.dtype` or `NumpyTensor` - The value(s) at the given indices. Note that the returned - object is a writable view into the original tensor, except - for the case when ``indices`` is a list. - - Examples - -------- - For one-dimensional spaces, indexing is as in linear arrays: - - >>> space = odl.rn(3) - >>> x = space.element([1, 2, 3]) - >>> x[0] - 1.0 - >>> x[1:] - rn(2).element([ 2., 3.]) - - In higher dimensions, the i-th index expression accesses the - i-th axis: - - >>> space = odl.rn((2, 3)) - >>> x = space.element([[1, 2, 3], - ... [4, 5, 6]]) - >>> x[0, 1] - 2.0 - >>> x[:, 1:] - rn((2, 2)).element( - [[ 2., 3.], - [ 5., 6.]] - ) - - Slices can be assigned to, except if lists are used for indexing: - - >>> y = x[:, ::2] # view into x - >>> y[:] = -9 - >>> x - rn((2, 3)).element( - [[-9., 2., -9.], - [-9., 5., -9.]] - ) - >>> y = x[[0, 1], [1, 2]] # not a view, won't modify x - >>> y - rn(2).element([ 2., -9.]) - >>> y[:] = 0 - >>> x - rn((2, 3)).element( - [[-9., 2., -9.], - [-9., 5., -9.]] - ) - """ - # Lazy implementation: index the array and deal with it - if isinstance(indices, NumpyTensor): - indices = indices.data - arr = self.data[indices] - - if np.isscalar(arr): - if self.space.field is not None: - return self.space.field.element(arr) - else: - return arr - else: - if is_numeric_dtype(self.dtype): - weighting = self.space.weighting - else: - weighting = None - space = type(self.space)( - arr.shape, dtype=self.dtype, exponent=self.space.exponent, - weighting=weighting) - return space.element(arr) - - def __setitem__(self, indices, values): - """Implement ``self[indices] = values``. - - Parameters - ---------- - indices : index expression - Integer, slice or sequence of these, defining the positions - of the data array which should be written to. - values : scalar, array-like or `NumpyTensor` - The value(s) that are to be assigned. - - If ``index`` is an integer, ``value`` must be a scalar. - - If ``index`` is a slice or a sequence of slices, ``value`` - must be broadcastable to the shape of the slice. - - Examples - -------- - For 1d spaces, entries can be set with scalars or sequences of - correct shape: - - >>> space = odl.rn(3) - >>> x = space.element([1, 2, 3]) - >>> x[0] = -1 - >>> x[1:] = (0, 1) - >>> x - rn(3).element([-1., 0., 1.]) - - It is also possible to use tensors of other spaces for - casting and assignment: - - >>> space = odl.rn((2, 3)) - >>> x = space.element([[1, 2, 3], - ... [4, 5, 6]]) - >>> x[0, 1] = -1 - >>> x - rn((2, 3)).element( - [[ 1., -1., 3.], - [ 4., 5., 6.]] - ) - >>> short_space = odl.tensor_space((2, 2), dtype='short') - >>> y = short_space.element([[-1, 2], - ... [0, 0]]) - >>> x[:, :2] = y - >>> x - rn((2, 3)).element( - [[-1., 2., 3.], - [ 0., 0., 6.]] - ) - - The Numpy assignment and broadcasting rules apply: - - >>> x[:] = np.array([[0, 0, 0], - ... [1, 1, 1]]) - >>> x - rn((2, 3)).element( - [[ 0., 0., 0.], - [ 1., 1., 1.]] - ) - >>> x[:, 1:] = [7, 8] - >>> x - rn((2, 3)).element( - [[ 0., 7., 8.], - [ 1., 7., 8.]] - ) - >>> x[:, ::2] = -2. - >>> x - rn((2, 3)).element( - [[-2., 7., -2.], - [-2., 7., -2.]] - ) - """ - if isinstance(indices, type(self)): - indices = indices.data - if isinstance(values, type(self)): - values = values.data - - self.data[indices] = values - - @property - def real(self): - """Real part of ``self``. - - Returns - ------- - real : `NumpyTensor` - Real part of this element as a member of a - `NumpyTensorSpace` with corresponding real data type. - - Examples - -------- - Get the real part: - - >>> space = odl.cn(3) - >>> x = space.element([1 + 1j, 2, 3 - 3j]) - >>> x.real - rn(3).element([ 1., 2., 3.]) - - Set the real part: - - >>> space = odl.cn(3) - >>> x = space.element([1 + 1j, 2, 3 - 3j]) - >>> zero = odl.rn(3).zero() - >>> x.real = zero - >>> x - cn(3).element([ 0.+1.j, 0.+0.j, 0.-3.j]) - - Other array-like types and broadcasting: - - >>> x.real = 1.0 - >>> x - cn(3).element([ 1.+1.j, 1.+0.j, 1.-3.j]) - >>> x.real = [2, 3, 4] - >>> x - cn(3).element([ 2.+1.j, 3.+0.j, 4.-3.j]) - """ - if self.space.is_real: - return self - elif self.space.is_complex: - real_space = self.space.astype(self.space.real_dtype) - return real_space.element(self.data.real) - else: - raise NotImplementedError('`real` not defined for non-numeric ' - 'dtype {}'.format(self.dtype)) - - @real.setter - def real(self, newreal): - """Setter for the real part. - - This method is invoked by ``x.real = other``. - - Parameters - ---------- - newreal : array-like or scalar - Values to be assigned to the real part of this element. - """ - self.real.data[:] = newreal - - @property - def imag(self): - """Imaginary part of ``self``. - - Returns - ------- - imag : `NumpyTensor` - Imaginary part this element as an element of a - `NumpyTensorSpace` with real data type. - - Examples - -------- - Get the imaginary part: - - >>> space = odl.cn(3) - >>> x = space.element([1 + 1j, 2, 3 - 3j]) - >>> x.imag - rn(3).element([ 1., 0., -3.]) - - Set the imaginary part: - - >>> space = odl.cn(3) - >>> x = space.element([1 + 1j, 2, 3 - 3j]) - >>> zero = odl.rn(3).zero() - >>> x.imag = zero - >>> x - cn(3).element([ 1.+0.j, 2.+0.j, 3.+0.j]) - - Other array-like types and broadcasting: - - >>> x.imag = 1.0 - >>> x - cn(3).element([ 1.+1.j, 2.+1.j, 3.+1.j]) - >>> x.imag = [2, 3, 4] - >>> x - cn(3).element([ 1.+2.j, 2.+3.j, 3.+4.j]) - """ - if self.space.is_real: - return self.space.zero() - elif self.space.is_complex: - real_space = self.space.astype(self.space.real_dtype) - return real_space.element(self.data.imag) - else: - raise NotImplementedError('`imag` not defined for non-numeric ' - 'dtype {}'.format(self.dtype)) - - @imag.setter - def imag(self, newimag): - """Setter for the imaginary part. - - This method is invoked by ``x.imag = other``. - - Parameters - ---------- - newimag : array-like or scalar - Values to be assigned to the imaginary part of this element. - - Raises - ------ - ValueError - If the space is real, i.e., no imagninary part can be set. - """ - if self.space.is_real: - raise ValueError('cannot set imaginary part in real spaces') - self.imag.data[:] = newimag - - def conj(self, out=None): - """Return the complex conjugate of ``self``. - - Parameters - ---------- - out : `NumpyTensor`, optional - Element to which the complex conjugate is written. - Must be an element of ``self.space``. - - Returns - ------- - out : `NumpyTensor` - The complex conjugate element. If ``out`` was provided, - the returned object is a reference to it. - - Examples - -------- - >>> space = odl.cn(3) - >>> x = space.element([1 + 1j, 2, 3 - 3j]) - >>> x.conj() - cn(3).element([ 1.-1.j, 2.-0.j, 3.+3.j]) - >>> out = space.element() - >>> result = x.conj(out=out) - >>> result - cn(3).element([ 1.-1.j, 2.-0.j, 3.+3.j]) - >>> result is out - True - - In-place conjugation: - - >>> result = x.conj(out=x) - >>> x - cn(3).element([ 1.-1.j, 2.-0.j, 3.+3.j]) - >>> result is x - True - """ - if self.space.is_real: - if out is None: - return self - else: - out[:] = self - return out - - if not is_numeric_dtype(self.space.dtype): - raise NotImplementedError('`conj` not defined for non-numeric ' - 'dtype {}'.format(self.dtype)) - - if out is None: - return self.space.element(self.data.conj()) - else: - if out not in self.space: - raise LinearSpaceTypeError('`out` {!r} not in space {!r}' - ''.format(out, self.space)) - self.data.conj(out.data) - return out - - def __ipow__(self, other): - """Return ``self **= other``.""" - try: - if other == int(other): - return super(NumpyTensor, self).__ipow__(other) - except TypeError: - pass - - np.power(self.data, other, out=self.data) - return self - - def __int__(self): - """Return ``int(self)``.""" - return int(self.data) - - def __long__(self): - """Return ``long(self)``. - - This method is only useful in Python 2. - """ - return long(self.data) - - def __float__(self): - """Return ``float(self)``.""" - return float(self.data) - - def __complex__(self): - """Return ``complex(self)``.""" - if self.size != 1: - raise TypeError('only size-1 tensors can be converted to ' - 'Python scalars') - return complex(self.data.ravel()[0]) - - def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): - """Interface to Numpy's ufunc machinery. - - This method is called by Numpy version 1.13 and higher as a single - point for the ufunc dispatch logic. An object implementing - ``__array_ufunc__`` takes over control when a `numpy.ufunc` is - called on it, allowing it to use custom implementations and - output types. - - This includes handling of in-place arithmetic like - ``npy_array += custom_obj``. In this case, the custom object's - ``__array_ufunc__`` takes precedence over the baseline - `numpy.ndarray` implementation. It will be called with - ``npy_array`` as ``out`` argument, which ensures that the - returned object is a Numpy array. For this to work properly, - ``__array_ufunc__`` has to accept Numpy arrays as ``out`` arguments. - - See the `corresponding NEP`_ and the `interface documentation`_ - for further details. See also the `general documentation on - Numpy ufuncs`_. - - .. note:: - This basic implementation casts inputs and - outputs to Numpy arrays and evaluates ``ufunc`` on those. - For `numpy.ndarray` based data storage, this incurs no - significant overhead compared to direct usage of Numpy arrays. - - For other (in particular non-local) implementations, e.g., - GPU arrays or distributed memory, overhead is significant due - to copies to CPU main memory. In those classes, the - ``__array_ufunc__`` mechanism should be overridden to use - native implementations if possible. - - .. note:: - When using operations that alter the shape (like ``reduce``), - or the data type (can be any of the methods), - the resulting array is wrapped in a space of the same - type as ``self.space``, propagating space properties like - `exponent` or `weighting` as closely as possible. - - Parameters - ---------- - ufunc : `numpy.ufunc` - Ufunc that should be called on ``self``. - method : str - Method on ``ufunc`` that should be called on ``self``. - Possible values: - - ``'__call__'``, ``'accumulate'``, ``'at'``, ``'outer'``, - ``'reduce'``, ``'reduceat'`` - - input1, ..., inputN : - Positional arguments to ``ufunc.method``. - kwargs : - Keyword arguments to ``ufunc.method``. - - Returns - ------- - ufunc_result : `Tensor`, `numpy.ndarray` or tuple - Result of the ufunc evaluation. If no ``out`` keyword argument - was given, the result is a `Tensor` or a tuple - of such, depending on the number of outputs of ``ufunc``. - If ``out`` was provided, the returned object or tuple entries - refer(s) to ``out``. - - Examples - -------- - We apply `numpy.add` to ODL tensors: - - >>> r3 = odl.rn(3) - >>> x = r3.element([1, 2, 3]) - >>> y = r3.element([-1, -2, -3]) - >>> x.__array_ufunc__(np.add, '__call__', x, y) - rn(3).element([ 0., 0., 0.]) - >>> np.add(x, y) # same mechanism for Numpy >= 1.13 - rn(3).element([ 0., 0., 0.]) - - As ``out``, a Numpy array or an ODL tensor can be given (wrapped - in a sequence): - - >>> out = r3.element() - >>> res = x.__array_ufunc__(np.add, '__call__', x, y, out=(out,)) - >>> out - rn(3).element([ 0., 0., 0.]) - >>> res is out - True - >>> out_arr = np.empty(3) - >>> res = x.__array_ufunc__(np.add, '__call__', x, y, out=(out_arr,)) - >>> out_arr - array([ 0., 0., 0.]) - >>> res is out_arr - True - - With multiple dimensions: - - >>> r23 = odl.rn((2, 3)) - >>> x = y = r23.one() - >>> x.__array_ufunc__(np.add, '__call__', x, y) - rn((2, 3)).element( - [[ 2., 2., 2.], - [ 2., 2., 2.]] - ) - - The ``ufunc.accumulate`` method retains the original `shape` and - `dtype`. The latter can be changed with the ``dtype`` parameter: - - >>> x = r3.element([1, 2, 3]) - >>> x.__array_ufunc__(np.add, 'accumulate', x) - rn(3).element([ 1., 3., 6.]) - >>> np.add.accumulate(x) # same mechanism for Numpy >= 1.13 - rn(3).element([ 1., 3., 6.]) - >>> x.__array_ufunc__(np.add, 'accumulate', x, dtype=complex) - cn(3).element([ 1.+0.j, 3.+0.j, 6.+0.j]) - - For multi-dimensional tensors, an optional ``axis`` parameter - can be provided: - - >>> z = r23.one() - >>> z.__array_ufunc__(np.add, 'accumulate', z, axis=1) - rn((2, 3)).element( - [[ 1., 2., 3.], - [ 1., 2., 3.]] - ) - - The ``ufunc.at`` method operates in-place. Here we add the second - operand ``[5, 10]`` to ``x`` at indices ``[0, 2]``: - - >>> x = r3.element([1, 2, 3]) - >>> x.__array_ufunc__(np.add, 'at', x, [0, 2], [5, 10]) - >>> x - rn(3).element([ 6., 2., 13.]) - - For outer-product-type operations, i.e., operations where the result - shape is the sum of the individual shapes, the ``ufunc.outer`` - method can be used: - - >>> x = odl.rn(2).element([0, 3]) - >>> y = odl.rn(3).element([1, 2, 3]) - >>> x.__array_ufunc__(np.add, 'outer', x, y) - rn((2, 3)).element( - [[ 1., 2., 3.], - [ 4., 5., 6.]] - ) - >>> y.__array_ufunc__(np.add, 'outer', y, x) - rn((3, 2)).element( - [[ 1., 4.], - [ 2., 5.], - [ 3., 6.]] - ) - - Using ``ufunc.reduce`` produces a scalar, which can be avoided with - ``keepdims=True``: - - >>> x = r3.element([1, 2, 3]) - >>> x.__array_ufunc__(np.add, 'reduce', x) - 6.0 - >>> x.__array_ufunc__(np.add, 'reduce', x, keepdims=True) - rn(1).element([ 6.]) - - In multiple dimensions, ``axis`` can be provided for reduction over - selected axes: - - >>> z = r23.element([[1, 2, 3], - ... [4, 5, 6]]) - >>> z.__array_ufunc__(np.add, 'reduce', z, axis=1) - rn(2).element([ 6., 15.]) - - Finally, ``add.reduceat`` is a combination of ``reduce`` and - ``at`` with rather flexible and complex semantics (see the - `reduceat documentation`_ for details): - - >>> x = r3.element([1, 2, 3]) - >>> x.__array_ufunc__(np.add, 'reduceat', x, [0, 1]) - rn(2).element([ 1., 5.]) - - References - ---------- - .. _corresponding NEP: - https://docs.scipy.org/doc/numpy/neps/ufunc-overrides.html - - .. _interface documentation: - https://docs.scipy.org/doc/numpy/reference/arrays.classes.html\ -#numpy.class.__array_ufunc__ - - .. _general documentation on Numpy ufuncs: - https://docs.scipy.org/doc/numpy/reference/ufuncs.html - - .. _reduceat documentation: - https://docs.scipy.org/doc/numpy/reference/generated/\ -numpy.ufunc.reduceat.html - """ - # Remark: this method differs from the parent implementation only - # in the propagation of additional space properties. - - # --- Process `out` --- # - - # Unwrap out if provided. The output parameters are all wrapped - # in one tuple, even if there is only one. - out_tuple = kwargs.pop('out', ()) - - # Check number of `out` args, depending on `method` - if method == '__call__' and len(out_tuple) not in (0, ufunc.nout): - raise ValueError( - "ufunc {}: need 0 or {} `out` arguments for " - "`method='__call__'`, got {}" - ''.format(ufunc.__name__, ufunc.nout, len(out_tuple))) - elif method != '__call__' and len(out_tuple) not in (0, 1): - raise ValueError( - 'ufunc {}: need 0 or 1 `out` arguments for `method={!r}`, ' - 'got {}'.format(ufunc.__name__, method, len(out_tuple))) - - # We allow our own tensors, the data container type and - # `numpy.ndarray` objects as `out` (see docs for reason for the - # latter) - valid_types = (type(self), type(self.data), np.ndarray) - if not all(isinstance(o, valid_types) or o is None - for o in out_tuple): - return NotImplemented - - # Assign to `out` or `out1` and `out2`, respectively - out = out1 = out2 = None - if len(out_tuple) == 1: - out = out_tuple[0] - elif len(out_tuple) == 2: - out1 = out_tuple[0] - out2 = out_tuple[1] - - # --- Process `inputs` --- # - - # Convert inputs that are ODL tensors to Numpy arrays so that the - # native Numpy ufunc is called later - inputs = tuple( - inp.asarray() if isinstance(inp, type(self)) else inp - for inp in inputs) - - # --- Get some parameters for later --- # - - # Arguments for `writable_array` and/or space constructors - out_dtype = kwargs.get('dtype', None) - if out_dtype is None: - array_kwargs = {} - else: - array_kwargs = {'dtype': out_dtype} - - exponent = self.space.exponent - weighting = self.space.weighting - - # --- Evaluate ufunc --- # - - if method == '__call__': - if ufunc.nout == 1: - # Make context for output (trivial one returns `None`) - if out is None: - out_ctx = nullcontext() - else: - out_ctx = writable_array(out, **array_kwargs) - - # Evaluate ufunc - with out_ctx as out_arr: - kwargs['out'] = out_arr - res = ufunc(*inputs, **kwargs) - - # Wrap result if necessary (lazily) - if out is None: - if is_floating_dtype(res.dtype): - # Weighting contains exponent - spc_kwargs = {'weighting': weighting} - else: - # No `exponent` or `weighting` applicable - spc_kwargs = {} - out_space = type(self.space)(self.shape, res.dtype, - **spc_kwargs) - out = out_space.element(res) - - return out - - elif ufunc.nout == 2: - # Make contexts for outputs (trivial ones return `None`) - if out1 is not None: - out1_ctx = writable_array(out1, **array_kwargs) - else: - out1_ctx = nullcontext() - if out2 is not None: - out2_ctx = writable_array(out2, **array_kwargs) - else: - out2_ctx = nullcontext() - - # Evaluate ufunc - with out1_ctx as out1_arr, out2_ctx as out2_arr: - kwargs['out'] = (out1_arr, out2_arr) - res1, res2 = ufunc(*inputs, **kwargs) - - # Wrap results if necessary (lazily) - # We don't use exponents or weightings since we don't know - # how to map them to the spaces - if out1 is None: - out1_space = type(self.space)(self.shape, res1.dtype) - out1 = out1_space.element(res1) - if out2 is None: - out2_space = type(self.space)(self.shape, res2.dtype) - out2 = out2_space.element(res2) - - return out1, out2 - - else: - raise NotImplementedError('nout = {} not supported' - ''.format(ufunc.nout)) - - else: # method != '__call__' - # Make context for output (trivial one returns `None`) - if out is None: - out_ctx = nullcontext() - else: - out_ctx = writable_array(out, **array_kwargs) - - # Evaluate ufunc method - with out_ctx as out_arr: - if method != 'at': - # No kwargs allowed for 'at' - kwargs['out'] = out_arr - res = getattr(ufunc, method)(*inputs, **kwargs) - - # Shortcut for scalar or no return value - if np.isscalar(res) or res is None: - # The first occurs for `reduce` with all axes, - # the second for in-place stuff (`at` currently) - return res - - # Wrap result if necessary (lazily) - if out is None: - if is_floating_dtype(res.dtype): - if res.shape != self.shape: - # Don't propagate weighting if shape changes - weighting = NumpyTensorSpaceConstWeighting(1.0, - exponent) - spc_kwargs = {'weighting': weighting} - else: - spc_kwargs = {} - - out_space = type(self.space)(res.shape, res.dtype, - **spc_kwargs) - out = out_space.element(res) - - return out - def _blas_is_applicable(*args): """Whether BLAS routines can be applied or not. @@ -1762,8 +887,8 @@ def _blas_is_applicable(*args): Parameters ---------- - x1,...,xN : `NumpyTensor` - The tensors to be tested for BLAS conformity. + x1,...,xN : numpy.ndarray + The arrays to be tested for BLAS conformity. Returns ------- @@ -1794,11 +919,11 @@ def _lincomb_impl(a, x1, b, x2, out): if size < THRESHOLD_SMALL: # Faster for small arrays - out.data[:] = a * x1.data + b * x2.data + out[:] = a * x1 + b * x2 return elif (size < THRESHOLD_MEDIUM or - not _blas_is_applicable(x1.data, x2.data, out.data)): + not _blas_is_applicable(x1, x2, out)): def fallback_axpy(x1, x2, n, a): """Fallback axpy implementation avoiding copy.""" @@ -1819,23 +944,23 @@ def fallback_copy(x1, x2, n): return x2 axpy, scal, copy = (fallback_axpy, fallback_scal, fallback_copy) - x1_arr = x1.data - x2_arr = x2.data - out_arr = out.data + x1_arr = x1 + x2_arr = x2 + out_arr = out else: # Need flat data for BLAS, otherwise in-place does not work. # Raveling must happen in fixed order for non-contiguous out, # otherwise 'A' is applied to arrays, which makes the outcome # dependent on their respective contiguousness. - if out.data.flags.f_contiguous: + if out.flags.f_contiguous: ravel_order = 'F' else: ravel_order = 'C' - x1_arr = x1.data.ravel(order=ravel_order) - x2_arr = x2.data.ravel(order=ravel_order) - out_arr = out.data.ravel(order=ravel_order) + x1_arr = x1.ravel(order=ravel_order) + x2_arr = x2.ravel(order=ravel_order) + out_arr = out.ravel(order=ravel_order) axpy, scal, copy = scipy.linalg.blas.get_blas_funcs( ['axpy', 'scal', 'copy'], arrays=(x1_arr, x2_arr, out_arr)) @@ -1887,120 +1012,32 @@ def fallback_copy(x1, x2, n): axpy(x1_arr, out_arr, size, a) -def _weighting(weights, exponent): - """Return a weighting whose type is inferred from the arguments.""" - if np.isscalar(weights): - weighting = NumpyTensorSpaceConstWeighting(weights, exponent) - elif weights is None: - weighting = NumpyTensorSpaceConstWeighting(1.0, exponent) - else: # last possibility: make an array - arr = np.asarray(weights) - weighting = NumpyTensorSpaceArrayWeighting(arr, exponent) - return weighting - - -def npy_weighted_inner(weights): - """Weighted inner product on `TensorSpace`'s as free function. - - Parameters - ---------- - weights : scalar or `array-like` - Weights of the inner product. A scalar is interpreted as a - constant weight, a 1-dim. array as a weighting vector. - - Returns - ------- - inner : `callable` - Inner product function with given weight. Constant weightings - are applicable to spaces of any size, for arrays the sizes - of the weighting and the space must match. - - See Also - -------- - NumpyTensorSpaceConstWeighting - NumpyTensorSpaceArrayWeighting - """ - return _weighting(weights, exponent=2.0).inner - - -def npy_weighted_norm(weights, exponent=2.0): - """Weighted norm on `TensorSpace`'s as free function. - - Parameters - ---------- - weights : scalar or `array-like` - Weights of the norm. A scalar is interpreted as a - constant weight, a 1-dim. array as a weighting vector. - exponent : positive `float` - Exponent of the norm. - - Returns - ------- - norm : `callable` - Norm function with given weight. Constant weightings - are applicable to spaces of any size, for arrays the sizes - of the weighting and the space must match. - - See Also - -------- - NumpyTensorSpaceConstWeighting - NumpyTensorSpaceArrayWeighting - """ - return _weighting(weights, exponent=exponent).norm - - -def npy_weighted_dist(weights, exponent=2.0): - """Weighted distance on `TensorSpace`'s as free function. - - Parameters - ---------- - weights : scalar or `array-like` - Weights of the distance. A scalar is interpreted as a - constant weight, a 1-dim. array as a weighting vector. - exponent : positive `float` - Exponent of the norm. - - Returns - ------- - dist : `callable` - Distance function with given weight. Constant weightings - are applicable to spaces of any size, for arrays the sizes - of the weighting and the space must match. - - See Also - -------- - NumpyTensorSpaceConstWeighting - NumpyTensorSpaceArrayWeighting - """ - return _weighting(weights, exponent=exponent).dist - - def _norm_default(x): """Default Euclidean norm implementation.""" # Lazy import to improve `import odl` time import scipy.linalg - if _blas_is_applicable(x.data): + if _blas_is_applicable(x): nrm2 = scipy.linalg.blas.get_blas_funcs('nrm2', dtype=x.dtype) norm = partial(nrm2, n=native(x.size)) else: norm = np.linalg.norm - return norm(x.data.ravel()) + return norm(x.ravel()) def _pnorm_default(x, p): """Default p-norm implementation.""" - return np.linalg.norm(x.data.ravel(), ord=p) + return np.linalg.norm(x.ravel(), ord=p) def _pnorm_diagweight(x, p, w): """Diagonally weighted p-norm implementation.""" # Ravel both in the same order (w is a numpy array) - order = 'F' if all(a.flags.f_contiguous for a in (x.data, w)) else 'C' + order = 'F' if all(a.flags.f_contiguous for a in (x, w)) else 'C' # This is faster than first applying the weights and then summing with # BLAS dot or nrm2 - xp = np.abs(x.data.ravel(order)) + xp = np.abs(x.ravel(order)) if p == float('inf'): xp *= w.ravel(order) return np.max(xp) @@ -2013,7 +1050,7 @@ def _pnorm_diagweight(x, p, w): def _inner_default(x1, x2): """Default Euclidean inner product implementation.""" # Ravel both in the same order - order = 'F' if all(a.data.flags.f_contiguous for a in (x1, x2)) else 'C' + order = 'F' if all(a.flags.f_contiguous for a in (x1, x2)) else 'C' if is_real_dtype(x1.dtype): if x1.size > THRESHOLD_MEDIUM: @@ -2021,341 +1058,105 @@ def _inner_default(x1, x2): return np.tensordot(x1, x2, [range(x1.ndim)] * 2) else: # Several times faster for small arrays - return np.dot(x1.data.ravel(order), - x2.data.ravel(order)) + return np.dot(x1.ravel(order), + x2.ravel(order)) else: # x2 as first argument because we want linearity in x1 - return np.vdot(x2.data.ravel(order), - x1.data.ravel(order)) + return np.vdot(x2.ravel(order), + x1.ravel(order)) # TODO: implement intermediate weighting schemes with arrays that are # broadcast, i.e. between scalar and full-blown in dimensionality? -class NumpyTensorSpaceArrayWeighting(ArrayWeighting): - - """Weighting of a `NumpyTensorSpace` by an array. - - This class defines a weighting by an array that has the same shape - as the tensor space. Since the space is not known to this class, - no checks of shape or data type are performed. - See ``Notes`` for mathematical details. - """ - - def __init__(self, array, exponent=2.0): - r"""Initialize a new instance. - - Parameters - ---------- - array : `array-like`, one-dim. - Weighting array of the inner product, norm and distance. - All its entries must be positive, however this is not - verified during initialization. - exponent : positive `float` - Exponent of the norm. For values other than 2.0, no inner - product is defined. - - Notes - ----- - - For exponent 2.0, a new weighted inner product with array - :math:`W` is defined as - - .. math:: - \langle A, B\rangle_W := - \langle W \odot A, B\rangle = - \langle w \odot a, b\rangle = - b^{\mathrm{H}} (w \odot a), - - where :math:`a, b, w` are the "flattened" counterparts of - tensors :math:`A, B, W`, respectively, :math:`b^{\mathrm{H}}` - stands for transposed complex conjugate and :math:`w \odot a` - for element-wise multiplication. - - - For other exponents, only norm and dist are defined. In the - case of exponent :math:`\infty`, the weighted norm is - - .. math:: - \| A\|_{W, \infty} := - \| W \odot A\|_{\infty} = - \| w \odot a\|_{\infty}, - - otherwise it is (using point-wise exponentiation) - - .. math:: - \| A\|_{W, p} := - \| W^{1/p} \odot A\|_{p} = - \| w^{1/p} \odot a\|_{\infty}. - - - Note that this definition does **not** fulfill the limit - property in :math:`p`, i.e. - - .. math:: - \| A\|_{W, p} \not\to - \| A\|_{W, \infty} \quad (p \to \infty) - - unless all weights are equal to 1. - - - The array :math:`W` may only have positive entries, otherwise - it does not define an inner product or norm, respectively. This - is not checked during initialization. - """ - if isinstance(array, NumpyTensor): - array = array.data - elif not isinstance(array, np.ndarray): - array = np.asarray(array) - super(NumpyTensorSpaceArrayWeighting, self).__init__( - array, impl='numpy', exponent=exponent) - - def __hash__(self): - """Return ``hash(self)``.""" - return hash((type(self), self.array.tobytes(), self.exponent)) - - def inner(self, x1, x2): - """Return the weighted inner product of ``x1`` and ``x2``. - - Parameters - ---------- - x1, x2 : `NumpyTensor` - Tensors whose inner product is calculated. - - Returns - ------- - inner : float or complex - The inner product of the two provided vectors. - """ - if self.exponent != 2.0: - raise NotImplementedError('no inner product defined for ' - 'exponent != 2 (got {})' - ''.format(self.exponent)) - else: - inner = _inner_default(x1 * self.array, x2) - if is_real_dtype(x1.dtype): - return float(inner) - else: - return complex(inner) - - def norm(self, x): - """Return the weighted norm of ``x``. - - Parameters - ---------- - x : `NumpyTensor` - Tensor whose norm is calculated. - - Returns - ------- - norm : float - The norm of the provided tensor. - """ - if self.exponent == 2.0: - norm_squared = self.inner(x, x).real # TODO: optimize?! - if norm_squared < 0: - norm_squared = 0.0 # Compensate for numerical error - return float(np.sqrt(norm_squared)) - else: - return float(_pnorm_diagweight(x, self.exponent, self.array)) - - -class NumpyTensorSpaceConstWeighting(ConstWeighting): - - """Weighting of a `NumpyTensorSpace` by a constant. - - See ``Notes`` for mathematical details. - """ - - def __init__(self, const, exponent=2.0): - r"""Initialize a new instance. - - Parameters - ---------- - const : positive float - Weighting constant of the inner product, norm and distance. - exponent : positive float - Exponent of the norm. For values other than 2.0, the inner - product is not defined. - - Notes - ----- - - For exponent 2.0, a new weighted inner product with constant - :math:`c` is defined as - - .. math:: - \langle a, b\rangle_c := - c \, \langle a, b\rangle_c = - c \, b^{\mathrm{H}} a, - - where :math:`b^{\mathrm{H}}` standing for transposed complex - conjugate. - - - For other exponents, only norm and dist are defined. In the - case of exponent :math:`\infty`, the weighted norm is defined - as - - .. math:: - \| a \|_{c, \infty} := - c\, \| a \|_{\infty}, - - otherwise it is - - .. math:: - \| a \|_{c, p} := - c^{1/p}\, \| a \|_{p}. - - - Note that this definition does **not** fulfill the limit - property in :math:`p`, i.e. - - .. math:: - \| a\|_{c, p} \not\to - \| a \|_{c, \infty} \quad (p \to \infty) - - unless :math:`c = 1`. - - - The constant must be positive, otherwise it does not define an - inner product or norm, respectively. - """ - super(NumpyTensorSpaceConstWeighting, self).__init__( - const, impl='numpy', exponent=exponent) - - def inner(self, x1, x2): - """Return the weighted inner product of ``x1`` and ``x2``. - - Parameters - ---------- - x1, x2 : `NumpyTensor` - Tensors whose inner product is calculated. - - Returns - ------- - inner : float or complex - The inner product of the two provided tensors. - """ - if self.exponent != 2.0: - raise NotImplementedError('no inner product defined for ' - 'exponent != 2 (got {})' - ''.format(self.exponent)) - else: - inner = self.const * _inner_default(x1, x2) - if x1.space.field is None: - return inner - else: - return x1.space.field.element(inner) - - def norm(self, x): - """Return the weighted norm of ``x``. - - Parameters - ---------- - x1 : `NumpyTensor` - Tensor whose norm is calculated. - - Returns - ------- - norm : float - The norm of the tensor. - """ - if self.exponent == 2.0: - return float(np.sqrt(self.const) * _norm_default(x)) - elif self.exponent == float('inf'): - return float(self.const * _pnorm_default(x, self.exponent)) - else: - return float((self.const ** (1 / self.exponent) * - _pnorm_default(x, self.exponent))) - - def dist(self, x1, x2): - """Return the weighted distance between ``x1`` and ``x2``. - - Parameters - ---------- - x1, x2 : `NumpyTensor` - Tensors whose mutual distance is calculated. - - Returns - ------- - dist : float - The distance between the tensors. - """ - if self.exponent == 2.0: - return float(np.sqrt(self.const) * _norm_default(x1 - x2)) - elif self.exponent == float('inf'): - return float(self.const * _pnorm_default(x1 - x2, self.exponent)) - else: - return float((self.const ** (1 / self.exponent) * - _pnorm_default(x1 - x2, self.exponent))) - - -class NumpyTensorSpaceCustomInner(CustomInner): +def _weighted_inner(x1, x2, weights): + """Weighted inner product on a `NumpyTensorSpace`.""" + if ( + np.isscalar(weights) + or (isinstance(weights, np.ndarray) and weights.size == 1) + ): + return _const_weighted_inner(x1, x2, weights) + elif isinstance(weights, np.ndarray) and weights.shape == x1.shape: + return _array_weighted_inner(x1, x2, weights) + else: + raise ValueError( + '`weights` is neither a constant nor an adequate array' + ) - """Class for handling a user-specified inner product.""" - def __init__(self, inner): - """Initialize a new instance. +def _array_weighted_inner(x1, x2, weights): + """Inner product weighted by an array (i.e., pointwise).""" + inner = _inner_default(x1 * weights, x2) + return inner.item() - Parameters - ---------- - inner : `callable` - The inner product implementation. It must accept two - `Tensor` arguments, return an element from their space's - field (real or complex number) and satisfy the following - conditions for all vectors ``x, y, z`` and scalars ``s``: - - - `` = conj()`` - - `` = s * + `` - - `` = 0`` if and only if ``x = 0`` - """ - super(NumpyTensorSpaceCustomInner, self).__init__(inner, impl='numpy') +def _const_weighted_inner(x1, x2, weight): + """Inner product weighted by a constant.""" + inner = weight * _inner_default(x1, x2) + return inner.item() -class NumpyTensorSpaceCustomNorm(CustomNorm): - """Class for handling a user-specified norm. - - Note that this removes ``inner``. - """ +def _weighted_norm(x, p, weights): + """Weighted p-norm on a `NumpyTensorSpace`.""" + if ( + np.isscalar(weights) + or (isinstance(weights, np.ndarray) and weights.size == 1) + ): + return _const_weighted_norm(x, p, weights) + elif isinstance(weights, np.ndarray) and weights.shape == x.shape: + return _array_weighted_norm(x, p, weights) + else: + raise ValueError( + '`weights` is neither a constant nor an adequate array' + ) - def __init__(self, norm): - """Initialize a new instance. - Parameters - ---------- - norm : `callable` - The norm implementation. It must accept a `Tensor` - argument, return a `float` and satisfy the following - conditions for all any two elements ``x, y`` and scalars - ``s``: - - - ``||x|| >= 0`` - - ``||x|| = 0`` if and only if ``x = 0`` - - ``||s * x|| = |s| * ||x||`` - - ``||x + y|| <= ||x|| + ||y||`` - """ - super(NumpyTensorSpaceCustomNorm, self).__init__(norm, impl='numpy') +def _array_weighted_norm(x, p, weights): + """Norm with exponent p, weighted by an array (i.e., pointwise).""" + if p == 2.0: + # TODO(kohr-h): optimize?! + norm_squared = _array_weighted_inner(x, x, weights).real + if norm_squared < 0: + norm_squared = 0.0 # Compensate for numerical error + return np.sqrt(norm_squared).item() + else: + return _pnorm_diagweight(x, p, weights).item() -class NumpyTensorSpaceCustomDist(CustomDist): +def _const_weighted_norm(x, p, weight): + """Norm with exponent p, weighted by a constant.""" + if p == 2.0: + return (np.sqrt(weight) * _norm_default(x)).item() + elif p == float('inf'): + return (weight * _pnorm_default(x, float('inf'))).item() + else: + return (weight ** (1 / p) * _pnorm_default(x, p)).item() + + +def _weighted_dist(x1, x2, p, weights): + """Weighted p-distance on a `NumpyTensorSpace`.""" + if ( + np.isscalar(weights) + or (isinstance(weights, np.ndarray) and weights.size == 1) + ): + return _const_weighted_dist(x1, x2, p, weights) + elif isinstance(weights, np.ndarray) and weights.shape == x1.shape: + return _array_weighted_dist(x1, x2, p, weights) + else: + raise ValueError( + "`weights` is neither a constant nor an adequate array" + ) - """Class for handling a user-specified distance in `TensorSpace`. - Note that this removes ``inner`` and ``norm``. - """ +def _array_weighted_dist(x1, x2, p, weights): + """Dist with exponent p, weighted by an array (one entry per subspace).""" + return _array_weighted_norm(x1 - x2, p, weights) - def __init__(self, dist): - """Initialize a new instance. - Parameters - ---------- - dist : `callable` - The distance function defining a metric on `TensorSpace`. It - must accept two `Tensor` arguments, return a `float` and - fulfill the following mathematical conditions for any three - elements ``x, y, z``: - - - ``dist(x, y) >= 0`` - - ``dist(x, y) = 0`` if and only if ``x = y`` - - ``dist(x, y) = dist(y, x)`` - - ``dist(x, y) <= dist(x, z) + dist(z, y)`` - """ - super(NumpyTensorSpaceCustomDist, self).__init__(dist, impl='numpy') +def _const_weighted_dist(x1, x2, p, weight): + """Dist with exponent p, weighted by a constant.""" + return _const_weighted_norm(x1 - x2, p, weight) if __name__ == '__main__': diff --git a/odl/space/pspace.py b/odl/space/pspace.py index bffded0a58b..0d233917175 100644 --- a/odl/space/pspace.py +++ b/odl/space/pspace.py @@ -10,21 +10,19 @@ from __future__ import absolute_import, division, print_function +import inspect 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 ( - ArrayWeighting, ConstWeighting, CustomDist, CustomInner, CustomNorm, - Weighting) -from odl.util import indent, is_real_dtype, signature_string -from odl.util.ufuncs import ProductSpaceUfuncs +from odl.util import indent, signature_string __all__ = ('ProductSpace',) +getargspec = getattr(inspect, "getfullargspec", inspect.getargspec) + class ProductSpace(LinearSpace): @@ -46,12 +44,11 @@ def __init__(self, *spaces, **kwargs): The individual spaces ("factors / parts") in the product space. Can also be given as ``space, n`` with ``n`` integer, in which case the power space ``space ** n`` is created. - exponent : non-zero float or ``float('inf')``, optional - Order of the product distance/norm, i.e. - - ``dist(x, y) = np.linalg.norm(x-y, ord=exponent)`` + exponent : float, optional + Order of the product distance/norm, roughly :: - ``norm(x) = np.linalg.norm(x, ord=exponent)`` + dist(x, y) = np.linalg.norm(x-y, ord=exponent) + norm(x) = np.linalg.norm(x, ord=exponent) Values ``0 <= exponent < 1`` are currently unsupported due to numerical instability. See ``Notes`` for further @@ -60,69 +57,20 @@ def __init__(self, *spaces, **kwargs): Default: 2.0 field : `Field`, optional - Scalar field of the resulting space. + Scalar field of the resulting space, must be given if no space + is provided. + Default: ``spaces[0].field`` weighting : optional Use weighted inner product, norm, and dist. The following types are supported as ``weighting``: - ``None`` : no weighting (default) - - `Weighting` : weighting class, used directly. Such a - class instance can be retrieved from the space by the - `ProductSpace.weighting` property. - - `array-like` : weigh each component with one entry from the - array. The array must be one-dimensional and have the same - length as the number of spaces. - - float : same weighting factor in each component - - Other Parameters - ---------------- - dist : callable, optional - The distance function defining a metric on the space. - It must accept two `ProductSpaceElement` arguments and - fulfill the following mathematical conditions for any - three space elements ``x, y, z``: - - - ``dist(x, y) >= 0`` - - ``dist(x, y) = 0`` if and only if ``x = y`` - - ``dist(x, y) = dist(y, x)`` - - ``dist(x, y) <= dist(x, z) + dist(z, y)`` - - By default, ``dist(x, y)`` is calculated as ``norm(x - y)``. - - Cannot be combined with: ``weighting, norm, inner`` - - norm : callable, optional - The norm implementation. It must accept an - `ProductSpaceElement` argument, return a float and satisfy the - following conditions for all space elements ``x, y`` and scalars - ``s``: - - - ``||x|| >= 0`` - - ``||x|| = 0`` if and only if ``x = 0`` - - ``||s * x|| = |s| * ||x||`` - - ``||x + y|| <= ||x|| + ||y||`` - - By default, ``norm(x)`` is calculated as ``inner(x, x)``. - - Cannot be combined with: ``weighting, dist, inner`` - - inner : callable, optional - The inner product implementation. It must accept two - `ProductSpaceElement` arguments, return a element from - the field of the space (real or complex number) and - satisfy the following conditions for all space elements - ``x, y, z`` and scalars ``s``: - - - `` = conj()`` - - `` = s * + `` - - `` = 0`` if and only if ``x = 0`` - - Cannot be combined with: ``weighting, dist, norm`` + - ``None`` : no weighting (default) + - `array-like` : weight each component with one entry from the + array. The array must be one-dimensional and have the same + size as the number of spaces. + - float : same weighting factor in each component Examples -------- @@ -157,7 +105,7 @@ class instance can be retrieved from the space by the **Norm:** - - :math:`p < \infty`: + - :math:`-\infty < p < \infty`: .. math:: \lVert x\rVert = @@ -168,9 +116,14 @@ class instance can be retrieved from the space by the .. math:: \lVert x\rVert = \max_i \lVert x_i \rVert_i + - :math:`p = -\infty`: + + .. math:: + \lVert x\rVert = \min_i \lVert x_i \rVert_i + **Distance:** - - :math:`p < \infty`: + - :math:`-\infty < p < \infty`: .. math:: d(x, y) = \left( \sum_{i=1}^d d_i(x_i, y_i)^p \right)^{1/p} @@ -180,13 +133,10 @@ class instance can be retrieved from the space by the .. math:: d(x, y) = \max_i d_i(x_i, y_i) - To implement own versions of these functions, you can use - the following snippet to gather the vector of norms (analogously - for inner products and distances):: + - :math:`p = -\infty`: - norms = np.fromiter( - (xi.norm() for xi in x), - dtype=np.float64, count=len(x)) + .. math:: + d(x, y) = \min_i d_i(x_i, y_i) See Also -------- @@ -194,24 +144,12 @@ class instance can be retrieved from the space by the ProductSpaceConstWeighting """ field = kwargs.pop('field', None) - dist = kwargs.pop('dist', None) - norm = kwargs.pop('norm', None) - inner = kwargs.pop('inner', None) weighting = kwargs.pop('weighting', None) - exponent = float(kwargs.pop('exponent', 2.0)) + exponent = kwargs.pop('exponent', 2.0) if kwargs: raise TypeError('got unexpected keyword arguments: {}' ''.format(kwargs)) - # Check validity of option combination (3 or 4 out of 4 must be None) - if sum(x is None for x in (dist, norm, inner, weighting)) < 3: - raise ValueError('invalid combination of options weighting, ' - 'dist, norm and inner') - - if any(x is not None for x in (dist, norm, inner)) and exponent != 2.0: - raise ValueError('`exponent` cannot be used together with ' - 'inner, norm or dist') - # Make a power space if the second argument is an integer. # For the case that the integer is 0, we already set the field here. if len(spaces) == 2 and isinstance(spaces[1], Integral): @@ -224,55 +162,80 @@ class instance can be retrieved from the space by the 'all arguments must be `LinearSpace` instances, or the ' 'first argument must be `LinearSpace` and the second ' 'integer; got {!r}'.format(spaces)) - if not all(spc.field == spaces[0].field for spc in spaces): + if not all(spc.field == spaces[0].field for spc in spaces[1:]): raise ValueError('all spaces must have the same field') # Assign spaces and field self.__spaces = tuple(spaces) - - # Cache for efficiency - self.__is_power_space = all(spc == self.spaces[0] - for spc in self.spaces[1:]) - - # Assing or infer field if field is None: - if len(self) == 0: - raise ValueError('no spaces provided, cannot deduce field') + if len(spaces) == 0: + raise ValueError( + '`field` must be given explicitly if no `spaces` are ' + 'provided' + ) else: - field = self.spaces[0].field + field = self.__spaces[0].field super(ProductSpace, self).__init__(field) - # Assign weighting - if weighting is not None: - if isinstance(weighting, Weighting): - self.__weighting = weighting - elif np.isscalar(weighting): - self.__weighting = ProductSpaceConstWeighting( - weighting, exponent) - elif weighting is None: - # Need to wait until dist, norm and inner are handled - pass - else: # last possibility: make a product space element - arr = np.asarray(weighting) - if arr.dtype == object: - raise ValueError('invalid weighting argument {}' - ''.format(weighting)) - if arr.ndim == 1: - self.__weighting = ProductSpaceArrayWeighting( - arr, exponent) - else: - raise ValueError('weighting array has {} dimensions, ' - 'expected 1'.format(arr.ndim)) - - elif dist is not None: - self.__weighting = ProductSpaceCustomDist(dist) - elif norm is not None: - self.__weighting = ProductSpaceCustomNorm(norm) - elif inner is not None: - self.__weighting = ProductSpaceCustomInner(inner) - else: # all None -> no weighing - self.__weighting = ProductSpaceConstWeighting(1.0, exponent) + # Cache power space property for efficiency + self.__is_power_space = all( + spc == self.__spaces[0] for spc in self.__spaces[1:] + ) + + # Exponent and weighting + self.__exponent = float(exponent) + if 0 < self.__exponent < 1: + raise ValueError( + "`exponent` between 0 and 1 currently unsupported" + ) + + if weighting is None: + self.__weighting = 1.0 + self.__weighting_type = 'const' + elif np.isscalar(weighting): + self.__weighting = float(weighting) + self.__weighting_type = 'const' + else: + weighting = np.atleast_1d(weighting) + if weighting.shape != (len(spaces),): + raise ValueError( + '`weighting` array must have shape `(n,)`, where `n` is ' + 'the number of spaces, but {} != {}' + ''.format(weighting.shape, (len(spaces),)) + ) + self.__weighting = weighting + self.__weighting_type = 'array' + + # Cached properties + self.__shape = None + self.__flat_spaces = None + self.__ufuncs = None + self.__reduce = None + + # --- Constructor args + + @property + def spaces(self): + """A tuple containing all spaces.""" + return self.__spaces + + @property + def weighting(self): + """This space's weighting factor(s).""" + return self.__weighting + + @property + def weighting_type(self): + """This space's weighting type.""" + return self.__weighting_type + + @property + def exponent(self): + """Exponent of norm and dist in this space.""" + return self.__exponent + + # --- Shape- and type-related def __len__(self): """Return ``len(self)``. @@ -302,25 +265,30 @@ def shape(self): >>> pspace2 = odl.ProductSpace(pspace, 3) >>> pspace2.shape (3, 2) - - If the space is a "pure" product space, shape recurses all the way - into the components: - >>> r2_2 = odl.ProductSpace(r2, 3) >>> r2_2.shape - (3, 2) + (3,) """ + if self.__shape is not None: + return self.__shape + if len(self) == 0: - return () - elif self.is_power_space: + self.__shape = () + return self.__shape + + shape = [len(self)] + spaces = self.spaces + is_power_space = self.is_power_space + while is_power_space: try: - sub_shape = self[0].shape + is_power_space = spaces[0].is_power_space except AttributeError: - sub_shape = () - else: - sub_shape = () + break + spaces = spaces[0].spaces + shape.append(len(spaces)) - return (len(self),) + sub_shape + self.__shape = tuple(shape) + return self.__shape @property def size(self): @@ -339,47 +307,19 @@ def size(self): >>> pspace2.size 6 """ - return (0 if self.shape == () else - int(np.prod(self.shape, dtype='int64'))) - - @property - def spaces(self): - """A tuple containing all spaces.""" - return self.__spaces - - @property - def is_power_space(self): - """``True`` if all member spaces are equal.""" - return self.__is_power_space - - @property - def exponent(self): - """Exponent of the product space norm/dist, ``None`` for custom.""" - return self.weighting.exponent - - @property - def weighting(self): - """This space's weighting scheme.""" - return self.__weighting - - @property - def is_weighted(self): - """Return ``True`` if the space is not weighted by constant 1.0.""" - return not ( - isinstance(self.weighting, ProductSpaceConstWeighting) and - self.weighting.const == 1.0) + return ( + 0 if self.shape == () else int(np.prod(self.shape, dtype='int64')) + ) @property def dtype(self): """The data type of this space. - This is only well defined if all subspaces have the same dtype. - Raises ------ AttributeError - If any of the subspaces does not implement `dtype` or if the dtype - of the subspaces does not match. + If any of the subspaces does not implement `dtype` or if the + dtypes of the subspaces do not match. """ dtypes = [space.dtype for space in self.spaces] @@ -388,6 +328,18 @@ def dtype(self): else: raise AttributeError("`dtype`'s of subspaces not equal") + # --- Analytic properties + + @property + def is_power_space(self): + """``True`` if all member spaces are equal.""" + return self.__is_power_space + + @property + def is_weighted(self): + """Return ``True`` if this space is not weighted by constant 1.0.""" + return not (np.isscalar(self.weighting) and float(self.weighting) == 1) + @property def is_real(self): """True if this is a space of real valued vectors.""" @@ -398,6 +350,54 @@ def is_complex(self): """True if this is a space of complex valued vectors.""" return all(spc.is_complex for spc in self.spaces) + def base_space(self, flat=False): + """For power spaces, return the base. + + Parameters + ---------- + flat : bool, optional + If ``True``, return the base of the flattened variant of a + higher-order power space. Otherwise, return the base of the + first level. + + Returns + ------- + base_space : `LinearSpace` + The base of the power space. + + Raises + ------ + TypeError + If ``self`` is not a power space at the requested level. + + Examples + -------- + >>> pspace = odl.ProductSpace(odl.rn(4), 3) + >>> pspace2 = odl.ProductSpace(pspace, 2) + >>> pspace2.base_space() + ProductSpace(rn(4), 3) + >>> pspace2.base_space(flat=True) + rn(4) + """ + if len(self) == 0: + raise ValueError('base undefined for spaces of size 0') + + if not flat: + if not self.is_power_space: + raise TypeError('{!r} is not a power space'.format(self)) + return self.spaces[0] + + flat = self._flatten() + if not all(space == flat[0] for space in flat[1:]): + # TODO(kohr-h): go as far as possible instead of raising an + # exception? + raise TypeError( + '{!r} is not a power space at the lowest level'.format(self) + ) + return flat[0] + + # --- Conversion + @property def real_space(self): """Variant of this space with real dtype.""" @@ -409,14 +409,14 @@ def complex_space(self): return ProductSpace(*[space.complex_space for space in self.spaces]) def astype(self, dtype): - """Return a copy of this space with new ``dtype``. + """Return a copy of this space with subspaces of given ``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 + dtype + Scalar data type of the constituents 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 @@ -429,13 +429,39 @@ def astype(self, dtype): raise ValueError('`None` is not a valid data type') dtype = np.dtype(dtype) - current_dtype = getattr(self, 'dtype', object) + current_dtypes = [ + getattr(space, 'dtype', None) for space in self.spaces + ] - if dtype == current_dtype: + if all(dt is not None and dt == dtype for dt in current_dtypes): return self else: - return ProductSpace(*[space.astype(dtype) - for space in self.spaces]) + return ProductSpace( + *[space.astype(dtype) for space in self.spaces] + ) + + # --- Element handling + + def _flatten(self, inputs=None): + if inputs is None and self.__flat_spaces is not None: + return self.__flat_spaces + + spaces = self.spaces + size = 1 + for n in self.shape: + size *= n + try: + spaces = sum((spaces[i].spaces for i in range(size)), ()) + if inputs is not None: + inputs = sum((tuple(inputs[i]) for i in range(size)), ()) + except AttributeError: + break + + self.__flat_spaces = spaces + if inputs is None: + return spaces + else: + return spaces, inputs def element(self, inp=None, cast=True): """Create an element in the product space. @@ -462,58 +488,80 @@ def element(self, inp=None, cast=True): Examples -------- >>> r2, r3 = odl.rn(2), odl.rn(3) - >>> vec_2, vec_3 = r2.element(), r3.element() - >>> r2x3 = ProductSpace(r2, r3) - >>> vec_2x3 = r2x3.element() - >>> vec_2.space == vec_2x3[0].space + >>> x2, x3 = r2.element(), r3.element() + >>> Z = odl.ProductSpace(r2, r3) + >>> z = Z.element() + >>> z in Z True - >>> vec_3.space == vec_2x3[1].space + >>> z[0] in Z[0] + True + >>> z[1] in Z[1] True Create an element of the product space >>> r2, r3 = odl.rn(2), odl.rn(3) - >>> prod = ProductSpace(r2, r3) + >>> Z = odl.ProductSpace(r2, r3) >>> x2 = r2.element([1, 2]) >>> x3 = r3.element([1, 2, 3]) - >>> x = prod.element([x2, x3]) - >>> x - ProductSpace(rn(2), rn(3)).element([ - [ 1., 2.], - [ 1., 2., 3.] - ]) + >>> z = Z.element([x2, x3]) + >>> z + array([array([ 1., 2.]), array([ 1., 2., 3.])], dtype=object) """ - # If data is given as keyword arg, prefer it over arg list - if inp is None: - inp = [space.element() for space in self.spaces] - if inp in self: return inp - if len(inp) != len(self): - raise ValueError('length of `inp` {} does not match length of ' - 'space {}'.format(len(inp), len(self))) - - if (all(isinstance(v, LinearSpaceElement) and v.space == space - for v, space in zip(inp, self.spaces))): - parts = list(inp) - elif cast: - # Delegate constructors - parts = [space.element(arg) - for arg, space in zip(inp, self.spaces)] + if inp is None: + flat_spaces = self._flatten() + flat_inp = [space.element() for space in flat_spaces] else: - raise TypeError('input {!r} not a sequence of elements of the ' - 'component spaces'.format(inp)) + flat_spaces, flat_inp = self._flatten(inp) - return self.element_type(self, parts) + if len(flat_inp) != len(flat_spaces): + raise ValueError( + "flattened size {} of input {!r} does not match this space's " + 'size {}'.format(len(flat_inp), inp, self.size) + ) - @property - def examples(self): - """Return examples from all sub-spaces.""" - for examples in product(*[spc.examples for spc in self.spaces]): - name = ', '.join(name for name, _ in examples) - element = self.element([elem for _, elem in examples]) - yield (name, element) + if cast: + flat_inp = [ + space.element(xi) for xi, space in zip(flat_inp, flat_spaces) + ] + elif not all(xi in space for xi, space in zip(flat_inp, flat_spaces)): + raise TypeError( + 'input {!r} not a sequence of elements of the component ' + 'spaces'.format(inp) + ) + + # Use an object array for final storage whose "outer shape" is equal + # to `self.shape`. + # Note: the array must be created in advance, since otherwise NumPy + # may still try to loop over the inputs. + # See https://github.com/numpy/numpy/issues/12479 + # TODO(kohr-h): maybe remove when above issue is resolved + ret = np.empty(len(flat_spaces), dtype=object) + for i, xi in enumerate(flat_inp): + ret[i] = xi + + shape = self.shape + if len(flat_spaces) > self.size: + shape += (len(flat_spaces) // self.size,) + return ret.reshape(shape) + + def to_scalar_dtype(self, elem): + """Convert power space element to NumPy array with scalar dtype.""" + + def comp_list_map(func): + def nested(x): + return list(map(func, x)) + + return nested + + nested_list = list + for _ in self.shape[1:]: + nested_list = comp_list_map(nested_list) + + return np.array(nested_list(elem)) def zero(self): """Create the zero element of the product space. @@ -532,14 +580,9 @@ def zero(self): Examples -------- - >>> r2, r3 = odl.rn(2), odl.rn(3) - >>> zero_2, zero_3 = r2.zero(), r3.zero() - >>> r2x3 = ProductSpace(r2, r3) - >>> zero_2x3 = r2x3.zero() - >>> zero_2 == zero_2x3[0] - True - >>> zero_3 == zero_2x3[1] - True + >>> Z = odl.ProductSpace(odl.rn(2), odl.rn(3)) + >>> Z.zero() + array([array([ 0., 0.]), array([ 0., 0., 0.])], dtype=object) """ return self.element([space.zero() for space in self.spaces]) @@ -560,46 +603,304 @@ def one(self): Examples -------- - >>> r2, r3 = odl.rn(2), odl.rn(3) - >>> one_2, one_3 = r2.one(), r3.one() - >>> r2x3 = ProductSpace(r2, r3) - >>> one_2x3 = r2x3.one() - >>> one_2 == one_2x3[0] - True - >>> one_3 == one_2x3[1] - True + >>> Z = odl.ProductSpace(odl.rn(2), odl.rn(3)) + >>> Z.one() + array([array([ 1., 1.]), array([ 1., 1., 1.])], dtype=object) """ return self.element([space.one() for space in self.spaces]) + @property + def examples(self): + """Return examples from all sub-spaces.""" + for examples in product(*[spc.examples for spc in self.spaces]): + name = ', '.join(name for name, _ in examples) + element = self.element([elem for _, elem in examples]) + yield (name, element) + + def apply(self, func, x): + """Apply a function to each component of an element. + + Parameters + ---------- + func : callable + Function that should be applied to each component of ``x``. + Must be of the form :: + + func(x[i]) -> result + + i.e., accept 1 argument, a component of ``x``, and return the + result for that component. It may choose to mutate the input + in-place. + x : numpy.ndarray + Element to which ``func`` should be applied. It must be an + element of this space, i.e., it must satisfy ``elem in self``. + + Returns + ------- + new_elem : numpy.ndarray + Result of applying the function componentwise. + + Examples + -------- + >>> pspace = odl.ProductSpace(odl.rn(2), odl.rn(3)) + >>> x = pspace.element([[1, -1], [2, 0, -3]]) + >>> pspace.apply(np.sign, x) + array([array([ 1., -1.]), array([ 1., 0., -1.])], dtype=object) + """ + if x not in self: + raise ValueError( + '`x` {!r} is not an element of {!r}'.format(x, self) + ) + + x_flat = x.ravel() + res = np.empty(self.size, dtype=object) + for i in range(self.size): + res[i] = func(x_flat[i]) + return res.reshape(self.shape) + + + def apply2(self, func, x): + """Apply an index-dependent function to each component of an element. + + Parameters + ---------- + func : callable + Function that should be applied to each component of ``x``. + Must be of the form :: + + func(x[i], i) -> result + + i.e., accept 2 arguments, a component of ``x`` and the + (multi-) index of the location of that component, and return the + result for that component. It may choose to mutate the input + in-place. + x : numpy.ndarray + Element to which ``func`` should be applied. It must be an + element of this space, i.e., it must satisfy ``elem in self``. + + Returns + ------- + new_elem : numpy.ndarray + Result of applying the function componentwise. + + Examples + -------- + >>> pspace = odl.ProductSpace(odl.rn(2), odl.rn(3)) + >>> x = pspace.element([[1, -1], [2, 0, -3]]) + >>> y = pspace.element([[1, 0], [0, 1, 2]]) + >>> pspace.apply2(lambda v, i: v * y[i], x) + array([array([ 1., -0.]), array([ 0., 0., -6.])], dtype=object) + """ + if x not in self: + raise ValueError( + '`x` {!r} is not an element of {!r}'.format(x, self) + ) + + x_flat = x.ravel() + res = np.empty(self.size, dtype=object) + for i in range(self.size): + idx = np.unravel_index(i, self.shape) + res[i] = func(x_flat[i], idx) + return res.reshape(self.shape) + + + def __contains__(self, other): + """Return ``other in self``.""" + # TODO: doctest + if not (isinstance(other, np.ndarray) and other.dtype == object): + return False + return all(oi in spc for oi, spc in zip(other, self.spaces)) + + # --- Space functions + def _lincomb(self, a, x, b, y, out): """Linear combination ``out = a*x + b*y``.""" - for space, xp, yp, outp in zip(self.spaces, x.parts, y.parts, - out.parts): - space._lincomb(a, xp, b, yp, outp) + for space, xi, yi, out_i in zip(self.spaces, x, y, out): + space._lincomb(a, xi, b, yi, out_i) - def _dist(self, x1, x2): - """Distance between two elements.""" - return self.weighting.dist(x1, x2) + def _inner(self, x1, x2): + """Inner product of two elements.""" + return self.field.element( + _weighted_inner(x1, x2, self.weighting, self.spaces) + ) def _norm(self, x): """Norm of an element.""" - return self.weighting.norm(x) + return _weighted_norm(x, self.exponent, self.weighting, self.spaces) - def _inner(self, x1, x2): - """Inner product of two elements.""" - return self.weighting.inner(x1, x2) + def _dist(self, x1, x2): + """Distance between two elements.""" + return _weighted_dist( + x1, x2, self.exponent, self.weighting, self.spaces + ) def _multiply(self, x1, x2, out): """Product ``out = x1 * x2``.""" - for spc, xp, yp, outp in zip(self.spaces, x1.parts, x2.parts, - out.parts): - spc._multiply(xp, yp, outp) + field = () if self.field is None else self.field + if x1 in field: + x1 = [x1] * len(self) + if x2 in field: + x2 = [x2] * len(self) + for spc, xi, yi, out_i in zip(self.spaces, x1, x2, out): + spc._multiply(xi, yi, out_i) def _divide(self, x1, x2, out): """Quotient ``out = x1 / x2``.""" - for spc, xp, yp, outp in zip(self.spaces, x1.parts, x2.parts, - out.parts): - spc._divide(xp, yp, outp) + field = () if self.field is None else self.field + if x1 in field: + x1 = [x1] * len(self) + if x2 in field: + x2 = [x2] * len(self) + for spc, xi, yi, out_i in zip(self.spaces, x1, x2, out): + spc._divide(xi, yi, out_i) + + @property + def ufuncs(self): + """Access to NumPy ufuncs.""" + if self.__ufuncs is not None: + return self.__ufuncs + + space = self + + class ProductSpaceUfuncs(object): + + """Accessor class for Ufuncs on product spaces.""" + + def __getattr__(self, name): + """Return ``self.name``.""" + from functools import partial + + npy_ufunc = getattr(np, name, None) + if not isinstance(npy_ufunc, np.ufunc): + raise ValueError('{!r} is not a ufunc'.format(name)) + + if npy_ufunc.nin == 1 and npy_ufunc.nout == 1: + return partial(space._ufunc_call_11, name) + elif npy_ufunc.nin == 1 and npy_ufunc.nout == 2: + return partial(space._ufunc_call_12, name) + elif npy_ufunc.nin == 2 and npy_ufunc.nout == 1: + return partial(space._ufunc_call_21, name) + elif npy_ufunc.nin == 2 and npy_ufunc.nout == 2: + return partial(space._ufunc_call_22, name) + else: + raise RuntimeError + + self.__ufuncs = ProductSpaceUfuncs() + return self.__ufuncs + + @property + def reduce(self): + """Access to NumPy reductions.""" + if self.__reduce is not None: + return self.__reduce + + space = self + + class ProductSpaceReduce(object): + + """Accessor class for reductions on product spaces.""" + + def __getattr__(self, name): + """Return ``self.name``.""" + from functools import partial + + npy_red = getattr(np, name) + try: + spec = getargspec(npy_red) + except (ValueError, TypeError): + raise ValueError( + '{!r} is not a valid reduction'.format(name) + ) + if 'keepdims' not in spec.args: + raise ValueError( + '{!r} is not a valid reduction'.format(name) + ) + + return partial(space._reduction_call, name) + + self.__reduce = ProductSpaceReduce() + return self.__reduce + + def _ufunc_call_11(self, name, x, out=None, **kwargs): + if out is None: + out = [None] * len(self.spaces) + res_list = [ + getattr(space.ufuncs, name)(xi, oi, **kwargs) + for xi, oi, space in zip(x, out, self.spaces) + ] + if out[0] is None: + out = np.empty(len(res_list), dtype=object) + for i in range(len(res_list)): + out[i] = res_list[i] + + return out + + def _ufunc_call_12(self, name, x, out1=None, out2=None, **kwargs): + if out1 is None: + out1 = [None] * len(self.spaces) + if out2 is None: + out2 = [None] * len(self.spaces) + + res_list = [ + getattr(space.ufuncs, name)(xi, o1i, o2i, **kwargs) + for xi, o1i, o2i, space in zip(x, out1, out2, self.spaces) + ] + if out1[0] is None: + out1 = np.empty(len(res_list), dtype=object) + for i in range(len(res_list)): + out1[i] = res_list[i][0] + if out2[0] is None: + out2 = np.empty(len(res_list), dtype=object) + for i in range(len(res_list)): + out2[i] = res_list[i][1] + + return out1, out2 + + def _ufunc_call_21(self, name, x1, x2, out=None, **kwargs): + if out is None: + out = [None] * len(self.spaces) + res_list = [ + getattr(space.ufuncs, name)(x1i, x2i, oi, **kwargs) + for x1i, x2i, oi, space in zip(x1, x2, out, self.spaces) + ] + if out[0] is None: + out = np.empty(len(res_list), dtype=object) + for i in range(len(res_list)): + out[i] = res_list[i] + + return out + + def _ufunc_call_22(self, name, x1, x2, out1=None, out2=None, **kwargs): + if out1 is None: + out1 = [None] * len(self.spaces) + if out2 is None: + out2 = [None] * len(self.spaces) + + res_list = [ + getattr(space.ufuncs, name)(x1i, x2i, o1i, o2i, **kwargs) + for x1i, x2i, o1i, o2i, space in zip(x1, x2, out1, out2, + self.spaces) + ] + if out1[0] is None: + out1 = np.empty(len(res_list), dtype=object) + for i in range(len(res_list)): + out1[i] = res_list[i][0] + if out2[0] is None: + out2 = np.empty(len(res_list), dtype=object) + for i in range(len(res_list)): + out2[i] = res_list[i][1] + + return out1, out2 + + def _reduction_call(self, name, x, axis=None, out=None, keepdims=False): + npy_red = getattr(np, name) + reds = [ + getattr(space.reduce, name)(xi) + for xi, space in zip(x, self.spaces) + ] + return npy_red(reds) + + # --- Misc --- # def __eq__(self, other): """Return ``self == other``. @@ -612,33 +913,50 @@ def __eq__(self, other): Examples -------- - >>> r2, r3 = odl.rn(2), odl.rn(3) - >>> rn, rm = odl.rn(2), odl.rn(3) - >>> r2x3, rnxm = ProductSpace(r2, r3), ProductSpace(rn, rm) - >>> r2x3 == rnxm + >>> Z1 = odl.ProductSpace(odl.rn(2), odl.rn(3)) + >>> Z2 = odl.ProductSpace(odl.rn(2), odl.rn(3)) + >>> Z1 == Z2 True - >>> r3x2 = ProductSpace(r3, r2) - >>> r2x3 == r3x2 + >>> swapped = odl.ProductSpace(odl.rn(3), odl.rn(2)) + >>> swapped == Z1 False - >>> r5 = ProductSpace(*[odl.rn(1)]*5) - >>> r2x3 == r5 + >>> r6 = odl.ProductSpace(*([odl.rn(1)] * 6)) + >>> r6 == Z1 False - >>> r5 = odl.rn(5) - >>> r2x3 == r5 + >>> r6 = odl.rn(6) + >>> r6 == Z1 False """ if other is self: return True - else: - return (isinstance(other, ProductSpace) and - len(self) == len(other) and - self.weighting == other.weighting and - all(x == y for x, y in zip(self.spaces, - other.spaces))) + elif not isinstance(other, ProductSpace): + return False + + weightings_equal = ( + ( + # Compare constants + self.weighting_type == 'const' + and np.isscalar(other.weighting) + and self.weighting == other.weighting + ) + # But only check identity for arrays + or self.weighting is other.weighting + ) + + return ( + len(self) == len(other) + and self.exponent == other.exponent + and weightings_equal + and all(s == o for s, o in zip(self.spaces, other.spaces)) + ) def __hash__(self): """Return ``hash(self)``.""" - return hash((type(self), self.spaces, self.weighting)) + if np.isscalar(self.weighting): + weighting_hash = hash(self.weighting) + else: + weighting_hash = hash(self.weighting.tobytes()) + return hash((type(self), self.spaces, self.exponent, weighting_hash)) def __getitem__(self, indices): """Return ``self[indices]``. @@ -733,659 +1051,17 @@ def __getitem__(self, indices): raise TypeError('`indices` must be integer, slice, tuple or ' 'list, got {!r}'.format(indices)) - def __str__(self): - """Return ``str(self)``.""" - if len(self) == 0: - return '{}' - elif self.is_power_space: - return '({}) ** {}'.format(self.spaces[0], len(self)) - else: - return ' x '.join(str(space) for space in self.spaces) + def show(self, elem, title=None, indices=None, **kwargs): + """Display the parts of this product space element graphically. - def __repr__(self): - """Return ``repr(self)``.""" - weight_str = self.weighting.repr_part - edgeitems = np.get_printoptions()['edgeitems'] - if len(self) == 0: - posargs = [] - posmod = '' - optargs = [('field', self.field, None)] - oneline = True - elif self.is_power_space: - posargs = [self.spaces[0], len(self)] - posmod = '!r' - optargs = [] - oneline = True - elif self.size <= 2 * edgeitems: - posargs = self.spaces - posmod = '!r' - optargs = [] - argstr = ', '.join(repr(s) for s in self.spaces) - oneline = (len(argstr + weight_str) <= 40 and - '\n' not in argstr + weight_str) - else: - posargs = (self.spaces[:edgeitems] + - ('...',) + - self.spaces[-edgeitems:]) - posmod = ['!r'] * edgeitems + ['!s'] + ['!r'] * edgeitems - optargs = [] - oneline = False - - if oneline: - inner_str = signature_string(posargs, optargs, sep=', ', - mod=[posmod, '!r']) - if weight_str: - inner_str = ', '.join([inner_str, weight_str]) - return '{}({})'.format(self.__class__.__name__, inner_str) - else: - inner_str = signature_string(posargs, optargs, sep=',\n', - mod=[posmod, '!r']) - if weight_str: - inner_str = ',\n'.join([inner_str, weight_str]) - return '{}(\n{}\n)'.format(self.__class__.__name__, - indent(inner_str)) - - @property - def element_type(self): - """`ProductSpaceElement`""" - return ProductSpaceElement - - -class ProductSpaceElement(LinearSpaceElement): - - """Elements of a `ProductSpace`.""" - - def __init__(self, space, parts): - """Initialize a new instance.""" - super(ProductSpaceElement, self).__init__(space) - self.__parts = tuple(parts) - - @property - def parts(self): - """Parts of this product space element.""" - return self.__parts - - @property - def shape(self): - """Number of values per axis in ``self``, computed recursively. - - The recursion ends at the fist level that does not have a shape. - - Raises - ------ - ValueError - If a `ProductSpace` is encountered that is not a power space. - - See Also - -------- - ProductSpace.shape - - Examples - -------- - >>> r4_3 = odl.ProductSpace(odl.rn(4), 3) - >>> x = r4_3.element() - >>> x.shape - (3, 4) - >>> r4_2_3 = odl.ProductSpace(r4_3, 2) - >>> y = r4_2_3.element() - >>> y.shape - (2, 3, 4) - """ - return self.space.shape - - @property - def ndim(self): - """Number axes in ``self``, computed recursively. - - Raises - ------ - ValueError - If a `ProductSpace` is encountered that is not a power space. - - See Also - -------- - shape - - Examples - -------- - >>> r4_3 = odl.ProductSpace(odl.rn(4), 3) - >>> x = r4_3.element() - >>> x.ndim - 2 - >>> r4_2_3 = odl.ProductSpace(r4_3, 2) - >>> y = r4_2_3.element() - >>> y.ndim - 3 - """ - return len(self.shape) - - @property - def size(self): - """Total number of involved spaces, computed recursively. - - See Also - -------- - ProductSpace.size - """ - return int(np.prod(self.shape)) - - @property - def dtype(self): - """The data type of the space of this element.""" - return self.space.dtype - - def __len__(self): - """Return ``len(self)``.""" - return len(self.space) - - @property - def nbytes(self): - """Total number of bytes in memory used by this element.""" - return self.space.nbytes - - def __eq__(self, other): - """Return ``self == other``. - - Overrides the default `LinearSpace` method since it is implemented with - the distance function, which is prone to numerical errors. This - function checks equality per component. - """ - if other is self: - return True - elif other not in self.space: - return False - else: - return all(sp == op for sp, op in zip(self.parts, other.parts)) - - def __getitem__(self, indices): - """Return ``self[indices]``.""" - if isinstance(indices, Integral): - return self.parts[indices] - elif isinstance(indices, slice): - return self.space[indices].element(self.parts[indices]) - elif isinstance(indices, list): - out_parts = [self.parts[i] for i in indices] - return self.space[indices].element(out_parts) - elif isinstance(indices, tuple): - if len(indices) == 0: - return ProductSpace().element() - elif len(indices) == 1: - # Tuple with a single entry - we just unpack and delegate - return self[indices[0]] - else: - # Tuple with multiple entries - if isinstance(indices[0], Integral): - # In case the first entry is an integer, we drop the - # axis and return directly from `parts` - return self.parts[indices[0]][indices[1:]] - else: - # indices[0] is a slice or list. We first retrieve the - # parts indexed in this axis. - # In any case we know that we want to keep this axis. - if isinstance(indices[0], list): - part = [self.parts[i] for i in indices[0]] - else: - part = self.parts[indices[0]] - - if (len(indices[1:]) == 1 and - not all(isinstance(p, ProductSpaceElement) - for p in part)): - # This case means we have "hit the bottom", i.e., - # there are non-ProductSpaces involved. In order - # not to retrieve scalar values from these - # elements, we use a slice of size 1. - idx = indices[1] - indexed = [p[idx:idx + 1] for p in part] - else: - # Here we're still in the "product space chain", - # so we can use recursion to go on. - indexed = [p[indices[1:]] for p in part] - - # Finally make a wrapping space for the indexed elements - new_space = ProductSpace(*(p.space for p in indexed)) - return new_space.element(indexed) - else: - raise TypeError('bad index type {}'.format(type(indices))) - - def __setitem__(self, indices, values): - """Implement ``self[indices] = values``.""" - # Get the parts to which we assign values - if isinstance(indices, Integral): - indexed_parts = (self.parts[indices],) - values = (values,) - elif isinstance(indices, slice): - indexed_parts = self.parts[indices] - elif isinstance(indices, list): - indexed_parts = tuple(self.parts[i] for i in indices) - elif isinstance(indices, tuple): - if len(indices) == 0: - return - else: - # We need to explicitly use __setitem__ here, otherwise - # __getitem__ is used and assigned to, which fails if - # a space like rn(3) is indexed at the very end. - part = self.parts[indices[0]] - if isinstance(part, LinearSpaceElement): - part.__setitem__(indices[1:], values) - else: - # part is a tuple - for p in part: - p.__setitem__(indices[1:], values) - return - else: - raise TypeError('bad index type {}'.format(type(indices))) - - # Do the assignment, with broadcasting if desired - try: - iter(values) - except TypeError: - # `values` is not iterable, assume it can be assigned to - # all indexed parts - for p in indexed_parts: - p[:] = values - else: - # `values` is iterable; it could still represent a single - # element of a power space. - if self.space.is_power_space and values in self.space[0]: - # Broadcast a single element across a power space - for p in indexed_parts: - p[:] = values - else: - # Now we really have one assigned value per part - if len(values) != len(indexed_parts): - raise ValueError( - 'length of iterable `values` not equal to number of ' - 'indexed parts ({} != {})' - ''.format(len(values), len(indexed_parts))) - for p, v in zip(indexed_parts, values): - p[:] = v - - def asarray(self, out=None): - """Extract the data of this vector as a numpy array. - - Only available if `is_power_space` is True. - - The ordering is such that it commutes with indexing:: - - self[ind].asarray() == self.asarray()[ind] - - Parameters - ---------- - out : `numpy.ndarray`, optional - Array in which the result should be written in-place. - Has to be contiguous and of the correct dtype and - shape. - - Raises - ------ - ValueError - If `is_power_space` is false. - - Examples - -------- - >>> spc = odl.ProductSpace(odl.rn(3), 2) - >>> x = spc.element([[ 1., 2., 3.], - ... [ 4., 5., 6.]]) - >>> x.asarray() - array([[ 1., 2., 3.], - [ 4., 5., 6.]]) - """ - if not self.space.is_power_space: - raise ValueError('cannot use `asarray` if `space.is_power_space` ' - 'is `False`') - else: - if out is None: - out = np.empty(self.shape, self.dtype) - - for i in range(len(self)): - out[i] = np.asarray(self[i]) - return out - - def __array__(self): - """An array representation of ``self``. - - Only available if `is_power_space` is True. - - The ordering is such that it commutes with indexing:: - - np.array(self[ind]) == np.array(self)[ind] - - Raises - ------ - ValueError - If `is_power_space` is false. - - Examples - -------- - >>> spc = odl.ProductSpace(odl.rn(3), 2) - >>> x = spc.element([[ 1., 2., 3.], - ... [ 4., 5., 6.]]) - >>> np.asarray(x) - array([[ 1., 2., 3.], - [ 4., 5., 6.]]) - """ - return self.asarray() - - def __array_wrap__(self, array): - """Return a new product space element wrapping the ``array``. - - Only available if `is_power_space` is ``True``. - - Parameters - ---------- - array : `numpy.ndarray` - Array to be wrapped. - - Returns - ------- - wrapper : `ProductSpaceElement` - Product space element wrapping ``array``. - """ - # HACK(kohr-h): This is to support (full) reductions like - # `np.sum(x)` for numpy>=1.16, where many such reductions - # moved from plain functions to `ufunc.reduce.*`, thus - # invoking the `__array__` and `__array_wrap__` machinery. - if array.shape == (): - return array.item() - - return self.space.element(array) - - @property - def ufuncs(self): - """`ProductSpaceUfuncs`, access to Numpy style ufuncs. - - These are always available if the underlying spaces are - `TensorSpace`. - - Examples - -------- - >>> r22 = odl.ProductSpace(odl.rn(2), 2) - >>> x = r22.element([[1, -2], [-3, 4]]) - >>> x.ufuncs.absolute() - ProductSpace(rn(2), 2).element([ - [ 1., 2.], - [ 3., 4.] - ]) - - These functions can also be used with non-vector arguments and - support broadcasting, per component and even recursively: - - >>> x.ufuncs.add([1, 2]) - ProductSpace(rn(2), 2).element([ - [ 2., 0.], - [-2., 6.] - ]) - >>> x.ufuncs.subtract(1) - ProductSpace(rn(2), 2).element([ - [ 0., -3.], - [-4., 3.] - ]) - - There is also support for various reductions (sum, prod, min, max): - - >>> x.ufuncs.sum() - 0.0 - - Writing to ``out`` is also supported: - - >>> y = r22.element() - >>> result = x.ufuncs.absolute(out=y) - >>> result - ProductSpace(rn(2), 2).element([ - [ 1., 2.], - [ 3., 4.] - ]) - >>> result is y - True - - See Also - -------- - odl.util.ufuncs.TensorSpaceUfuncs - Base class for ufuncs in `TensorSpace` spaces, subspaces may - override this for greater efficiency. - odl.util.ufuncs.ProductSpaceUfuncs - For a list of available ufuncs. - """ - return ProductSpaceUfuncs(self) - - @property - def real(self): - """Real part of the element. - - The real part can also be set using ``x.real = other``, where ``other`` - is array-like or scalar. - - Examples - -------- - >>> space = odl.ProductSpace(odl.cn(3), odl.cn(2)) - >>> x = space.element([[1 + 1j, 2, 3 - 3j], - ... [-1 + 2j, -2 - 3j]]) - >>> x.real - ProductSpace(rn(3), rn(2)).element([ - [ 1., 2., 3.], - [-1., -2.] - ]) - - The real part can also be set using different array-like types: - - >>> x.real = space.real_space.zero() - >>> x - ProductSpace(cn(3), cn(2)).element([ - [ 0.+1.j, 0.+0.j, 0.-3.j], - [ 0.+2.j, 0.-3.j] - ]) - - >>> x.real = 1.0 - >>> x - ProductSpace(cn(3), cn(2)).element([ - [ 1.+1.j, 1.+0.j, 1.-3.j], - [ 1.+2.j, 1.-3.j] - ]) - - >>> x.real = [[2, 3, 4], [5, 6]] - >>> x - ProductSpace(cn(3), cn(2)).element([ - [ 2.+1.j, 3.+0.j, 4.-3.j], - [ 5.+2.j, 6.-3.j] - ]) - """ - real_part = [part.real for part in self.parts] - return self.space.real_space.element(real_part) - - @real.setter - def real(self, newreal): - """Setter for the real part. - - This method is invoked by ``x.real = other``. - - Parameters - ---------- - newreal : array-like or scalar - Values to be assigned to the real part of this element. - """ - try: - iter(newreal) - except TypeError: - # `newreal` is not iterable, assume it can be assigned to - # all indexed parts - for part in self.parts: - part.real = newreal - return - - if self.space.is_power_space: - try: - # Set same value in all parts - for part in self.parts: - part.real = newreal - except (ValueError, TypeError): - # Iterate over all parts and set them separately - for part, new_re in zip(self.parts, newreal): - part.real = new_re - pass - elif len(newreal) == len(self): - for part, new_re in zip(self.parts, newreal): - part.real = new_re - else: - raise ValueError( - 'dimensions of the new real part does not match the space, ' - 'got element {} to set real part of {}'.format(newreal, self)) - - @property - def imag(self): - """Imaginary part of the element. - - The imaginary part can also be set using ``x.imag = other``, where - ``other`` is array-like or scalar. - - - Examples - -------- - >>> space = odl.ProductSpace(odl.cn(3), odl.cn(2)) - >>> x = space.element([[1 + 1j, 2, 3 - 3j], - ... [-1 + 2j, -2 - 3j]]) - >>> x.imag - ProductSpace(rn(3), rn(2)).element([ - [ 1., 0., -3.], - [ 2., -3.] - ]) - - The imaginary part can also be set using different array-like types: - - >>> x.imag = space.real_space.zero() - >>> x - ProductSpace(cn(3), cn(2)).element([ - [ 1.+0.j, 2.+0.j, 3.+0.j], - [-1.+0.j, -2.+0.j] - ]) - - >>> x.imag = 1.0 - >>> x - ProductSpace(cn(3), cn(2)).element([ - [ 1.+1.j, 2.+1.j, 3.+1.j], - [-1.+1.j, -2.+1.j] - ]) - - >>> x.imag = [[2, 3, 4], [5, 6]] - >>> x - ProductSpace(cn(3), cn(2)).element([ - [ 1.+2.j, 2.+3.j, 3.+4.j], - [-1.+5.j, -2.+6.j] - ]) - """ - imag_part = [part.imag for part in self.parts] - return self.space.real_space.element(imag_part) - - @imag.setter - def imag(self, newimag): - """Setter for the imaginary part. - - This method is invoked by ``x.imag = other``. - - Parameters - ---------- - newimag : array-like or scalar - Values to be assigned to the imaginary part of this element. - """ - try: - iter(newimag) - except TypeError: - # `newimag` is not iterable, assume it can be assigned to - # all indexed parts - for part in self.parts: - part.imag = newimag - return - - if self.space.is_power_space: - try: - # Set same value in all parts - for part in self.parts: - part.imag = newimag - except (ValueError, TypeError): - # Iterate over all parts and set them separately - for part, new_im in zip(self.parts, newimag): - part.imag = new_im - pass - elif len(newimag) == len(self): - for part, new_im in zip(self.parts, newimag): - part.imag = new_im - else: - raise ValueError( - 'dimensions of the new imaginary part does not match the ' - 'space, got element {} to set real part of {}}' - ''.format(newimag, self)) - - def conj(self): - """Complex conjugate of the element.""" - complex_conj = [part.conj() for part in self.parts] - return self.space.element(complex_conj) - - def __str__(self): - """Return ``str(self)``.""" - return repr(self) - - def __repr__(self): - """Return ``repr(self)``. - - Examples - -------- - >>> from odl import rn # need to import rn into namespace - >>> r2, r3 = odl.rn(2), odl.rn(3) - >>> r2x3 = ProductSpace(r2, r3) - >>> x = r2x3.element([[1, 2], [3, 4, 5]]) - >>> eval(repr(x)) == x - True - - The result is readable: - - >>> x - ProductSpace(rn(2), rn(3)).element([ - [ 1., 2.], - [ 3., 4., 5.] - ]) - - Nestled spaces work as well: - - >>> X = ProductSpace(r2x3, r2x3) - >>> x = X.element([[[1, 2], [3, 4, 5]],[[1, 2], [3, 4, 5]]]) - >>> eval(repr(x)) == x - True - >>> x - ProductSpace(ProductSpace(rn(2), rn(3)), 2).element([ - [ - [ 1., 2.], - [ 3., 4., 5.] - ], - [ - [ 1., 2.], - [ 3., 4., 5.] - ] - ]) - """ - inner_str = '[\n' - if len(self) < 5: - inner_str += ',\n'.join('{}'.format( - _indent(_strip_space(part))) for part in self.parts) - else: - inner_str += ',\n'.join('{}'.format( - _indent(_strip_space(part))) for part in self.parts[:3]) - inner_str += ',\n ...\n' - inner_str += ',\n'.join('{}'.format( - _indent(_strip_space(part))) for part in self.parts[-1:]) - - inner_str += '\n]' - - return '{!r}.element({})'.format(self.space, inner_str) - - def show(self, title=None, indices=None, **kwargs): - """Display the parts of this product space element graphically. - - Parameters - ---------- - title : string, optional - Title of the figures - - indices : int, slice, tuple or list, optional - Display parts of ``self`` in the way described in the following. + Parameters + ---------- + elem : numpy.ndarray with ``dtype == object`` + Element to display using the properties of this space. + title : string, optional + Title of the figures + indices : int, slice, tuple or list, optional + Display parts of ``elem`` in the way described in the following. A single list of integers selects the corresponding parts of this vector. @@ -1397,9 +1073,9 @@ def show(self, title=None, indices=None, **kwargs): The types of the first entry trigger the following behaviors: - - ``int``: take the part corresponding to this index - - ``slice``: take a subset of the parts - - ``None``: equivalent to ``slice(None)``, i.e., everything + - ``int``: take the part corresponding to this index + - ``slice``: take a subset of the parts + - ``None``: equivalent to ``slice(None)``, i.e., everything Typical use cases are displaying of selected parts, which can be achieved with a list, e.g., ``indices=[0, 2]`` for parts @@ -1411,12 +1087,10 @@ def show(self, title=None, indices=None, **kwargs): indexes the parts only, i.e., is treated roughly as ``(indices, Ellipsis)``. In particular, for ``None``, all parts are shown with default slicing. - in_figs : sequence of `matplotlib.figure.Figure`, optional Update these figures instead of creating new ones. Typically the return value of an earlier call to ``show`` is used for this parameter. - kwargs Additional arguments passed on to the ``show`` methods of the parts. @@ -1436,14 +1110,16 @@ def show(self, title=None, indices=None, **kwargs): odl.util.graphics.show_discrete_data : Underlying implementation """ + elem = self.element(elem) + if title is None: title = 'ProductSpaceElement' if indices is None: - if len(self) < 5: - indices = list(range(len(self))) + if len(elem) < 5: + indices = list(range(len(elem))) else: - indices = list(np.linspace(0, len(self) - 1, 4, dtype=int)) + indices = list(np.linspace(0, len(elem) - 1, 4, dtype=int)) else: if (isinstance(indices, tuple) or (isinstance(indices, list) and @@ -1459,7 +1135,7 @@ def show(self, title=None, indices=None, **kwargs): indices = slice(None) if isinstance(indices, slice): - indices = list(range(*indices.indices(len(self)))) + indices = list(range(*indices.indices(len(elem)))) elif isinstance(indices, Integral): indices = [indices] else: @@ -1470,412 +1146,190 @@ def show(self, title=None, indices=None, **kwargs): in_figs = [None] * len(indices) if in_figs is None else in_figs figs = [] - parts = self[indices] + parts = elem[indices] if len(parts) == 0: return () elif len(parts) == 1: # Don't extend the title if there is only one plot - fig = parts[0].show(title=title, fig=in_figs[0], **kwargs) + fig = self.spaces[0].show( + parts[0], title=title, fig=in_figs[0], **kwargs + ) figs.append(fig) else: # Extend titles by indexed part to make them distinguishable - for i, part, fig in zip(indices, parts, in_figs): - fig = part.show(title='{}. Part {}'.format(title, i), fig=fig, - **kwargs) + for i, xi, space, fig in zip(indices, parts, self.spaces, in_figs): + fig = space.show( + xi, title='{}. Part {}'.format(title, i), fig=fig, + **kwargs + ) figs.append(fig) return tuple(figs) - -# --- Add arithmetic operators that broadcast --- # - - -def _broadcast_arithmetic(op): - """Return ``op(self, other)`` with broadcasting. - - Parameters - ---------- - op : string - Name of the operator, e.g. ``'__add__'``. - - Returns - ------- - broadcast_arithmetic_op : function - Function intended to be used as a method for `ProductSpaceVector` - which performs broadcasting if possible. - - Notes - ----- - Broadcasting is the operation of "applying an operator multiple times" in - some sense. For example: - - .. math:: - (1, 2) + 1 = (2, 3) - - is a form of broadcasting. In this implementation, we only allow "single - layer" broadcasting, i.e., we do not support broadcasting over several - product spaces at once. - """ - def _broadcast_arithmetic_impl(self, other): - if (self.space.is_power_space and other in self.space[0]): - results = [] - for xi in self: - res = getattr(xi, op)(other) - if res is NotImplemented: - return NotImplemented - else: - results.append(res) - - return self.space.element(results) + def __str__(self): + """Return ``str(self)``.""" + if len(self) == 0: + return '{}' + elif self.is_power_space: + return '({}) ** {}'.format(self.spaces[0], len(self)) else: - return getattr(LinearSpaceElement, op)(self, other) - - # Set docstring - docstring = """Broadcasted {op}.""".format(op=op) - _broadcast_arithmetic_impl.__doc__ = docstring - - return _broadcast_arithmetic_impl - - -for op in ['add', 'sub', 'mul', 'div', 'truediv']: - for modifier in ['', 'r', 'i']: - name = '__{}{}__'.format(modifier, op) - setattr(ProductSpaceElement, name, _broadcast_arithmetic(name)) - - -class ProductSpaceArrayWeighting(ArrayWeighting): - - """Array weighting for `ProductSpace`. - - This class defines a weighting that has a different value for - each index defined in a given space. - See ``Notes`` for mathematical details. - """ - - def __init__(self, array, exponent=2.0): - r"""Initialize a new instance. - - Parameters - ---------- - array : 1-dim. `array-like` - Weighting array of the inner product. - exponent : positive float, optional - Exponent of the norm. For values other than 2.0, no inner - product is defined. - - Notes - ----- - - For exponent 2.0, a new weighted inner product with array - :math:`w` is defined as - - .. math:: - \langle x, y \rangle_w = \langle w \odot x, y \rangle - - with component-wise multiplication :math:`w \odot x`. For other - exponents, only ``norm`` and ``dist`` are defined. In the case - of exponent ``inf``, the weighted norm is - - .. math:: - \|x\|_{w,\infty} = \|w \odot x\|_\infty, - - otherwise it is - - .. math:: - \|x\|_{w,p} = \|w^{1/p} \odot x\|_p. - - - Note that this definition does **not** fulfill the limit property - in :math:`p`, i.e., - - .. math:: - \|x\|_{w,p} \not\to \|x\|_{w,\infty} - \quad\text{for } p \to \infty - - unless :math:`w = (1,...,1)`. The reason for this choice - is that the alternative with the limit property consists in - ignoring the weights altogether. - - - The array may only have positive entries, otherwise it does not - define an inner product or norm, respectively. This is not checked - during initialization. - """ - super(ProductSpaceArrayWeighting, self).__init__( - array, impl='numpy', exponent=exponent) - - def inner(self, x1, x2): - """Calculate the array-weighted inner product of two elements. - - Parameters - ---------- - x1, x2 : `ProductSpaceElement` - Elements whose inner product is calculated. + return ' x '.join(str(space) for space in self.spaces) - Returns - ------- - inner : float or complex - The inner product of the two provided elements. - """ - if self.exponent != 2.0: - raise NotImplementedError('no inner product defined for ' - 'exponent != 2 (got {})' - ''.format(self.exponent)) - - inners = np.fromiter( - (x1i.inner(x2i) for x1i, x2i in zip(x1, x2)), - dtype=x1[0].space.dtype, count=len(x1)) - - inner = np.dot(inners, self.array) - if is_real_dtype(x1[0].dtype): - return float(inner) + def __repr__(self): + """Return ``repr(self)``.""" + # TODO(kohr-h): verify that this is correct + if np.isscalar(self.weighting): + weight_str = '' if self.weighting == 1.0 else str(self.weighting) else: - return complex(inner) - - def norm(self, x): - """Calculate the array-weighted norm of an element. - - Parameters - ---------- - x : `ProductSpaceElement` - Element whose norm is calculated. - - Returns - ------- - norm : float - The norm of the provided element. - """ - if self.exponent == 2.0: - norm_squared = self.inner(x, x).real # TODO: optimize?! - return np.sqrt(norm_squared) + weight_str = np.array2string(self.weighting) + edgeitems = np.get_printoptions()['edgeitems'] + if len(self) == 0: + posargs = [] + posmod = '' + optargs = [('field', self.field, None)] + oneline = True + elif self.is_power_space: + posargs = [self.spaces[0], len(self)] + posmod = '!r' + optargs = [] + oneline = True + elif self.size <= 2 * edgeitems: + posargs = self.spaces + posmod = '!r' + optargs = [] + argstr = ', '.join(repr(s) for s in self.spaces) + oneline = (len(argstr + weight_str) <= 40 and + '\n' not in argstr + weight_str) else: - norms = np.fromiter( - (xi.norm() for xi in x), dtype=np.float64, count=len(x)) - if self.exponent in (1.0, float('inf')): - norms *= self.array - else: - norms *= self.array ** (1.0 / self.exponent) - - return float(np.linalg.norm(norms, ord=self.exponent)) - - -class ProductSpaceConstWeighting(ConstWeighting): - - """Constant weighting for `ProductSpace`. - - """ - - def __init__(self, constant, exponent=2.0): - r"""Initialize a new instance. - - Parameters - ---------- - constant : positive float - Weighting constant of the inner product - exponent : positive float, optional - Exponent of the norm. For values other than 2.0, no inner - product is defined. - - Notes - ----- - - For exponent 2.0, a new weighted inner product with constant - :math:`c` is defined as - - .. math:: - \langle x, y \rangle_c = c\, \langle x, y \rangle. - - For other exponents, only ``norm`` and ```dist`` are defined. - In the case of exponent ``inf``, the weighted norm is - - .. math:: - \|x\|_{c,\infty} = c\, \|x\|_\infty, - - otherwise it is - - .. math:: - \|x\|_{c,p} = c^{1/p} \, \|x\|_p. - - - Note that this definition does **not** fulfill the limit property - in :math:`p`, i.e., - - .. math:: - \|x\|_{c,p} \not\to \|x\|_{c,\infty} - \quad \text{for } p \to \infty - - unless :math:`c = 1`. The reason for this choice - is that the alternative with the limit property consists in - ignoring the weight altogether. - - - The constant must be positive, otherwise it does not define an - inner product or norm, respectively. - """ - super(ProductSpaceConstWeighting, self).__init__( - constant, impl='numpy', exponent=exponent) - - def inner(self, x1, x2): - """Calculate the constant-weighted inner product of two elements. - - Parameters - ---------- - x1, x2 : `ProductSpaceElement` - Elements whose inner product is calculated. - - Returns - ------- - inner : float or complex - The inner product of the two provided elements. - """ - if self.exponent != 2.0: - raise NotImplementedError('no inner product defined for ' - 'exponent != 2 (got {})' - ''.format(self.exponent)) - - inners = np.fromiter( - (x1i.inner(x2i) for x1i, x2i in zip(x1, x2)), - dtype=x1[0].space.dtype, count=len(x1)) - - inner = self.const * np.sum(inners) - return x1.space.field.element(inner) - - def norm(self, x): - """Calculate the constant-weighted norm of an element. - - Parameters - ---------- - x1 : `ProductSpaceElement` - Element whose norm is calculated. + posargs = (self.spaces[:edgeitems] + + ('...',) + + self.spaces[-edgeitems:]) + posmod = ['!r'] * edgeitems + ['!s'] + ['!r'] * edgeitems + optargs = [] + oneline = False - Returns - ------- - norm : float - The norm of the element. - """ - if self.exponent == 2.0: - norm_squared = self.inner(x, x).real # TODO: optimize?! - return np.sqrt(norm_squared) + if oneline: + inner_str = signature_string(posargs, optargs, sep=', ', + mod=[posmod, '!r']) + if weight_str: + inner_str = ', '.join([inner_str, weight_str]) + return '{}({})'.format(self.__class__.__name__, inner_str) else: - norms = np.fromiter( - (xi.norm() for xi in x), dtype=np.float64, count=len(x)) - - if self.exponent in (1.0, float('inf')): - return (self.const * - float(np.linalg.norm(norms, ord=self.exponent))) - else: - return (self.const ** (1 / self.exponent) * - float(np.linalg.norm(norms, ord=self.exponent))) - - def dist(self, x1, x2): - """Calculate the constant-weighted distance between two elements. - - Parameters - ---------- - x1, x2 : `ProductSpaceElement` - Elements whose mutual distance is calculated. + inner_str = signature_string(posargs, optargs, sep=',\n', + mod=[posmod, '!r']) + if weight_str: + inner_str = ',\n'.join([inner_str, weight_str]) + return '{}(\n{}\n)'.format(self.__class__.__name__, + indent(inner_str)) - Returns - ------- - dist : float - The distance between the elements. - """ - dnorms = np.fromiter( - ((x1i - x2i).norm() for x1i, x2i in zip(x1, x2)), - dtype=np.float64, count=len(x1)) - if self.exponent == float('inf'): - return self.const * np.linalg.norm(dnorms, ord=self.exponent) +def _weighted_inner(x1, x2, weights, spaces): + """Weighted inner product on a `ProductSpace`.""" + if ( + np.isscalar(weights) + or (isinstance(weights, np.ndarray) and weights.size == 1) + ): + return _const_weighted_inner(x1, x2, weights, spaces) + elif isinstance(weights, np.ndarray) and weights.ndim == 1: + return _array_weighted_inner(x1, x2, weights, spaces) + else: + raise ValueError("`weights` is neither a constant nor a 1D array") + + +def _array_weighted_inner(x1, x2, weights, spaces): + """Inner product weighted by an array (one entry per subspace).""" + inners = np.array( + [space.inner(x1i, x2i) for space, x1i, x2i in zip(spaces, x1, x2)] + ) + inner = np.dot(inners, weights) + return inner.item() + + +def _const_weighted_inner(x1, x2, weight, spaces): + """Inner product weighted by a constant.""" + inners = np.array( + [space.inner(x1i, x2i) for space, x1i, x2i in zip(spaces, x1, x2)] + ) + return (weight * np.sum(inners)).item() + + +def _weighted_norm(x, p, weights, spaces): + """Weighted p-norm on a `ProductSpace`.""" + if ( + np.isscalar(weights) + or (isinstance(weights, np.ndarray) and weights.size == 1) + ): + return _const_weighted_norm(x, p, weights, spaces) + elif isinstance(weights, np.ndarray) and weights.ndim == 1: + return _array_weighted_norm(x, p, weights, spaces) + else: + raise ValueError("`weights` is neither a constant nor a 1D array") + + +def _array_weighted_norm(x, p, weights, spaces): + """Norm with exponent p, weighted by an array (one entry per subspace).""" + if p == 2.0: + # TODO(kohr-h): optimize? + norm_squared = _array_weighted_inner(x, x, weights, spaces).real + return np.sqrt(norm_squared).item() + else: + norms = np.array([space.norm(xi) for space, xi in zip(spaces, x)]) + if p == 1.0: + norms *= weights + elif p not in {float('inf'), 0.0, -float('inf')}: + norms *= weights ** (1 / p) + + return np.linalg.norm(norms, ord=p).item() + + +def _const_weighted_norm(x, p, weight, spaces): + """Norm with exponent p, weighted by a constant.""" + if p == 2.0: + # TODO(kohr-h): optimize? + norm_squared = _const_weighted_inner(x, x, weight, spaces).real + return np.sqrt(norm_squared).item() + else: + norms = np.array([space.norm(xi) for space, xi in zip(spaces, x)]) + if p in {float('inf'), 0.0, -float('inf')}: + return np.linalg.norm(norms, ord=p).item() else: - return (self.const ** (1 / self.exponent) * - np.linalg.norm(dnorms, ord=self.exponent)) - - -class ProductSpaceCustomInner(CustomInner): - - """Class for handling a user-specified inner products.""" - - def __init__(self, inner): - """Initialize a new instance. - - Parameters - ---------- - inner : callable - The inner product implementation. It must accept two - `ProductSpaceElement` arguments, return a element from - the field of the space (real or complex number) and - satisfy the following conditions for all space elements - ``x, y, z`` and scalars ``s``: - - - `` = conj()`` - - `` = s * + `` - - `` = 0`` if and only if ``x = 0`` - """ - super(ProductSpaceCustomInner, self).__init__( - impl='numpy', inner=inner) - - -class ProductSpaceCustomNorm(CustomNorm): - - """Class for handling a user-specified norm on `ProductSpace`. - - Note that this removes ``inner``. - """ - - def __init__(self, norm): - """Initialize a new instance. - - Parameters - ---------- - norm : callable - The norm implementation. It must accept a - `ProductSpaceElement` argument, return a float and satisfy - the following conditions for all space elements - ``x, y`` and scalars ``s``: - - - ``||x|| >= 0`` - - ``||x|| = 0`` if and only if ``x = 0`` - - ``||s * x|| = |s| * ||x||`` - - ``||x + y|| <= ||x|| + ||y||`` - """ - super(ProductSpaceCustomNorm, self).__init__(norm, impl='numpy') - - -class ProductSpaceCustomDist(CustomDist): - - """Class for handling a user-specified distance on `ProductSpace`. - - Note that this removes ``inner`` and ``norm``. - """ - - def __init__(self, dist): - """Initialize a new instance. - - Parameters - ---------- - dist : callable - The distance function defining a metric on - `ProductSpace`. It must accept two `ProductSpaceElement` - arguments and fulfill the following mathematical conditions - for any three space elements ``x, y, z``: - - - ``dist(x, y) >= 0`` - - ``dist(x, y) = 0`` if and only if ``x = y`` - - ``dist(x, y) = dist(y, x)`` - - ``dist(x, y) <= dist(x, z) + dist(z, y)`` - """ - super(ProductSpaceCustomDist, self).__init__(dist, impl='numpy') - - -def _strip_space(x): - """Strip the SPACE.element( ... ) part from a repr.""" - r = repr(x) - space_repr = '{!r}.element('.format(x.space) - if r.startswith(space_repr) and r.endswith(')'): - r = r[len(space_repr):-1] - return r - - -def _indent(x): - """Indent a string by 4 characters.""" - lines = x.splitlines() - for i, line in enumerate(lines): - lines[i] = ' ' + line - return '\n'.join(lines) + return (weight ** (1 / p) * np.linalg.norm(norms, ord=p)).item() + + +def _weighted_dist(x1, x2, p, weights, spaces): + """Weighted p-distance on a `ProductSpace`.""" + if ( + np.isscalar(weights) + or (isinstance(weights, np.ndarray) and weights.size == 1) + ): + return _const_weighted_dist(x1, x2, p, weights, spaces) + elif isinstance(weights, np.ndarray) and weights.ndim == 1: + return _array_weighted_dist(x1, x2, p, weights, spaces) + else: + raise ValueError("`weights` is neither a constant nor a 1D array") + + +def _array_weighted_dist(x1, x2, p, weights, spaces): + """Dist with exponent p, weighted by an array (one entry per subspace).""" + norms = np.array( + [space.norm(x1i - x2i) for space, x1i, x2i in zip(spaces, x1, x2)] + ) + if p not in {float('inf'), 0.0, -float('inf')}: + norms *= weights ** (1 / p) + + return np.linalg.norm(norms, ord=p).item() + + +def _const_weighted_dist(x1, x2, p, weight, spaces): + """Dist with exponent p, weighted by a constant.""" + dists = np.array( + [space.dist(x1i, x2i) for space, x1i, x2i in zip(spaces, x1, x2)] + ) + + if p in {float('inf'), 0.0, -float('inf')}: + return np.linalg.norm(dists, ord=p).item() + else: + return (weight ** (1 / p) * np.linalg.norm(dists, ord=p)).item() if __name__ == '__main__': diff --git a/odl/space/space_utils.py b/odl/space/space_utils.py index 1130bcb8c93..3899c52133c 100644 --- a/odl/space/space_utils.py +++ b/odl/space/space_utils.py @@ -9,83 +9,12 @@ """Utility functions for space implementations.""" from __future__ import print_function, division, absolute_import -import numpy as np from odl.set import RealNumbers, ComplexNumbers from odl.space.entry_points import tensor_space_impl -__all__ = ('vector', 'tensor_space', 'cn', 'rn') - - -def vector(array, dtype=None, order=None, impl='numpy'): - """Create a vector from an array-like object. - - Parameters - ---------- - array : `array-like` - Array from which to create the vector. Scalars become - one-dimensional vectors. - dtype : optional - Set the data type of the vector manually with this option. - By default, the space type is inferred from the input data. - order : {None, 'C', 'F'}, optional - Axis ordering of the data storage. For the default ``None``, - no contiguousness is enforced, avoiding a copy if possible. - impl : str, optional - Impmlementation back-end for the space. See - `odl.space.entry_points.tensor_space_impl_names` for available - options. - - Returns - ------- - vector : `Tensor` - Vector created from the input array. Its concrete type depends - on the provided arguments. - - Notes - ----- - This is a convenience function and not intended for use in - speed-critical algorithms. - - Examples - -------- - Create one-dimensional vectors: - - >>> odl.vector([1, 2, 3]) # No automatic cast to float - tensor_space(3, dtype=int).element([1, 2, 3]) - >>> odl.vector([1, 2, 3], dtype=float) - rn(3).element([ 1., 2., 3.]) - >>> odl.vector([1, 2 - 1j, 3]) - cn(3).element([ 1.+0.j, 2.-1.j, 3.+0.j]) - - Non-scalar types are also supported: - - >>> odl.vector([True, True, False]) - tensor_space(3, dtype=bool).element([ True, True, False]) - - The function also supports multi-dimensional input: - - >>> odl.vector([[1, 2, 3], - ... [4, 5, 6]]) - tensor_space((2, 3), dtype=int).element( - [[1, 2, 3], - [4, 5, 6]] - ) - """ - # Sanitize input - arr = np.array(array, copy=False, order=order, ndmin=1) - if arr.dtype is object: - raise ValueError('invalid input data resulting in `dtype==object`') - - # Set dtype - if dtype is not None: - space_dtype = dtype - else: - space_dtype = arr.dtype - - space = tensor_space(arr.shape, dtype=space_dtype, impl=impl) - return space.element(arr) +__all__ = ('tensor_space', 'cn', 'rn') def tensor_space(shape, dtype=None, impl='numpy', **kwargs): diff --git a/odl/space/weighting.py b/odl/space/weighting.py deleted file mode 100644 index 0c236548fea..00000000000 --- a/odl/space/weighting.py +++ /dev/null @@ -1,876 +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/. - -"""Weightings for finite-dimensional spaces.""" - -from __future__ import print_function, division, absolute_import -from builtins import object -import numpy as np - -from odl.space.base_tensors import TensorSpace -from odl.util import array_str, signature_string, indent - - -__all__ = ('MatrixWeighting', 'ArrayWeighting', 'ConstWeighting', - 'CustomInner', 'CustomNorm', 'CustomDist') - - -class Weighting(object): - - """Abstract base class for weighting of finite-dimensional spaces. - - This class and its subclasses serve as a simple means to evaluate - and compare weighted inner products, norms and metrics semantically - rather than by identity on a pure function level. - - The functions are implemented similarly to `Operator`, - but without extra type checks of input parameters - this is done in - the callers of the `LinearSpace` instance where these - functions are being used. - """ - - def __init__(self, impl, exponent=2.0): - """Initialize a new instance. - - Parameters - ---------- - impl : string - Specifier for the implementation backend - exponent : positive float, optional - Exponent of the norm. For values other than 2.0, the inner - product is not defined. - """ - self.__impl = str(impl).lower() - self.__exponent = float(exponent) - if self.exponent <= 0: - raise ValueError('only positive exponents or inf supported, ' - 'got {}'.format(exponent)) - - @property - def impl(self): - """Implementation backend of this weighting.""" - return self.__impl - - @property - def exponent(self): - """Exponent of this weighting.""" - return self.__exponent - - def __eq__(self, other): - """Return ``self == other``. - - Returns - ------- - equal : bool - ``True`` if ``other`` is a the same weighting, ``False`` - otherwise. - - Notes - ----- - This operation must be computationally cheap, i.e. no large - arrays may be compared entry-wise. That is the task of the - `equiv` method. - """ - return (isinstance(other, Weighting) and - self.impl == other.impl and - self.exponent == other.exponent) - - def __hash__(self): - """Return ``hash(self)``.""" - return hash((type(self), self.impl, self.exponent)) - - def equiv(self, other): - """Test if ``other`` is an equivalent weighting. - - Should be overridden, default tests for equality. - - Returns - ------- - equivalent : bool - ``True`` if ``other`` is a `Weighting` instance which - yields the same result as this inner product for any - input, ``False`` otherwise. - """ - return self == other - - def inner(self, x1, x2): - """Return the inner product of two elements. - - Parameters - ---------- - x1, x2 : `LinearSpaceElement` - Elements whose inner product is calculated. - - Returns - ------- - inner : float or complex - The inner product of the two provided elements. - """ - raise NotImplementedError - - def norm(self, x): - """Calculate the norm of an element. - - This is the standard implementation using `inner`. - Subclasses should override it for optimization purposes. - - Parameters - ---------- - x1 : `LinearSpaceElement` - Element whose norm is calculated. - - Returns - ------- - norm : float - The norm of the element. - """ - return float(np.sqrt(self.inner(x, x).real)) - - def dist(self, x1, x2): - """Calculate the distance between two elements. - - This is the standard implementation using `norm`. - Subclasses should override it for optimization purposes. - - Parameters - ---------- - x1, x2 : `LinearSpaceElement` - Elements whose mutual distance is calculated. - - Returns - ------- - dist : float - The distance between the elements. - """ - return self.norm(x1 - x2) - - -class MatrixWeighting(Weighting): - - """Weighting of a space by a matrix. - - The exact definition of the weighted inner product, norm and - distance functions depend on the concrete space. - - The matrix must be Hermitian and posivive definite, otherwise it - does not define an inner product or norm, respectively. This is not - checked during initialization. - """ - - def __init__(self, matrix, impl, exponent=2.0, **kwargs): - """Initialize a new instance. - - Parameters - ---------- - matrix : `scipy.sparse.spmatrix` or 2-dim. `array-like` - Square weighting matrix of the inner product - impl : string - Specifier for the implementation backend - exponent : positive float, optional - Exponent of the norm. For values other than 2.0, the inner - product is not defined. - If ``matrix`` is a sparse matrix, only 1.0, 2.0 and ``inf`` - are allowed. - precomp_mat_pow : bool, optional - If ``True``, precompute the matrix power ``W ** (1/p)`` - during initialization. This has no effect if ``exponent`` - is 1.0, 2.0 or ``inf``. - - Default: ``False`` - - cache_mat_pow : bool, optional - If ``True``, cache the matrix power ``W ** (1/p)``. This can - happen either during initialization or in the first call to - ``norm`` or ``dist``, resp. This has no effect if - ``exponent`` is 1.0, 2.0 or ``inf``. - - Default: ``True`` - - cache_mat_decomp : bool, optional - If ``True``, cache the eigenbasis decomposition of the - matrix. This can happen either during initialization or in - the first call to ``norm`` or ``dist``, resp. This has no - effect if ``exponent`` is 1.0, 2.0 or ``inf``. - - Default: ``False`` - - Notes - ----- - The matrix power ``W ** (1/p)`` is computed by eigenbasis - decomposition:: - - eigval, eigvec = scipy.linalg.eigh(matrix) - mat_pow = (eigval ** p * eigvec).dot(eigvec.conj().T) - - Depending on the matrix size, this can be rather expensive. - """ - # Lazy import to improve `import odl` time - import scipy.sparse - - # TODO: fix dead link `scipy.sparse.spmatrix` - precomp_mat_pow = kwargs.pop('precomp_mat_pow', False) - self._cache_mat_pow = bool(kwargs.pop('cache_mat_pow', True)) - self._cache_mat_decomp = bool(kwargs.pop('cache_mat_decomp', False)) - super(MatrixWeighting, self).__init__(impl=impl, exponent=exponent) - - # Check and set matrix - if scipy.sparse.isspmatrix(matrix): - self._matrix = matrix - else: - self._matrix = np.asarray(matrix) - if self._matrix.dtype == object: - raise ValueError('invalid matrix {}'.format(matrix)) - elif self._matrix.ndim != 2: - raise ValueError('matrix {} is {}-dimensional instead of ' - '2-dimensional' - ''.format(matrix, self._matrix.ndim)) - - if self._matrix.shape[0] != self._matrix.shape[1]: - raise ValueError('matrix has shape {}, expected a square matrix' - ''.format(self._matrix.shape)) - - if (scipy.sparse.isspmatrix(self.matrix) and - self.exponent not in (1.0, 2.0, float('inf'))): - raise NotImplementedError('sparse matrices only supported for ' - 'exponent 1.0, 2.0 or `inf`') - - # Compute the power and decomposition if desired - self._eigval = self._eigvec = None - if self.exponent in (1.0, float('inf')): - self._mat_pow = self.matrix - elif precomp_mat_pow and self.exponent != 2.0: - eigval, eigvec = self.matrix_decomp() - if self._cache_mat_decomp: - self._eigval, self._eigvec = eigval, eigvec - eigval_pow = eigval ** (1.0 / self.exponent) - else: - eigval_pow = eigval - eigval_pow **= 1.0 / self.exponent - self._mat_pow = (eigval_pow * eigvec).dot(eigvec.conj().T) - else: - self._mat_pow = None - - @property - def matrix(self): - """Weighting matrix of this inner product.""" - return self._matrix - - def is_valid(self): - """Test if the matrix is positive definite Hermitian. - - If the matrix decomposition is available, this test checks - if all eigenvalues are positive. - Otherwise, the test tries to calculate a Cholesky decomposition, - which can be very time-consuming for large matrices. Sparse - matrices are not supported. - """ - # Lazy import to improve `import odl` time - import scipy.sparse - - if scipy.sparse.isspmatrix(self.matrix): - raise NotImplementedError('validation not supported for sparse ' - 'matrices') - elif self._eigval is not None: - return np.all(np.greater(self._eigval, 0)) - else: - try: - np.linalg.cholesky(self.matrix) - return np.array_equal(self.matrix, self.matrix.conj().T) - except np.linalg.LinAlgError: - return False - - def matrix_decomp(self, cache=None): - """Compute a Hermitian eigenbasis decomposition of the matrix. - - Parameters - ---------- - cache : bool or None, optional - If ``True``, store the decomposition internally. For None, - the ``cache_mat_decomp`` from class initialization is used. - - Returns - ------- - eigval : `numpy.ndarray` - One-dimensional array of eigenvalues. Its length is equal - to the number of matrix rows. - eigvec : `numpy.ndarray` - Two-dimensional array of eigenvectors. It has the same shape - as the decomposed matrix. - - See Also - -------- - scipy.linalg.decomp.eigh : - Implementation of the decomposition. Standard parameters - are used here. - - Raises - ------ - NotImplementedError - if the matrix is sparse (not supported by scipy 0.17) - """ - # Lazy import to improve `import odl` time - import scipy.linalg - import scipy.sparse - - # TODO: fix dead link `scipy.linalg.decomp.eigh` - if scipy.sparse.isspmatrix(self.matrix): - raise NotImplementedError('sparse matrix not supported') - - if cache is None: - cache = self._cache_mat_decomp - - if self._eigval is None or self._eigvec is None: - eigval, eigvec = scipy.linalg.eigh(self.matrix) - if cache: - self._eigval = eigval - self._eigvec = eigvec - else: - eigval, eigvec = self._eigval, self._eigvec - - return eigval, eigvec - - def __eq__(self, other): - """Return ``self == other``. - - Returns - ------- - equals : bool - ``True`` if other is a `MatrixWeighting` instance - with **identical** matrix, ``False`` otherwise. - - See Also - -------- - equiv : test for equivalent inner products - """ - if other is self: - return True - - return (super(MatrixWeighting, self).__eq__(other) and - self.matrix is getattr(other, 'matrix', None)) - - def __hash__(self): - """Return ``hash(self)``.""" - # TODO: Better hash for matrix? - return hash((super(MatrixWeighting, self).__hash__(), - self.matrix.tobytes())) - - def equiv(self, other): - """Test if other is an equivalent weighting. - - Returns - ------- - equivalent : bool - ``True`` if ``other`` is a `Weighting` instance with the same - `Weighting.impl`, which yields the same result as this - weighting for any input, ``False`` otherwise. This is checked - by entry-wise comparison of matrices/arrays/constants. - """ - # Lazy import to improve `import odl` time - import scipy.sparse - - # Optimization for equality - if self == other: - return True - - elif self.exponent != getattr(other, 'exponent', -1): - return False - - elif isinstance(other, MatrixWeighting): - if self.matrix.shape != other.matrix.shape: - return False - - if scipy.sparse.isspmatrix(self.matrix): - if other.matrix_issparse: - # Optimization for different number of nonzero elements - if self.matrix.nnz != other.matrix.nnz: - return False - else: - # Most efficient out-of-the-box comparison - return (self.matrix != other.matrix).nnz == 0 - else: # Worst case: compare against dense matrix - return np.array_equal(self.matrix.todense(), other.matrix) - - else: # matrix of `self` is dense - if other.matrix_issparse: - return np.array_equal(self.matrix, other.matrix.todense()) - else: - return np.array_equal(self.matrix, other.matrix) - - elif isinstance(other, ArrayWeighting): - if scipy.sparse.isspmatrix(self.matrix): - return (np.array_equiv(self.matrix.diagonal(), - other.array) and - np.array_equal(self.matrix.asformat('dia').offsets, - np.array([0]))) - else: - return np.array_equal( - self.matrix, other.array * np.eye(self.matrix.shape[0])) - - elif isinstance(other, ConstWeighting): - if scipy.sparse.isspmatrix(self.matrix): - return (np.array_equiv(self.matrix.diagonal(), other.const) and - np.array_equal(self.matrix.asformat('dia').offsets, - np.array([0]))) - else: - return np.array_equal( - self.matrix, other.const * np.eye(self.matrix.shape[0])) - else: - return False - - @property - def repr_part(self): - """Return a string usable in a space's ``__repr__`` method.""" - # Lazy import to improve `import odl` time - import scipy.sparse - - if scipy.sparse.isspmatrix(self.matrix): - optargs = [('matrix', str(self.matrix), '')] - else: - optargs = [('matrix', array_str(self.matrix, nprint=10), '')] - - optargs.append(('exponent', self.exponent, 2.0)) - return signature_string([], optargs, mod=[[], ['!s', ':.4']]) - - def __repr__(self): - """Return ``repr(self)``.""" - if self.matrix_issparse: - posargs = ['<{} sparse matrix, format {}, {} nonzero entries>' - ''.format(self.matrix.shape, self.matrix.format, - self.matrix.nnz)] - else: - posargs = [array_str(self.matrix, nprint=10)] - - optargs = [('exponent', self.exponent, 2.0)] - inner_str = signature_string(posargs, optargs, sep=',\n', - mod=['!s', '']) - return '{}(\n{}\n)'.format(self.__class__.__name__, indent(inner_str)) - - def __str__(self): - """Return ``str(self)``.""" - return repr(self) - - -class ArrayWeighting(Weighting): - - """Weighting of a space by an array. - - The exact definition of the weighted inner product, norm and - distance functions depend on the concrete space. - - The array may only have positive entries, otherwise it does not - define an inner product or norm, respectively. This is not checked - during initialization. - """ - - def __init__(self, array, impl, exponent=2.0): - """Initialize a new instance. - - Parameters - ---------- - array : `array-like` - Weighting array of inner product, norm and distance. - Native `Tensor` instances are stored as-is without copying. - impl : string - Specifier for the implementation backend. - exponent : positive float, optional - Exponent of the norm. For values other than 2.0, the inner - product is not defined. - """ - super(ArrayWeighting, self).__init__(impl=impl, exponent=exponent) - - # We apply array duck-typing to allow all kinds of Numpy-array-like - # data structures without change - array_attrs = ('shape', 'dtype', 'itemsize') - if (all(hasattr(array, attr) for attr in array_attrs) and - not isinstance(array, TensorSpace)): - self.__array = array - else: - raise TypeError('`array` {!r} does not look like a valid array' - ''.format(array)) - - @property - def array(self): - """Weighting array of this instance.""" - return self.__array - - def is_valid(self): - """Return True if the array is a valid weight, i.e. positive.""" - return np.all(np.greater(self.array, 0)) - - def __eq__(self, other): - """Return ``self == other``. - - Returns - ------- - equals : bool - ``True`` if ``other`` is an `ArrayWeighting` instance with - **identical** array, False otherwise. - - See Also - -------- - equiv : test for equivalent inner products - """ - if other is self: - return True - - return (super(ArrayWeighting, self).__eq__(other) and - self.array is getattr(other, 'array', None)) - - def __hash__(self): - """Return ``hash(self)``.""" - # TODO: Better hash for array? - return hash((super(ArrayWeighting, self).__hash__(), - self.array.tobytes())) - - def equiv(self, other): - """Return True if other is an equivalent weighting. - - Returns - ------- - equivalent : bool - ``True`` if ``other`` is a `Weighting` instance with the same - `Weighting.impl`, which yields the same result as this - weighting for any input, ``False`` otherwise. This is checked - by entry-wise comparison of arrays/constants. - """ - # Optimization for equality - if self == other: - return True - elif (not isinstance(other, Weighting) or - self.exponent != other.exponent): - return False - elif isinstance(other, MatrixWeighting): - return other.equiv(self) - elif isinstance(other, ConstWeighting): - return np.array_equiv(self.array, other.const) - else: - return np.array_equal(self.array, other.array) - - @property - def repr_part(self): - """String usable in a space's ``__repr__`` method.""" - optargs = [('weighting', array_str(self.array, nprint=10), ''), - ('exponent', self.exponent, 2.0)] - return signature_string([], optargs, sep=',\n', - mod=[[], ['!s', ':.4']]) - - def __repr__(self): - """Return ``repr(self)``.""" - posargs = [array_str(self.array)] - optargs = [('exponent', self.exponent, 2.0)] - inner_str = signature_string(posargs, optargs, sep=',\n', - mod=['!s', ':.4']) - return '{}(\n{}\n)'.format(self.__class__.__name__, indent(inner_str)) - - def __str__(self): - """Return ``str(self)``.""" - return repr(self) - - -class ConstWeighting(Weighting): - - """Weighting of a space by a constant.""" - - def __init__(self, const, impl, exponent=2.0): - """Initialize a new instance. - - Parameters - ---------- - constant : positive float - Weighting constant of the inner product. - impl : string - Specifier for the implementation backend. - exponent : positive float, optional - Exponent of the norm. For values other than 2.0, the inner - product is not defined. - """ - super(ConstWeighting, self).__init__(impl=impl, exponent=exponent) - self.__const = float(const) - - if self.const <= 0: - raise ValueError('expected positive constant, got {}' - ''.format(const)) - if not np.isfinite(self.const): - raise ValueError('`const` {} is invalid'.format(const)) - - @property - def const(self): - """Weighting constant of this inner product.""" - return self.__const - - def __eq__(self, other): - """Return ``self == other``. - - Returns - ------- - equal : bool - ``True`` if ``other`` is a `ConstWeighting` instance with the - same constant, ``False`` otherwise. - """ - if other is self: - return True - - return (super(ConstWeighting, self).__eq__(other) and - self.const == getattr(other, 'const', None)) - - def __hash__(self): - """Return ``hash(self)``.""" - return hash((super(ConstWeighting, self).__hash__(), self.const)) - - def equiv(self, other): - """Test if other is an equivalent weighting. - - Returns - ------- - equivalent : bool - ``True`` if other is a `Weighting` instance with the same - `Weighting.impl`, which yields the same result as this - weighting for any input, ``False`` otherwise. This is checked - by entry-wise comparison of matrices/arrays/constants. - """ - if isinstance(other, ConstWeighting): - return self == other - elif isinstance(other, (ArrayWeighting, MatrixWeighting)): - return other.equiv(self) - else: - return False - - @property - def repr_part(self): - """String usable in a space's ``__repr__`` method.""" - optargs = [('weighting', self.const, 1.0), - ('exponent', self.exponent, 2.0)] - return signature_string([], optargs, mod=':.4') - - def __repr__(self): - """Return ``repr(self)``.""" - posargs = [self.const] - optargs = [('exponent', self.exponent, 2.0)] - return '{}({})'.format(self.__class__.__name__, - signature_string(posargs, optargs)) - - def __str__(self): - """Return ``str(self)``.""" - return repr(self) - - -class CustomInner(Weighting): - - """Class for handling a user-specified inner product.""" - - def __init__(self, inner, impl): - """Initialize a new instance. - - Parameters - ---------- - inner : callable - The inner product implementation. It must accept two - `LinearSpaceElement` arguments, return an element from - their space's field (real or complex number) and - satisfy the following conditions for all space elements - ``x, y, z`` and scalars ``s``: - - - `` = conj()`` - - `` = s * + `` - - `` = 0`` if and only if ``x = 0`` - - impl : string - Specifier for the implementation backend. - """ - super(CustomInner, self).__init__(impl=impl, exponent=2.0) - - if not callable(inner): - raise TypeError('`inner` {!r} is not callable' - ''.format(inner)) - self.__inner = inner - - @property - def inner(self): - """Custom inner product of this instance..""" - return self.__inner - - def __eq__(self, other): - """Return ``self == other``. - - Returns - ------- - equal : bool - ``True`` if other is a `CustomInner` - instance with the same inner product, ``False`` otherwise. - """ - return (super(CustomInner, self).__eq__(other) and - self.inner == other.inner) - - def __hash__(self): - """Return ``hash(self)``.""" - return hash((super(CustomInner, self).__hash__(), self.inner)) - - @property - def repr_part(self): - """String usable in a space's ``__repr__`` method.""" - optargs = [('inner', self.inner, '')] - return signature_string([], optargs, mod='!r') - - def __repr__(self): - """Return ``repr(self)``.""" - posargs = [self.inner] - optargs = [] - inner_str = signature_string(posargs, optargs, mod='!r') - return '{}({})'.format(self.__class__.__name__, inner_str) - - -class CustomNorm(Weighting): - - """Class for handling a user-specified norm. - - Note that this removes ``inner``. - """ - - def __init__(self, norm, impl): - """Initialize a new instance. - - Parameters - ---------- - norm : callable - The norm implementation. It must accept a - `LinearSpaceElement` argument, return a float and satisfy - the following conditions for all space elements - ``x, y`` and scalars ``s``: - - - ``||x|| >= 0`` - - ``||x|| = 0`` if and only if ``x = 0`` - - ``||s * x|| = |s| * ||x||`` - - ``||x + y|| <= ||x|| + ||y||`` - impl : string - Specifier for the implementation backend - """ - super(CustomNorm, self).__init__(impl=impl, exponent=1.0) - - if not callable(norm): - raise TypeError('`norm` {!r} is not callable' - ''.format(norm)) - self.__norm = norm - - def inner(self, x1, x2): - """Inner product is not defined for custom distance.""" - raise NotImplementedError('`inner` not defined for custom norm') - - @property - def norm(self): - """Custom norm of this instance..""" - return self.__norm - - def __eq__(self, other): - """Return ``self == other``. - - Returns - ------- - equal : bool - ``True`` if other is a `CustomNorm` instance with the same - norm, ``False`` otherwise. - """ - return (super(CustomNorm, self).__eq__(other) and - self.norm == other.norm) - - def __hash__(self): - """Return ``hash(self)``.""" - return hash((super(CustomNorm, self).__hash__(), self.norm)) - - @property - def repr_part(self): - """Return a string usable in a space's ``__repr__`` method.""" - optargs = [('norm', self.norm, ''), - ('exponent', self.exponent, 2.0)] - return signature_string([], optargs, mod=[[], ['!r', ':.4']]) - - def __repr__(self): - """Return ``repr(self)``.""" - posargs = [self.norm] - optargs = [('exponent', self.exponent, 2.0)] - inner_str = signature_string(posargs, optargs, mod=['!r', ':.4']) - return '{}({})'.format(self.__class__.__name__, inner_str) - - -class CustomDist(Weighting): - - """Class for handling a user-specified distance. - - Note that this removes ``inner`` and ``norm``. - """ - - def __init__(self, dist, impl): - """Initialize a new instance. - - Parameters - ---------- - dist : callable - The distance function defining a metric on a `LinearSpace`. - It must accept two `LinearSpaceElement` arguments, return a - float and and fulfill the following mathematical conditions - for any three space elements ``x, y, z``: - - - ``dist(x, y) >= 0`` - - ``dist(x, y) = 0`` if and only if ``x = y`` - - ``dist(x, y) = dist(y, x)`` - - ``dist(x, y) <= dist(x, z) + dist(z, y)`` - impl : string - Specifier for the implementation backend - """ - super(CustomDist, self).__init__(impl=impl, exponent=1.0) - - if not callable(dist): - raise TypeError('`dist` {!r} is not callable' - ''.format(dist)) - self.__dist = dist - - @property - def dist(self): - """Custom distance of this instance..""" - return self.__dist - - def inner(self, x1, x2): - """Inner product is not defined for custom distance.""" - raise NotImplementedError('`inner` not defined for custom distance') - - def norm(self, x): - """Norm is not defined for custom distance.""" - raise NotImplementedError('`norm` not defined for custom distance') - - def __eq__(self, other): - """Return ``self == other``. - - Returns - ------- - equal : bool - ``True`` if other is a `CustomDist` instance with the same - dist, ``False`` otherwise. - """ - return (super(CustomDist, self).__eq__(other) and - self.dist == other.dist) - - def __hash__(self): - """Return ``hash(self)``.""" - return hash((super(CustomDist, self).__hash__(), self.dist)) - - @property - def repr_part(self): - """Return a string usable in a space's ``__repr__`` method.""" - optargs = [('dist', self.dist, '')] - return signature_string([], optargs, mod=['', '!r']) - - def __repr__(self): - """Return ``repr(self)``.""" - posargs = [self.dist] - optargs = [] - inner_str = signature_string(posargs, optargs, mod=['!r', '']) - return '{}({})'.format(self.__class__.__name__, inner_str) - - -if __name__ == '__main__': - from odl.util.testutils import run_doctests - run_doctests() diff --git a/odl/test/deform/linearized_deform_test.py b/odl/test/deform/linearized_deform_test.py index ebd2136d4ef..c5e36fc1996 100644 --- a/odl/test/deform/linearized_deform_test.py +++ b/odl/test/deform/linearized_deform_test.py @@ -160,31 +160,31 @@ def test_fixed_templ_init(): template = space.element(template_function) # Valid input - op = LinDeformFixedTempl(template) + op = LinDeformFixedTempl(space, template) assert repr(op) != '' - op = LinDeformFixedTempl(template, domain=space.astype('float32') ** 1) + op = LinDeformFixedTempl(space, template, interp='nearest') + assert repr(op) != '' + op = LinDeformFixedTempl(space, template_function) assert repr(op) != '' - - # Invalid input - with pytest.raises(TypeError): - # template_function not a DiscretizedSpaceElement - LinDeformFixedTempl(template_function) def test_fixed_templ_call(space, interp): """Test call of linearized deformation with fixed template.""" + if space.dtype.kind == 'c': + pytest.xfail('wrongly using complex displacement field') + # Define the analytic template as the hat function and its gradient template = space.element(template_function) - deform_op = LinDeformFixedTempl(template, interp=interp) + deform_op = LinDeformFixedTempl(space, template, interp=interp) # Calculate result and exact result true_deformed_templ = space.element(deformed_template) deformed_templ = deform_op(disp_field_factory(space.ndim)) # Verify that the result is within error limits - error = (true_deformed_templ - deformed_templ).norm() - rlt_err = error / deformed_templ.norm() - assert rlt_err < error_bound(interp) + error = space.norm(true_deformed_templ - deformed_templ) + rel_err = error / space.norm(deformed_templ) + assert rel_err < error_bound(interp) def test_fixed_templ_deriv(space, interp): @@ -196,7 +196,7 @@ def test_fixed_templ_deriv(space, interp): template = space.element(template_function) disp_field = disp_field_factory(space.ndim) vector_field = vector_field_factory(space.ndim) - fixed_templ_op = LinDeformFixedTempl(template, interp=interp) + fixed_templ_op = LinDeformFixedTempl(space, template, interp=interp) # Calculate result fixed_templ_op_deriv = fixed_templ_op.derivative(disp_field) @@ -206,9 +206,9 @@ def test_fixed_templ_deriv(space, interp): fixed_templ_deriv_exact = space.element(fixed_templ_deriv) # Verify that the result is within error limits - error = (fixed_templ_deriv_exact - fixed_templ_deriv_comp).norm() - rlt_err = error / fixed_templ_deriv_comp.norm() - assert rlt_err < error_bound(interp) + error = space.norm(fixed_templ_deriv_exact - fixed_templ_deriv_comp) + rel_err = error / space.norm(fixed_templ_deriv_comp) + assert rel_err < error_bound(interp) # --- LinDeformFixedDisp --- # @@ -217,87 +217,85 @@ def test_fixed_templ_deriv(space, interp): def test_fixed_disp_init(): """Test init and props of lin. deformation with fixed displacement.""" space = odl.uniform_discr(0, 1, 5) - disp_field = space.tangent_bundle.element( - disp_field_factory(space.ndim)) + disp_field = space.tangent_bundle.element(disp_field_factory(space.ndim)) # Valid input - op = LinDeformFixedDisp(disp_field) + op = LinDeformFixedDisp(space, disp_field) assert repr(op) != '' - op = LinDeformFixedDisp(disp_field, templ_space=space) + op = LinDeformFixedDisp(space, disp_field, interp='nearest') assert repr(op) != '' + # Okay in 1D + op = LinDeformFixedDisp(space, disp_field[0], interp='nearest') # Non-valid input - with pytest.raises(TypeError): # displacement not ProductSpaceElement - LinDeformFixedDisp(space.one()) - 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 DiscretizedSpace + LinDeformFixedDisp(bad_pspace, disp_field) + with pytest.raises(TypeError): # templ_space not based on DiscreteLp bad_pspace = odl.ProductSpace(odl.rn(2), 1) - LinDeformFixedDisp(disp_field, bad_pspace) - with pytest.raises(TypeError): # wrong dtype on templ_space - wrong_dtype = odl.ProductSpace(space.astype(complex), 1) - LinDeformFixedDisp(disp_field, wrong_dtype) + LinDeformFixedDisp(bad_pspace, disp_field) with pytest.raises(ValueError): # vector field spaces don't match bad_space = odl.uniform_discr(0, 1, 10) - LinDeformFixedDisp(disp_field, bad_space) + LinDeformFixedDisp(bad_space, disp_field) def test_fixed_disp_call(space, interp): """Test call of lin. deformation with fixed displacement.""" + if space.dtype.kind == 'c': + pytest.xfail('wrongly using complex displacement field') + template = space.element(template_function) disp_field = space.real_space.tangent_bundle.element( - disp_field_factory(space.ndim)) + disp_field_factory(space.ndim) + ) # Calculate result and exact result - deform_op = LinDeformFixedDisp( - disp_field, templ_space=space, interp=interp - ) + deform_op = LinDeformFixedDisp(space, disp_field, interp) deformed_templ = deform_op(template) true_deformed_templ = space.element(deformed_template) # Verify that the result is within error limits - error = (true_deformed_templ - deformed_templ).norm() - rlt_err = error / deformed_templ.norm() - assert rlt_err < error_bound(interp) + error = space.norm(true_deformed_templ - deformed_templ) + rel_err = error / space.norm(deformed_templ) + assert rel_err < error_bound(interp) def test_fixed_disp_inv(space, interp): """Test inverse of lin. deformation with fixed displacement.""" + if space.dtype.kind == 'c': + pytest.xfail('wrongly using complex displacement field') + # Set up template and displacement field template = space.element(template_function) disp_field = space.real_space.tangent_bundle.element( disp_field_factory(space.ndim)) # Verify that the inverse is in fact a (left and right) inverse - deform_op = LinDeformFixedDisp( - disp_field, templ_space=space, interp=interp - ) + deform_op = LinDeformFixedDisp(space, disp_field, interp) result_op_inv = deform_op(deform_op.inverse(template)) - error = (result_op_inv - template).norm() - rel_err = error / template.norm() + error = space.norm(result_op_inv - template) + rel_err = error / space.norm(template) assert rel_err < 2 * error_bound(interp) # need a bit more tolerance result_inv_op = deform_op.inverse(deform_op(template)) - error = (result_inv_op - template).norm() - rel_err = error / template.norm() + error = space.norm(result_inv_op - template) + rel_err = error / space.norm(template) assert rel_err < 2 * error_bound(interp) # need a bit more tolerance def test_fixed_disp_adj(space, interp): """Test adjoint of lin. deformation with fixed displacement.""" + if space.dtype.kind == 'c': + pytest.xfail('wrongly using complex displacement field') + # Set up template and displacement field template = space.element(template_function) disp_field = space.real_space.tangent_bundle.element( disp_field_factory(space.ndim)) # Calculate result - deform_op = LinDeformFixedDisp( - disp_field, templ_space=space, interp=interp - ) + deform_op = LinDeformFixedDisp(space, disp_field, interp) deformed_templ_adj = deform_op.adjoint(template) # Calculate the analytic result @@ -306,14 +304,14 @@ def test_fixed_disp_adj(space, interp): true_deformed_templ_adj *= exp_div # Verify that the result is within error limits - error = (deformed_templ_adj - true_deformed_templ_adj).norm() - rel_err = error / true_deformed_templ_adj.norm() + error = space.norm(deformed_templ_adj - true_deformed_templ_adj) + rel_err = error / space.norm(true_deformed_templ_adj) assert rel_err < error_bound(interp) # Verify the adjoint definition = deformed_templ = deform_op(template) - inner1 = deformed_templ.inner(template) - inner2 = template.inner(deformed_templ_adj) + inner1 = space.inner(deformed_templ, template) + inner2 = space.inner(template, deformed_templ_adj) assert inner1 == pytest.approx(inner2, abs=.1) diff --git a/odl/test/discr/diff_ops_test.py b/odl/test/discr/diff_ops_test.py index d8c6caab752..0cb83064d4b 100644 --- a/odl/test/discr/diff_ops_test.py +++ b/odl/test/discr/diff_ops_test.py @@ -234,41 +234,43 @@ def test_part_deriv_init(): def test_part_deriv(space, method, padding): - """Discretized partial derivative.""" + """Check partial derivative operator.""" if isinstance(padding, tuple): pad_mode, pad_const = padding else: pad_mode, pad_const = padding, 0 - dom_vec = noise_element(space) - dom_vec_arr = dom_vec.asarray() + x = noise_element(space) for axis in range(space.ndim): - partial = PartialDerivative(space, axis=axis, method=method, - pad_mode=pad_mode, - pad_const=pad_const) - # Compare to helper function - dx = space.cell_sides[axis] - diff = finite_diff(dom_vec_arr, axis=axis, dx=dx, method=method, - pad_mode=pad_mode, - pad_const=pad_const) + pderiv = PartialDerivative( + space, axis=axis, method=method, pad_mode=pad_mode, + pad_const=pad_const + ) + p_x = pderiv(x) + + step = space.cell_sides[axis] + diff_x = finite_diff( + x, axis=axis, dx=step, method=method, pad_mode=pad_mode, + pad_const=pad_const + ) - partial_vec = partial(dom_vec) - assert all_almost_equal(partial_vec, diff) + assert all_almost_equal(p_x, diff_x) # Test adjoint operator - derivative = partial.derivative() - ran_vec = noise_element(space) - deriv_vec = derivative(dom_vec) - adj_vec = derivative.adjoint(ran_vec) - lhs = ran_vec.inner(deriv_vec) - rhs = dom_vec.inner(adj_vec) + deriv_op = pderiv.derivative() + y = noise_element(space) + dp_x = deriv_op(x) + dp_adj_y = deriv_op.adjoint(y) + inner_dom = space.inner(x, dp_adj_y) + inner_ran = space.inner(dp_x, y) # Check not to use trivial data - assert lhs != 0 - assert rhs != 0 - assert lhs == pytest.approx(rhs, rel=dtype_tol(space.dtype)) + assert inner_dom != 0 + assert inner_ran != 0 + rtol = dtype_tol(space.dtype) + assert inner_dom == pytest.approx(inner_ran, rel=rtol) # --- Gradient --- # @@ -303,7 +305,7 @@ def test_gradient_init(): def test_gradient(space, method, padding): - """Discretized spatial gradient operator.""" + """Check spatial gradient operator.""" with pytest.raises(TypeError): Gradient(odl.rn(1), method=method) @@ -312,47 +314,47 @@ def test_gradient(space, method, padding): else: pad_mode, pad_const = padding, 0 - # DiscretizedSpaceElement - dom_vec = noise_element(space) - dom_vec_arr = dom_vec.asarray() + x = noise_element(space) - # gradient - grad = Gradient(space, method=method, - pad_mode=pad_mode, - pad_const=pad_const) - grad_vec = grad(dom_vec) - assert len(grad_vec) == space.ndim + grad = Gradient( + space, method=method, pad_mode=pad_mode, pad_const=pad_const + ) + grad_x = grad(x) + assert len(grad_x) == space.ndim - # computation of gradient components with helper function - for axis, dx in enumerate(space.cell_sides): - diff = finite_diff(dom_vec_arr, axis=axis, dx=dx, method=method, - pad_mode=pad_mode, - pad_const=pad_const) - - assert all_almost_equal(grad_vec[axis].asarray(), diff) + # Compare with results from helper function + for axis, step in enumerate(space.cell_sides): + diff_x = finite_diff( + x, axis=axis, dx=step, method=method, pad_mode=pad_mode, + pad_const=pad_const + ) + assert all_almost_equal(grad_x[axis], diff_x) # Test adjoint operator - derivative = grad.derivative() - ran_vec = noise_element(derivative.range) - deriv_grad_vec = derivative(dom_vec) - adj_grad_vec = derivative.adjoint(ran_vec) - lhs = ran_vec.inner(deriv_grad_vec) - rhs = dom_vec.inner(adj_grad_vec) + deriv_op = grad.derivative() + y = noise_element(deriv_op.range) + dg_x = deriv_op(x) + dg_adj_y = deriv_op.adjoint(y) + inner_dom = deriv_op.domain.inner(x, dg_adj_y) + inner_ran = deriv_op.range.inner(dg_x, y) # Check not to use trivial data - assert lhs != 0 - assert rhs != 0 - assert lhs == pytest.approx(rhs, rel=dtype_tol(space.dtype)) + assert inner_dom != 0 + assert inner_ran != 0 + rtol = dtype_tol(space.dtype) + assert inner_dom == pytest.approx(inner_ran, rel=rtol) - # Higher-dimensional arrays + # Check that higher-dimensional versions at least run lin_size = 3 for ndim in [1, 3, 6]: space = odl.uniform_discr([0.] * ndim, [1.] * ndim, [lin_size] * ndim) - dom_vec = odl.phantom.cuboid(space, [0.2] * ndim, [0.8] * ndim) + x = odl.phantom.cuboid(space, [0.2] * ndim, [0.8] * ndim) + + grad = Gradient( + space, method=method, pad_mode=pad_mode, pad_const=pad_const + ) + grad(x) - grad = Gradient(space, method=method, pad_mode=pad_mode, - pad_const=pad_const) - grad(dom_vec) # --- Divergence --- # @@ -386,7 +388,7 @@ def test_divergence_init(): def test_divergence(space, method, padding): - """Discretized spatial divergence operator.""" + """Check spatial divergence operator.""" # Invalid space with pytest.raises(TypeError): Divergence(range=odl.rn(1), method=method) @@ -397,36 +399,38 @@ def test_divergence(space, method, padding): pad_mode, pad_const = padding, 0 # Operator instance - div = Divergence(range=space, method=method, - pad_mode=pad_mode, - pad_const=pad_const) + div = Divergence( + range=space, method=method, pad_mode=pad_mode, pad_const=pad_const + ) # Apply operator - dom_vec = noise_element(div.domain) - div_dom_vec = div(dom_vec) + x = noise_element(div.domain) + div_x = div(x) - # computation of divergence with helper function + # Comparision with results from helper function expected_result = np.zeros(space.shape) - for axis, dx in enumerate(space.cell_sides): - expected_result += finite_diff(dom_vec[axis], axis=axis, dx=dx, - method=method, pad_mode=pad_mode, - pad_const=pad_const) - - assert all_almost_equal(expected_result, div_dom_vec.asarray()) - - # Adjoint operator - derivative = div.derivative() - deriv_div_dom_vec = derivative(dom_vec) - ran_vec = noise_element(div.range) - adj_div_ran_vec = derivative.adjoint(ran_vec) - - # Adjoint condition - lhs = ran_vec.inner(deriv_div_dom_vec) - rhs = dom_vec.inner(adj_div_ran_vec) + for axis, step in enumerate(space.cell_sides): + expected_result += finite_diff( + x[axis], axis=axis, dx=step, method=method, pad_mode=pad_mode, + pad_const=pad_const + ) + + assert all_almost_equal(div_x, expected_result) + + # Test adjoint operator + deriv_op = div.derivative() + dd_x = deriv_op(x) + y = noise_element(div.range) + dd_adj_y = deriv_op.adjoint(y) + + inner_dom = deriv_op.domain.inner(x, dd_adj_y) + inner_ran = deriv_op.range.inner(dd_x, y) + # Check not to use trivial data - assert lhs != 0 - assert rhs != 0 - assert lhs == pytest.approx(rhs, rel=dtype_tol(space.dtype)) + assert inner_dom != 0 + assert inner_ran != 0 + rtol = dtype_tol(space.dtype) + assert inner_dom == pytest.approx(inner_ran, rel=rtol) # --- Laplacian --- # @@ -462,36 +466,38 @@ def test_laplacian(space, padding): lap = Laplacian(space, pad_mode=pad_mode, pad_const=pad_const) # Apply operator - dom_vec = noise_element(space) - div_dom_vec = lap(dom_vec) + x = noise_element(space) + lap_x = lap(x) # computation of divergence with helper function expected_result = np.zeros(space.shape) - for axis, dx in enumerate(space.cell_sides): - diff_f = finite_diff(dom_vec.asarray(), axis=axis, dx=dx ** 2, - method='forward', pad_mode=pad_mode, - pad_const=pad_const) - diff_b = finite_diff(dom_vec.asarray(), axis=axis, dx=dx ** 2, - method='backward', pad_mode=pad_mode, - pad_const=pad_const) + for axis, step in enumerate(space.cell_sides): + diff_f = finite_diff( + x, axis=axis, dx=step ** 2, method='forward', pad_mode=pad_mode, + pad_const=pad_const + ) + diff_b = finite_diff( + x, axis=axis, dx=step ** 2, method='backward', pad_mode=pad_mode, + pad_const=pad_const + ) expected_result += diff_f - diff_b - assert all_almost_equal(expected_result, div_dom_vec.asarray()) + assert all_almost_equal(lap_x, expected_result) - # Adjoint operator - derivative = lap.derivative() - deriv_lap_dom_vec = derivative(dom_vec) - ran_vec = noise_element(lap.range) - adj_lap_ran_vec = derivative.adjoint(ran_vec) + # Check adjoint operator + deriv_op = lap.derivative() + dl_x = deriv_op(x) + y = noise_element(lap.range) + dl_adj_y = deriv_op.adjoint(y) - # Adjoint condition - lhs = ran_vec.inner(deriv_lap_dom_vec) - rhs = dom_vec.inner(adj_lap_ran_vec) + inner_dom = deriv_op.domain.inner(x, dl_adj_y) + inner_ran = deriv_op.range.inner(dl_x, y) # Check not to use trivial data - assert lhs != 0 - assert rhs != 0 - assert lhs == pytest.approx(rhs, rel=dtype_tol(space.dtype)) + assert inner_dom != 0 + assert inner_ran != 0 + rtol = dtype_tol(space.dtype) + assert inner_dom == pytest.approx(inner_ran, rel=rtol) if __name__ == '__main__': diff --git a/odl/test/discr/discr_ops_test.py b/odl/test/discr/discr_ops_test.py index fea5d16da46..de0c2712c7d 100644 --- a/odl/test/discr/discr_ops_test.py +++ b/odl/test/discr/discr_ops_test.py @@ -17,7 +17,8 @@ from odl.discr.discr_ops import _SUPPORTED_RESIZE_PAD_MODES from odl.space.entry_points import tensor_space_impl from odl.util import is_numeric_dtype, is_real_floating_dtype -from odl.util.testutils import dtype_tol, noise_element +from odl.util.testutils import all_equal, dtype_tol, noise_element + # --- pytest fixtures --- # @@ -205,7 +206,7 @@ def test_resizing_op_deriv(padding): def test_resizing_op_inverse(padding, odl_tspace_impl): - + """Check the inverse of ResizingOperator.""" impl = odl_tspace_impl pad_mode, pad_const = padding dtypes = [dt for dt in tensor_space_impl(impl).available_dtypes() @@ -221,11 +222,11 @@ def test_resizing_op_inverse(padding, odl_tspace_impl): # Only left inverse if the operator extends in all axes x = noise_element(space) - assert res_op.inverse(res_op(x)) == x + assert all_equal(res_op.inverse(res_op(x)), x) def test_resizing_op_adjoint(padding, odl_tspace_impl): - + """Check the adjoint of ResizingOperator.""" impl = odl_tspace_impl pad_mode, pad_const = padding dtypes = [dt for dt in tensor_space_impl(impl).available_dtypes() @@ -244,13 +245,11 @@ def test_resizing_op_adjoint(padding, odl_tspace_impl): res_op.adjoint return - elem = noise_element(space) - res_elem = noise_element(res_space) - inner1 = res_op(elem).inner(res_elem) - inner2 = elem.inner(res_op.adjoint(res_elem)) - assert inner1 == pytest.approx( - inner2, rel=space.size * dtype_tol(dtype) - ) + x = noise_element(space) + y = noise_element(res_space) + inner_dom = res_op.domain.inner(x, res_op.adjoint(y)) + inner_ran = res_op.range.inner(res_op(x), y) + assert inner_dom == pytest.approx(inner_ran, rel=dtype_tol(dtype)) def test_resizing_op_mixed_uni_nonuni(): @@ -282,11 +281,11 @@ def test_resizing_op_mixed_uni_nonuni(): assert np.array_equal(result, true_result) # Test adjoint - elem = noise_element(space) - res_elem = noise_element(res_op.range) - inner1 = res_op(elem).inner(res_elem) - inner2 = elem.inner(res_op.adjoint(res_elem)) - assert inner1 == pytest.approx(inner2) + x = noise_element(space) + y = noise_element(res_op.range) + inner_dom = res_op.domain.inner(x, res_op.adjoint(y)) + inner_ran = res_op.range.inner(res_op(x), y) + assert inner_dom == pytest.approx(inner_ran) if __name__ == '__main__': diff --git a/odl/test/discr/discr_space_test.py b/odl/test/discr/discr_space_test.py index 4b254e5d494..e748a5f3e06 100644 --- a/odl/test/discr/discr_space_test.py +++ b/odl/test/discr/discr_space_test.py @@ -11,23 +11,19 @@ from __future__ import division import numpy as np +import pytest import odl -import pytest -from odl.discr.discr_space import DiscretizedSpace, DiscretizedSpaceElement +from odl.discr.discr_space import DiscretizedSpace from odl.space.base_tensors import TensorSpace -from odl.space.npy_tensors import NumpyTensor -from odl.space.weighting import ConstWeighting -from odl.util.testutils import ( - all_almost_equal, all_equal, noise_elements, simple_fixture) +from odl.util.testutils import all_almost_equal, all_equal, simple_fixture + # --- Pytest fixtures --- # exponent = simple_fixture('exponent', [2.0, 1.0, float('inf'), 0.5, 1.5]) -power = simple_fixture('power', [1.0, 2.0, 0.5, -0.5, -1.0, -2.0]) shape = simple_fixture('shape', [(2, 3, 4), (3, 4), (2,), (1,), (1, 1, 1)]) -power = simple_fixture('power', [1.0, 2.0, 0.5, -0.5, -1.0, -2.0]) # --- DiscretizedSpace --- # @@ -75,17 +71,16 @@ def test_empty(): assert repr(discr) != '' elem = discr.element(1.0) - assert np.array_equal(elem.asarray(), 1.0) - assert np.array_equal(elem.real, 1.0) - assert np.array_equal(elem.imag, 0.0) - assert np.array_equal(elem.conj(), 1.0) + assert elem.shape == () + assert elem.size == 1 + assert elem == 1.0 # --- uniform_discr --- # -def test_factory_dtypes(odl_tspace_impl): - """Check dtypes of spaces from factory function.""" +def test_uniform_discr_dtypes(odl_tspace_impl): + """Check the uniform_discr factory function wrt dtypes.""" impl = odl_tspace_impl real_float_dtypes = [np.float32, np.float64] nonfloat_dtypes = [np.int8, np.int16, np.int32, np.int64, @@ -110,7 +105,7 @@ def test_factory_dtypes(odl_tspace_impl): else: assert isinstance(discr.tspace, TensorSpace) assert discr.tspace.impl == impl - assert discr.tspace.element().space.dtype == dtype + assert discr.tspace.element().dtype == dtype for dtype in complex_float_dtypes: try: @@ -121,7 +116,7 @@ def test_factory_dtypes(odl_tspace_impl): assert isinstance(discr.tspace, TensorSpace) assert discr.tspace.impl == impl assert discr.is_complex - assert discr.tspace.element().space.dtype == dtype + assert discr.tspace.element().dtype == dtype def test_uniform_discr_init_real(odl_tspace_impl): @@ -173,61 +168,41 @@ def test_uniform_discr_init_complex(odl_tspace_impl): # --- DiscretizedSpace methods --- # -def test_discretizedspace_element(): +def test_discretizedspace_element(odl_elem_order): """Test creation and membership of DiscretizedSpace elements.""" - # Creation from scratch - # 1D - discr = odl.uniform_discr(0, 1, 3) - weight = 1.0 if exponent == float('inf') else discr.cell_volume - tspace = odl.rn(3, weighting=weight) - elem = discr.element() - assert elem in discr - assert elem.tensor in tspace - - # 2D - discr = odl.uniform_discr([0, 0], [1, 1], (3, 3)) - weight = 1.0 if exponent == float('inf') else discr.cell_volume - tspace = odl.rn((3, 3), weighting=weight) - elem = discr.element() - assert elem in discr - assert elem.tensor in tspace - + order = odl_elem_order -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]) + space = odl.uniform_discr(0, 1, 3) + assert space.element() in space + elem = space.element([1, 2, 3]) + assert np.array_equal(elem, [1, 2, 3]) - assert isinstance(elem, DiscretizedSpaceElement) - assert isinstance(elem.tensor, NumpyTensor) - assert all_equal(elem.tensor, [1, 2, 3]) + other_space = odl.uniform_discr(0, 1, 4) + assert other_space.element() not in space + other_space = odl.uniform_discr(0, 1, 3, dtype=complex) + assert other_space.element() not in space + # 2D + space = odl.uniform_discr([0, 0], [1, 1], (2, 2)) + assert space.element() in space -def test_element_from_array_2d(odl_elem_order): - """Test element in 2d with different orderings.""" - order = odl_elem_order - discr = odl.uniform_discr([0, 0], [1, 1], [2, 2]) - elem = discr.element([[1, 2], + elem = space.element([[1, 2], [3, 4]], order=order) - - assert isinstance(elem, DiscretizedSpaceElement) - assert isinstance(elem.tensor, NumpyTensor) - assert all_equal(elem, [[1, 2], - [3, 4]]) + assert np.array_equal(elem, [[1, 2], + [3, 4]]) if order is None: - assert elem.tensor.data.flags[discr.default_order + '_CONTIGUOUS'] + assert elem.flags[space.default_order + '_CONTIGUOUS'] else: - assert elem.tensor.data.flags[order + '_CONTIGUOUS'] + assert elem.flags[order + '_CONTIGUOUS'] with pytest.raises(ValueError): - discr.element([1, 2, 3]) # wrong size & shape + space.element([1, 2, 3]) # wrong size & shape with pytest.raises(ValueError): - discr.element([1, 2, 3, 4]) # wrong shape + space.element([1, 2, 3, 4]) # wrong shape with pytest.raises(ValueError): - discr.element([[1], + space.element([[1], [2], [3], [4]]) # wrong shape @@ -343,685 +318,75 @@ def test_discretizedspace_zero_one(): assert np.array_equal(one, [1, 1, 1]) -def test_equals_space(exponent, odl_tspace_impl): +def test_discretizedspace_equals(exponent, odl_tspace_impl): + """Check equality testing between spaces.""" impl = odl_tspace_impl - x1 = odl.uniform_discr(0, 1, 3, exponent=exponent, impl=impl) - x2 = odl.uniform_discr(0, 1, 3, exponent=exponent, impl=impl) - y = odl.uniform_discr(0, 1, 4, exponent=exponent, impl=impl) - - assert x1 is x1 - assert x1 is not x2 - assert x1 is not y - assert x1 == x1 - assert x1 == x2 - assert x1 != y - assert hash(x1) == hash(x2) - assert hash(x1) != hash(y) - - -def test_equals_vec(exponent, odl_tspace_impl): - impl = odl_tspace_impl - discr = odl.uniform_discr(0, 1, 3, exponent=exponent, impl=impl) - discr2 = odl.uniform_discr(0, 1, 4, exponent=exponent, impl=impl) - x1 = discr.element([1, 2, 3]) - x2 = discr.element([1, 2, 3]) - y = discr.element([2, 2, 3]) - z = discr2.element([1, 2, 3, 4]) - - assert x1 is x1 - assert x1 is not x2 - assert x1 is not y - assert x1 == x1 - assert x1 == x2 - assert x1 != y - assert x1 != z - - -def _test_unary_operator(discr, function): - # Verify that the statement y=function(x) gives equivalent results - # to NumPy - x_arr, x = noise_elements(discr) - y_arr = function(x_arr) - y = function(x) - assert all_almost_equal([x, y], [x_arr, y_arr]) - - -def _test_binary_operator(discr, function): - # Verify that the statement z=function(x,y) gives equivalent results - # to NumPy - [x_arr, y_arr], [x, y] = noise_elements(discr, 2) - z_arr = function(x_arr, y_arr) - z = function(x, y) - assert all_almost_equal([x, y, z], [x_arr, y_arr, z_arr]) - - -def test_operators(odl_tspace_impl): - impl = odl_tspace_impl - # Test of all operator overloads against the corresponding NumPy - # implementation - discr = odl.uniform_discr(0, 1, 10, impl=impl) - - # Unary operators - _test_unary_operator(discr, lambda x: +x) - _test_unary_operator(discr, lambda x: -x) - - # Scalar addition - for scalar in [-31.2, -1, 0, 1, 2.13]: - def iadd(x): - x += scalar - _test_unary_operator(discr, iadd) - _test_unary_operator(discr, lambda x: x + scalar) - - # Scalar subtraction - for scalar in [-31.2, -1, 0, 1, 2.13]: - def isub(x): - x -= scalar - _test_unary_operator(discr, isub) - _test_unary_operator(discr, lambda x: x - scalar) - - # Scalar multiplication - for scalar in [-31.2, -1, 0, 1, 2.13]: - def imul(x): - x *= scalar - _test_unary_operator(discr, imul) - _test_unary_operator(discr, lambda x: x * scalar) - - # Scalar division - for scalar in [-31.2, -1, 1, 2.13]: - def idiv(x): - x /= scalar - _test_unary_operator(discr, idiv) - _test_unary_operator(discr, lambda x: x / scalar) - - # Incremental operations - def iadd(x, y): - x += y - - def isub(x, y): - x -= y - - def imul(x, y): - x *= y - - def idiv(x, y): - x /= y - - _test_binary_operator(discr, iadd) - _test_binary_operator(discr, isub) - _test_binary_operator(discr, imul) - _test_binary_operator(discr, idiv) - - # Incremental operators with aliased inputs - def iadd_aliased(x): - x += x - - def isub_aliased(x): - x -= x - - def imul_aliased(x): - x *= x - - def idiv_aliased(x): - x /= x - - _test_unary_operator(discr, iadd_aliased) - _test_unary_operator(discr, isub_aliased) - _test_unary_operator(discr, imul_aliased) - _test_unary_operator(discr, idiv_aliased) - - # Binary operators - _test_binary_operator(discr, lambda x, y: x + y) - _test_binary_operator(discr, lambda x, y: x - y) - _test_binary_operator(discr, lambda x, y: x * y) - _test_binary_operator(discr, lambda x, y: x / y) - - # Binary with aliased inputs - _test_unary_operator(discr, lambda x: x + x) - _test_unary_operator(discr, lambda x: x - x) - _test_unary_operator(discr, lambda x: x * x) - _test_unary_operator(discr, lambda x: x / x) - - -def test_getitem(): - discr = odl.uniform_discr(0, 1, 3) - elem = discr.element([1, 2, 3]) - - assert all_equal(elem, [1, 2, 3]) - - -def test_getslice(): - discr = odl.uniform_discr(0, 1, 3) - elem = discr.element([1, 2, 3]) - - assert isinstance(elem[:], NumpyTensor) - assert all_equal(elem[:], [1, 2, 3]) - - discr = odl.uniform_discr(0, 1, 3, dtype='complex') - elem = discr.element([1 + 2j, 2 - 2j, 3]) - - assert isinstance(elem[:], NumpyTensor) - assert all_equal(elem[:], [1 + 2j, 2 - 2j, 3]) - - -def test_setitem(): - discr = odl.uniform_discr(0, 1, 3) - elem = discr.element([1, 2, 3]) - elem[0] = 4 - elem[1] = 5 - elem[2] = 6 - - assert all_equal(elem, [4, 5, 6]) - - -def test_setitem_nd(): - - # 1D - discr = odl.uniform_discr(0, 1, 3) - elem = discr.element([1, 2, 3]) - - elem[:] = [4, 5, 6] - assert all_equal(elem, [4, 5, 6]) - - elem[:] = np.array([3, 2, 1]) - assert all_equal(elem, [3, 2, 1]) - - elem[:] = 0 - assert all_equal(elem, [0, 0, 0]) - - elem[:] = [1] - assert all_equal(elem, [1, 1, 1]) - - with pytest.raises(ValueError): - elem[:] = [0, 0] # bad shape - - with pytest.raises(ValueError): - elem[:] = [0, 0, 1, 2] # bad shape - - # 2D - discr = odl.uniform_discr([0, 0], [1, 1], [3, 2]) - - elem = discr.element([[1, 2], - [3, 4], - [5, 6]]) - - elem[:] = [[-1, -2], - [-3, -4], - [-5, -6]] - assert all_equal(elem, [[-1, -2], - [-3, -4], - [-5, -6]]) - - arr = np.arange(6, 12).reshape([3, 2]) - elem[:] = arr - assert all_equal(elem, arr) - - elem[:] = 0 - assert all_equal(elem, np.zeros(elem.shape)) - - elem[:] = [1] - assert all_equal(elem, np.ones(elem.shape)) - - elem[:] = [0, 0] # broadcasting assignment - assert all_equal(elem, np.zeros(elem.shape)) - - with pytest.raises(ValueError): - elem[:] = [0, 0, 0] # bad shape - - with pytest.raises(ValueError): - elem[:] = np.arange(6) # bad shape (6,) - - with pytest.raises(ValueError): - elem[:] = np.ones((2, 3))[..., np.newaxis] # bad shape (2, 3, 1) - - with pytest.raises(ValueError): - arr = np.arange(6, 12).reshape([3, 2]) - elem[:] = arr.T # bad shape (2, 3) + space1 = odl.uniform_discr(0, 1, 3, exponent=exponent, impl=impl) + space2 = odl.uniform_discr(0, 1, 3, exponent=exponent, impl=impl) + other_space = odl.uniform_discr(0, 1, 4, exponent=exponent, impl=impl) - # nD - shape = (3,) * 3 + (4,) * 3 - discr = odl.uniform_discr([0] * 6, [1] * 6, shape) - size = np.prod(shape) - elem = discr.element(np.zeros(shape)) + assert space1 == space1 + assert space1 == space2 + assert space1 != other_space + assert hash(space1) == hash(space2) + assert hash(space1) != hash(other_space) - arr = np.arange(size).reshape(shape) - elem[:] = arr - assert all_equal(elem, arr) - - elem[:] = 0 - assert all_equal(elem, np.zeros(elem.shape)) - - elem[:] = [1] - assert all_equal(elem, np.ones(elem.shape)) - - with pytest.raises(ValueError): - # Reversed shape -> bad - elem[:] = np.arange(size).reshape((4,) * 3 + (3,) * 3) - - -def test_setslice(): - discr = odl.uniform_discr(0, 1, 3) - elem = discr.element([1, 2, 3]) - - elem[:] = [4, 5, 6] - assert all_equal(elem, [4, 5, 6]) - - -def test_asarray_2d(odl_elem_order): - """Test the asarray method.""" - order = odl_elem_order - discr = odl.uniform_discr([0, 0], [1, 1], [2, 2]) - elem = discr.element([[1, 2], - [3, 4]], order=order) - - arr = elem.asarray() - assert all_equal(arr, [[1, 2], - [3, 4]]) - if order is None: - assert arr.flags[discr.default_order + '_CONTIGUOUS'] - else: - assert arr.flags[order + '_CONTIGUOUS'] - - # test out parameter - out_c = np.empty([2, 2], order='C') - result_c = elem.asarray(out=out_c) - assert result_c is out_c - assert all_equal(out_c, [[1, 2], - [3, 4]]) - out_f = np.empty([2, 2], order='F') - result_f = elem.asarray(out=out_f) - assert result_f is out_f - assert all_equal(out_f, [[1, 2], - [3, 4]]) - - # Try wrong shape - out_wrong_shape = np.empty([2, 3]) - with pytest.raises(ValueError): - elem.asarray(out=out_wrong_shape) - - -def test_transpose(): - discr = odl.uniform_discr([0, 0], [1, 1], [2, 2]) - x = discr.element([[1, 2], [3, 4]]) - y = discr.element([[5, 6], [7, 8]]) - - assert isinstance(x.T, odl.Operator) - assert x.T.is_linear - - assert x.T(y) == x.inner(y) - assert x.T.T == x - assert all_equal(x.T.adjoint(1.0), x) - - -def test_cell_sides(): +def test_discretizedspace_cell_sides(): + """Check correctness of cell_sides.""" # Non-degenerated case, should be same as cell size - discr = odl.uniform_discr([0, 0], [1, 1], [2, 2]) - elem = discr.element() - - assert all_equal(discr.cell_sides, [0.5] * 2) - assert all_equal(elem.cell_sides, [0.5] * 2) + space = odl.uniform_discr([0, 0], [1, 1], [2, 2]) + assert all_equal(space.cell_sides, [0.5] * 2) # Degenerated case, uses interval size in 1-point dimensions - discr = odl.uniform_discr([0, 0], [1, 1], [2, 1]) - elem = discr.element() - - assert all_equal(discr.cell_sides, [0.5, 1]) - assert all_equal(elem.cell_sides, [0.5, 1]) + space = odl.uniform_discr([0, 0], [1, 1], [2, 1]) + assert all_equal(space.cell_sides, [0.5, 1]) -def test_cell_volume(): +def test_discretizedspace_cell_volume(): + """Check correctness of cell_volume.""" # Non-degenerated case - discr = odl.uniform_discr([0, 0], [1, 1], [2, 2]) - elem = discr.element() - - assert discr.cell_volume == 0.25 - assert elem.cell_volume == 0.25 + space = odl.uniform_discr([0, 0], [1, 1], [2, 2]) + assert space.cell_volume == 0.25 # Degenerated case, uses interval size in 1-point dimensions - discr = odl.uniform_discr([0, 0], [1, 1], [2, 1]) - elem = discr.element() - - assert discr.cell_volume == 0.5 - assert elem.cell_volume == 0.5 + space = odl.uniform_discr([0, 0], [1, 1], [2, 1]) + assert space.cell_volume == 0.5 -def test_astype(): - - rdiscr = odl.uniform_discr([0, 0], [1, 1], [2, 2], dtype='float64') - cdiscr = odl.uniform_discr([0, 0], [1, 1], [2, 2], dtype='complex128') - rdiscr_s = odl.uniform_discr([0, 0], [1, 1], [2, 2], dtype='float32') - cdiscr_s = odl.uniform_discr([0, 0], [1, 1], [2, 2], dtype='complex64') +def test_discretizedspace_astype(): + """Check conversion of spaces using astype().""" + rspace = odl.uniform_discr([0, 0], [1, 1], [2, 2], dtype='float64') + cspace = odl.uniform_discr([0, 0], [1, 1], [2, 2], dtype='complex128') + rspace_s = odl.uniform_discr([0, 0], [1, 1], [2, 2], dtype='float32') + cspace_s = odl.uniform_discr([0, 0], [1, 1], [2, 2], dtype='complex64') # Real - assert rdiscr.astype('float32') == rdiscr_s - assert rdiscr.astype('float64') is rdiscr - assert rdiscr.real_space is rdiscr - assert rdiscr.astype('complex64') == cdiscr_s - assert rdiscr.astype('complex128') == cdiscr - assert rdiscr.complex_space == cdiscr + assert rspace.astype('float32') == rspace_s + assert rspace.astype('float64') is rspace + assert rspace.real_space is rspace + assert rspace.astype('complex64') == cspace_s + assert rspace.astype('complex128') == cspace + assert rspace.complex_space == cspace # Complex - assert cdiscr.astype('complex64') == cdiscr_s - assert cdiscr.astype('complex128') is cdiscr - assert cdiscr.complex_space is cdiscr - assert cdiscr.astype('float32') == rdiscr_s - assert cdiscr.astype('float64') == rdiscr - assert cdiscr.real_space == rdiscr + assert cspace.astype('complex64') == cspace_s + assert cspace.astype('complex128') is cspace + assert cspace.complex_space is cspace + assert cspace.astype('float32') == rspace_s + assert cspace.astype('float64') == rspace + assert cspace.real_space == rspace # More exotic dtype - discr = odl.uniform_discr([0, 0], [1, 1], [2, 2], dtype=bool) - as_float = discr.astype(float) + space = odl.uniform_discr([0, 0], [1, 1], [2, 2], dtype=bool) + as_float = space.astype(float) assert as_float.dtype == float assert not as_float.is_weighted - as_complex = discr.astype(complex) + as_complex = space.astype(complex) assert as_complex.dtype == complex assert not as_complex.is_weighted -def test_ufuncs(odl_tspace_impl, odl_ufunc): - """Test ufuncs in ``x.ufuncs`` against direct Numpy ufuncs.""" - impl = odl_tspace_impl - space = odl.uniform_discr([0, 0], [1, 1], (2, 3), impl=impl) - name = odl_ufunc - - # Get the ufunc from numpy as reference - npy_ufunc = getattr(np, name) - nin = npy_ufunc.nin - nout = npy_ufunc.nout - if (np.issubsctype(space.dtype, np.floating) and - name in ['bitwise_and', - 'bitwise_or', - 'bitwise_xor', - 'invert', - 'left_shift', - 'right_shift']): - # Skip integer only methods if floating point type - return - - # Create some data - arrays, elements = noise_elements(space, nin + nout) - in_arrays = arrays[:nin] - out_arrays = arrays[nin:] - data_elem = elements[0] - out_elems = elements[nin:] - - if nout == 1: - out_arr_kwargs = {'out': out_arrays[0]} - out_elem_kwargs = {'out': out_elems[0]} - elif nout > 1: - out_arr_kwargs = {'out': out_arrays[:nout]} - out_elem_kwargs = {'out': out_elems[:nout]} - - # Get function to call, using both interfaces: - # - vec.ufunc(other_args) - # - np.ufunc(vec, other_args) - elem_fun_old = getattr(data_elem.ufuncs, name) - in_elems_old = elements[1:nin] - elem_fun_new = npy_ufunc - in_elems_new = elements[:nin] - - # Out-of-place - with np.errstate(all='ignore'): # avoid pytest warnings - npy_result = npy_ufunc(*in_arrays) - odl_result_old = elem_fun_old(*in_elems_old) - assert all_almost_equal(npy_result, odl_result_old) - odl_result_new = elem_fun_new(*in_elems_new) - assert all_almost_equal(npy_result, odl_result_new) - - # Test type of output - if nout == 1: - assert isinstance(odl_result_old, space.element_type) - assert isinstance(odl_result_new, space.element_type) - elif nout > 1: - for i in range(nout): - assert isinstance(odl_result_old[i], space.element_type) - assert isinstance(odl_result_new[i], space.element_type) - - # In-place with ODL objects as `out` - with np.errstate(all='ignore'): # avoid pytest warnings - npy_result = npy_ufunc(*in_arrays, **out_arr_kwargs) - odl_result_old = elem_fun_old(*in_elems_old, **out_elem_kwargs) - assert all_almost_equal(npy_result, odl_result_old) - odl_result_new = elem_fun_new(*in_elems_new, **out_elem_kwargs) - assert all_almost_equal(npy_result, odl_result_new) - - # Check that returned stuff refers to given out - if nout == 1: - assert odl_result_old is out_elems[0] - assert odl_result_new is out_elems[0] - elif nout > 1: - for i in range(nout): - assert odl_result_old[i] is out_elems[i] - assert odl_result_new[i] is out_elems[i] - - # In-place with Numpy array as `out` for new interface - out_arrays_new = tuple(np.empty_like(arr) for arr in out_arrays) - if nout == 1: - out_arr_kwargs_new = {'out': out_arrays_new[0]} - elif nout > 1: - out_arr_kwargs_new = {'out': out_arrays_new[:nout]} - - with np.errstate(all='ignore'): # avoid pytest warnings - odl_result_arr_new = elem_fun_new(*in_elems_new, - **out_arr_kwargs_new) - assert all_almost_equal(npy_result, odl_result_arr_new) - - if nout == 1: - assert odl_result_arr_new is out_arrays_new[0] - elif nout > 1: - for i in range(nout): - assert odl_result_arr_new[i] is out_arrays_new[i] - - # In-place with data container (tensor) as `out` for new interface - out_tensors_new = tuple(space.tspace.element(np.empty_like(arr)) - for arr in out_arrays) - if nout == 1: - out_tens_kwargs_new = {'out': out_tensors_new[0]} - elif nout > 1: - out_tens_kwargs_new = {'out': out_tensors_new[:nout]} - - with np.errstate(all='ignore'): # avoid pytest warnings - odl_result_tens_new = elem_fun_new(*in_elems_new, - **out_tens_kwargs_new) - assert all_almost_equal(npy_result, odl_result_tens_new) - - if nout == 1: - assert odl_result_tens_new is out_tensors_new[0] - elif nout > 1: - for i in range(nout): - assert odl_result_tens_new[i] is out_tensors_new[i] - - # Check `ufunc.at` - indices = ([0, 0, 1], - [0, 1, 2]) - - mod_array = in_arrays[0].copy() - mod_elem = in_elems_new[0].copy() - if nout > 1: - return # currently not supported by Numpy - if nin == 1: - with np.errstate(all='ignore'): # avoid pytest warnings - npy_result = npy_ufunc.at(mod_array, indices) - odl_result = npy_ufunc.at(mod_elem, indices) - elif nin == 2: - other_array = in_arrays[1][indices] - other_elem = in_elems_new[1][indices] - with np.errstate(all='ignore'): # avoid pytest warnings - npy_result = npy_ufunc.at(mod_array, indices, other_array) - odl_result = npy_ufunc.at(mod_elem, indices, other_elem) - - assert all_almost_equal(odl_result, npy_result) - - # Check `ufunc.reduce` - if nin == 2 and nout == 1: - in_array = in_arrays[0] - in_elem = in_elems_new[0] - - # We only test along one axis since some binary ufuncs are not - # re-orderable, in which case Numpy raises a ValueError - with np.errstate(all='ignore'): # avoid pytest warnings - npy_result = npy_ufunc.reduce(in_array) - odl_result = npy_ufunc.reduce(in_elem) - assert all_almost_equal(odl_result, npy_result) - # In-place using `out` (with ODL vector and array) - out_elem = odl_result.space.element() - out_array = np.empty(odl_result.shape, - dtype=odl_result.dtype) - npy_ufunc.reduce(in_elem, out=out_elem) - npy_ufunc.reduce(in_elem, out=out_array) - assert all_almost_equal(out_elem, odl_result) - assert all_almost_equal(out_array, odl_result) - # Using a specific dtype - try: - npy_result = npy_ufunc.reduce(in_array, dtype=complex) - except TypeError: - # Numpy finds no matching loop, bail out - return - else: - odl_result = npy_ufunc.reduce(in_elem, dtype=complex) - assert odl_result.dtype == npy_result.dtype - assert all_almost_equal(odl_result, npy_result) - - # Other ufunc method use the same interface, to we don't perform - # extra tests for them. - - -def test_ufunc_corner_cases(odl_tspace_impl): - """Check if some corner cases are handled correctly.""" - impl = odl_tspace_impl - space = odl.uniform_discr([0, 0], [1, 1], (2, 3), impl=impl) - x = space.element([[-1, 0, 1], - [1, 2, 3]]) - space_no_w = odl.uniform_discr([0, 0], [1, 1], (2, 3), impl=impl, - weighting=1.0) - - # --- UFuncs with nin = 1, nout = 1 --- # - - with pytest.raises(ValueError): - # Too many arguments - x.__array_ufunc__(np.sin, '__call__', x, np.ones((2, 3))) - - # Check that `out=(None,)` is the same as not providing `out` - res = x.__array_ufunc__(np.sin, '__call__', x, out=(None,)) - assert all_almost_equal(res, np.sin(x.asarray())) - # Check that the result space is the same - assert res.space == space - - # Check usage of `order` argument - for order in ('C', 'F'): - res = x.__array_ufunc__(np.sin, '__call__', x, order=order) - assert all_almost_equal(res, np.sin(x.asarray())) - assert res.tensor.data.flags[order + '_CONTIGUOUS'] - - # Check usage of `dtype` argument - res = x.__array_ufunc__(np.sin, '__call__', x, dtype=complex) - assert all_almost_equal(res, np.sin(x.asarray(), dtype=complex)) - assert res.dtype == complex - - # Check propagation of weightings - y = space_no_w.one() - res = y.__array_ufunc__(np.sin, '__call__', y) - assert res.space.weighting == space_no_w.weighting - y = space_no_w.one() - res = y.__array_ufunc__(np.sin, '__call__', y) - assert res.space.weighting == space_no_w.weighting - - # --- UFuncs with nin = 2, nout = 1 --- # - - with pytest.raises(ValueError): - # Too few arguments - x.__array_ufunc__(np.add, '__call__', x) - - with pytest.raises(ValueError): - # Too many outputs - out1, out2 = np.empty_like(x), np.empty_like(x) - x.__array_ufunc__(np.add, '__call__', x, x, out=(out1, out2)) - - # Check that npy_array += odl_vector works - arr = np.ones((2, 3)) - arr += x - assert all_almost_equal(arr, x.asarray() + 1) - # For Numpy >= 1.13, this will be equivalent - arr = np.ones((2, 3)) - res = x.__array_ufunc__(np.add, '__call__', arr, x, out=(arr,)) - assert all_almost_equal(arr, x.asarray() + 1) - assert res is arr - - # --- `accumulate` --- # - - res = x.__array_ufunc__(np.add, 'accumulate', x) - assert all_almost_equal(res, np.add.accumulate(x.asarray())) - assert res.space == space - arr = np.empty_like(x) - res = x.__array_ufunc__(np.add, 'accumulate', x, out=(arr,)) - assert all_almost_equal(arr, np.add.accumulate(x.asarray())) - assert res is arr - - # `accumulate` with other dtype - res = x.__array_ufunc__(np.add, 'accumulate', x, dtype='float32') - assert res.dtype == 'float32' - - # Error scenarios - with pytest.raises(ValueError): - # Too many `out` arguments - out1, out2 = np.empty_like(x), np.empty_like(x) - x.__array_ufunc__(np.add, 'accumulate', x, out=(out1, out2)) - - # --- `reduce` --- # - - res = x.__array_ufunc__(np.add, 'reduce', x) - assert all_almost_equal(res, np.add.reduce(x.asarray())) - - with pytest.raises(ValueError): - x.__array_ufunc__(np.add, 'reduce', x, keepdims=True) - - # With `out` argument and `axis` - out_ax0 = np.empty(3) - res = x.__array_ufunc__(np.add, 'reduce', x, axis=0, out=(out_ax0,)) - assert all_almost_equal(out_ax0, np.add.reduce(x.asarray(), axis=0)) - assert res is out_ax0 - out_ax1 = odl.rn(2).element() - res = x.__array_ufunc__(np.add, 'reduce', x, axis=1, out=(out_ax1,)) - assert all_almost_equal(out_ax1, np.add.reduce(x.asarray(), axis=1)) - assert res is out_ax1 - - # 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))) - - # Constant weighting should be preserved (recomputed from cell - # volume) - y = space.one() - res = y.__array_ufunc__(np.add, 'reduce', y, axis=0) - assert res.space.weighting.const == pytest.approx(space.cell_sides[1]) - - # Check that `exponent` is propagated - space_1 = odl.uniform_discr([0, 0], [1, 1], (2, 3), impl=impl, - exponent=1) - z = space_1.one() - res = z.__array_ufunc__(np.add, 'reduce', z, axis=0) - assert res.space.exponent == 1 - - # --- `outer` --- # - - # Check that weightings are propagated correctly - x = y = space.one() - res = x.__array_ufunc__(np.add, 'outer', x, y) - assert isinstance(res.space.weighting, ConstWeighting) - assert res.space.weighting.const == pytest.approx(x.space.weighting.const * - y.space.weighting.const) - - x = space.one() - y = space_no_w.one() - res = x.__array_ufunc__(np.add, 'outer', x, y) - assert isinstance(res.space.weighting, ConstWeighting) - assert res.space.weighting.const == pytest.approx(x.space.weighting.const) - - x = y = space_no_w.one() - res = x.__array_ufunc__(np.add, 'outer', x, y) - assert not res.space.is_weighted - - def test_real_imag(odl_tspace_impl, odl_elem_order): """Check if real and imaginary parts can be read and written to.""" impl = odl_tspace_impl @@ -1029,112 +394,14 @@ def test_real_imag(odl_tspace_impl, odl_elem_order): tspace_cls = odl.space.entry_points.tensor_space_impl(impl) for dtype in filter(odl.util.is_complex_floating_dtype, tspace_cls.available_dtypes()): - cdiscr = odl.uniform_discr([0, 0], [1, 1], [2, 2], dtype=dtype, + cspace = odl.uniform_discr([0, 0], [1, 1], [2, 2], dtype=dtype, impl=impl) - rdiscr = cdiscr.real_space + rspace = cspace.real_space - # Get real and imag - x = cdiscr.element([[1 - 1j, 2 - 2j], + x = cspace.element([[1 - 1j, 2 - 2j], [3 - 3j, 4 - 4j]], order=order) - assert x.real in rdiscr - assert all_equal(x.real, [[1, 2], - [3, 4]]) - assert x.imag in rdiscr - assert all_equal(x.imag, [[-1, -2], - [-3, -4]]) - - # Set with different data types and shapes - for assigntype in (lambda x: x, tuple, rdiscr.element): - - # Using setters - x = cdiscr.zero() - x.real = assigntype([[2, 3], - [4, 5]]) - assert all_equal(x.real, [[2, 3], - [4, 5]]) - - x = cdiscr.zero() - x.imag = assigntype([[4, 5], - [6, 7]]) - assert all_equal(x.imag, [[4, 5], - [6, 7]]) - - # With [:] assignment - x = cdiscr.zero() - x.real[:] = assigntype([[2, 3], - [4, 5]]) - assert all_equal(x.real, [[2, 3], - [4, 5]]) - - x = cdiscr.zero() - x.imag[:] = assigntype([[2, 3], - [4, 5]]) - assert all_equal(x.imag, [[2, 3], - [4, 5]]) - - # Setting with scalars - x = cdiscr.zero() - x.real = 1 - assert all_equal(x.real, [[1, 1], - [1, 1]]) - - x = cdiscr.zero() - x.imag = -1 - assert all_equal(x.imag, [[-1, -1], - [-1, -1]]) - - # Incompatible shapes - with pytest.raises(ValueError): - x.real = [4, 5, 6, 7] - with pytest.raises(ValueError): - x.imag = [4, 5, 6, 7] - - -def test_reduction(odl_tspace_impl, odl_reduction): - impl = odl_tspace_impl - name = odl_reduction - space = odl.uniform_discr([0, 0], [1, 1], [2, 2], impl=impl) - - reduction = getattr(np, name) - - # Create some data - x_arr, x = noise_elements(space, 1) - assert reduction(x_arr) == pytest.approx(getattr(x.ufuncs, name)()) - - -def test_power(odl_tspace_impl, power): - impl = odl_tspace_impl - space = odl.uniform_discr([0, 0], [1, 1], [2, 2], impl=impl) - - x_arr, x = noise_elements(space, 1) - x_pos_arr = np.abs(x_arr) - x_neg_arr = -x_pos_arr - x_pos = np.abs(x) - x_neg = -x_pos - - if int(power) != power: - # Make input positive to get real result - for y in [x_pos_arr, x_neg_arr, x_pos, x_neg]: - y += 0.1 - - with np.errstate(invalid='ignore'): - true_pos_pow = np.power(x_pos_arr, power) - true_neg_pow = np.power(x_neg_arr, power) - - if int(power) != power and impl == 'cuda': - with pytest.raises(ValueError): - x_pos ** power - with pytest.raises(ValueError): - x_pos **= power - else: - with np.errstate(invalid='ignore'): - assert all_almost_equal(x_pos ** power, true_pos_pow) - assert all_almost_equal(x_neg ** power, true_neg_pow) - - x_pos **= power - x_neg **= power - assert all_almost_equal(x_pos, true_pos_pow) - assert all_almost_equal(x_neg, true_neg_pow) + assert x.real in rspace + assert x.imag in rspace def test_inner_nonuniform(): @@ -1142,15 +409,13 @@ def test_inner_nonuniform(): 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.DiscretizedSpace(part, tspace) + space = odl.DiscretizedSpace(part, tspace) - one = discr.one() - linear = discr.element(lambda x: x) + linear = space.element(lambda x: x) # Exact inner product is the integral from 0 to 5 of x, which is 5**2 / 2 exact_inner = 5 ** 2 / 2.0 - inner = one.inner(linear) - assert inner == pytest.approx(exact_inner) + assert space.inner(space.one(), linear) == pytest.approx(exact_inner) def test_norm_nonuniform(): @@ -1158,101 +423,105 @@ def test_norm_nonuniform(): 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.DiscretizedSpace(part, tspace) + space = odl.DiscretizedSpace(part, tspace) - sqrt = discr.element(lambda x: np.sqrt(x)) + sqrt = space.element(lambda x: np.sqrt(x)) # Exact norm is the square root of the integral from 0 to 5 of x, # which is sqrt(5**2 / 2) exact_norm = np.sqrt(5 ** 2 / 2.0) - norm = sqrt.norm() - assert norm == pytest.approx(exact_norm) + assert space.norm(sqrt) == pytest.approx(exact_norm) def test_norm_interval(exponent): + """Check norm computation on a 1D interval.""" # 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 - discr = odl.uniform_discr(0, 1, 10, exponent=p) + space = odl.uniform_discr(0, 1, 10, exponent=p) - func = discr.element(lambda x: x ** 2) + func = space.element(lambda x: x ** 2) if p == float('inf'): - assert func.norm() <= 1 # Max at boundary not hit + assert space.norm(func) <= 1 # Max at boundary not hit else: true_norm = (1 + 2 * p) ** (-1 / p) - assert func.norm() == pytest.approx(true_norm, rel=1e-2) + assert space.norm(func) == pytest.approx(true_norm, rel=1e-2) def test_norm_rectangle(exponent): + """Check norm computation on a 2D rectangle.""" # Test the function f(x) = x_0^2 * x_1^3 on (0, 1) x (-1, 1). Its # L^p-norm is ((1 + 2*p) * (1 + 3 * p) / 2)^(-1/p) for finite p # and 1 for p=inf p = exponent - discr = odl.uniform_discr([0, -1], [1, 1], (20, 30), exponent=p) + space = odl.uniform_discr([0, -1], [1, 1], (20, 30), exponent=p) - func = discr.element(lambda x: x[0] ** 2 * x[1] ** 3) + func = space.element(lambda x: x[0] ** 2 * x[1] ** 3) if p == float('inf'): - assert func.norm() <= 1 # Max at boundary not hit + assert space.norm(func) <= 1 # Max at boundary not hit else: true_norm = ((1 + 2 * p) * (1 + 3 * p) / 2) ** (-1 / p) - assert func.norm() == pytest.approx(true_norm, rel=1e-2) + assert space.norm(func) == pytest.approx(true_norm, rel=1e-2) def test_norm_rectangle_boundary(odl_tspace_impl, exponent): - # Check the constant function 1 in different situations regarding the - # placement of the outermost grid points. - impl = odl_tspace_impl + """Check norm computation with different boundary settings. + This test uses the constant function ``f(x) = 1`` to check whether the + boundary correction for the norm reproduces the volume of the spatial + domain as the correct norm of ``f``. + """ + impl = odl_tspace_impl dtype = 'float32' # Standard case - discr = odl.uniform_discr( + space = odl.uniform_discr( [-1, -2], [1, 2], (4, 8), dtype=dtype, impl=impl, exponent=exponent ) if exponent == float('inf'): - assert discr.one().norm() == 1 + assert space.norm(space.one()) == 1 else: assert ( - discr.one().norm() - == pytest.approx(discr.domain.volume ** (1 / exponent)) + space.norm(space.one()) + == pytest.approx(space.domain.volume ** (1 / exponent)) ) # Nodes on the boundary (everywhere) - discr = odl.uniform_discr( + space = 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 + assert space.norm(space.one()) == 1 else: assert ( - discr.one().norm() - == pytest.approx(discr.domain.volume ** (1 / exponent)) + space.norm(space.one()) + == pytest.approx(space.domain.volume ** (1 / exponent)) ) # Nodes on the boundary (selective) - discr = odl.uniform_discr( + space = 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 + assert space.norm(space.one()) == 1 else: assert ( - discr.one().norm() - == pytest.approx(discr.domain.volume ** (1 / exponent)) + space.norm(space.one()) + == pytest.approx(space.domain.volume ** (1 / exponent)) ) - discr = odl.uniform_discr( + space = 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 + assert space.norm(space.one()) == 1 else: assert ( - discr.one().norm() - == pytest.approx(discr.domain.volume ** (1 / exponent)) + space.norm(space.one()) + == pytest.approx(space.domain.volume ** (1 / exponent)) ) # Completely arbitrary boundary @@ -1263,14 +532,14 @@ def test_norm_rectangle_boundary(odl_tspace_impl, exponent): 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 = DiscretizedSpace(part, tspace) + space = DiscretizedSpace(part, tspace) if exponent == float('inf'): - assert discr.one().norm() == 1 + assert space.norm(space.one()) == 1 else: assert ( - discr.one().norm() - == pytest.approx(discr.domain.volume ** (1 / exponent)) + space.norm(space.one()) + == pytest.approx(space.domain.volume ** (1 / exponent)) ) diff --git a/odl/test/discr/grid_test.py b/odl/test/discr/grid_test.py index e91827038ba..60acd7b302b 100644 --- a/odl/test/discr/grid_test.py +++ b/odl/test/discr/grid_test.py @@ -44,7 +44,7 @@ def test_RectGrid_init_raise(): unsorted[2] = -1 with_dups = np.arange(4) with_dups[3] = 2 - unsorted_with_dups = unsorted.copy() + unsorted_with_dups = np.copy(unsorted) unsorted_with_dups[3] = 0 with_nan = np.arange(4, dtype=float) with_nan[3] = np.nan diff --git a/odl/test/largescale/solvers/nonsmooth/default_functionals_slow_test.py b/odl/test/largescale/solvers/nonsmooth/default_functionals_slow_test.py index 1092fcd60b4..c39f5804097 100644 --- a/odl/test/largescale/solvers/nonsmooth/default_functionals_slow_test.py +++ b/odl/test/largescale/solvers/nonsmooth/default_functionals_slow_test.py @@ -114,7 +114,7 @@ def functional(request, linear_offset, quadratic_offset, dual): def proximal_objective(functional, x, y): """Objective function of the proximal optimization problem.""" - return functional(y) + (1.0 / 2.0) * (x - y).norm() ** 2 + return functional(y) + (1.0 / 2.0) * functional.domain.norm(x - y) ** 2 def test_proximal_defintion(functional, stepsize): @@ -185,7 +185,7 @@ def test_proximal_defintion(functional, stepsize): def convex_conj_objective(functional, x, y): """Objective function of the convex conjugate problem.""" - return x.inner(y) - functional(x) + return functional.domain.inner(x, y) - functional(x) def func_convex_conj_has_call(functional): @@ -226,7 +226,7 @@ def test_convex_conj_defintion(functional): f_convex_conj_y = f_convex_conj(y) x = noise_element(functional.domain) - lhs = x.inner(y) - functional(x) + lhs = functional.domain.inner(x, y) - functional(x) if not lhs <= f_convex_conj_y + EPS: print(repr(functional), repr(f_convex_conj), x, y, lhs, diff --git a/odl/test/largescale/space/tensor_space_slow_test.py b/odl/test/largescale/space/tensor_space_slow_test.py index 8399755f1b5..7829024623e 100644 --- a/odl/test/largescale/space/tensor_space_slow_test.py +++ b/odl/test/largescale/space/tensor_space_slow_test.py @@ -73,78 +73,34 @@ def test_ndarray_init(tspace): assert all_almost_equal(x0, x) -def test_getitem(tspace): - indices = np.random.randint(0, tspace.size - 1, 5) - indices = np.unravel_index(indices, tspace.shape) - - x0 = np.arange(tspace.size).reshape(tspace.shape) - x = tspace.element(x0) - - for index in zip(*indices): - assert x[index] == np.ravel_multi_index(index, tspace.shape) - - -def test_setitem(tspace): - indices = np.random.randint(0, tspace.size - 1, 5) - indices = np.unravel_index(indices, tspace.shape) - - x = tspace.zero() - - for index in zip(*indices): - flat_index = np.ravel_multi_index(index, tspace.shape) - x[index] = -flat_index - assert x[index] == -flat_index - - def test_inner(tspace): weighting_const = tspace.weighting.const - [xarr, yarr], [x, y] = noise_elements(tspace, 2) - correct_inner = np.vdot(yarr, xarr) * weighting_const - assert ( tspace.inner(x, y) == pytest.approx(correct_inner, rel=dtype_tol(tspace.dtype)) ) - assert ( - x.inner(y) - == pytest.approx(correct_inner, rel=dtype_tol(tspace.dtype)) - ) def test_norm(tspace): weighting_const = tspace.weighting.const - xarr, x = noise_elements(tspace) - correct_norm = np.linalg.norm(xarr) * np.sqrt(weighting_const) - assert ( tspace.norm(x) == pytest.approx(correct_norm, rel=dtype_tol(tspace.dtype)) ) - assert ( - x.norm() - == pytest.approx(correct_norm, rel=dtype_tol(tspace.dtype)) - ) def test_dist(tspace): weighting_const = tspace.weighting.const - [xarr, yarr], [x, y] = noise_elements(tspace, 2) - correct_dist = np.linalg.norm(xarr - yarr) * np.sqrt(weighting_const) - assert ( tspace.dist(x, y) == pytest.approx(correct_dist, rel=dtype_tol(tspace.dtype)) ) - assert ( - x.dist(y) - == pytest.approx(correct_dist, rel=dtype_tol(tspace.dtype)) - ) def _test_lincomb(space, a, b, discontig): @@ -215,122 +171,5 @@ def test_lincomb(tspace): _test_lincomb(tspace, a, b, discontig=True) -def _test_member_lincomb(spc, a): - # Validates vector member lincomb against the result on host - - # Generate vectors - [x_host, y_host], [x_device, y_device] = noise_elements(spc, 2) - - # Host side calculation - y_host[:] = a * x_host - - # Device side calculation - y_device.lincomb(a, x_device) - - # CUDA only uses floats, so require 2 digits - assert all_almost_equal(y_device, y_host, ndigits=2) - - -def test_member_lincomb(tspace): - scalar_values = [0, 1, -1, 3.41, 10.0, 1.0001] - for a in scalar_values: - _test_member_lincomb(tspace, a) - - -def _test_unary_operator(spc, function): - # Verify that the statement y=function(x) gives equivalent - # results to Numpy. - x_arr, x = noise_elements(spc) - - y_arr = function(x_arr) - y = function(x) - - assert all_almost_equal([x, y], - [x_arr, y_arr]) - - -def _test_binary_operator(spc, function): - # Verify that the statement z=function(x,y) gives equivalent - # results to Numpy. - [x_arr, y_arr], [x, y] = noise_elements(spc, 2) - - z_arr = function(x_arr, y_arr) - z = function(x, y) - - assert all_almost_equal([x, y, z], - [x_arr, y_arr, z_arr]) - - -def test_operators(tspace): - # Test of all operator overloads against the corresponding - # Numpy implementation - - # Unary operators - _test_unary_operator(tspace, lambda x: +x) - _test_unary_operator(tspace, lambda x: -x) - - # Scalar multiplication - for scalar in [-31.2, -1, 0, 1, 2.13]: - def imul(x): - x *= scalar - _test_unary_operator(tspace, imul) - _test_unary_operator(tspace, lambda x: x * scalar) - - # Scalar division - for scalar in [-31.2, -1, 1, 2.13]: - def idiv(x): - x /= scalar - _test_unary_operator(tspace, idiv) - _test_unary_operator(tspace, lambda x: x / scalar) - - # Incremental operations - def iadd(x, y): - x += y - - def isub(x, y): - x -= y - - def imul(x, y): - x *= y - - def idiv(x, y): - x /= y - - _test_binary_operator(tspace, iadd) - _test_binary_operator(tspace, isub) - _test_binary_operator(tspace, imul) - _test_binary_operator(tspace, idiv) - - # Incremental operators with aliased inputs - def iadd_aliased(x): - x += x - - def isub_aliased(x): - x -= x - - def imul_aliased(x): - x *= x - - def idiv_aliased(x): - x /= x - - _test_unary_operator(tspace, iadd_aliased) - _test_unary_operator(tspace, isub_aliased) - _test_unary_operator(tspace, imul_aliased) - _test_unary_operator(tspace, idiv_aliased) - - # Binary operators - _test_binary_operator(tspace, lambda x, y: x + y) - _test_binary_operator(tspace, lambda x, y: x - y) - _test_binary_operator(tspace, lambda x, y: x * y) - _test_binary_operator(tspace, lambda x, y: x / y) - - # Binary with aliased inputs - _test_unary_operator(tspace, lambda x: x + x) - _test_unary_operator(tspace, lambda x: x - x) - _test_unary_operator(tspace, lambda x: x * x) - _test_unary_operator(tspace, lambda x: x / x) - - if __name__ == '__main__': odl.util.test_file(__file__) diff --git a/odl/test/largescale/tomo/analytic_slow_test.py b/odl/test/largescale/tomo/analytic_slow_test.py index 8d6d9e913f5..ef2a300de6a 100644 --- a/odl/test/largescale/tomo/analytic_slow_test.py +++ b/odl/test/largescale/tomo/analytic_slow_test.py @@ -189,8 +189,8 @@ def test_fbp_reconstruction(projector): fbp_result = fbp_operator(projections) # Allow 30 % error - maxerr = vol.norm() * 0.3 - error = vol.dist(fbp_result) + maxerr = projector.domain.norm(vol) * 0.3 + error = projector.domain.dist(vol, fbp_result) assert error < maxerr @@ -224,8 +224,8 @@ def test_fbp_reconstruction_filters(filter_type, frequency_scaling, weighting): fbp_result = fbp_operator(projections) - maxerr = vol.norm() / 5.0 - error = vol.dist(fbp_result) + maxerr = projector.domain.norm(vol) / 5.0 + error = projector.domain.dist(vol, fbp_result) assert error < maxerr diff --git a/odl/test/largescale/tomo/ray_transform_slow_test.py b/odl/test/largescale/tomo/ray_transform_slow_test.py index b0fc6a63d98..c8a7f41ce29 100644 --- a/odl/test/largescale/tomo/ray_transform_slow_test.py +++ b/odl/test/largescale/tomo/ray_transform_slow_test.py @@ -17,8 +17,8 @@ import odl from odl.tomo.util.testutils import ( skip_if_no_astra, skip_if_no_astra_cuda, skip_if_no_skimage) -from odl.util.testutils import simple_fixture, skip_if_no_largescale - +from odl.util.testutils import ( + all_almost_equal, simple_fixture, skip_if_no_largescale) # --- pytest fixtures --- # @@ -189,8 +189,8 @@ def test_adjoint(projector): backproj = projector.adjoint(proj) # Verify the identity = - result_AxAx = proj.inner(proj) - result_xAtAx = backproj.inner(vol) + result_AxAx = projector.range.inner(proj, proj) + result_xAtAx = projector.domain.inner(backproj, vol) assert result_AxAx == pytest.approx(result_xAtAx, rel=rtol) @@ -212,7 +212,7 @@ def test_adjoint_of_adjoint(projector): proj_adj_adj_adj = projector.adjoint.adjoint.adjoint(proj) # Verify A^*(y) == ((A^*)^*)^*(x) - assert proj_adj == proj_adj_adj_adj + assert all_almost_equal(proj_adj, proj_adj_adj_adj) def test_reconstruction(projector): @@ -235,11 +235,11 @@ def test_reconstruction(projector): niter=20) # Make sure the result is somewhat close to the actual result - maxerr = vol.norm() * 0.5 + maxerr = projector.domain.norm(vol) * 0.5 if np.issubsctype(projector.domain.dtype, np.complexfloating): # Error has double the amount of components practically maxerr *= np.sqrt(2) - assert recon.dist(vol) < maxerr + assert projector.domain.dist(recon, vol) < maxerr if __name__ == '__main__': diff --git a/odl/test/largescale/trafos/fourier_slow_test.py b/odl/test/largescale/trafos/fourier_slow_test.py index 0650210570a..4e06e0e5775 100644 --- a/odl/test/largescale/trafos/fourier_slow_test.py +++ b/odl/test/largescale/trafos/fourier_slow_test.py @@ -79,8 +79,8 @@ def charfun_freq_ball(x): ball_dom_ft = ft(ball_dom) ball_ran_ift = ft.adjoint(ball_ran) assert ( - ball_dom.inner(ball_ran_ift) - == pytest.approx(ball_ran.inner(ball_dom_ft), rel=0.1) + ft.domain.inner(ball_dom, ball_ran_ift) + == pytest.approx(ft.range.inner(ball_ran, ball_dom_ft), rel=0.1) ) diff --git a/odl/test/operator/operator_test.py b/odl/test/operator/operator_test.py index df7c771a97d..bc4354ed1e3 100644 --- a/odl/test/operator/operator_test.py +++ b/odl/test/operator/operator_test.py @@ -16,18 +16,14 @@ import odl from odl import ( - FunctionalLeftVectorMult, MatrixOperator, OpDomainError, Operator, - OperatorComp, OperatorLeftScalarMult, OperatorLeftVectorMult, - OperatorRightScalarMult, OperatorRightVectorMult, OperatorSum, - OpRangeError, OpTypeError) + 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) -try: - getargspec = inspect.getfullargspec -except AttributeError: - getargspec = inspect.getargspec +getargspec = getattr(inspect, "getfullargspec", inspect.getargspec) # --- Fixtures --- # @@ -83,7 +79,7 @@ def check_call(operator, x, expected): # In-place check, aliased if operator.domain == operator.range: - y = x.copy() + y = operator.domain.copy(x) operator(y, out=y) assert all_almost_equal(y, expected) @@ -185,19 +181,34 @@ def test_operator_scaling(dom_eq_ran): check_call(op * scalar, x, mult_sq_np(mat, scalar * xarr)) # Fail when scaling by wrong scalar type (complex number) - wrongscalars = [1j, [1, 2], (1, 2)] - for wrongscalar in wrongscalars: + for bad_scalar in [1j, 'a']: with pytest.raises(TypeError): - OperatorLeftScalarMult(op, wrongscalar) + OperatorLeftScalarMult(op, bad_scalar) with pytest.raises(TypeError): - OperatorRightScalarMult(op, wrongscalar) + OperatorRightScalarMult(op, bad_scalar) with pytest.raises(TypeError): - op * wrongscalar + op * bad_scalar with pytest.raises(TypeError): - wrongscalar * op + bad_scalar * op + + for wrong_shape in [[1, 2], [[1, 2, 3]]]: + with pytest.raises(TypeError): + OperatorLeftScalarMult(op, wrong_shape) + + with pytest.raises(TypeError): + OperatorRightScalarMult(op, wrong_shape) + + with pytest.raises(TypeError): + op * wrong_shape + + # Multiplication from with non-scalar of wrong shape should result in + # `NotImplemented` + for non_scalar in [[1, 2], np.ones(5)]: + assert op.__mul__(non_scalar) is NotImplemented + assert op.__rmul__(non_scalar) is NotImplemented def test_operator_vector_mult(dom_eq_ran): @@ -583,10 +594,10 @@ def test_functional_adjoint(): op = SumFunctional(r3) - assert op.adjoint(3) == r3.element([3, 3, 3]) + assert all_almost_equal(op.adjoint(3), r3.element([3, 3, 3])) x = r3.element([1, 2, 3]) - assert op.adjoint.adjoint(x) == op(x) + assert all_almost_equal(op.adjoint.adjoint(x), op(x)) def test_functional_addition(): @@ -643,32 +654,6 @@ def test_functional_scale(): scalar * y * np.ones(3)) -def test_functional_left_vector_mult(): - r3 = odl.rn(3) - r4 = odl.rn(4) - - op = SumFunctional(r3) - x = r3.element([1, 2, 3]) - y = r4.element([3, 2, 1, 5]) - - # Test a range of scalars (scalar multiplication could implement - # optimizations for (-1, 0, 1). - C = FunctionalLeftVectorMult(op, y) - - assert C.is_linear - assert C.adjoint.is_linear - - assert all_almost_equal(C(x), y * np.sum(x)) - assert all_almost_equal(C.adjoint(y), y.inner(y) * np.ones(3)) - assert all_almost_equal(C.adjoint.adjoint(x), C(x)) - - # Using operator overloading - assert all_almost_equal((y * op)(x), - y * np.sum(x)) - assert all_almost_equal((y * op).adjoint(y), - y.inner(y) * np.ones(3)) - - def test_functional_right_vector_mult(): r3 = odl.rn(3) @@ -791,7 +776,10 @@ def test_nonlinear_functional_operators(): assert C(x) == pytest.approx(mat(x / 2.0)) -# test functions to dispatch +# Test functions to dispatch +# First doc line is the true signature +# Second doc line contains `has_out` and `out_optional` booleans +# Third doc line indicates whether the signature is OK for Operator._call def f1(x): """f1(x) False, False @@ -910,7 +898,6 @@ def func(request): def test_function_signature(func): - true_sig = func.__doc__.splitlines()[0].strip() sig = _function_signature(func) assert true_sig == sig @@ -918,17 +905,17 @@ def test_function_signature(func): def test_dispatch_call_args(func): # Unbound functions - true_has, true_opt = eval(func.__doc__.splitlines()[1].strip()) + true_has_out, true_out_opt = eval(func.__doc__.splitlines()[1].strip()) good = func.__doc__.splitlines()[2].strip() == 'good' if good: truespec = getargspec(func) truespec.args.insert(0, 'self') - has, opt, spec = _dispatch_call_args(unbound_call=func) + has_out, out_opt, spec = _dispatch_call_args(unbound_call=func) - assert has == true_has - assert opt == true_opt + assert has_out == true_has_out + assert out_opt == true_out_opt assert spec == truespec else: with pytest.raises(ValueError): @@ -938,6 +925,7 @@ def test_dispatch_call_args(func): def test_dispatch_call_args_class(): # Two sneaky classes whose _call method would pass the signature check + # because it looks okay from the second argument on class WithStaticMethod(object): @staticmethod def _call(x, y, out): diff --git a/odl/test/operator/oputils_test.py b/odl/test/operator/oputils_test.py index 54b0d63f0d5..116eb1dba47 100644 --- a/odl/test/operator/oputils_test.py +++ b/odl/test/operator/oputils_test.py @@ -1,4 +1,4 @@ -# Copyright 2014-2017 The ODL contributors +# Copyright 2014-2019 The ODL contributors # # This file is part of ODL. # @@ -7,6 +7,7 @@ # obtain one at https://mozilla.org/MPL/2.0/. from __future__ import division + import numpy as np import pytest @@ -27,6 +28,7 @@ def test_matrix_representation(): assert all_almost_equal(A, matrix_repr) +@pytest.mark.xfail(reason='currently broken') def test_matrix_representation_product_to_lin_space(): """Verify that the matrix repr works for product spaces. @@ -48,6 +50,7 @@ def test_matrix_representation_product_to_lin_space(): assert np.linalg.norm(B - matrix_repr[0, :, 1, :]) == pytest.approx(0) +@pytest.mark.xfail(reason='currently broken') def test_matrix_representation_lin_space_to_product(): """Verify that the matrix repr works for product spaces. @@ -71,6 +74,7 @@ def test_matrix_representation_lin_space_to_product(): assert np.linalg.norm(B - matrix_repr[1, :, 0, :]) == pytest.approx(0) +@pytest.mark.xfail(reason='currently broken') def test_matrix_representation_product_to_product(): """Verify that the matrix repr works for product spaces. @@ -119,7 +123,7 @@ def _call(self, x, out): return odl.rn(np.random.rand(4)) nonlin_op = MyOp() - with pytest.raises(TypeError): + with pytest.raises(ValueError): matrix_representation(nonlin_op) @@ -137,7 +141,7 @@ def _call(self, x, out): return odl.rn(np.random.rand(4)) nonlin_op = MyOp() - with pytest.raises(TypeError): + with pytest.raises(ValueError): matrix_representation(nonlin_op) diff --git a/odl/test/operator/pspace_ops_test.py b/odl/test/operator/pspace_ops_test.py index 7f7e1f572b7..4ebda270ee1 100644 --- a/odl/test/operator/pspace_ops_test.py +++ b/odl/test/operator/pspace_ops_test.py @@ -1,4 +1,4 @@ -# Copyright 2014-2017 The ODL contributors +# Copyright 2014-2019 The ODL contributors # # This file is part of ODL. # @@ -7,12 +7,12 @@ # obtain one at https://mozilla.org/MPL/2.0/. from __future__ import division + import pytest import odl from odl.util.testutils import all_almost_equal, simple_fixture - base_op = simple_fixture( 'base_op', [odl.IdentityOperator(odl.rn(3)), @@ -127,10 +127,10 @@ def test_pspace_op_project_call(): x = r3.element([1, 2, 3]) z = op.domain.element([x]) - assert x == op(z)[0] - assert x == op(z, out=op.range.element())[0] - assert x == op(z)[1] - assert x == op(z, out=op.range.element())[1] + assert all_almost_equal(op(z)[0], x) + assert all_almost_equal(op(z, out=op.range.element())[0], x) + assert all_almost_equal(op(z)[1], x) + assert all_almost_equal(op(z, out=op.range.element())[1], x) def test_pspace_op_diagonal_call(): @@ -143,8 +143,8 @@ def test_pspace_op_diagonal_call(): y = r3.element([7, 8, 9]) z = op.domain.element([x, y]) - assert z == op(z) - assert z == op(z, out=op.range.element()) + assert all_almost_equal(op(z), z) + assert all_almost_equal(op(z, out=op.range.element()), z) def test_pspace_op_swap_call(): @@ -156,10 +156,10 @@ def test_pspace_op_swap_call(): x = r3.element([1, 2, 3]) y = r3.element([7, 8, 9]) z = op.domain.element([x, y]) - result = op.domain.element([y, x]) + true_result = op.domain.element([y, x]) - assert result == op(z) - assert result == op(z, out=op.range.element()) + assert all_almost_equal(op(z), true_result) + assert all_almost_equal(op(z, out=op.range.element()), true_result) def test_comp_proj(): @@ -169,12 +169,12 @@ def test_comp_proj(): x = r3xr3.element([[1, 2, 3], [4, 5, 6]]) proj_0 = odl.ComponentProjection(r3xr3, 0) - assert x[0] == proj_0(x) - assert x[0] == proj_0(x, out=proj_0.range.element()) + assert all_almost_equal(proj_0(x), x[0]) + assert all_almost_equal(proj_0(x, out=proj_0.range.element()), x[0]) proj_1 = odl.ComponentProjection(r3xr3, 1) - assert x[1] == proj_1(x) - assert x[1] == proj_1(x, out=proj_1.range.element()) + assert all_almost_equal(proj_1(x), x[1]) + assert all_almost_equal(proj_1(x, out=proj_1.range.element()), x[1]) def test_comp_proj_slice(): @@ -186,8 +186,8 @@ def test_comp_proj_slice(): [7, 8, 9]]) proj = odl.ComponentProjection(r33, slice(0, 2)) - assert x[0:2] == proj(x) - assert x[0:2] == proj(x, out=proj.range.element()) + assert all_almost_equal(proj(x), x[0:2]) + assert all_almost_equal(proj(x, out=proj.range.element()), x[0:2]) def test_comp_proj_indices(): @@ -199,8 +199,8 @@ def test_comp_proj_indices(): [7, 8, 9]]) proj = odl.ComponentProjection(r33, [0, 2]) - assert x[[0, 2]] == proj(x) - assert x[[0, 2]] == proj(x, out=proj.range.element()) + assert all_almost_equal(proj(x), x[[0, 2]]) + assert all_almost_equal(proj(x, out=proj.range.element()), x[[0, 2]]) def test_comp_proj_adjoint(): @@ -213,15 +213,19 @@ def test_comp_proj_adjoint(): [0, 0, 0]]) proj_0 = odl.ComponentProjection(r3xr3, 0) - assert result_0 == proj_0.adjoint(x) - assert result_0 == proj_0.adjoint(x, out=proj_0.domain.element()) + assert all_almost_equal(proj_0.adjoint(x), result_0) + assert all_almost_equal( + proj_0.adjoint(x, out=proj_0.domain.element()), result_0 + ) result_1 = r3xr3.element([[0, 0, 0], [1, 2, 3]]) proj_1 = odl.ComponentProjection(r3xr3, 1) - assert result_1 == proj_1.adjoint(x) - assert result_1 == proj_1.adjoint(x, out=proj_1.domain.element()) + assert all_almost_equal(proj_1.adjoint(x), result_1) + assert all_almost_equal( + proj_1.adjoint(x, out=proj_1.domain.element()), result_1 + ) def test_comp_proj_adjoint_slice(): @@ -236,8 +240,8 @@ def test_comp_proj_adjoint_slice(): [0, 0, 0]]) proj = odl.ComponentProjection(r33, slice(0, 2)) - assert result == proj.adjoint(x) - assert result == proj.adjoint(x, out=proj.domain.element()) + assert all_almost_equal(proj.adjoint(x), result) + assert all_almost_equal(proj.adjoint(x, out=proj.domain.element()), result) if __name__ == '__main__': diff --git a/odl/test/operator/tensor_ops_test.py b/odl/test/operator/tensor_ops_test.py index 2f81e42a7a9..470d499a60b 100644 --- a/odl/test/operator/tensor_ops_test.py +++ b/odl/test/operator/tensor_ops_test.py @@ -11,6 +11,7 @@ from __future__ import division import numpy as np +import pytest import scipy.sparse import odl @@ -211,7 +212,7 @@ def test_pointwise_norm_gradient_real(exponent): direction = noise_element(vfspace) # Computing expected result - tmp = pwnorm(point).ufuncs.power(1 - exponent) + tmp = np.power(pwnorm(point), 1 - exponent) v_field = vfspace.element() for i in range(len(v_field)): v_field[i] = tmp * point[i] * np.abs(point[i]) ** (exponent - 2) @@ -231,7 +232,7 @@ def test_pointwise_norm_gradient_real(exponent): direction = noise_element(vfspace) # Computing expected result - tmp = pwnorm(point).ufuncs.power(1 - exponent) + tmp = np.power(pwnorm(point), 1 - exponent) v_field = vfspace.element() for i in range(len(v_field)): v_field[i] = tmp * point[i] * np.abs(point[i]) ** (exponent - 2) @@ -440,7 +441,7 @@ def test_pointwise_inner_adjoint(): testarr = np.array([[1 + 1j, 2], [3, 4 - 2j]]) - true_inner_adj = testarr[None, :, :] * array + true_inner_adj = list(testarr[None, :, :] * array) testfunc = fspace.element(testarr) testfunc_pwinner_adj = pwinner.adjoint(testfunc) @@ -464,7 +465,7 @@ def test_pointwise_inner_adjoint(): testarr = np.array([[1 + 1j, 2], [3, 4 - 2j]]) - true_inner_adj = testarr[None, :, :] * array + true_inner_adj = list(testarr[None, :, :] * array) testfunc = fspace.element(testarr) testfunc_pwinner_adj = pwinner.adjoint(testfunc) @@ -490,7 +491,8 @@ def test_pointwise_inner_adjoint_weighted(): testarr = np.array([[1 + 1j, 2], [3, 4 - 2j]]) - true_inner_adj = testarr[None, :, :] * array # same as unweighted case + # same as unweighted case + true_inner_adj = list(testarr[None, :, :] * array) testfunc = fspace.element(testarr) testfunc_pwinner_adj = pwinner.adjoint(testfunc) @@ -506,7 +508,7 @@ def test_pointwise_inner_adjoint_weighted(): testarr = np.array([[1 + 1j, 2], [3, 4 - 2j]]) - true_inner_adj = 2 * testarr[None, :, :] * array # w / v = (2, 2, 2) + true_inner_adj = list(2 * testarr[None, :, :] * array) # w / v = (2, 2, 2) testfunc = fspace.element(testarr) testfunc_pwinner_adj = pwinner.adjoint(testfunc) @@ -691,11 +693,11 @@ def test_matrix_op_adjoint(matrix): x = noise_element(dmat_op.domain) y = noise_element(dmat_op.range) - inner_ran = dmat_op(x).inner(y) - inner_dom = x.inner(dmat_op.adjoint(y)) + inner_ran = dmat_op.range.inner(dmat_op(x), y) + inner_dom = dmat_op.domain.inner(x, dmat_op.adjoint(y)) assert inner_ran == pytest.approx(inner_dom, rel=tol, abs=tol) - inner_ran = smat_op(x).inner(y) - inner_dom = x.inner(smat_op.adjoint(y)) + inner_ran = smat_op.range.inner(smat_op(x), y) + inner_dom = smat_op.domain.inner(x, smat_op.adjoint(y)) assert inner_ran == pytest.approx(inner_dom, rel=tol, abs=tol) # Multi-dimensional case @@ -703,8 +705,8 @@ def test_matrix_op_adjoint(matrix): mat_op = MatrixOperator(dense_matrix, domain, axis=2) x = noise_element(mat_op.domain) y = noise_element(mat_op.range) - inner_ran = mat_op(x).inner(y) - inner_dom = x.inner(mat_op.adjoint(y)) + inner_ran = mat_op.range.inner(mat_op(x), y) + inner_dom = mat_op.domain.inner(x, mat_op.adjoint(y)) assert inner_ran == pytest.approx(inner_dom, rel=tol, abs=tol) @@ -740,10 +742,14 @@ def test_sampling_operator_adjoint(): sampling_points = [[0, 1, 1, 0]] x = space.element([1, 2, 3]) op = odl.SamplingOperator(space, sampling_points) - assert op.adjoint(op(x)).inner(x) == pytest.approx(op(x).inner(op(x))) + inner_dom = op.domain.inner(x, op.adjoint(op(x))) + inner_ran = op.range.inner(op(x), op(x)) + assert inner_dom == pytest.approx(inner_ran) op = odl.SamplingOperator(space, sampling_points, variant='integrate') - assert op.adjoint(op(x)).inner(x) == pytest.approx(op(x).inner(op(x))) + inner_dom = op.domain.inner(x, op.adjoint(op(x))) + inner_ran = op.range.inner(op(x), op(x)) + assert inner_dom == pytest.approx(inner_ran) # 2d space space = odl.uniform_discr([-1, -1], [1, 1], shape=(2, 3)) @@ -752,12 +758,16 @@ def test_sampling_operator_adjoint(): sampling_points = [[0, 1, 1, 0], [0, 1, 2, 0]] op = odl.SamplingOperator(space, sampling_points) - assert op.adjoint(op(x)).inner(x) == pytest.approx(op(x).inner(op(x))) + inner_dom = op.domain.inner(x, op.adjoint(op(x))) + inner_ran = op.range.inner(op(x), op(x)) + assert inner_dom == pytest.approx(inner_ran) # The ``'integrate'`` variant adjoint puts ones at the indices in # `sampling_points``, multiplied by their multiplicity: op = odl.SamplingOperator(space, sampling_points, variant='integrate') - assert op.adjoint(op(x)).inner(x) == pytest.approx(op(x).inner(op(x))) + inner_dom = op.domain.inner(x, op.adjoint(op(x))) + inner_ran = op.range.inner(op(x), op(x)) + assert inner_dom == pytest.approx(inner_ran) if __name__ == '__main__': diff --git a/odl/test/set/sets_test.py b/odl/test/set/sets_test.py index a8b1288d476..2d6079ac520 100644 --- a/odl/test/set/sets_test.py +++ b/odl/test/set/sets_test.py @@ -1,4 +1,4 @@ -# Copyright 2014-2017 The ODL contributors +# Copyright 2014-2019 The ODL contributors # # This file is part of ODL. # @@ -7,11 +7,12 @@ # obtain one at https://mozilla.org/MPL/2.0/. from __future__ import division + import pytest import odl -from odl.set.sets import (EmptySet, UniversalSet, Strings, ComplexNumbers, - RealNumbers, Integers) +from odl.set.sets import ( + ComplexNumbers, EmptySet, Integers, RealNumbers, Strings) def test_empty_set(): @@ -34,28 +35,6 @@ def test_empty_set(): assert X.element() is None -def test_universal_set(): - X = UniversalSet() - Z = Integers() - - # __contains - assert None in X - assert 1 in X - - # Contains_set - assert X.contains_set(X) - assert X.contains_set(Z) - assert not X.contains_set(1) - - # __eq__ - assert X == X - assert X != Z - - # element - assert X.element() is None - assert X.element(1) == 1 - - def test_strings(): S1 = Strings(1) S6 = Strings(6) diff --git a/odl/test/set/space_test.py b/odl/test/set/space_test.py index 2356c2918b4..3413dfd28ba 100644 --- a/odl/test/set/space_test.py +++ b/odl/test/set/space_test.py @@ -1,4 +1,4 @@ -# Copyright 2014-2017 The ODL contributors +# Copyright 2014-2019 The ODL contributors # # This file is part of ODL. # @@ -9,70 +9,39 @@ from __future__ import division import pytest import odl -from odl.util.testutils import simple_fixture, noise_element +from odl.util.testutils import simple_fixture # --- pytest fixtures --- # -hilbert_spaces = [odl.rn(3), odl.cn(3), odl.uniform_discr(0, 1, 3)] -normed_spaces = [odl.rn(3, exponent=1)] + hilbert_spaces -metric_spaces = normed_spaces -linear_spaces = metric_spaces - -hilbert_space = simple_fixture('hilbert_space', hilbert_spaces) -normed_space = simple_fixture('normed_space', normed_spaces) -metric_space = simple_fixture('metric_space', metric_spaces) -linear_space = simple_fixture('linear_space', linear_spaces) +spaces = [ + odl.rn(3), odl.cn(3), odl.rn(3, exponent=1), odl.uniform_discr(0, 1, 3) +] +space = simple_fixture('space', spaces) # --- LinearSpace tests --- # -def test_hash(linear_space): - """Verify that hashing spaces works but elements doesnt.""" - hsh = hash(linear_space) +def test_hash(space): + """Verify that hashing of spaces works.""" + hsh = hash(space) # Check that the trivial hash algorithm is not used - assert hsh != id(linear_space) - - x = noise_element(linear_space) - with pytest.raises(TypeError): - hash(x) - - -def test_equality(metric_space): - """Verify that equality testing works.""" - x = noise_element(metric_space) - y = noise_element(metric_space) - - assert x == x - assert y == y - assert x != y - - -def test_comparsion(linear_space): - """Verify that spaces and elements in spaces cannot be compared.""" - with pytest.raises(TypeError): - linear_space <= linear_space - with pytest.raises(TypeError): - linear_space < linear_space - with pytest.raises(TypeError): - linear_space >= linear_space - with pytest.raises(TypeError): - linear_space > linear_space + assert hsh != id(space) - x = noise_element(linear_space) - y = noise_element(linear_space) +def test_comparsion_raises(space): + """Verify that spaces cannot be compared.""" with pytest.raises(TypeError): - x <= y + space <= space with pytest.raises(TypeError): - x < y + space < space with pytest.raises(TypeError): - x >= y + space >= space with pytest.raises(TypeError): - x > y + space > space if __name__ == '__main__': diff --git a/odl/test/solvers/functional/default_functionals_test.py b/odl/test/solvers/functional/default_functionals_test.py index 0b2f0a6cf8b..8e5bae4fd07 100644 --- a/odl/test/solvers/functional/default_functionals_test.py +++ b/odl/test/solvers/functional/default_functionals_test.py @@ -1,4 +1,4 @@ -# Copyright 2014-2017 The ODL contributors +# Copyright 2014-2019 The ODL contributors # # This file is part of ODL. # @@ -9,26 +9,25 @@ """Test for the default functionals.""" from __future__ import division + import numpy as np -import scipy.special import pytest +import scipy.special import odl from odl.util.testutils import all_almost_equal, noise_element, simple_fixture -from odl.solvers.functional.default_functionals import ( - KullbackLeiblerConvexConj, KullbackLeiblerCrossEntropyConvexConj) - # --- pytest fixtures --- # -scalar = simple_fixture('scalar', [0.01, 2.7, np.array(5.0), 10, -2, -0.2, - -np.array(7.1), 0]) +scalar = simple_fixture( + 'scalar', [0.01, 2.7, np.array(5.0), 10, -2, -0.2, -np.array(7.1), 0] +) sigma = simple_fixture('sigma', [0.001, 2.7, 10]) -exponent = simple_fixture('sigma', [1, 2, 1.5, 2.5, -1.6]) +exponent = simple_fixture('exponent', [1, 2, 1.5, 2.5]) -space_params = ['r10', 'uniform_discr', 'power_space_unif_discr'] +space_params = ['r10', 'Lp', 'Lp ** 2'] space_ids = [' space={} '.format(p) for p in space_params] @@ -39,138 +38,156 @@ def space(request, odl_tspace_impl): if name == 'r10': return odl.rn(10, impl=impl) - elif name == 'uniform_discr': + elif name == 'Lp': return odl.uniform_discr(0, 1, 7, impl=impl) - elif name == 'power_space_unif_discr': - # Discretization parameters + elif name == 'Lp ** 2': space = odl.uniform_discr(0, 1, 7, impl=impl) return odl.ProductSpace(space, 2) # --- functional tests --- # +def _test_op(op, x, true_res): + assert op.domain == op.range + + assert all_almost_equal(op(x), true_res) # out-of-place + out = op.range.element() + op(x, out=out) + assert all_almost_equal(out, true_res) # in-place + y = op.domain.copy(x) + op(y, out=y) + assert all_almost_equal(y, true_res) # in-place, aliased + + def test_L1_norm(space, sigma): """Test the L1-norm.""" - sigma = float(sigma) func = odl.solvers.L1Norm(space) x = noise_element(space) + F = space.ufuncs + R = space.reduce + sigma = float(sigma) - # Test functional evaluation - expected_result = np.abs(x).inner(space.one()) - assert func(x) == pytest.approx(expected_result) - - # Test gradient - expecting sign function - expected_result = func.domain.element(np.sign(x)) - assert all_almost_equal(func.gradient(x), expected_result) - - # Test proximal - expecting the following: - # | x_i + sigma, if x_i < -sigma - # z_i = { 0, if -sigma <= x_i <= sigma - # | x_i - sigma, if x_i > sigma - tmp = np.zeros(space.shape) - orig = x.asarray() - tmp[orig > sigma] = orig[orig > sigma] - sigma - tmp[orig < -sigma] = orig[orig < -sigma] + sigma - expected_result = space.element(tmp) - assert all_almost_equal(func.proximal(sigma)(x), expected_result) - - # Test convex conjugate - expecting 0 if |x|_inf <= 1, infty else - func_cc = func.convex_conj - norm_larger_than_one = 1.1 * x / np.max(np.abs(x)) - assert func_cc(norm_larger_than_one) == np.inf + # Evaluation + assert func(x) == pytest.approx(space.inner(F.abs(x), space.one())) - norm_less_than_one = 0.9 * x / np.max(np.abs(x)) - assert func_cc(norm_less_than_one) == 0 + # Gradient + func_grad_x = F.sign(x) + _test_op(func.gradient, x, func_grad_x) - norm_equal_to_one = x / np.max(np.abs(x)) - assert func_cc(norm_equal_to_one) == 0 + # Proximal + func_prox_x = F.sign(x) * F.maximum(F.abs(x) - sigma, 0) + _test_op(func.proximal(sigma), x, func_prox_x) - # Gradient of the convex conjugate (not implemeted) - with pytest.raises(NotImplementedError): - func_cc.gradient + # CC evaluation + # NB: L1 CC is the indicator of the inf-norm unit ball, not covered by + # `test_indicator_lp_unit_ball` + func_cc = func.convex_conj + inf_norm_x = R.max(F.abs(x)) + # Elements with norm > 1 --> inf + for c in [2.0, 1.1, 1 + 1e-5]: + norm_gt_1 = (c / inf_norm_x) * x + assert func_cc(norm_gt_1) == np.inf, 'c={}'.format(c) + + # Elements with norm < 1 --> 0 + for c in [0.0, 0.9, 1 - 1e-5]: + norm_lt_1 = (c / inf_norm_x) * x + assert func_cc(norm_lt_1) == 0, 'c={}'.format(c) - # Test proximal of the convex conjugate - expecting x / max(1, |x|) - expected_result = x / np.maximum(1, np.abs(x)) - assert all_almost_equal(func_cc.proximal(sigma)(x), expected_result) + # CC gradient not implemented - # Verify that the biconjugate is the functional itself + # CC proximal + func_cc_prox_x = x / F.maximum(1, F.abs(x)) + _test_op(func_cc.proximal(sigma), x, func_cc_prox_x) + + # Biconjugate func_cc_cc = func_cc.convex_conj - assert isinstance(func_cc_cc, odl.solvers.L1Norm) + assert func_cc_cc(x) == pytest.approx(func(x)) def test_indicator_lp_unit_ball(space, sigma, exponent): """Test for indicator function on unit ball.""" - x = noise_element(space) - one_elem = space.one() - func = odl.solvers.IndicatorLpUnitBall(space, exponent) + x = noise_element(space) + F = space.ufuncs - # Test functional evaluation + # Evaluation p_norm_x = np.power( - func.domain.element(np.power(np.abs(x), exponent)).inner(one_elem), - 1.0 / exponent) + space.inner(F.power(F.abs(x), exponent), space.one()), 1 / exponent + ) + for c in [2.0, 1.1, 1 + 1e-5]: + norm_gt_1 = (c / p_norm_x) * x + assert func(norm_gt_1) == np.inf, 'c={}'.format(c) - norm_larger_than_one = 1.01 * x / p_norm_x - assert func(norm_larger_than_one) == np.inf + for c in [0.0, 0.9, 1 - 1e-5]: + norm_lt_1 = (c / p_norm_x) * x + assert func(norm_lt_1) == 0, 'c={}'.format(c) - norm_less_than_one = 0.99 * x / p_norm_x - assert func(norm_less_than_one) == 0 + # Gradient not implemented + + # Proximal + if exponent in {2, float('inf')}: + func_prox_x = x if p_norm_x <= 1 else x / p_norm_x + _test_op(func.proximal(sigma), x, func_prox_x) def test_L2_norm(space, sigma): """Test the L2-norm.""" func = odl.solvers.L2Norm(space) x = noise_element(space) - x_norm = x.norm() - - # Test functional evaluation - expected_result = np.sqrt((x ** 2).inner(space.one())) - assert func(x) == pytest.approx(expected_result) + x_norm = space.norm(x) + zero = space.zero() - # Test gradient - if x_norm > 0: - expected_result = x / x.norm() - assert all_almost_equal(func.gradient(x), expected_result) + # Evaluation + assert func(x) == pytest.approx(np.sqrt(space.inner(x ** 2, space.one()))) - # Verify that the gradient at zero is zero - assert all_almost_equal(func.gradient(func.domain.zero()), space.zero()) - - # Test proximal operator - expecting - # x * (1 - sigma/||x||) if ||x|| > sigma, 0 else - norm_less_than_sigma = 0.99 * sigma * x / x_norm - assert all_almost_equal(func.proximal(sigma)(norm_less_than_sigma), - space.zero()) - - norm_larger_than_sigma = 1.01 * sigma * x / x_norm - expected_result = (norm_larger_than_sigma * - (1.0 - sigma / norm_larger_than_sigma.norm())) - assert all_almost_equal(func.proximal(sigma)(norm_larger_than_sigma), - expected_result) + # Gradient + func_grad_x = x / x_norm + _test_op(func.gradient, x, func_grad_x) + assert all_almost_equal(func.gradient(zero), zero) - # Test convex conjugate + # Proximal + # x * (1 - sigma/||x||) if ||x|| > sigma, else 0 + for c in [2.0, 1.1, 1 + 1e-5]: + # ||y|| > sigma + y = (c * sigma / x_norm) * x + func_prox_y = y * (1 - sigma / space.norm(y)) + _test_op(func.proximal(sigma), y, func_prox_y) + + for c in [0.0, 0.9, 1 - 1e-5]: + # ||y|| < sigma + y = (c * sigma / x_norm) * x + func_prox_y = zero + _test_op(func.proximal(sigma), y, func_prox_y) + + # CC evaluation + # 0 if ||x|| < 1, else infty func_cc = func.convex_conj + for c in [2.0, 1.1, 1 + 1e-5]: + # ||y|| > 1 + y = (c / x_norm) * x + assert func_cc(y) == np.inf + + for c in [0.0, 0.9, 1 - 1e-5]: + # ||y|| < 1 + y = (c / x_norm) * x + assert func_cc(y) == 0 + + # CC gradient not implemented + + # CC proximal + # x if ||x||_2 < 1, else x/||x|| + for c in [2.0, 1.1, 1 + 1e-5]: + # ||y|| > 1 + y = (c / x_norm) * x + func_cc_prox_y = x / x_norm + _test_op(func_cc.proximal(sigma), y, func_cc_prox_y) + + for c in [0.0, 0.9, 1 - 1e-5]: + # ||y|| < 1 + y = (c / x_norm) * x + func_cc_prox_y = y + _test_op(func_cc.proximal(sigma), y, func_cc_prox_y) - # Test evaluation of the convex conjugate - expecting - # 0 if ||x|| < 1, infty else - norm_larger_than_one = 1.01 * x / x_norm - assert func_cc(norm_larger_than_one) == np.inf - - norm_less_than_one = 0.99 * x / x_norm - assert func_cc(norm_less_than_one) == 0 - - # Gradient of the convex conjugate (not implemeted) - with pytest.raises(NotImplementedError): - func_cc.gradient - - # Test the proximal of the convex conjugate - expecting - # x if ||x||_2 < 1, x/||x|| else - if x_norm < 1: - expected_result = x - else: - expected_result = x / x_norm - assert all_almost_equal(func_cc.proximal(sigma)(x), expected_result) - - # Verify that the biconjugate is the functional itself func_cc_cc = func_cc.convex_conj assert func_cc_cc(x) == pytest.approx(func(x)) @@ -179,79 +196,59 @@ def test_L2_norm_squared(space, sigma): """Test the squared L2-norm.""" func = odl.solvers.L2NormSquared(space) x = noise_element(space) - x_norm = x.norm() + x_norm = space.norm(x) - # Test functional evaluation - expected_result = x_norm ** 2 - assert func(x) == pytest.approx(expected_result) + # Evaluation + assert func(x) == pytest.approx(x_norm ** 2) - # Test gradient - expected_result = 2.0 * x - assert all_almost_equal(func.gradient(x), expected_result) + # Gradient + func_grad_x = 2 * x + _test_op(func.gradient, x, func_grad_x) - # Test proximal operator - expected_result = x / (1 + 2.0 * sigma) - assert all_almost_equal(func.proximal(sigma)(x), expected_result) + # Proximal + func_prox_x = x / (1 + 2 * sigma) + _test_op(func.proximal(sigma), x, func_prox_x) - # Test convex conjugate + # CC Evaluation func_cc = func.convex_conj + assert func_cc(x) == pytest.approx(x_norm ** 2 / 4) - # Test evaluation of the convex conjugate - expected_result = x_norm ** 2 / 4.0 - assert func_cc(x) == pytest.approx(expected_result) - - # Test gradient of the convex conjugate - expected_result = x / 2.0 - assert all_almost_equal(func_cc.gradient(x), expected_result) + # CC Gradient + func_cc_grad_x = x / 2 + _test_op(func_cc.gradient, x, func_cc_grad_x) - # Test proximal of the convex conjugate - expected_result = x / (1 + sigma / 2.0) - assert all_almost_equal(func_cc.proximal(sigma)(x), expected_result) + # CC Proximal + func_prox_cc_x = x / (1 + sigma / 2) + _test_op(func_cc.proximal(sigma), x, func_prox_cc_x) - # Verify that the biconjugate is the functional itself + # Biconjugate func_cc_cc = func_cc.convex_conj - - # Check that they evaluate to the same value assert func_cc_cc(x) == pytest.approx(func(x)) - # Check that their gradients evaluate to the same value - assert all_almost_equal(func_cc_cc.gradient(x), func.gradient(x)) - def test_constant_functional(space, scalar): """Test the constant functional.""" constant = float(scalar) func = odl.solvers.ConstantFunctional(space, constant=scalar) x = noise_element(space) + sigma = 1.5 - assert func.constant == constant - - # Test functional evaluation assert func(x) == constant - - # Test gradient - expecting zero operator assert isinstance(func.gradient, odl.ZeroOperator) - - # Test proximal operator - expecting identity - sigma = 1.5 assert isinstance(func.proximal(sigma), odl.IdentityOperator) - # Test convex conjugate + # CC Evaluation + # -constant if x = 0, else infty func_cc = func.convex_conj - - # Test evaluation of the convex conjugate - expecting - # -constant if x=0, infty else assert func_cc(x) == np.inf assert func_cc(space.zero()) == -constant - # Gradient of the convex conjugate (not implemeted) - with pytest.raises(NotImplementedError): - func_cc.gradient + # CC Gradient not implemented - # Proximal of the convex conjugate - expecting zero operator + # CC Proximal assert isinstance(func_cc.proximal(sigma), odl.ZeroOperator) - # Verify that the biconjugate is the functional itself + # Biconjugate func_cc_cc = func_cc.convex_conj assert isinstance(func_cc_cc, odl.solvers.ConstantFunctional) assert func_cc_cc.constant == constant @@ -260,192 +257,270 @@ def test_constant_functional(space, scalar): def test_zero_functional(space): """Test the zero functional.""" zero_func = odl.solvers.ZeroFunctional(space) - assert isinstance(zero_func, odl.solvers.ConstantFunctional) - assert zero_func.constant == 0 + assert zero_func(space.one()) == 0 def test_kullback_leibler(space): - """Test the kullback leibler functional and its convex conjugate.""" - # The prior needs to be positive - prior = np.abs(noise_element(space)) + 0.1 - + """Test the Kullback-Leibler functional and its convex conjugate.""" + F = space.ufuncs + R = space.reduce + prior = F.abs(noise_element(space)) + 0.1 # must be positive func = odl.solvers.KullbackLeibler(space, prior) + x = F.abs(noise_element(space)) + 0.1 # must be positive + one = space.one() + sigma = 1.2 + + # Evaluation + assert func(x) == pytest.approx( + space.inner((x - prior + prior * F.log(prior / x)), one) + ) + # If any component is nonpositive, the result should be infinity + assert func(-x) == np.inf + if not isinstance(space, odl.ProductSpace): + y = space.copy(x) + y[0] = 0 + assert func(y) == np.inf + # Points where `prior` is 0 should contribute 0 + if not isinstance(space, odl.ProductSpace): + prior2 = space.zero() + prior2[:prior2.shape[0] // 2] = 2 + # Fraction of nonzero elements in prior2 + nz_frac = (prior2.shape[0] // 2) / prior2.shape[0] + func2 = odl.solvers.KullbackLeibler(space, prior2) + # Where prior is 1, we integrate y - 2 + 2 * log(2 / y), + # elsewhere we integrate y + # Testing with y = 1 + assert func2(one) == pytest.approx( + (-1 + 2 * np.log(2)) * space.inner(one, one) * nz_frac + + space.inner(one, one) * (1 - nz_frac) + ) - # The fucntional is only defined for positive elements - x = np.abs(noise_element(space)) + 0.1 - one_elem = space.one() - - # Evaluation of the functional - expected_result = ( - x - prior + prior * np.log(prior / x) - ).inner(one_elem) - assert func(x) == pytest.approx(expected_result) - - # Check property for prior - assert all_almost_equal(func.prior, prior) + # Gradient + func_grad_x = 1 - prior / x + _test_op(func.gradient, x, func_grad_x) - # For elements with (a) negative components it should return inf - x_neg = noise_element(space) - x_neg = x_neg - x_neg.ufuncs.max() - assert func(x_neg) == np.inf + # Proximal + func_prox_x = ( + x - sigma + F.sqrt((x - sigma) ** 2 + 4 * sigma * prior) + ) / 2 + _test_op(func.proximal(sigma), x, func_prox_x) - # The gradient - expected_result = 1 - prior / x - assert all_almost_equal(func.gradient(x), expected_result) + # CC evaluation + func_cc = func.convex_conj + # integral of -prior * log(1 - x) if x < 1 everywhere, otherwise infinity + y = 0.99 * x / R.max(x) # max(y) < 1 + assert func_cc(y) == pytest.approx( + -space.inner(prior * F.log(1 - y), one) + ) + y = 1.01 * x / R.max(x) # max(y) > 1 + assert func_cc(y) == np.inf + + # CC gradient + y = 0.99 * x / R.max(x) + func_cc_grad_y = prior / (1 - y) + _test_op(func_cc.gradient, y, func_cc_grad_y) + + # CC proximal + y = 0.99 * x / R.max(x) + func_cc_prox_y = (y + 1 - F.sqrt((y - 1) ** 2 + 4 * sigma * prior)) / 2 + _test_op(func_cc.proximal(sigma), y, func_cc_prox_y) + + # Biconjugate + func_cc_cc = func_cc.convex_conj + assert func_cc_cc(x) == pytest.approx(func(x)) - # The proximal operator - sigma = np.random.rand() - expected_result = odl.solvers.proximal_convex_conj( - odl.solvers.proximal_convex_conj_kl(space, g=prior))(sigma)(x) - assert all_almost_equal(func.proximal(sigma)(x), expected_result) - # The convex conjugate functional - cc_func = func.convex_conj +def test_kullback_leibler_cross_entropy(space): + """Test the kullback leibler cross entropy and its convex conjugate.""" + F = space.ufuncs + prior = F.abs(noise_element(space)) + 0.1 # must be positive + func = odl.solvers.KullbackLeiblerCrossEntropy(space, prior) + x = F.abs(noise_element(space)) + 0.1 # must be positive + one = space.one() + zero = space.zero() + sigma = 1.2 + + # Evaluation + assert func(x) == pytest.approx( + space.inner(prior - x + x * F.log(x / prior), one) + ) + # At x=0, the expected value is the integral of the prior + assert func(zero) == pytest.approx(space.inner(prior, one)) + # If any component is negative, the result should be infinity + assert func(-x) == np.inf + # If any prior component is 0, the result should be infinity + if not isinstance(space, odl.ProductSpace): + prior2 = space.copy(x) + prior2[0] = 0 + func2 = odl.solvers.KullbackLeiblerCrossEntropy(space, prior2) + assert func2(x) == np.inf - assert isinstance(cc_func, KullbackLeiblerConvexConj) + # Gradient + func_grad_x = prior - 1 + F.log(x / prior) + _test_op(func.gradient, x, func_grad_x) - # The convex conjugate functional is only finite for elements with all - # components smaller than 1. - x = noise_element(space) - x = x - x.ufuncs.max() + 0.99 + # Proximal + # sigma * W(prior * exp(x / sigma) / sigma) + if isinstance(space, odl.ProductSpace): + arg = prior * F.exp(x / sigma) / sigma + func_prox_x = sigma * space.apply(scipy.special.lambertw, arg).real + else: + func_prox_x = sigma * scipy.special.lambertw( + prior * F.exp(x / sigma) / sigma + ).real + _test_op(func.proximal(sigma), x, func_prox_x) - # Evaluation of convex conjugate - expected_result = - (prior * np.log(1 - x)).inner(one_elem) - assert cc_func(x) == pytest.approx(expected_result) + # CC evaluation + # integral of prior * (exp(x) - 1) + func_cc = func.convex_conj + x = noise_element(space) # convex conjugate is defined for any x + assert func_cc(x) == pytest.approx( + space.inner(prior * (F.exp(x) - 1), one) + ) + + # CC gradient + func_cc_grad_x = prior * F.exp(x) + _test_op(func_cc.gradient, x, func_cc_grad_x) + + # CC proximal + # x - W(prior * exp(x) * sigma) + if isinstance(space, odl.ProductSpace): + arg = sigma * prior * F.exp(x) + func_cc_prox_x = x - space.apply(scipy.special.lambertw, arg).real + else: + func_cc_prox_x = x - scipy.special.lambertw( + sigma * prior * F.exp(x) + ).real + _test_op(func_cc.proximal(sigma), x, func_cc_prox_x) - x_wrong = noise_element(space) - x_wrong = x_wrong - x_wrong.ufuncs.max() + 1.01 - assert cc_func(x_wrong) == np.inf + # Biconjugate + func_cc_cc = func_cc.convex_conj + x = F.abs(noise_element(space)) + 0.1 # need positive again + assert func_cc_cc(x) == pytest.approx(func(x)) - # The gradient of the convex conjugate - expected_result = prior / (1 - x) - assert all_almost_equal(cc_func.gradient(x), expected_result) - # The proximal of the convex conjugate - expected_result = 0.5 * (1 + x - np.sqrt((x - 1) ** 2 + 4 * sigma * prior)) - assert all_almost_equal(cc_func.proximal(sigma)(x), expected_result) +def test_quadratic_form(space): + """Test the quadratic form functional.""" + # TODO: move this to largescale tests + if False: + # Non-symmetric operator + mat = np.eye(space.size) + mat[0, 1] = 1 + operator = odl.MatrixOperator(mat, domain=space, range=space) - # The biconjugate, which is the functional itself since it is proper, - # convex and lower-semicontinuous - cc_cc_func = cc_func.convex_conj + mat_sym = (mat + mat.T) / 2 + mat_sym_inv = np.linalg.inv(mat_sym) + sym_inv_op = odl.MatrixOperator(mat_sym_inv, domain=space, range=space) + kwargs['operator_sym_inv'] = sym_inv_op - # Check that they evaluate the same - assert cc_cc_func(x) == pytest.approx(func(x)) + def prox_inv_fact(sigma): + minv = np.linalg.inv(np.eye(space.size) + sigma * mat_sym) + return odl.MatrixOperator(minv, domain=space, range=space) + kwargs['operator_prox_inv_fact'] = prox_inv_fact -def test_kullback_leibler_cross_entorpy(space): - """Test the kullback leibler cross entropy and its convex conjugate.""" - # The prior needs to be positive - prior = noise_element(space) - prior = space.element(np.abs(prior)) + I = odl.IdentityOperator(space) + vector = noise_element(space) + constant = np.random.rand() - func = odl.solvers.KullbackLeiblerCrossEntropy(space, prior) + def prox_inv_fact(sigma): + return odl.ScalingOperator(space, 1 / (1 + sigma)) - # The fucntional is only defined for positive elements x = noise_element(space) - x = func.domain.element(np.abs(x)) - one_elem = space.one() + sigma = 1.2 - # Evaluation of the functional - expected_result = ((prior - x + x * np.log(x / prior)) - .inner(one_elem)) - assert func(x) == pytest.approx(expected_result) + # Quadratic form with operator, vector and constant - # Check property for prior - assert all_almost_equal(func.prior, prior) + func = odl.solvers.QuadraticForm( + space, operator=I, vector=vector, constant=constant, + operator_sym_inv=I, operator_prox_inv_fact=prox_inv_fact + ) - # For elements with (a) negative components it should return inf - x_neg = noise_element(space) - x_neg = x_neg - x_neg.ufuncs.max() - assert func(x_neg) == np.inf + # Evaluation + assert func(x) == pytest.approx( + space.inner(x, x) + space.inner(x, vector) + constant + ) - # The gradient - expected_result = np.log(x / prior) - assert all_almost_equal(func.gradient(x), expected_result) + # Gradient + func_grad_x = 2 * x + vector + _test_op(func.gradient, x, func_grad_x) - # The proximal operator - sigma = np.random.rand() - prox = odl.solvers.proximal_convex_conj( - odl.solvers.proximal_convex_conj_kl_cross_entropy(space, g=prior)) - expected_result = prox(sigma)(x) - assert all_almost_equal(func.proximal(sigma)(x), expected_result) + # Proximal + func_prox_x = (x - sigma * vector) / (1 + 2 * sigma) + _test_op(func.proximal(sigma), x, func_prox_x) - # The convex conjugate functional - cc_func = func.convex_conj + # CC evaluation + func_cc = func.convex_conj + assert func_cc(x) == pytest.approx( + space.inner(x - vector, x - vector) / 4 - constant + ) - assert isinstance(cc_func, KullbackLeiblerCrossEntropyConvexConj) + # CC gradient has nothing special - # The convex conjugate functional is defined for all values of x. - x = noise_element(space) + # CC proximal + # Same as above, but with vector -> -vector/2 and sigma -> sigma/2 + # in the denominator + func_cc_prox_x = (x + sigma * vector / 2) / (1 + sigma / 2) + _test_op(func_cc.proximal(sigma), x, func_cc_prox_x) - # Evaluation of convex conjugate - expected_result = (prior * (np.exp(x) - 1)).inner(one_elem) - assert cc_func(x) == pytest.approx(expected_result) + # Quadratic form without operator, i.e., an affine functional - # The gradient of the convex conjugate - expected_result = prior * np.exp(x) - assert all_almost_equal(cc_func.gradient(x), expected_result) + func_affine = odl.solvers.QuadraticForm( + space, vector=vector, constant=constant + ) + # Evaluation + assert func_affine(x) == pytest.approx(space.inner(x, vector) + constant) - # The proximal of the convex conjugate - expected_result = (x - - scipy.special.lambertw(sigma * prior * np.exp(x)).real) - assert all_almost_equal(cc_func.proximal(sigma)(x), expected_result) + # Gradient + func_affine_grad_x = vector + _test_op(func_affine.gradient, x, func_affine_grad_x) - # The biconjugate, which is the functional itself since it is proper, - # convex and lower-semicontinuous - cc_cc_func = cc_func.convex_conj + # Proximal + func_affine_prox_x = x - sigma * vector + _test_op(func_affine.proximal(sigma), x, func_affine_prox_x) - # Check that they evaluate the same - assert cc_cc_func(x) == pytest.approx(func(x)) + # CC evaluation + # Translation of IndicatorZero by `vector` with offset `-constant` + func_affine_cc = func_affine.convex_conj + assert func_affine_cc(vector) == -constant + assert func_affine_cc(vector + 1) == float('inf') + # CC gradient not implemented -def test_quadratic_form(space): - """Test the quadratic form functional.""" - operator = odl.IdentityOperator(space) - vector = space.one() - constant = 0.363 - func = odl.solvers.QuadraticForm(operator, vector, constant) + # CC prox + # projection onto the point set `{vector}` + func_affine_cc_prox_x = vector + _test_op(func_affine_cc.proximal(sigma), x, func_affine_cc_prox_x) - x = noise_element(space) + # Quadratic form without vector - # Checking that values is stored correctly - assert func.operator == operator - assert func.vector == vector - assert func.constant == constant + func_no_vec = odl.solvers.QuadraticForm( + space, I, constant=constant, operator_sym_inv=I, + operator_prox_inv_fact=prox_inv_fact - # Evaluation of the functional - expected_result = x.inner(operator(x)) + vector.inner(x) + constant - assert func(x) == pytest.approx(expected_result) + ) - # The gradient - expected_gradient = 2 * operator(x) + vector - assert all_almost_equal(func.gradient(x), expected_gradient) + # Evaluation + assert func_no_vec(x) == pytest.approx(space.inner(x, x) + constant) - # The convex conjugate - assert isinstance(func.convex_conj, odl.solvers.QuadraticForm) + # Gradient + func_no_vec_grad_x = 2 * x + _test_op(func_no_vec.gradient, x, func_no_vec_grad_x) - # Test for linear functional - func_no_operator = odl.solvers.QuadraticForm(vector=vector, - constant=constant) - expected_result = vector.inner(x) + constant - assert func_no_operator(x) == pytest.approx(expected_result) + # Proximal + func_no_vec_prox_x = x / (1 + 2 * sigma) + _test_op(func_no_vec.proximal(sigma), x, func_no_vec_prox_x) - expected_gradient = vector - assert all_almost_equal(func_no_operator.gradient(x), expected_gradient) + # CC evaluation + func_no_vec_cc = func_no_vec.convex_conj + assert func_no_vec_cc(x) == pytest.approx(space.inner(x, x) / 4 - constant) - # The convex conjugate is a translation of the IndicatorZero - func_no_operator_cc = func_no_operator.convex_conj - assert isinstance(func_no_operator_cc, - odl.solvers.FunctionalTranslation) - assert isinstance(func_no_operator_cc.functional, - odl.solvers.IndicatorZero) - assert func_no_operator_cc(vector) == -constant - assert np.isinf(func_no_operator_cc(vector + 2.463)) + # CC gradient has nothing special - # Test with no offset - func_no_offset = odl.solvers.QuadraticForm(operator, constant=constant) - expected_result = x.inner(operator(x)) + constant - assert func_no_offset(x) == pytest.approx(expected_result) + # CC proximal + # Same as above, but with sigma/2 in the denominator + func_no_vec_cc_prox_x = x / (1 + sigma / 2) + _test_op(func_no_vec_cc.proximal(sigma), x, func_no_vec_cc_prox_x) def test_separable_sum(space): @@ -456,26 +531,28 @@ def test_separable_sum(space): x = noise_element(space) y = noise_element(space) - # Initialization and calling + # Evaluation func = odl.solvers.SeparableSum(l1, l2) assert func([x, y]) == pytest.approx(l1(x) + l2(y)) - power_func = odl.solvers.SeparableSum(l1, 5) assert power_func([x, x, x, x, x]) == pytest.approx(5 * l1(x)) # Gradient - grad = func.gradient([x, y]) - assert grad[0] == l1.gradient(x) - assert grad[1] == l2.gradient(y) + func_grad_xy = [l1.gradient(x), l2.gradient(y)] + _test_op(func.gradient, [x, y], func_grad_xy) # Proximal - sigma = 1.0 - prox = func.proximal(sigma)([x, y]) - assert prox[0] == l1.proximal(sigma)(x) - assert prox[1] == l2.proximal(sigma)(y) + sigma = 1.2 + func_prox_xy = [l1.proximal(sigma)(x), l2.proximal(sigma)(y)] + _test_op(func.proximal(sigma), [x, y], func_prox_xy) - # Convex conjugate - assert func.convex_conj([x, y]) == l1.convex_conj(x) + l2.convex_conj(y) + # CC evaluation + assert func.convex_conj([x, y]) == pytest.approx( + l1.convex_conj(x) + l2.convex_conj(y) + ) + + # CC is a SeparableSum of the convex conjugates, remaining test cases + # are thus covered def test_moreau_envelope_l1(): @@ -498,16 +575,17 @@ def test_moreau_envelope_l1(): def test_moreau_envelope_l2_sq(space, sigma): - """Test for the Moreau envelope with l2 norm squared.""" + """Test for the Moreau envelope with squared L2 norm.""" - # Result is ||x||_2^2 / (1 + 2 sigma) + # Result is ||x||_2^2 / (1 + 2 * sigma) # Gradient is x * 2 / (1 + 2 * sigma) l2_sq = odl.solvers.L2NormSquared(space) smoothed_l2_sq = odl.solvers.MoreauEnvelope(l2_sq, sigma=sigma) x = noise_element(space) - assert all_almost_equal(smoothed_l2_sq.gradient(x), - x * 2 / (1 + 2 * sigma)) + assert all_almost_equal( + smoothed_l2_sq.gradient(x), x * 2 / (1 + 2 * sigma) + ) def test_weighted_separablesum(space): @@ -527,98 +605,68 @@ def test_weighted_separablesum(space): def test_weighted_proximal_L2_norm_squared(space): - """Test for the weighted proximal of the squared L2 norm""" - - # Define the functional on the space. + """Test for the weighted proximal of the squared L2 norm.""" func = odl.solvers.L2NormSquared(space) - - # Set the stepsize as a random element of the spaces - # with elements between 1 and 10. sigma = odl.phantom.uniform_noise(space, 1, 10) - - # Start at the one vector. x = space.one() - # Calculate the proximal point in-place and out-of-place - p_ip = space.element() - func.proximal(sigma)(x, out=p_ip) - p_oop = func.proximal(sigma)(x) - - # Both should contain the same vector now. - assert all_almost_equal(p_ip, p_oop) - - # Check if the subdifferential inequalities are satisfied. + # Check if the subdifferential inequalities are satisfied: # p = prox_{sigma * f}(x) iff (x - p)/sigma = grad f(p) - assert all_almost_equal(func.gradient(p_ip), - (x - p_ip) / sigma) + prox = func.proximal(sigma)(x) + assert all_almost_equal( + func.gradient(prox), (x - prox) / sigma + ) + prox_ip = space.element() + func.proximal(sigma)(x, out=prox_ip) + assert all_almost_equal(prox, prox_ip) -def test_weighted_proximal_L1_norm_far(space): - """Test for the weighted proximal of the L1 norm away from zero""" - # Define the functional on the space. +def test_weighted_proximal_L1_norm_far(space): + """Test for the weighted proximal of the L1 norm away from zero.""" func = odl.solvers.L1Norm(space) - - # Set the stepsize as a random element of the spaces - # with elements between 1 and 10. sigma = odl.phantom.noise.uniform_noise(space, 1, 10) + x = 100 * space.one() # no problem with differentiability - # Start far away from zero so that the L1 norm will be differentiable - # at the result. - x = 100 * space.one() - - # Calculate the proximal point in-place and out-of-place - p_ip = space.element() - func.proximal(sigma)(x, out=p_ip) - p_oop = func.proximal(sigma)(x) - - # Both should contain the same vector now. - assert all_almost_equal(p_ip, p_oop) - - # Check if the subdifferential inequalities are satisfied. + # Check if the subdifferential inequalities are satisfied: # p = prox_{sigma * f}(x) iff (x - p)/sigma = grad f(p) - assert all_almost_equal(func.gradient(p_ip), (x - p_ip) / sigma) + prox = func.proximal(sigma)(x) + assert all_almost_equal( + func.gradient(prox), (x - prox) / sigma + ) + + prox_ip = space.element() + func.proximal(sigma)(x, out=prox_ip) + assert all_almost_equal(prox, prox_ip) def test_weighted_proximal_L1_norm_close(space): """Test for the weighted proximal of the L1 norm near zero""" - - # Set the space. space = odl.rn(5) - - # Define the functional on the space. func = odl.solvers.L1Norm(space) - - # Set the stepsize. sigma = [0.1, 0.2, 0.5, 1.0, 2.0] - - # Set the starting point. x = 0.5 * space.one() - # Calculate the proximal point in-place and out-of-place - p_ip = space.element() - func.proximal(sigma)(x, out=p_ip) - p_oop = func.proximal(sigma)(x) - - # Both should contain the same vector now. - assert all_almost_equal(p_ip, p_oop) - - # Check if this equals the expected result. + prox = func.proximal(sigma)(x) expected_result = [0.4, 0.3, 0.0, 0.0, 0.0] - assert all_almost_equal(expected_result, p_ip) + assert all_almost_equal(prox, expected_result) + + prox_ip = space.element() + func.proximal(sigma)(x, out=prox_ip) + assert all_almost_equal(prox, prox_ip) def test_bregman_functional_no_gradient(space): """Test Bregman distance for functional without gradient.""" - + F = space.ufuncs ind_func = odl.solvers.IndicatorNonnegativity(space) point = np.abs(noise_element(space)) subgrad = noise_element(space) # Any element in the domain is ok bregman_dist = odl.solvers.BregmanDistance(ind_func, point, subgrad) - x = np.abs(noise_element(space)) + x = F.abs(noise_element(space)) - expected_result = -subgrad.inner(x - point) + expected_result = space.inner(-subgrad, x - point) assert all_almost_equal(bregman_dist(x), expected_result) # However, since the functional is not differentialbe we cannot call the diff --git a/odl/test/solvers/functional/functional_test.py b/odl/test/solvers/functional/functional_test.py index 3616677ea85..385b800ea20 100644 --- a/odl/test/solvers/functional/functional_test.py +++ b/odl/test/solvers/functional/functional_test.py @@ -1,4 +1,4 @@ -# Copyright 2014-2017 The ODL contributors +# Copyright 2014-2019 The ODL contributors # # This file is part of ODL. # @@ -9,21 +9,16 @@ """Test for the Functional class.""" from __future__ import division + import numpy as np import pytest import odl from odl.operator import OpTypeError -from odl.util.testutils import ( - all_almost_equal, dtype_ndigits, dtype_tol, noise_element, simple_fixture) from odl.solvers.functional.default_functionals import ( KullbackLeiblerConvexConj) - - -# TODO: maybe add tests for if translations etc. belongs to the wrong space. -# These tests don't work as intended now, since casting is possible between -# spaces with the same number of discretization points. - +from odl.util.testutils import ( + all_almost_equal, dtype_ndigits, dtype_tol, noise_element, simple_fixture) # --- pytest fixtures --- # @@ -34,6 +29,13 @@ space_params = ['r10', 'uniform_discr', 'power_space_unif_discr'] space_ids = [' space={} '.format(p) for p in space_params] +# Fixtures for test_functional_quadratic_perturb +linear_term = simple_fixture('linear_term', [False, True]) +quadratic_coeff = simple_fixture('quadratic_coeff', [0.0, 2.13]) + + +# --- Unittests --- # + @pytest.fixture(scope="module", ids=space_ids, params=space_params) def space(request, odl_tspace_impl): @@ -132,7 +134,7 @@ def functional(request, space): return func -# --- functional tests --- # +# --- Functional tests --- # def test_derivative(functional): @@ -148,64 +150,66 @@ def test_derivative(functional): functional.derivative(functional.domain.zero()) return - x = noise_element(functional.domain) - y = noise_element(functional.domain) + space = functional.domain + F = space.ufuncs + R = space.reduce + x = noise_element(space) + y = noise_element(space) - if (isinstance(functional, odl.solvers.KullbackLeibler) or - isinstance(functional, odl.solvers.KullbackLeiblerCrossEntropy)): - # The functional is not defined for values <= 0 - x = x.ufuncs.absolute() - y = y.ufuncs.absolute() + if ( + isinstance(functional, odl.solvers.KullbackLeibler) + or isinstance(functional, odl.solvers.KullbackLeiblerCrossEntropy) + ): + # This functional is not defined for values <= 0 + x = F.abs(x) + y = F.abs(y) if isinstance(functional, KullbackLeiblerConvexConj): - # The functional is not defined for values >= 1 - x = x - x.ufuncs.max() + 0.99 - y = y - y.ufuncs.max() + 0.99 + # This functional is not defined for values >= 1 + x = x - R.max(x) + 0.99 + y = y - R.max(y) + 0.99 # Compute a "small" step size according to dtype of space - step = float(np.sqrt(np.finfo(functional.domain.dtype).eps)) + step = float(np.sqrt(np.finfo(space.dtype).eps)) - # Numerical test of gradient, only low accuracy can be guaranteed. - assert all_almost_equal((functional(x + step * y) - functional(x)) / step, - y.inner(functional.gradient(x)), - ndigits=1) + # Numerical test of gradient, only low accuracy can be guaranteed + assert all_almost_equal( + (functional(x + step * y) - functional(x)) / step, + space.inner(y, functional.gradient(x)), + ndigits=1, + ) # Check that derivative and gradient is consistent - assert all_almost_equal(functional.derivative(x)(y), - y.inner(functional.gradient(x))) + assert all_almost_equal( + functional.derivative(x)(y), space.inner(y, functional.gradient(x)) + ) def test_arithmetic(): - """Test that all standard arithmetic works.""" + """Test that standard arithmetic works as expected.""" space = odl.rn(3) - - # Create elements needed for later functional = odl.solvers.L2Norm(space).translated([1, 2, 3]) functional2 = odl.solvers.L2NormSquared(space) operator = odl.IdentityOperator(space) - space.element([4, 5, 6]) - x = noise_element(functional.domain) - y = noise_element(functional.domain) + x = noise_element(space) + y = noise_element(space) scalar = np.pi # Simple tests here, more in depth comes later - assert functional(x) == functional(x) assert functional(x) != functional2(x) assert (scalar * functional)(x) == scalar * functional(x) - assert (scalar * (scalar * functional))(x) == scalar**2 * functional(x) + assert (scalar * (scalar * functional))(x) == scalar ** 2 * functional(x) assert (functional * scalar)(x) == functional(scalar * x) - assert ((functional * scalar) * scalar)(x) == functional(scalar**2 * x) + assert ((functional * scalar) * scalar)(x) == functional(scalar ** 2 * x) assert (functional + functional2)(x) == functional(x) + functional2(x) assert (functional - functional2)(x) == functional(x) - functional2(x) assert (functional * operator)(x) == functional(operator(x)) - assert all_almost_equal((y * functional)(x), y * functional(x)) - assert all_almost_equal((y * (y * functional))(x), (y * y) * functional(x)) assert all_almost_equal((functional * y)(x), functional(y * x)) assert all_almost_equal(((functional * y) * y)(x), functional((y * y) * x)) def test_left_scalar_mult(space, scalar): """Test for right and left multiplication of a functional with a scalar.""" - # Less strict checking for single precision ndigits = dtype_ndigits(space.dtype) rtol = dtype_tol(space.dtype) @@ -214,45 +218,47 @@ def test_left_scalar_mult(space, scalar): lmul_func = scalar * func if scalar == 0: - assert isinstance(scalar * func, odl.solvers.ZeroFunctional) + assert (scalar * func)(x) == 0 + # Return early in this case as many things are undefined return - # Test functional evaluation assert lmul_func(x) == pytest.approx(scalar * func(x), rel=rtol) - - # Test gradient of left scalar multiplication - assert all_almost_equal(lmul_func.gradient(x), scalar * func.gradient(x), - ndigits) - - # Test derivative of left scalar multiplication + assert all_almost_equal( + lmul_func.gradient(x), scalar * func.gradient(x), ndigits + ) p = noise_element(space) - assert all_almost_equal(lmul_func.derivative(x)(p), - scalar * (func.derivative(x))(p), - ndigits) + assert all_almost_equal( + lmul_func.derivative(x)(p), + scalar * (func.derivative(x))(p), + ndigits, + ) - # Test convex conjugate. This requires positive scaling to work - pos_scalar = abs(scalar) + pos_scalar = abs(scalar) + 1e-4 neg_scalar = -pos_scalar with pytest.raises(ValueError): + # Not a convex functional, should raise (neg_scalar * func).convex_conj - assert all_almost_equal((pos_scalar * func).convex_conj(x), - pos_scalar * func.convex_conj(x / pos_scalar), - ndigits) + assert all_almost_equal( + (pos_scalar * func).convex_conj(x), + pos_scalar * func.convex_conj(x / pos_scalar), + ndigits, + ) - # Test proximal operator. This requires scaling to be positive. sigma = 1.2 with pytest.raises(ValueError): + # Not a convex functional, should raise (neg_scalar * func).proximal(sigma) - assert all_almost_equal((pos_scalar * func).proximal(sigma)(x), - func.proximal(sigma * pos_scalar)(x)) + assert all_almost_equal( + (pos_scalar * func).proximal(sigma)(x), + func.proximal(sigma * pos_scalar)(x), + ) def test_right_scalar_mult(space, scalar): """Test for right and left multiplication of a functional with a scalar.""" - # Less strict checking for single precision ndigits = dtype_ndigits(space.dtype) rtol = dtype_tol(space.dtype) @@ -261,414 +267,321 @@ def test_right_scalar_mult(space, scalar): rmul_func = func * scalar if scalar == 0: - # expecting the constant functional x -> func(0) - assert isinstance(rmul_func, odl.solvers.ConstantFunctional) - assert all_almost_equal(rmul_func(x), func(space.zero()), - ndigits) + # Should yield `func(0)` for any input + assert all_almost_equal( + rmul_func(x), func(space.zero()), ndigits + ) # Nothing more to do, rest is part of ConstantFunctional test return - # Test functional evaluation assert rmul_func(x) == pytest.approx(func(scalar * x), rel=rtol) - # Test gradient of right scalar multiplication - assert all_almost_equal(rmul_func.gradient(x), - scalar * func.gradient(scalar * x), - ndigits) - - # Test derivative of right scalar multiplication + # Chain rule for gradient: grad[f(c * .)] = c * grad[f](c * .) + assert all_almost_equal( + rmul_func.gradient(x), scalar * func.gradient(scalar * x), ndigits, + ) + # Same for derivative p = noise_element(space) - assert all_almost_equal(rmul_func.derivative(x)(p), - scalar * func.derivative(scalar * x)(p), - ndigits) + assert all_almost_equal( + rmul_func.derivative(x)(p), + scalar * func.derivative(scalar * x)(p), + ndigits, + ) - # Test convex conjugate conjugate - assert all_almost_equal(rmul_func.convex_conj(x), - func.convex_conj(x / scalar), - ndigits) + # Scaling and convex conjugate: [f(c * .)]^* = f^*(1/c * .) + assert all_almost_equal( + rmul_func.convex_conj(x), func.convex_conj(x / scalar), ndigits, + ) - # Test proximal operator + # Scaling and proximal: prox[s * f(c * .)] = 1/c * prox[s*c^2 * f](c * .) sigma = 1.2 assert all_almost_equal( rmul_func.proximal(sigma)(x), (1.0 / scalar) * func.proximal(sigma * scalar ** 2)(x * scalar), - ndigits) + ndigits, + ) - # Verify that for linear functionals, left multiplication is used. + # Verify that for linear functionals, left multiplication is used func = odl.solvers.ZeroFunctional(space) assert isinstance(func * scalar, odl.solvers.FunctionalLeftScalarMult) def test_functional_composition(space): """Test composition from the right with an operator.""" - # Less strict checking for single precision ndigits = dtype_ndigits(space.dtype) rtol = dtype_tol(space.dtype) - func = odl.solvers.L2NormSquared(space) + x = noise_element(space) - # Verify that an error is raised if an invalid operator is used - # (e.g. wrong range) - scalar = 2.1 - wrong_space = odl.uniform_discr(1, 2, 10) - op_wrong = odl.operator.ScalingOperator(wrong_space, scalar) - - with pytest.raises(OpTypeError): - func * op_wrong - - # Test composition with operator from the right - op = odl.operator.ScalingOperator(space, scalar) + op = odl.operator.ScalingOperator(space, 2.0) func_op_comp = func * op assert isinstance(func_op_comp, odl.solvers.Functional) - x = noise_element(space) assert func_op_comp(x) == pytest.approx(func(op(x)), rel=rtol) - # Test gradient and derivative with composition from the right - assert all_almost_equal(func_op_comp.gradient(x), - (op.adjoint * func.gradient * op)(x), - ndigits) - + # Chain rule for composition: grad[f o A] = A^* o grad[f] o A + assert all_almost_equal( + func_op_comp.gradient(x), + (op.adjoint * func.gradient * op)(x), + ndigits, + ) + # Same for derivative p = noise_element(space) - assert all_almost_equal(func_op_comp.derivative(x)(p), - (op.adjoint * func.gradient * op)(x).inner(p), - ndigits) + assert all_almost_equal( + func_op_comp.derivative(x)(p), + space.inner((op.adjoint * func.gradient * op)(x), p), + ndigits, + ) + + wrong_space = odl.uniform_discr(1, 2, 10) + op_wrong = odl.operator.ScalingOperator(wrong_space, 2.1) + + with pytest.raises(OpTypeError): + func * op_wrong def test_functional_sum(space): """Test for the sum of two functionals.""" - # Less strict checking for single precision ndigits = dtype_ndigits(space.dtype) rtol = dtype_tol(space.dtype) - func1 = odl.solvers.L2NormSquared(space) func2 = odl.solvers.L2Norm(space) - # Verify that an error is raised if one operand is "wrong" - op = odl.operator.IdentityOperator(space) - with pytest.raises(OpTypeError): - func1 + op - - wrong_space = odl.uniform_discr(1, 2, 10) - func_wrong_domain = odl.solvers.L2Norm(wrong_space) - with pytest.raises(OpTypeError): - func1 + func_wrong_domain - func_sum = func1 + func2 x = noise_element(space) p = noise_element(space) - # Test functional evaluation assert func_sum(x) == pytest.approx(func1(x) + func2(x), rel=rtol) - # Test gradient and derivative - assert all_almost_equal(func_sum.gradient(x), - func1.gradient(x) + func2.gradient(x), - ndigits) - + # grad[f + g] = grad[f] + grad[g] + assert all_almost_equal( + func_sum.gradient(x), func1.gradient(x) + func2.gradient(x), ndigits + ) assert ( - func_sum.derivative(x)(p) == - pytest.approx( - func1.gradient(x).inner(p) + func2.gradient(x).inner(p), - rel=rtol) + func_sum.derivative(x)(p) + == pytest.approx( + space.inner(func1.gradient(x), p) + + space.inner(func2.gradient(x), p), + rel=rtol, + ) ) - # Verify that proximal raises - with pytest.raises(NotImplementedError): - func_sum.proximal + op = odl.operator.IdentityOperator(space) + with pytest.raises(OpTypeError): + func1 + op + + wrong_space = odl.uniform_discr(1, 2, 10) + func_wrong_domain = odl.solvers.L2Norm(wrong_space) + with pytest.raises(OpTypeError): + func1 + func_wrong_domain - # Test the convex conjugate raises - with pytest.raises(NotImplementedError): - func_sum.convex_conj(x) def test_functional_plus_scalar(space): """Test for sum of functioanl and scalar.""" - # Less strict checking for single precision ndigits = dtype_ndigits(space.dtype) rtol = dtype_tol(space.dtype) - func = odl.solvers.L2NormSquared(space) scalar = -1.3 - # Test for scalar not in the field (field of unifor_discr is RealNumbers) - complex_scalar = 1j - with pytest.raises(TypeError): - func + complex_scalar - func_scalar_sum = func + scalar x = noise_element(space) p = noise_element(space) - # Test for evaluation assert func_scalar_sum(x) == pytest.approx(func(x) + scalar, rel=rtol) - # Test for derivative and gradient - assert all_almost_equal(func_scalar_sum.gradient(x), func.gradient(x), - ndigits) - + # grad[f + c] = grad[f] + assert all_almost_equal( + func_scalar_sum.gradient(x), func.gradient(x), ndigits + ) assert ( - func_scalar_sum.derivative(x)(p) == - pytest.approx(func.gradient(x).inner(p), rel=rtol) + func_scalar_sum.derivative(x)(p) + == pytest.approx(space.inner(func.gradient(x), p), rel=rtol) ) - # Test proximal operator + # Proximal is unaffected by constant shift sigma = 1.2 - assert all_almost_equal(func_scalar_sum.proximal(sigma)(x), - func.proximal(sigma)(x), - ndigits) + assert all_almost_equal( + func_scalar_sum.proximal(sigma)(x), func.proximal(sigma)(x), ndigits + ) - # Test convex conjugate + # [f + c]^* = f^* - c assert ( - func_scalar_sum.convex_conj(x) == - pytest.approx(func.convex_conj(x) - scalar, rel=rtol) + func_scalar_sum.convex_conj(x) + == pytest.approx(func.convex_conj(x) - scalar, rel=rtol) + ) + assert all_almost_equal( + func_scalar_sum.convex_conj.gradient(x), + func.convex_conj.gradient(x), + ndigits, ) - assert all_almost_equal(func_scalar_sum.convex_conj.gradient(x), - func.convex_conj.gradient(x), - ndigits) + complex_scalar = 1j # not in space.field + with pytest.raises(TypeError): + func + complex_scalar def test_translation_of_functional(space): - """Test for the translation of a functional: (f(. - y))^*.""" - # Less strict checking for single precision + """Test for the translation of a functional.""" ndigits = dtype_ndigits(space.dtype) - rtol = dtype_tol(space.dtype) - - # The translation; an element in the domain - translation = noise_element(space) - - test_functional = odl.solvers.L2NormSquared(space) - translated_functional = test_functional.translated(translation) + transl = noise_element(space) + func = odl.solvers.L2NormSquared(space) + func_tr = func.translated(transl) x = noise_element(space) - # Test for evaluation of the functional - expected_result = test_functional(x - translation) - assert all_almost_equal(translated_functional(x), expected_result, - ndigits) - - # Test for the gradient - expected_result = test_functional.gradient(x - translation) - translated_gradient = translated_functional.gradient - assert all_almost_equal(translated_gradient(x), expected_result, - ndigits) + assert all_almost_equal(func_tr(x), func(x - transl), ndigits) + assert all_almost_equal( + func_tr.gradient(x), func.gradient(x - transl), ndigits + ) - # Test for proximal + # prox[s * f(. - t)] = t + prox[s * f](. - y) sigma = 1.2 - # The helper function below is tested explicitly in proximal_utils_test - expected_result = odl.solvers.proximal_translation( - test_functional.proximal, translation)(sigma)(x) - assert all_almost_equal(translated_functional.proximal(sigma)(x), - expected_result, ndigits) - - # Test for conjugate functional - # The helper function below is tested explicitly further down in this file - expected_result = odl.solvers.FunctionalQuadraticPerturb( - test_functional.convex_conj, linear_term=translation)(x) - assert all_almost_equal(translated_functional.convex_conj(x), - expected_result, ndigits) - - # Test for derivative in direction p - p = noise_element(space) + assert all_almost_equal( + func_tr.proximal(sigma)(x), + transl + func.proximal(sigma)(x - transl), + ndigits, + ) - # Explicit computation in point x, in direction p: - expected_result = p.inner(test_functional.gradient(x - translation)) - assert all_almost_equal(translated_functional.derivative(x)(p), - expected_result, ndigits) + # [f(. - t)]^* = f^* + + assert all_almost_equal( + func_tr.convex_conj(x), + func.convex_conj(x) + space.inner(x, transl), + ndigits, + ) - # Test for optimized implementation, when translating a translated + # Test for optimized implementation when translating a translated # functional - second_translation = noise_element(space) - double_translated_functional = translated_functional.translated( - second_translation) - - # Evaluation - assert ( - double_translated_functional(x) == - pytest.approx(test_functional(x - translation - second_translation), - rel=rtol) - ) + transl2 = noise_element(space) + func_tr_twice = func_tr.translated(transl2) + assert all_almost_equal(func_tr_twice.translation, transl + transl2) def test_translation_proximal_stepsizes(): """Test for stepsize types for proximal of a translated functional.""" - # Set up space, functional and a point where to evaluate the proximal. space = odl.rn(2) - functional = odl.solvers.L2NormSquared(space) - translation = functional.translated([0.5, 0.5]) + func = odl.solvers.L2NormSquared(space) + func_tr = func.translated([0.5, 0.5]) x = space.one() - # Define different forms of the same stepsize. - stepsize = space.element([0.5, 2.0]) - stepsize_list = [0.5, 2.0] - stepsize_array = np.asarray([0.5, 2.0]) - - # Calculate the proximals for each of the stepsizes. - y = translation.convex_conj.proximal(stepsize)(x) - y_list = translation.convex_conj.proximal(stepsize_list)(x) - y_array = translation.convex_conj.proximal(stepsize_array)(x) + y = func_tr.convex_conj.proximal(space.element([0.5, 2.0]))(x) + y_list = func_tr.convex_conj.proximal([0.5, 2.0])(x) expected_result = [0.6, 0.0] - - # Now, all the results should be equal to the expected result. assert all_almost_equal(y, expected_result) assert all_almost_equal(y_list, expected_result) - assert all_almost_equal(y_array, expected_result) def test_multiplication_with_vector(space): """Test for multiplying a functional with a vector, both left and right.""" - # Less strict checking for single precision ndigits = dtype_ndigits(space.dtype) rtol = dtype_tol(space.dtype) - x = noise_element(space) y = noise_element(space) func = odl.solvers.L2NormSquared(space) - wrong_space = odl.uniform_discr(1, 2, 10) - y_other_space = noise_element(wrong_space) - - # Multiplication from the right. Make sure it is a - # FunctionalRightVectorMult func_times_y = func * y - assert isinstance(func_times_y, odl.solvers.FunctionalRightVectorMult) - - expected_result = func(y * x) - assert func_times_y(x) == pytest.approx(expected_result, rel=rtol) - - # Test for the gradient. - # Explicit calculations: 2*y*y*x - expected_result = 2.0 * y * y * x - assert all_almost_equal(func_times_y.gradient(x), expected_result, - ndigits) - - # Test for convex_conj - cc_func_times_y = func_times_y.convex_conj - # Explicit calculations: 1/4 * ||x/y||_2^2 - expected_result = 1.0 / 4.0 * (x / y).norm()**2 - assert cc_func_times_y(x) == pytest.approx(expected_result, rel=rtol) - - # Make sure that right muliplication is not allowed with vector from - # another space - with pytest.raises(TypeError): - func * y_other_space + assert isinstance(func_times_y, odl.solvers.Functional) + assert func_times_y(x) == pytest.approx(func(y * x), rel=rtol) - # Multiplication from the left. Make sure it is a FunctionalLeftVectorMult - y_times_func = y * func - assert isinstance(y_times_func, odl.FunctionalLeftVectorMult) + # Gradient should be 2 * y^2 * x + assert all_almost_equal(func_times_y.gradient(x), 2 * y * y * x, ndigits) - expected_result = y * func(x) - assert all_almost_equal(y_times_func(x), expected_result, ndigits) - - # Now, multiplication with vector from another space is ok (since it is the - # same as scaling that vector with the scalar returned by the functional). - y_other_times_func = y_other_space * func - assert isinstance(y_other_times_func, odl.FunctionalLeftVectorMult) - - expected_result = y_other_space * func(x) - assert all_almost_equal(y_other_times_func(x), expected_result, - ndigits) - - -# Fixtures for test_functional_quadratic_perturb -linear_term = simple_fixture('linear_term', [False, True]) -quadratic_coeff = simple_fixture('quadratic_coeff', [0.0, 2.13]) + # Convex conjugate should be 1/4 * ||x/y||_2^2 + assert func_times_y.convex_conj(x) == pytest.approx( + 1 / 4 * func(x / y), rel=rtol + ) def test_functional_quadratic_perturb(space, linear_term, quadratic_coeff): - """Test for the functional f(.) + a | . |^2 + .""" - # Less strict checking for single precision + """Test for the functional ``f(.) + a | . |^2 + ``.""" ndigits = dtype_ndigits(space.dtype) rtol = dtype_tol(space.dtype) - - orig_func = odl.solvers.L2NormSquared(space) + func = odl.solvers.L2NormSquared(space) + x = noise_element(space) if linear_term: + linear_term_arg = linear_term = noise_element(space) + else: linear_term_arg = None linear_term = space.zero() - else: - linear_term_arg = linear_term = noise_element(space) - - # Creating the functional ||x||_2^2 and add the quadratic perturbation - functional = odl.solvers.FunctionalQuadraticPerturb( - orig_func, - quadratic_coeff=quadratic_coeff, - linear_term=linear_term_arg) - # Create an element in the space, in which to evaluate - x = noise_element(space) + func_quad_perturb = odl.solvers.FunctionalQuadraticPerturb( + func, quadratic_coeff, linear_term_arg, + ) - # Test for evaluation of the functional assert ( - functional(x) == - pytest.approx(orig_func(x) + - quadratic_coeff * x.inner(x) + - x.inner(linear_term), - rel=rtol) + func_quad_perturb(x) + == pytest.approx( + func(x) + + quadratic_coeff * space.inner(x, x) + + space.inner(x, linear_term), + rel=rtol, + ) ) - # Test for the gradient + # grad[f + a * <., .> + <., u> + c] = grad[f] + 2*a * . + u assert all_almost_equal( - functional.gradient(x), - orig_func.gradient(x) + 2.0 * quadratic_coeff * x + linear_term, - ndigits + func_quad_perturb.gradient(x), + func.gradient(x) + 2.0 * quadratic_coeff * x + linear_term, + ndigits, ) - # Test for the proximal operator if it exists sigma = 1.2 + # prox[s * (f + a * <., .> + <., u> + c)] = + # = prox[s * alpha * f]((. - s * u) * alpha), alpha = 1 / (2 * s * a + 1) # Explicit computation gives - c = 1 / np.sqrt(2 * sigma * quadratic_coeff + 1) - prox = orig_func.proximal(sigma * c ** 2) - expected_result = prox((x - sigma * linear_term) * c ** 2) - assert all_almost_equal(functional.proximal(sigma)(x), - expected_result, - ndigits) - - # Test convex conjugate functional - if quadratic_coeff == 0: - expected = orig_func.convex_conj.translated(linear_term)(x) - assert functional.convex_conj(x) == pytest.approx(expected, rel=rtol) - - # Test proximal of the convex conjugate - cconj_prox = odl.solvers.proximal_convex_conj(functional.proximal) + alpha = 1 / (2 * sigma * quadratic_coeff + 1) assert all_almost_equal( - functional.convex_conj.proximal(sigma)(x), - cconj_prox(sigma)(x), - ndigits) + func_quad_perturb.proximal(sigma)(x), + func.proximal(sigma * alpha)((x - sigma * linear_term) * alpha), + ndigits, + ) + + # Convex conjugate only known for zero quadratic term + if quadratic_coeff == 0: + # [f + <., u>]^* = f^*(. - u) + assert func_quad_perturb.convex_conj(x) == pytest.approx( + func.convex_conj(x - linear_term), rel=rtol + ) def test_bregman(functional): """Test for the Bregman distance of a functional.""" - rtol = dtype_tol(functional.domain.dtype) + space = functional.domain + F = space.ufuncs + R = space.reduce + rtol = dtype_tol(space.dtype) if isinstance(functional, FUNCTIONALS_WITHOUT_DERIVATIVE): # IndicatorFunction has no gradient with pytest.raises(NotImplementedError): - functional.gradient(functional.domain.zero()) + functional.gradient(space.zero()) return - y = noise_element(functional.domain) - x = noise_element(functional.domain) + y = noise_element(space) + x = noise_element(space) - if (isinstance(functional, odl.solvers.KullbackLeibler) or - isinstance(functional, odl.solvers.KullbackLeiblerCrossEntropy)): + if ( + isinstance(functional, odl.solvers.KullbackLeibler) + or isinstance(functional, odl.solvers.KullbackLeiblerCrossEntropy) + ): # The functional is not defined for values <= 0 - x = x.ufuncs.absolute() - y = y.ufuncs.absolute() + x = F.abs(x) + y = F.abs(y) if isinstance(functional, KullbackLeiblerConvexConj): # The functional is not defined for values >= 1 - x = x - x.ufuncs.max() + 0.99 - y = y - y.ufuncs.max() + 0.99 + x = x - R.max(x) + 0.99 + y = y - R.max(y) + 0.99 grad = functional.gradient(y) quadratic_func = odl.solvers.QuadraticForm( - vector=-grad, constant=-functional(y) + grad.inner(y)) + space, vector=-grad, constant=-functional(y) + space.inner(grad, y) + ) expected_func = functional + quadratic_func assert ( - functional.bregman(y, grad)(x) == - pytest.approx(expected_func(x), rel=rtol) + functional.bregman(y, grad)(x) + == pytest.approx(expected_func(x), rel=rtol) ) diff --git a/odl/test/solvers/iterative/iterative_test.py b/odl/test/solvers/iterative/iterative_test.py index c3f75c533a4..38794bf12e3 100644 --- a/odl/test/solvers/iterative/iterative_test.py +++ b/odl/test/solvers/iterative/iterative_test.py @@ -31,37 +31,50 @@ def iterative_solver(request): if solver_name == 'steepest_descent': def solver(op, x, rhs): - norm2 = op.adjoint(op(x)).norm() / x.norm() + space = op.domain + norm2 = space.norm(op.adjoint(op(x))) / space.norm(x) func = odl.solvers.L2NormSquared(op.domain) * (op - rhs) odl.solvers.steepest_descent(func, x, line_search=0.5 / norm2) + elif solver_name == 'adam': def solver(op, x, rhs): - norm2 = op.adjoint(op(x)).norm() / x.norm() + space = op.domain + norm2 = space.norm(op.adjoint(op(x))) / space.norm(x) func = odl.solvers.L2NormSquared(op.domain) * (op - rhs) odl.solvers.adam(func, x, learning_rate=4.0 / norm2, maxiter=150) + elif solver_name == 'landweber': def solver(op, x, rhs): - norm2 = op.adjoint(op(x)).norm() / x.norm() + space = op.domain + norm2 = space.norm(op.adjoint(op(x))) / space.norm(x) odl.solvers.landweber(op, x, rhs, niter=50, omega=0.5 / norm2) + elif solver_name == 'conjugate_gradient': def solver(op, x, rhs): odl.solvers.conjugate_gradient(op, x, rhs, niter=10) + elif solver_name == 'conjugate_gradient_normal': def solver(op, x, rhs): odl.solvers.conjugate_gradient_normal(op, x, rhs, niter=10) + elif solver_name == 'mlem': def solver(op, x, rhs): odl.solvers.mlem(op, x, rhs, niter=10) + elif solver_name == 'osmlem': def solver(op, x, rhs): odl.solvers.osmlem([op, op], x, [rhs, rhs], niter=10) + elif solver_name == 'kaczmarz': def solver(op, x, rhs): - norm2 = op.adjoint(op(x)).norm() / x.norm() - odl.solvers.kaczmarz([op, op], x, [rhs, rhs], niter=20, - omega=0.5 / norm2) + space = op.domain + norm2 = space.norm(op.adjoint(op(x))) / space.norm(x) + odl.solvers.kaczmarz( + [op, op], x, [rhs, rhs], niter=20, omega=0.5 / norm2 + ) + else: raise ValueError('solver not valid') @@ -118,10 +131,12 @@ def test_steepst_descent(): rosenbrock = odl.solvers.RosenbrockFunctional(space, scale) line_search = odl.solvers.BacktrackingLineSearch( - rosenbrock, 0.1, 0.01) + rosenbrock, tau=0.1, discount=0.01 + ) x = rosenbrock.domain.zero() - odl.solvers.steepest_descent(rosenbrock, x, maxiter=40, - line_search=line_search) + odl.solvers.steepest_descent( + rosenbrock, x, maxiter=40, line_search=line_search + ) assert all_almost_equal(x, [1, 1, 1], ndigits=2) diff --git a/odl/test/solvers/nonsmooth/admm_test.py b/odl/test/solvers/nonsmooth/admm_test.py index a6011ba10ed..2011ef4ee09 100644 --- a/odl/test/solvers/nonsmooth/admm_test.py +++ b/odl/test/solvers/nonsmooth/admm_test.py @@ -26,12 +26,12 @@ def test_admm_lin_input_handling(): # Check that the algorithm runs. With the above operators and functionals, # the algorithm should not modify the initial value. x0 = noise_element(space) - x = x0.copy() + x = space.copy(x0) niter = 3 admm_linearized(x, f, g, L, tau=1.0, sigma=1.0, niter=niter) - assert x == x0 + assert all_almost_equal(x, x0) # Check that a provided callback is actually called class CallbackTest(Callback): diff --git a/odl/test/solvers/nonsmooth/difference_convex_test.py b/odl/test/solvers/nonsmooth/difference_convex_test.py index 1afa5ece7b7..17810c5841e 100644 --- a/odl/test/solvers/nonsmooth/difference_convex_test.py +++ b/odl/test/solvers/nonsmooth/difference_convex_test.py @@ -57,15 +57,15 @@ def test_dca(): # Set up some space elements for the solvers to use x = space.element(-0.5) - x_dca = x.copy() - x_prox_dca = x.copy() - x_doubleprox = x.copy() - x_simpl = x.copy() + x_dca = space.copy(x) + x_prox_dca = space.copy(x) + x_doubleprox = space.copy(x) + x_simpl = space.copy(x) # Some additional parameters for some of the solvers phi = odl.solvers.ZeroFunctional(space) y = space.element(3) - y_simpl = y.copy() + y_simpl = space.copy(y) gamma = 1 mu = 1 K = odl.IdentityOperator(space) diff --git a/odl/test/solvers/nonsmooth/douglas_rachford_test.py b/odl/test/solvers/nonsmooth/douglas_rachford_test.py index e2d4b925332..06447286c43 100644 --- a/odl/test/solvers/nonsmooth/douglas_rachford_test.py +++ b/odl/test/solvers/nonsmooth/douglas_rachford_test.py @@ -23,46 +23,38 @@ def test_primal_dual_input_handling(): """Test to see that input is handled correctly.""" + space = odl.uniform_discr(0, 1, 10) - space1 = odl.uniform_discr(0, 1, 10) - - lin_ops = [odl.ZeroOperator(space1), odl.ZeroOperator(space1)] - g = [odl.solvers.ZeroFunctional(space1), - odl.solvers.ZeroFunctional(space1)] - f = odl.solvers.ZeroFunctional(space1) + lin_ops = [odl.ZeroOperator(space), odl.ZeroOperator(space)] + g = [odl.solvers.ZeroFunctional(space), + odl.solvers.ZeroFunctional(space)] + f = odl.solvers.ZeroFunctional(space) # Check that the algorithm runs. With the above operators, the algorithm # returns the input. - x0 = noise_element(space1) - x = x0.copy() + x0 = noise_element(space) + x = space.copy(x0) niter = 3 - douglas_rachford_pd(x, f, g, lin_ops, tau=1.0, - sigma=[1.0, 1.0], niter=niter) - - assert x == x0 + douglas_rachford_pd( + x, f, g, lin_ops, tau=1.0, sigma=[1.0, 1.0], niter=niter + ) + assert all_almost_equal(x, x0) # Testing that sizes needs to agree: - # Too few sigma_i:s + # Too few sigmas with pytest.raises(ValueError): douglas_rachford_pd(x, f, g, lin_ops, tau=1.0, sigma=[1.0], niter=niter) # Too many operators - g_too_many = [odl.solvers.ZeroFunctional(space1), - odl.solvers.ZeroFunctional(space1), - odl.solvers.ZeroFunctional(space1)] + g_too_many = [odl.solvers.ZeroFunctional(space), + odl.solvers.ZeroFunctional(space), + odl.solvers.ZeroFunctional(space)] with pytest.raises(ValueError): douglas_rachford_pd(x, f, g_too_many, lin_ops, tau=1.0, sigma=[1.0, 1.0], niter=niter) - # Test for correct space - space2 = odl.uniform_discr(1, 2, 10) - x = noise_element(space2) - with pytest.raises(ValueError): - douglas_rachford_pd(x, f, g, lin_ops, tau=1.0, - sigma=[1.0, 1.0], niter=niter) - def test_primal_dual_l1(): """Verify that the correct value is returned for l1 dist optimization. @@ -73,26 +65,17 @@ def test_primal_dual_l1(): which has optimum value data_1. """ - - # Define the space space = odl.rn(5) - - # Operator L = [odl.IdentityOperator(space)] + data1 = odl.util.testutils.noise_element(space) + data2 = odl.util.testutils.noise_element(space) - # Data - data_1 = odl.util.testutils.noise_element(space) - data_2 = odl.util.testutils.noise_element(space) + f = odl.solvers.L1Norm(space).translated(data1) + g = [0.5 * odl.solvers.L1Norm(space).translated(data2)] - # Proximals - f = odl.solvers.L1Norm(space).translated(data_1) - g = [0.5 * odl.solvers.L1Norm(space).translated(data_2)] - - # Solve with f term dominating x = space.zero() douglas_rachford_pd(x, f, g, L, tau=3.0, sigma=[1.0], niter=10) - - assert all_almost_equal(x, data_1, ndigits=2) + assert all_almost_equal(x, data1, ndigits=2) def test_primal_dual_no_operator(): @@ -104,24 +87,13 @@ def test_primal_dual_no_operator(): which has optimum value data_1. """ - - # Define the space space = odl.rn(5) - - # Operator L = [] - - # Data data_1 = odl.util.testutils.noise_element(space) - - # Proximals f = odl.solvers.L1Norm(space).translated(data_1) g = [] - - # Solve with f term dominating x = space.zero() douglas_rachford_pd(x, f, g, L, tau=3.0, sigma=[], niter=10) - assert all_almost_equal(x, data_1, ndigits=2) diff --git a/odl/test/solvers/nonsmooth/forward_backward_test.py b/odl/test/solvers/nonsmooth/forward_backward_test.py index ab6c36cf245..82337bb0faa 100644 --- a/odl/test/solvers/nonsmooth/forward_backward_test.py +++ b/odl/test/solvers/nonsmooth/forward_backward_test.py @@ -1,4 +1,4 @@ -# Copyright 2014-2017 The ODL contributors +# Copyright 2014-2019 The ODL contributors # # This file is part of ODL. # @@ -9,13 +9,13 @@ """Test for the forward-backward solver.""" from __future__ import division + import pytest import odl from odl.solvers import forward_backward_pd from odl.util.testutils import all_almost_equal, noise_element -# Places for the accepted error when comparing results HIGH_ACCURACY = 8 LOW_ACCURACY = 4 @@ -23,46 +23,39 @@ def test_forward_backward_input_handling(): """Test to see that input is handled correctly.""" - space1 = odl.uniform_discr(0, 1, 10) + space = odl.uniform_discr(0, 1, 10) - lin_ops = [odl.ZeroOperator(space1), odl.ZeroOperator(space1)] - g = [odl.solvers.ZeroFunctional(space1), - odl.solvers.ZeroFunctional(space1)] - f = odl.solvers.ZeroFunctional(space1) - h = odl.solvers.ZeroFunctional(space1) + L = [odl.ZeroOperator(space), odl.ZeroOperator(space)] + g = [odl.solvers.ZeroFunctional(space), + odl.solvers.ZeroFunctional(space)] + f = odl.solvers.ZeroFunctional(space) + h = odl.solvers.ZeroFunctional(space) # Check that the algorithm runs. With the above operators, the algorithm # returns the input. - x0 = noise_element(space1) - x = x0.copy() + x0 = noise_element(space) + x = space.copy(x0) niter = 3 - forward_backward_pd(x, f, g, lin_ops, h, tau=1.0, + forward_backward_pd(x, f, g, L, h, tau=1.0, sigma=[1.0, 1.0], niter=niter) - assert x == x0 + assert all_almost_equal(x, x0) # Testing that sizes needs to agree: - # Too few sigma_i:s + # Too few sigmas with pytest.raises(ValueError): - forward_backward_pd(x, f, g, lin_ops, h, tau=1.0, + forward_backward_pd(x, f, g, L, h, tau=1.0, sigma=[1.0], niter=niter) # Too many operators - g_too_many = [odl.solvers.ZeroFunctional(space1), - odl.solvers.ZeroFunctional(space1), - odl.solvers.ZeroFunctional(space1)] + g_too_many = [odl.solvers.ZeroFunctional(space), + odl.solvers.ZeroFunctional(space), + odl.solvers.ZeroFunctional(space)] with pytest.raises(ValueError): - forward_backward_pd(x, f, g_too_many, lin_ops, h, + forward_backward_pd(x, f, g_too_many, L, h, tau=1.0, sigma=[1.0, 1.0], niter=niter) - # Test for correct space - space2 = odl.uniform_discr(1, 2, 10) - x = noise_element(space2) - with pytest.raises(ValueError): - forward_backward_pd(x, f, g, lin_ops, h, tau=1.0, - sigma=[1.0, 1.0], niter=niter) - def test_forward_backward_basic(): """Test for the forward-backward solver by minimizing ||x||_2^2. @@ -74,10 +67,9 @@ def test_forward_backward_basic(): and here we take f(x) = g(x) = 0, h(x) = ||x||_2^2 and L is the zero-operator. """ - space = odl.rn(10) - lin_ops = [odl.ZeroOperator(space)] + L = [odl.ZeroOperator(space)] g = [odl.solvers.ZeroFunctional(space)] f = odl.solvers.ZeroFunctional(space) h = odl.solvers.L2NormSquared(space) @@ -85,7 +77,7 @@ def test_forward_backward_basic(): x = noise_element(space) x_global_min = space.zero() - forward_backward_pd(x, f, g, lin_ops, h, tau=0.5, + forward_backward_pd(x, f, g, L, h, tau=0.5, sigma=[1.0], niter=10) assert all_almost_equal(x, x_global_min, ndigits=HIGH_ACCURACY) diff --git a/odl/test/solvers/nonsmooth/primal_dual_hybrid_gradient_test.py b/odl/test/solvers/nonsmooth/primal_dual_hybrid_gradient_test.py index c2cbeb1cc8d..183c701264e 100644 --- a/odl/test/solvers/nonsmooth/primal_dual_hybrid_gradient_test.py +++ b/odl/test/solvers/nonsmooth/primal_dual_hybrid_gradient_test.py @@ -1,4 +1,4 @@ -# Copyright 2014-2017 The ODL contributors +# Copyright 2014-2019 The ODL contributors # # This file is part of ODL. # @@ -9,14 +9,14 @@ """Test for the Primal-Dual Hybrid Gradient algorithm.""" from __future__ import division + import numpy as np import odl from odl.solvers import pdhg from odl.util.testutils import all_almost_equal -# Places for the accepted error when comparing results -PLACES = 8 +NDIGITS = 8 # Algorithm parameters TAU = 0.3 @@ -29,53 +29,39 @@ def test_pdhg_simple_space(): """Test for the Primal-Dual Hybrid Gradient algorithm.""" - - # Create a discretized image space space = odl.uniform_discr(0, 1, DATA.size) - - # Operator op = odl.IdentityOperator(space) + x = space.element(DATA) - # Starting point (image) - discr_vec = op.domain.element(DATA) - - # Relaxation variable required to resume iteration - discr_vec_relax = discr_vec.copy() + # Relaxation and dual variables required to resume iteration + x_relax = space.copy(x) + y = op.range.zero() - # Dual variable required to resume iteration - discr_dual = op.range.zero() - - # Functional, use the same functional for F^* and G + # Use the same functional for f^* and g f = odl.solvers.ZeroFunctional(space) g = f.convex_conj - # Run the algorithm - pdhg(discr_vec, f, g, op, niter=1, tau=TAU, sigma=SIGMA, theta=THETA, - callback=None, x_relax=discr_vec_relax, y=discr_dual) - - # Explicit computation - vec_expl = (1 - TAU * SIGMA) * DATA - - assert all_almost_equal(discr_vec, vec_expl, PLACES) - - # Explicit computation of the value of the relaxation variable - vec_relax_expl = (1 + THETA) * vec_expl - THETA * DATA - - assert all_almost_equal(discr_vec_relax, vec_relax_expl, PLACES) + # Run one iteration of the algorithm and compare against explicit + # calculation + pdhg(x, f, g, op, niter=1, tau=TAU, sigma=SIGMA, theta=THETA, + x_relax=x_relax, y=y) + x_expected = (1 - TAU * SIGMA) * DATA + assert all_almost_equal(x, x_expected, NDIGITS) + x_relax_expected = (1 + THETA) * x_expected - THETA * DATA + assert all_almost_equal(x_relax, x_relax_expected, NDIGITS) # Resume iteration with previous x but without previous relaxation - pdhg(discr_vec, f, g, op, niter=1, tau=TAU, sigma=SIGMA, theta=THETA) - - vec_expl *= (1 - SIGMA * TAU) - assert all_almost_equal(discr_vec, vec_expl, PLACES) + pdhg(x, f, g, op, niter=1, tau=TAU, sigma=SIGMA, theta=THETA) + x_expected *= (1 - SIGMA * TAU) + assert all_almost_equal(x, x_expected, NDIGITS) # Resume iteration with x1 as above and with relaxation parameter - discr_vec[:] = vec_expl - pdhg(discr_vec, f, g, op, niter=1, tau=TAU, sigma=SIGMA, theta=THETA, - x_relax=discr_vec_relax, y=discr_dual) + x[:] = x_expected + pdhg(x, f, g, op, niter=1, tau=TAU, sigma=SIGMA, theta=THETA, + x_relax=x_relax, y=y) - vec_expl = vec_expl - TAU * SIGMA * (DATA + vec_relax_expl) - assert all_almost_equal(discr_vec, vec_expl, PLACES) + x_expected = x_expected - TAU * SIGMA * (DATA + x_relax_expected) + assert all_almost_equal(x, x_expected, NDIGITS) # Test acceleration parameter: use output argument for the relaxation # variable since otherwise two iterations are required for the @@ -84,53 +70,45 @@ def test_pdhg_simple_space(): # theta=1 without acceleration # Relaxation parameter 1 and no acceleration - discr_vec = op.domain.element(DATA) - discr_vec_relax_no_gamma = op.domain.element(DATA) - pdhg(discr_vec, f, g, op, niter=1, tau=TAU, sigma=SIGMA, theta=1, - gamma_primal=None, x_relax=discr_vec_relax_no_gamma) + x = op.domain.element(DATA) + x_relax_no_gamma = op.domain.element(DATA) + pdhg(x, f, g, op, niter=1, tau=TAU, sigma=SIGMA, theta=1, + gamma_primal=None, x_relax=x_relax_no_gamma) # Acceleration parameter 0, overwrites relaxation parameter - discr_vec = op.domain.element(DATA) - discr_vec_relax_g0 = op.domain.element(DATA) - pdhg(discr_vec, f, g, op, niter=1, tau=TAU, sigma=SIGMA, theta=0, - gamma_primal=0, x_relax=discr_vec_relax_g0) + x = op.domain.element(DATA) + x_relax_g0 = op.domain.element(DATA) + pdhg(x, f, g, op, niter=1, tau=TAU, sigma=SIGMA, theta=0, + gamma_primal=0, x_relax=x_relax_g0) - assert discr_vec != discr_vec_relax_no_gamma - assert all_almost_equal(discr_vec_relax_no_gamma, discr_vec_relax_g0) + assert not all_almost_equal(x, x_relax_no_gamma) + assert all_almost_equal(x_relax_no_gamma, x_relax_g0) # Test callback execution - pdhg(discr_vec, f, g, op, niter=1, tau=TAU, sigma=SIGMA, theta=THETA, + pdhg(x, f, g, op, niter=1, tau=TAU, sigma=SIGMA, theta=THETA, callback=odl.solvers.CallbackPrintIteration()) def test_pdhg_product_space(): """Test the PDHG algorithm using a product space operator.""" - - # Create a discretized image space space = odl.uniform_discr(0, 1, DATA.size) - - # Operator - identity = odl.IdentityOperator(space) - - # Create broadcasting operator - prod_op = odl.BroadcastOperator(identity, -2 * identity) + I = odl.IdentityOperator(space) + op = odl.BroadcastOperator(I, -2 * I) # Starting point for explicit computation - discr_vec_0 = prod_op.domain.element(DATA) + x_0 = space.element(DATA) # Copy to be overwritten by the algorithm - discr_vec = discr_vec_0.copy() + x = space.copy(DATA) - # Proximal operator using the same factory function for F^* and G - f = odl.solvers.ZeroFunctional(prod_op.domain) - g = odl.solvers.ZeroFunctional(prod_op.range).convex_conj + # Using f and g such that f = g^* + f = odl.solvers.ZeroFunctional(op.domain) + g = odl.solvers.ZeroFunctional(op.range).convex_conj - # Run the algorithm - pdhg(discr_vec, f, g, prod_op, niter=1, tau=TAU, sigma=SIGMA, theta=THETA) + pdhg(x, f, g, op, niter=1, tau=TAU, sigma=SIGMA, theta=THETA) - vec_expl = discr_vec_0 - TAU * SIGMA * prod_op.adjoint( - prod_op(discr_vec_0)) - assert all_almost_equal(discr_vec, vec_expl, PLACES) + x_expected = x_0 - TAU * SIGMA * op.adjoint(op(x_0)) + assert all_almost_equal(x, x_expected, NDIGITS) if __name__ == '__main__': diff --git a/odl/test/solvers/nonsmooth/proximal_operator_test.py b/odl/test/solvers/nonsmooth/proximal_operator_test.py index e0f85b54e25..8a350f3bb65 100644 --- a/odl/test/solvers/nonsmooth/proximal_operator_test.py +++ b/odl/test/solvers/nonsmooth/proximal_operator_test.py @@ -1,4 +1,4 @@ -# Copyright 2014-2017 The ODL contributors +# Copyright 2014-2019 The ODL contributors # # This file is part of ODL. # @@ -9,527 +9,289 @@ """Tests for the factory functions to create proximal operators.""" from __future__ import division + import numpy as np import scipy.special import odl from odl.solvers.nonsmooth.proximal_operators import ( - combine_proximals, proximal_const_func, - proximal_box_constraint, proximal_nonnegativity, + combine_proximals, proximal_box_constraint, proximal_const_func, + proximal_convex_conj_kl, proximal_convex_conj_kl_cross_entropy, proximal_convex_conj_l1, proximal_convex_conj_l1_l2, - proximal_l2, - proximal_convex_conj_l2_squared, - proximal_convex_conj_kl, proximal_convex_conj_kl_cross_entropy) -from odl.util.testutils import all_almost_equal - + proximal_convex_conj_l2_squared, proximal_l2) +from odl.util.testutils import all_almost_equal, simple_fixture # Places for the accepted error when comparing results HIGH_ACC = 8 LOW_ACC = 4 -def test_proximal_const_func(): - """Proximal factory for the constnat mapping G(x) = c.""" - - # Image space - space = odl.uniform_discr(0, 1, 10) - - # Element in the image space where the proximal operator is evaluated - x = space.element(np.arange(-5, 5)) - - # Factory function returning the proximal operator - prox_factory = proximal_const_func(space) - - # Initialize proximal operator of G (with an unused parameter) - prox = prox_factory(None) +# --- pytest fixtures --- # - # prox_tau[G](x) = x = identity operator - assert isinstance(prox, odl.IdentityOperator) +lower = simple_fixture('lower', [None, -2, 'elem']) +upper = simple_fixture('upper', [None, 2, 'elem']) +with_g = simple_fixture('with_g', [False, True]) +sigma = simple_fixture('sigma', [0.5, 1.2, 'elem']) +scaling = simple_fixture('scaling', [2.5, 'elem']) - # Optimal point of the auxiliary minimization problem prox_tau[G] - x_opt = prox(x) - # Identity map - assert x == x_opt +# --- Unittests --- # -def test_proximal_box_constraint(): - """Proximal factory for indicator function for non-negativity.""" - - # Image space +def test_prox_const_func(): + """Test proximal of the constant functional.""" space = odl.uniform_discr(0, 1, 10) - - # Element in the image space where the proximal operator is evaluated x = space.element(np.arange(-5, 5)) - for lower in [None, -2, -2.0 * space.one()]: - for upper in [None, 2, 2.0 * space.one()]: - # Factory function returning the proximal operator - prox_factory = proximal_box_constraint(space, - lower=lower, upper=upper) - prox = prox_factory(1.0) - result = prox(x).asarray() - - # Create reference - lower_np = -np.inf if lower is None else lower - upper_np = np.inf if upper is None else upper - result_np = np.minimum(np.maximum(x, lower_np), upper_np).asarray() - - # Verify equal result - assert all_almost_equal(result_np, result) + prox = proximal_const_func(space)(1.0) + prox_x = prox(x) + assert all_almost_equal(prox_x, x) -def test_proximal_nonnegativity(): - """Proximal factory for indicator function for non-negativity.""" - - # Image space +def test_prox_box_constraint(lower, upper): + """Test proximal of the box constraint indicator.""" space = odl.uniform_discr(0, 1, 10) - - # Element in the image space where the proximal operator is evaluated x = space.element(np.arange(-5, 5)) - # Factory function returning the proximal operator - prox_factory = proximal_nonnegativity(space) + if lower == 'elem': + lower = -2.0 * space.one() - # Initialize proximal operator of G (with an unused parameter) - prox = prox_factory(1.0) + if upper == 'elem': + upper = 2.0 * space.one() - # Optimal point returned by the proximal operator - result = prox(x) + prox = proximal_box_constraint(space, lower, upper)(1.0) + prox_x = prox(x) - # prox_tau[G](x) = non-negativity thresholding - assert all(result.asarray() >= 0) + lower_np = -np.inf if lower is None else lower + upper_np = np.inf if upper is None else upper + true_prox_x = np.minimum(np.maximum(x, lower_np), upper_np) + assert all_almost_equal(prox_x, true_prox_x) -def test_combine_proximal(): - """Function to combine proximal factory functions. + +def test_combine_proximals(): + """Test function to combine proximal factory functions. The combine function makes use of the separable sum property of proximal operators. """ - - # Image space space = odl.uniform_discr(0, 1, 10) + # Combination works at the level of factories + prox_fact = proximal_const_func(space) + combined_prox_fact = combine_proximals(prox_fact, prox_fact) - # Factory function returning the proximal operator - prox_factory = proximal_const_func(space) + prox = combined_prox_fact(1.0) - # Combine factory function of proximal operators - combined_prox_factory = combine_proximals(prox_factory, prox_factory) + I = odl.IdentityOperator(space) + true_prox = odl.DiagonalOperator(I, I) + x = true_prox.domain.element([np.arange(-5, 5), np.arange(-5, 5)]) + assert all_almost_equal(prox(x), true_prox(x)) + out = true_prox.range.element() + prox(x, out=out) + assert all_almost_equal(out, true_prox(x)) - # Initialize combine proximal operator - prox = combined_prox_factory(1) - assert isinstance(prox, odl.Operator) - - # Explicit construction of the combine proximal operator - prox_verify = odl.ProductSpaceOperator( - [[odl.IdentityOperator(space), None], - [None, odl.IdentityOperator(space)]]) - - # Create an element in the domain of the operator - x = prox_verify.domain.element([np.arange(-5, 5), np.arange(-5, 5)]) - - # Allocate output element - out = prox_verify.range.element() - - # Apply explicitly constructed and factory-function-combined proximal - # operators - assert prox(x) == prox_verify(x) - - # Test output argument - assert prox(x, out) == prox_verify(x) - - # Identity mapping - assert out == x - - -def test_proximal_l2_wo_data(): - """Proximal factory for the L2-norm.""" - - # Image space +def test_prox_l2(with_g): + """Test proximal of the L2 norm.""" space = odl.uniform_discr(0, 1, 10) - - # Factory function returning the proximal operator lam = 2.0 - prox_factory = proximal_l2(space, lam=lam) - - # Initialize the proximal operator sigma = 3.0 - prox = prox_factory(sigma) - assert isinstance(prox, odl.Operator) + if with_g: + g = space.element(np.arange(-5, 5)) + prox = proximal_l2(space, lam, g)(sigma) + else: + g = space.zero() + prox = proximal_l2(space, lam)(sigma) - # Elements x = space.element(np.arange(-5, 5)) - x_small = x * 0.5 * lam * sigma / x.norm() - x_big = x * 2.0 * lam * sigma / x.norm() + x_norm = space.norm(x) + x_small = g + x * 0.5 * lam * sigma / x_norm + x_large = g + x * 2.0 * lam * sigma / x_norm - # Explicit computation: - x_small_opt = x_small * 0 - x_big_opt = (1 - lam * sigma / x_big.norm()) * x_big + true_prox_x_small = g + const = lam * sigma / space.norm(x_large - g) + true_prox_x_large = (1 - const) * x_large + const * g - assert all_almost_equal(prox(x_small), x_small_opt, HIGH_ACC) - assert all_almost_equal(prox(x_big), x_big_opt, HIGH_ACC) + assert all_almost_equal(prox(x_small), true_prox_x_small, HIGH_ACC) + assert all_almost_equal(prox(x_large), true_prox_x_large, HIGH_ACC) + out = space.element() + prox(x_large, out=out) + assert all_almost_equal(out, true_prox_x_large, HIGH_ACC) + prox(x_large, out=x_large) + assert all_almost_equal(x_large, true_prox_x_large, HIGH_ACC) -def test_proximal_l2_with_data(): - """Proximal factory for the L2-norm with data term.""" - # Image space - space = odl.uniform_discr(0, 1, 10) - - # Create data - g = space.element(np.arange(-5, 5)) - - # Factory function returning the proximal operator - lam = 2.0 - prox_factory = proximal_l2(space, lam=lam, g=g) - - # Initialize the proximal operator - sigma = 3.0 - prox = prox_factory(sigma) - - assert isinstance(prox, odl.Operator) - - # Elements - x = space.element(np.arange(-5, 5)) - x_small = g + x * 0.5 * lam * sigma / x.norm() - x_big = g + x * 2.0 * lam * sigma / x.norm() - - # Explicit computation: - x_small_opt = g - const = lam * sigma / (x_big - g).norm() - x_big_opt = (1 - const) * x_big + const * g - - assert all_almost_equal(prox(x_small), x_small_opt, HIGH_ACC) - assert all_almost_equal(prox(x_big), x_big_opt, HIGH_ACC) - - -def test_proximal_convconj_l2_sq_wo_data(): - """Proximal factory for the convex conjugate of the L2-norm.""" - - # Image space +def test_prox_cconj_l2_sq(with_g): + """Test proximal of the squared L2 norm convex conjugate.""" space = odl.uniform_discr(0, 10, 10) - - # Create an element in the image space - x_arr = np.arange(-5, 5) - x = space.element(x_arr) - - # Factory function returning the proximal operator - lam = 2 - prox_factory = proximal_convex_conj_l2_squared(space, lam=lam) - - # Initialize the proximal operators - sigma = 0.25 * space.one() - sigmav = sigma * space.one() - prox = prox_factory(sigma) - proxv = prox_factory(sigmav) - - assert isinstance(prox, odl.Operator) - assert isinstance(proxv, odl.Operator) - - # Allocate output elements - x_out = space.element() - x_outv = space.element() - - # Optimal point returned by the proximal operator - prox(x, x_out) - proxv(x, x_outv) - - # Explicit computation: x / (1 + sigma / (2 * lambda)) - x_verify = x / (1 + sigma / (2 * lam)) - - assert all_almost_equal(x_out, x_verify, HIGH_ACC) - assert all_almost_equal(x_outv, x_verify, HIGH_ACC) - - -def test_proximal_convconj_l2_sq_with_data(): - """Proximal factory for the convex conjugate of the L2-norm.""" - - # Image space - space = odl.uniform_discr(0, 1, 10) - - # Create an element in the image space - x_arr = np.arange(-5, 5) - x = space.element(x_arr) - - # Create data - g = space.element(-2 * x_arr) - - # Factory function returning the proximal operator - lam = 2 - prox_factory = proximal_convex_conj_l2_squared(space, lam=lam, g=g) - - # Initialize the proximal operator - sigma = 0.25 - prox = prox_factory(sigma) - - assert isinstance(prox, odl.Operator) - - # Allocate output element - x_out = space.element() - - # Optimal point returned by the proximal operator - prox(x, x_out) - - # Explicit computation: (x - sigma * g) / (1 + sigma / (2 * lambda)) - x_verify = (x - sigma * g) / (1 + sigma / (2 * lam)) - - assert all_almost_equal(x_out, x_verify, HIGH_ACC) - - -def test_proximal_convconj_l1_simple_space_without_data(): - """Proximal factory for the convex conjugate of the L1-norm.""" - - # Image space - space = odl.uniform_discr(0, 1, 10) - - # Image element - x_arr = np.arange(-5, 5) - x = space.element(x_arr) - - # Factory function returning the proximal operator lam = 2 - prox_factory = proximal_convex_conj_l1(space, lam=lam) - - # Initialize the proximal operator of F^* sigma = 0.25 - prox = prox_factory(sigma) - - assert isinstance(prox, odl.Operator) + sigma_elem = sigma * space.one() - # Apply the proximal operator returning its optimal point - # Explicit computation: x / max(lam, |x|) - denom = np.maximum(lam, np.sqrt(x_arr ** 2)) - x_exact = lam * x_arr / denom + if with_g: + g = space.element(-2 * np.arange(-5, 5)) + prox = proximal_convex_conj_l2_squared(space, lam, g)(sigma) + prox_elem = proximal_convex_conj_l2_squared(space, lam, g)(sigma_elem) + else: + g = space.zero() + prox = proximal_convex_conj_l2_squared(space, lam)(sigma) + prox_elem = proximal_convex_conj_l2_squared(space, lam)(sigma_elem) - # Using out - x_opt = space.element() - x_result = prox(x, x_opt) - assert x_result is x_opt - assert all_almost_equal(x_opt, x_exact, HIGH_ACC) - - # Without out - x_result = prox(x) - assert all_almost_equal(x_result, x_exact, HIGH_ACC) + x = space.element(np.arange(-5, 5)) + true_prox_x = (x - sigma * g) / (1 + sigma / (2 * lam)) - # With aliased out - x_result = prox(x, x) - assert all_almost_equal(x_result, x_exact, HIGH_ACC) + assert all_almost_equal(prox(x), true_prox_x, HIGH_ACC) + out = space.element() + prox(x, out=out) + assert all_almost_equal(out, true_prox_x, HIGH_ACC) + prox(x, out=x) + assert all_almost_equal(x, true_prox_x, HIGH_ACC) + x = space.element(np.arange(-5, 5)) + assert all_almost_equal(prox_elem(x), true_prox_x, HIGH_ACC) + out = space.element() + prox_elem(x, out=out) + assert all_almost_equal(out, true_prox_x, HIGH_ACC) + prox_elem(x, out=x) + assert all_almost_equal(x, true_prox_x, HIGH_ACC) -def test_proximal_convconj_l1_simple_space_with_data(): - """Proximal factory for the convex conjugate of the L1-norm.""" - # Image space +def test_prox_conv_l1(with_g): + """Test proximal of the L1 norm convex conjugate.""" space = odl.uniform_discr(0, 1, 10) - x_arr = np.arange(-5, 5) - x = space.element(x_arr) - - # RHS data - g_arr = np.arange(10, 0, -1) - g = space.element(g_arr) - - # Factory function returning the proximal operator + F = space.ufuncs lam = 2 - prox_factory = proximal_convex_conj_l1(space, lam=lam, g=g) - - # Initialize the proximal operator of F^* sigma = 0.25 - prox = prox_factory(sigma) - - assert isinstance(prox, odl.Operator) - - # Apply the proximal operator returning its optimal point - x_opt = space.element() - prox(x, x_opt) - # Explicit computation: (x - sigma * g) / max(lam, |x - sigma * g|) - denom = np.maximum(lam, np.abs(x_arr - sigma * g_arr)) - x0_verify = lam * (x_arr - sigma * g_arr) / denom + if with_g: + g = space.element(np.arange(10, 0, -1)) + prox = proximal_convex_conj_l1(space, lam, g)(sigma) + else: + g = space.zero() + prox = proximal_convex_conj_l1(space, lam)(sigma) - assert all_almost_equal(x_opt, x0_verify, HIGH_ACC) - - -def test_proximal_convconj_l1_product_space(): - """Proximal factory for the convex conjugate of the L1-norm using - product spaces.""" - - # Product space for matrix of operators - op_domain = odl.ProductSpace(odl.uniform_discr(0, 1, 10), 2) + x = space.element(np.arange(-5, 5)) + true_prox_x = lam * (x - sigma * g) / F.maximum(lam, F.abs(x - sigma * g)) - # Element in the product space where the proximal operator is evaluated - x0_arr = np.arange(-5, 5) - x1_arr = np.arange(10, 0, -1) - x = op_domain.element([x0_arr, x1_arr]) + assert all_almost_equal(prox(x), true_prox_x, HIGH_ACC) + out = space.element() + prox(x, out=out) + assert all_almost_equal(out, true_prox_x, HIGH_ACC) + prox(x, out=x) + assert all_almost_equal(x, true_prox_x, HIGH_ACC) - # Create a data element in the product space - g0_arr = x1_arr.copy() - g1_arr = x0_arr.copy() - g = op_domain.element([g0_arr, g1_arr]) - # Factory function returning the proximal operator +def test_prox_cconj_l1_l2(): + """Test proximal of the L1-L2 norm convex conjugate.""" + pspace = odl.ProductSpace(odl.uniform_discr(0, 1, 10), 2) + Fb = pspace[0].ufuncs lam = 2 - prox_factory = proximal_convex_conj_l1_l2(op_domain, lam=lam, g=g) - - # Initialize the proximal operator sigma = 0.25 - prox = prox_factory(sigma) - - assert isinstance(prox, odl.Operator) - # Apply the proximal operator returning its optimal point - x_opt = prox(x) + x = pspace.element([np.arange(-5, 5), np.arange(10, 0, -1)]) + g = pspace.copy(x)[::-1] - # Explicit computation: (x - sigma * g) / max(lam, |x - sigma * g|) - denom = np.maximum(lam, - np.sqrt((x0_arr - sigma * g0_arr) ** 2 + - (x1_arr - sigma * g1_arr) ** 2)) - x_verify = lam * (x - sigma * g) / denom + prox = proximal_convex_conj_l1_l2(pspace, lam, g)(sigma) - # Compare components - assert all_almost_equal(x_verify, x_opt) + # (x - sigma * g) / max(lam, |x - sigma * g|) + denom = Fb.maximum(lam, Fb.hypot(*(x - sigma * g))) + true_prox_x = [lam * (xi - sigma * gi) / denom for xi, gi in zip(x, g)] + assert all_almost_equal(prox(x), true_prox_x, HIGH_ACC) + out = pspace.element() + prox(x, out=out) + assert all_almost_equal(out, true_prox_x, HIGH_ACC) + prox(x, out=x) + assert all_almost_equal(x, true_prox_x, HIGH_ACC) -def test_proximal_convconj_kl_simple_space(): - """Test for proximal factory for the convex conjugate of KL divergence.""" - # Image space +def test_prox_cconj_kl(): + """Test proximal of the KL divergence convex conjugate.""" space = odl.uniform_discr(0, 1, 10) - - # Element in image space where the proximal operator is evaluated - x = space.element(np.arange(-5, 5)) - - # Data - g = space.element(np.arange(10, 0, -1)) - - # Factory function returning the proximal operator + F = space.ufuncs lam = 2 - prox_factory = proximal_convex_conj_kl(space, lam=lam, g=g) - - # Initialize the proximal operator of F^* sigma = 0.25 - prox = prox_factory(sigma) - - assert isinstance(prox, odl.Operator) - - # Allocate an output element - x_opt = space.element() - - # Apply the proximal operator returning its optimal point - prox(x, x_opt) + g = space.element(np.arange(10, 0, -1)) + prox = proximal_convex_conj_kl(space, lam, g)(sigma) - # Explicit computation: - x_verify = (lam + x - np.sqrt((x - lam) ** 2 + 4 * lam * sigma * g)) / 2 + x = space.element(np.arange(-5, 5)) + true_prox_x = (lam + x - F.sqrt((x - lam) ** 2 + 4 * lam * sigma * g)) / 2 - assert all_almost_equal(x_opt, x_verify, HIGH_ACC) + assert all_almost_equal(prox(x), true_prox_x, HIGH_ACC) + out = space.element() + prox(x, out=out) + assert all_almost_equal(out, true_prox_x, HIGH_ACC) + prox(x, out=x) + assert all_almost_equal(x, true_prox_x, HIGH_ACC) def test_proximal_convconj_kl_product_space(): """Test for product spaces in proximal for conjugate of KL divergence""" - - # Product space for matrix of operators - op_domain = odl.ProductSpace(odl.uniform_discr(0, 1, 10), 2) - - # Element in the product space where the proximal operator is evaluated - x0_arr = np.arange(-5, 5) - x1_arr = np.arange(10, 0, -1) - x = op_domain.element([x0_arr, x1_arr]) - - # Element in the product space with given data - g0_arr = x1_arr.copy() - g1_arr = x0_arr.copy() - g = op_domain.element([g0_arr, g1_arr]) - - # Factory function returning the proximal operator + pspace = odl.ProductSpace(odl.uniform_discr(0, 1, 10), 2) + F = pspace.ufuncs lam = 2 - prox_factory = proximal_convex_conj_kl(op_domain, lam=lam, g=g) - - # Initialize the proximal operator sigma = 0.25 - prox = prox_factory(sigma) - - assert isinstance(prox, odl.Operator) - # Allocate an output element - x_opt = op_domain.element() + x = pspace.element([np.arange(-5, 5), np.arange(10, 0, -1)]) + g = pspace.copy(x)[::-1] + prox = proximal_convex_conj_kl(pspace, lam, g)(sigma) + true_prox_x = (lam + x - F.sqrt((x - lam) ** 2 + 4 * lam * sigma * g)) / 2 - # Apply the proximal operator returning its optimal point - prox(x, x_opt) - - # Explicit computation: - x_verify = (lam + x - np.sqrt((x - lam) ** 2 + 4 * lam * sigma * g)) / 2 - - # Compare components - assert all_almost_equal(x_verify, x_opt) + assert all_almost_equal(prox(x), true_prox_x, HIGH_ACC) + out = pspace.element() + prox(x, out=out) + assert all_almost_equal(out, true_prox_x, HIGH_ACC) + prox(x, out=x) + assert all_almost_equal(x, true_prox_x, HIGH_ACC) def test_proximal_convconj_kl_cross_entropy(): """Test for proximal of convex conjugate of cross entropy KL divergence.""" - - # Image space space = odl.uniform_discr(0, 1, 10) - - # Data - g = space.element(np.arange(10, 0, -1)) - - # Factory function returning the proximal operator + F = space.ufuncs lam = 2 - prox_factory = proximal_convex_conj_kl_cross_entropy(space, lam=lam, g=g) - - # Initialize the proximal operator of F^* sigma = 0.25 - prox = prox_factory(sigma) - - assert isinstance(prox, odl.Operator) + g = space.element(np.arange(10, 0, -1)) + prox = proximal_convex_conj_kl_cross_entropy(space, lam, g)(sigma) - # Element in image space where the proximal operator is evaluated x = space.element(np.arange(-5, 5)) + true_prox_x = x - lam * scipy.special.lambertw( + sigma / lam * g * F.exp(x / lam) + ).real - prox_val = prox(x) - - # Explicit computation: - x_verify = x - lam * scipy.special.lambertw( - sigma / lam * g * np.exp(x / lam)).real - - assert all_almost_equal(prox_val, x_verify, HIGH_ACC) + assert all_almost_equal(prox(x), true_prox_x, HIGH_ACC) + out = space.element() + prox(x, out=out) + assert all_almost_equal(out, true_prox_x, HIGH_ACC) + prox(x, out=x) + assert all_almost_equal(x, true_prox_x, HIGH_ACC) - # Test in-place evaluation - x_inplace = space.element() - prox(x, out=x_inplace) - assert all_almost_equal(x_inplace, x_verify, HIGH_ACC) - - -def test_proximal_arg_scaling(): +def test_proximal_arg_scaling(sigma, scaling): """Test for proximal argument scaling.""" - - # Set the underlying space. space = odl.uniform_discr(0, 1, 10) - - # Set the functional and the prox factory. func = odl.solvers.L2NormSquared(space) - prox_factory = odl.solvers.proximal_l2_squared(space) - # Set the point where the proximal operator will be evaluated. + if sigma == 'elem': + sigma = odl.phantom.noise.uniform_noise(space, 1, 10) + + if scaling == 'elem': + scaling = odl.phantom.noise.uniform_noise(space, 1, 10) + + # Scaling happens at the level of factories + prox_fact = odl.solvers.proximal_l2_squared(space) + prox_scal = odl.solvers.proximal_arg_scaling(prox_fact, scaling)(sigma) + x = space.one() + prox_x = prox_scal(x) - # Set the scaling parameters. - for alpha in [2, odl.phantom.noise.uniform_noise(space, 1, 10)]: - # Scale the proximal factories - prox_scaled = odl.solvers.proximal_arg_scaling(prox_factory, alpha) - - # Set the step size. - for sigma in [2, odl.phantom.noise.uniform_noise(space, 1, 10)]: - # Evaluation of the proximals - p = prox_scaled(sigma)(x) - - # Now we know that p = Prox_{sigma g}(x) where g(x) = f(alpha x), - # i.e., (x - p)/sigma = grad g(p) = alpha * grad f(alpha p). - lhs = (x - p) / sigma - rhs = alpha * func.gradient(alpha * p) - assert all_almost_equal(lhs, rhs) + # Check that p = Prox_{sigma g}(x) where g(x) = f(scaling * x), + # i.e., (x - p)/sigma = grad g(p) = scaling * grad f(scaling p). + lhs = (x - prox_x) / sigma + rhs = scaling * func.gradient(scaling * prox_x) + assert all_almost_equal(lhs, rhs) if __name__ == '__main__': diff --git a/odl/test/solvers/smooth/smooth_test.py b/odl/test/solvers/smooth/smooth_test.py index 3bea3e366ac..c8203a67b0b 100644 --- a/odl/test/solvers/smooth/smooth_test.py +++ b/odl/test/solvers/smooth/smooth_test.py @@ -1,4 +1,4 @@ -# Copyright 2014-2017 The ODL contributors +# Copyright 2014-2019 The ODL contributors # # This file is part of ODL. # @@ -9,13 +9,15 @@ """Test for the smooth solvers.""" from __future__ import division + import pytest + import odl from odl.operator import OpNotImplementedError - -nonlinear_cg_beta = odl.util.testutils.simple_fixture('nonlinear_cg_beta', - ['FR', 'PR', 'HS', 'DY']) +nonlinear_cg_beta = odl.util.testutils.simple_fixture( + 'nonlinear_cg_beta', ['FR', 'PR', 'HS', 'DY'] +) @pytest.fixture(scope="module", params=['l2_squared', 'l2_squared_scaled', @@ -29,8 +31,7 @@ def functional(request): return odl.solvers.L2NormSquared(space) elif name == 'l2_squared_scaled': space = odl.uniform_discr(0, 1, 3) - scaling = odl.MultiplyOperator(space.element([1, 2, 3]), - domain=space) + scaling = odl.MultiplyOperator(space, [1, 2, 3]) return odl.solvers.L2NormSquared(space) * scaling elif name == 'quadratic_form': space = odl.rn(3) @@ -41,10 +42,11 @@ def functional(request): vector = space.element([1, 2, 3]) # Calibrate so that functional is zero in optimal point - constant = 1 / 4 * vector.inner(matrix.inverse(vector)) + constant = 1 / 4 * space.inner(vector, matrix.inverse(vector)) return odl.solvers.QuadraticForm( - operator=matrix, vector=vector, constant=constant) + space, operator=matrix, vector=vector, constant=constant + ) elif name == 'rosenbrock': # Moderately ill-behaved rosenbrock functional. rosenbrock = odl.solvers.RosenbrockFunctional(odl.rn(2), scale=2) diff --git a/odl/test/solvers/util/steplen_test.py b/odl/test/solvers/util/steplen_test.py index 9e1987b4953..8baff89ca8c 100644 --- a/odl/test/solvers/util/steplen_test.py +++ b/odl/test/solvers/util/steplen_test.py @@ -24,7 +24,7 @@ def test_backtracking_line_search(): for direction in [space.element([1, 0]), space.element([-1, 0]), space.element([-1, -1])]: - dir_derivative = func.gradient(x).inner(direction) + dir_derivative = space.inner(func.gradient(x), direction) steplen = line_search(x, direction, dir_derivative) assert func(x + steplen * direction) < func(x) @@ -42,7 +42,7 @@ def test_constant_line_search(): for direction in [space.element([1, 0]), space.element([-1, 0]), space.element([-1, -1])]: - dir_derivative = func.gradient(x).inner(direction) + dir_derivative = space.inner(func.gradient(x), direction) steplen = line_search(x, direction, dir_derivative) assert steplen == 0.57 @@ -60,7 +60,7 @@ def test_line_search_from_iternum(): for n, direction in enumerate([space.element([1, 0]), space.element([-1, 0]), space.element([-1, -1])]): - dir_derivative = func.gradient(x).inner(direction) + dir_derivative = space.inner(func.gradient(x), direction) steplen = line_search(x, direction, dir_derivative) assert steplen == 1 / (n + 1) diff --git a/odl/test/space/pspace_test.py b/odl/test/space/pspace_test.py index 303319e4d2e..784b22f0bcf 100644 --- a/odl/test/space/pspace_test.py +++ b/odl/test/space/pspace_test.py @@ -1,4 +1,4 @@ -# Copyright 2014-2017 The ODL contributors +# Copyright 2014-2019 The ODL contributors # # This file is part of ODL. # @@ -9,20 +9,50 @@ from __future__ import division import numpy as np import pytest -import operator import odl +from odl.space import ProductSpace from odl.util.testutils import ( all_equal, all_almost_equal, noise_elements, noise_element, simple_fixture) -exponent = simple_fixture('exponent', [2.0, 1.0, float('inf'), 0.5, 1.5]) +# --- Helpers --- # + + +def _inner(x1, x2, w): + if w is None: + w = 1.0 + inners = np.array([np.vdot(x2_i, x1_i) for x1_i, x2_i in zip(x1, x2)]) + return np.sum(w * inners) + + +def _norm(x, p, w): + if w is None: + w = 1.0 + norms = np.array([np.linalg.norm(xi.ravel()) for xi in x]) + if p in {float('inf'), 0.0, -float('inf')}: + return np.linalg.norm(norms.ravel(), p) + else: + w = np.asarray(w, dtype=float) + return np.linalg.norm((w ** (1 / p) * norms).ravel(), p) + + +def _dist(x1, x2, p, w): + return _norm([x1_i - x2_i for x1_i, x2_i in zip(x1, x2)], p, w) + + +# --- pytest Fixtures --- # + + +exponent = simple_fixture('exponent', [2.0, 1.0, float('inf'), 0.0, 1.5]) +weighting = simple_fixture('weighting', [None, 2.0, [1.5, 2.5]]) space_params = ['product_space', 'power_space'] space_ids = [' space={} '.format(p) for p in space_params] -elem_params = ['space', 'real_space', 'numpy_array', 'array', 'scalar', - '1d_array'] +elem_params = [ + 'space', 'real_space', 'numpy_array', 'array', 'scalar', '1d_array' +] elem_ids = [' element={} '.format(p) for p in elem_params] @@ -41,43 +71,38 @@ def space(request): return space -@pytest.fixture(scope="module", ids=elem_ids, params=elem_params) -def newpart(request, space): - element_form = request.param.strip() - - if element_form == 'space': - tmp = noise_element(space) - newreal = space.element(tmp.real) - elif element_form == 'real_space': - newreal = noise_element(space).real - elif element_form == 'numpy_array': - tmp = noise_element(space) - newreal = [tmp[0].real.asarray(), tmp[1].real.asarray()] - elif element_form == 'array': - if space.is_power_space: - newreal = [[0, 1, 2], [3, 4, 5]] - else: - newreal = [[0, 1, 2], [3, 4]] - elif element_form == 'scalar': - newreal = np.random.randn() - elif element_form == '1d_array': - if not space.is_power_space: - pytest.skip('arrays matching only one dimension can only be used ' - 'for power spaces') - newreal = [0, 1, 2] - else: - raise ValueError('undefined form of element') +# --- Tests --- # + + +def test_init_pspace(): + """Test initialization patterns and options for ``ProductSpace``.""" + r2 = odl.rn(2) + r3 = odl.rn(3) - return newreal + ProductSpace(r2, r3) + ProductSpace(r2, r3, r3, r2) + ProductSpace(r2, r3, exponent=1.0) + ProductSpace(r2, r3, field=odl.RealNumbers()) + ProductSpace(r2, r3, weighting=0.5) + ProductSpace(r2, r3, weighting=[0.5, 2]) + ProductSpace(r2, 4) + r2 * r3 + r2 ** 3 -def test_emptyproduct(): + # Make sure `repr` at works at least in the very basic case + assert repr(ProductSpace(r2, r3)) != '' + + +def test_empty_pspace(): + """Test that empty product spaces have sensible behavior.""" with pytest.raises(ValueError): + # Requires explicit `field` odl.ProductSpace() - reals = odl.RealNumbers() - spc = odl.ProductSpace(field=reals) - assert spc.field == reals + field = odl.RealNumbers() + spc = odl.ProductSpace(field=field) + assert spc.field == field assert spc.size == 0 assert spc.is_real assert spc.is_complex @@ -86,118 +111,93 @@ def test_emptyproduct(): spc[0] -def test_RxR(): - H = odl.rn(2) - HxH = odl.ProductSpace(H, H) - - # Check the basic properties - assert len(HxH) == 2 - assert HxH.shape == (2, 2) - assert HxH.size == 4 - assert HxH.dtype == H.dtype - assert HxH.spaces[0] is H - assert HxH.spaces[1] is H - assert HxH.is_power_space - assert not HxH.is_weighted - assert HxH.is_real - assert not HxH.is_complex - - v1 = H.element([1, 2]) - v2 = H.element([3, 4]) - v = HxH.element([v1, v2]) - u = HxH.element([[1, 2], [3, 4]]) - - assert all_equal([v1, v2], v) - assert all_equal([v1, v2], u) - - -def test_equals_space(exponent): +def test_pspace_basic_properties(): + """Verify basic properties of product spaces.""" r2 = odl.rn(2) - r2x3_1 = odl.ProductSpace(r2, 3, exponent=exponent) - r2x3_2 = odl.ProductSpace(r2, 3, exponent=exponent) - r2x4 = odl.ProductSpace(r2, 4, exponent=exponent) - - assert r2x3_1 is r2x3_1 - assert r2x3_1 is not r2x3_2 - assert r2x3_1 is not r2x4 - assert r2x3_1 == r2x3_1 - assert r2x3_1 == r2x3_2 - assert r2x3_1 != r2x4 - assert hash(r2x3_1) == hash(r2x3_2) - assert hash(r2x3_1) != hash(r2x4) - + r3 = odl.rn(3) -def test_equals_vec(exponent): - r2 = odl.rn(2) - r2x3 = odl.ProductSpace(r2, 3, exponent=exponent) - r2x4 = odl.ProductSpace(r2, 4, exponent=exponent) + # Non-power space + pspace = odl.ProductSpace(r2, r3) + assert len(pspace) == 2 + assert pspace.shape == (2,) + assert pspace.size == 2 + assert pspace.spaces[0] == r2 + assert pspace.spaces[1] == r3 + assert not pspace.is_power_space + assert not pspace.is_weighted + assert pspace.is_real + assert not pspace.is_complex - x1 = r2x3.zero() - x2 = r2x3.zero() - y = r2x3.one() - z = r2x4.zero() + r2_x = r2.element([1, 2]) + r3_x = r3.element([3, 4, 5]) + x = pspace.element([r2_x, r3_x]) + y = pspace.element([[1, 2], [3, 4, 5]]) + assert all_equal(x, y) + assert all_equal(x, [r2_x, r3_x]) + + # Power space + pspace = odl.ProductSpace(r3, 2) + assert len(pspace) == 2 + assert pspace.shape == (2,) + assert pspace.size == 2 + assert pspace.spaces[0] == pspace.spaces[1] == r3 + assert pspace.is_power_space + assert not pspace.is_weighted + assert pspace.is_real + assert not pspace.is_complex - assert x1 is x1 - assert x1 is not x2 - assert x1 is not y - assert x1 == x1 - assert x1 == x2 - assert x1 != y - assert x1 != z + x1 = r3.element([0, 1, 2]) + x2 = r3.element([3, 4, 5]) + x = pspace.element([x1, x2]) + y = pspace.element([[0, 1, 2], [3, 4, 5]]) + assert all_equal(x, y) + assert all_equal(x, [x1, x2]) -def test_is_power_space(): +def test_pspace_equality(exponent): + """Verify equality checking of product spaces.""" r2 = odl.rn(2) - r2x3 = odl.ProductSpace(r2, 3) - assert len(r2x3) == 3 - assert r2x3.is_power_space - assert r2x3.spaces[0] is r2 - assert r2x3.spaces[1] is r2 - assert r2x3.spaces[2] is r2 - - r2r2r2 = odl.ProductSpace(r2, r2, r2) - assert r2x3 == r2r2r2 - - -def test_mixed_space(): - """Verify that a mixed productspace is handled properly.""" - r2_1 = odl.rn(2, dtype='float64') - r2_2 = odl.rn(2, dtype='float32') - pspace = odl.ProductSpace(r2_1, r2_2) - - assert not pspace.is_power_space - assert pspace.spaces[0] is r2_1 - assert pspace.spaces[1] is r2_2 - assert pspace.is_real - assert not pspace.is_complex - - # dtype not well defined for this space - with pytest.raises(AttributeError): - pspace.dtype + pspace_1 = odl.ProductSpace(r2, 3, exponent=exponent) + pspace_1_same = odl.ProductSpace(r2, 3, exponent=exponent) + pspace_2 = odl.ProductSpace(r2, 4, exponent=exponent) + pspace_3 = odl.ProductSpace( + r2, 3, exponent=1.0 if exponent != 1.0 else 2.0 + ) + assert pspace_1 == pspace_1 + assert pspace_1 == pspace_1_same + assert pspace_1 != pspace_2 + assert pspace_1 != pspace_3 + assert hash(pspace_1) == hash(pspace_1_same) + assert hash(pspace_1) != hash(pspace_2) + assert hash(pspace_1) != hash(pspace_3) -def test_element(): - H = odl.rn(2) - HxH = odl.ProductSpace(H, H) - HxH.element([[1, 2], [3, 4]]) +# TODO(kohr-h): higher-order spaces +def test_pspace_element(): + """Test element creation in product spaces.""" + r2 = odl.rn(2) + pspace = odl.ProductSpace(r2, r2) + x = pspace.element([[1, 2], [3, 4]]) + assert x in pspace - # wrong length + # Wrong length with pytest.raises(ValueError): - HxH.element([[1, 2]]) + pspace.element([[1, 2]]) with pytest.raises(ValueError): - HxH.element([[1, 2], [3, 4], [5, 6]]) + pspace.element([[1, 2], [3, 4], [5, 6]]) - # wrong length of subspace element + # Wrong length of subspace element with pytest.raises(ValueError): - HxH.element([[1, 2, 3], [4, 5]]) + pspace.element([[1, 2, 3], [4, 5]]) with pytest.raises(ValueError): - HxH.element([[1, 2], [3, 4, 5]]) + pspace.element([[1, 2], [3, 4, 5]]) -def test_lincomb(): +def test_pspace_lincomb(): + """Test linear combination in product spaces.""" H = odl.rn(2) HxH = odl.ProductSpace(H, H) @@ -219,7 +219,8 @@ def test_lincomb(): assert all_almost_equal(z, expected) -def test_multiply(): +def test_pspace_multiply(): + """Test multiplication in product spaces.""" H = odl.rn(2) HxH = odl.ProductSpace(H, H) @@ -238,256 +239,48 @@ def test_multiply(): assert all_almost_equal(z, expected) -def test_metric(): - H = odl.rn(2) - v11 = H.element([1, 2]) - v12 = H.element([5, 3]) - - v21 = H.element([1, 2]) - v22 = H.element([8, 9]) - - # 1-norm - HxH = odl.ProductSpace(H, H, exponent=1.0) - w1 = HxH.element([v11, v12]) - w2 = HxH.element([v21, v22]) - assert (HxH.dist(w1, w2) == - pytest.approx(H.dist(v11, v21) + H.dist(v12, v22))) - - # 2-norm - HxH = odl.ProductSpace(H, H, exponent=2.0) - w1 = HxH.element([v11, v12]) - w2 = HxH.element([v21, v22]) - assert ( - HxH.dist(w1, w2) == - pytest.approx((H.dist(v11, v21) ** 2 + H.dist(v12, v22) ** 2) ** 0.5) - ) - - # inf norm - HxH = odl.ProductSpace(H, H, exponent=float('inf')) - w1 = HxH.element([v11, v12]) - w2 = HxH.element([v21, v22]) - assert (HxH.dist(w1, w2) == - pytest.approx(max(H.dist(v11, v21), H.dist(v12, v22)))) - - -def test_norm(): - H = odl.rn(2) - v1 = H.element([1, 2]) - v2 = H.element([5, 3]) - - # 1-norm - HxH = odl.ProductSpace(H, H, exponent=1.0) - w = HxH.element([v1, v2]) - assert HxH.norm(w) == pytest.approx(H.norm(v1) + H.norm(v2)) - - # 2-norm - HxH = odl.ProductSpace(H, H, exponent=2.0) - w = HxH.element([v1, v2]) - assert (HxH.norm(w) == - pytest.approx((H.norm(v1) ** 2 + H.norm(v2) ** 2) ** (1 / 2.0))) - - # inf norm - HxH = odl.ProductSpace(H, H, exponent=float('inf')) - w = HxH.element([v1, v2]) - assert HxH.norm(w) == pytest.approx(max(H.norm(v1), H.norm(v2))) - - -def test_inner(): - H = odl.rn(2) - v1 = H.element([1, 2]) - v2 = H.element([5, 3]) - - u1 = H.element([2, 3]) - u2 = H.element([6, 4]) - - HxH = odl.ProductSpace(H, H) - v = HxH.element([v1, v2]) - u = HxH.element([u1, u2]) - assert HxH.inner(v, u) == pytest.approx(H.inner(v1, u1) + H.inner(v2, u2)) - - -def test_vector_weighting(exponent): +def test_pspace_dist(exponent, weighting): + """Test product space distance function implementation.""" r2 = odl.rn(2) - r2x = r2.element([1, -1]) - r2y = r2.element([-2, 3]) - # inner = -5, dist = 5, norms = (sqrt(2), sqrt(13)) - r3 = odl.rn(3) - r3x = r3.element([3, 4, 4]) - r3y = r3.element([1, -2, 1]) - # inner = -1, dist = 7, norms = (sqrt(41), sqrt(6)) - - inners = [-5, -1] - norms_x = [np.sqrt(2), np.sqrt(41)] - dists = [5, 7] - - weight = [0.5, 1.5] - pspace = odl.ProductSpace(r2, r3, weighting=weight, exponent=exponent) - x = pspace.element((r2x, r3x)) - y = pspace.element((r2y, r3y)) - - if exponent == 2.0: - true_inner = np.sum(np.multiply(inners, weight)) - assert all_almost_equal(x.inner(y), true_inner) - - if exponent == float('inf'): - true_norm_x = np.linalg.norm( - np.multiply(norms_x, weight), ord=exponent) - else: - true_norm_x = np.linalg.norm( - np.multiply(norms_x, np.power(weight, 1 / exponent)), - ord=exponent) + pspace = odl.ProductSpace(r2, r3, exponent=exponent, weighting=weighting) - assert all_almost_equal(x.norm(), true_norm_x) + (x1_arr, x2_arr), (x1, x2) = noise_elements(r2, 2) + (y1_arr, y2_arr), (y1, y2) = noise_elements(r3, 2) + z1 = pspace.element([x1, y1]) + z2 = pspace.element([x2, y2]) - if exponent == float('inf'): - true_dist = np.linalg.norm( - np.multiply(dists, weight), ord=exponent) - else: - true_dist = np.linalg.norm( - np.multiply(dists, np.power(weight, 1 / exponent)), - ord=exponent) - assert all_almost_equal(x.dist(y), true_dist) + true_dist = _dist([x1_arr, y1_arr], [x2_arr, y2_arr], exponent, weighting) + assert pspace.dist(z1, z2) == pytest.approx(true_dist) -def test_const_weighting(exponent): +def test_pspace_norm(exponent, weighting): + """Test product space norm implementation.""" r2 = odl.rn(2) - r2x = r2.element([1, -1]) - r2y = r2.element([-2, 3]) - # inner = -5, dist = 5, norms = (sqrt(2), sqrt(13)) - r3 = odl.rn(3) - r3x = r3.element([3, 4, 4]) - r3y = r3.element([1, -2, 1]) - # inner = -1, dist = 7, norms = (sqrt(41), sqrt(6)) - - inners = [-5, -1] - norms_x = [np.sqrt(2), np.sqrt(41)] - dists = [5, 7] - - weight = 2.0 - pspace = odl.ProductSpace(r2, r3, weighting=weight, exponent=exponent) - x = pspace.element((r2x, r3x)) - y = pspace.element((r2y, r3y)) - - if exponent == 2.0: - true_inner = weight * np.sum(inners) - assert all_almost_equal(x.inner(y), true_inner) - - if exponent == float('inf'): - true_norm_x = weight * np.linalg.norm(norms_x, ord=exponent) - else: - true_norm_x = (weight ** (1 / exponent) * - np.linalg.norm(norms_x, ord=exponent)) - - assert all_almost_equal(x.norm(), true_norm_x) - - if exponent == float('inf'): - true_dist = weight * np.linalg.norm(dists, ord=exponent) - else: - true_dist = (weight ** (1 / exponent) * - np.linalg.norm(dists, ord=exponent)) - - assert all_almost_equal(x.dist(y), true_dist) + pspace = odl.ProductSpace(r2, r3, exponent=exponent, weighting=weighting) + x_arr, x = noise_elements(r2) + y_arr, y = noise_elements(r3) + z = pspace.element([x, y]) -def custom_inner(x1, x2): - inners = np.fromiter( - (x1p.inner(x2p) for x1p, x2p in zip(x1.parts, x2.parts)), - dtype=x1.space[0].dtype, count=len(x1)) + true_norm = _norm([x_arr, y_arr], exponent, weighting) + assert pspace.norm(z) == pytest.approx(true_norm) - return x1.space.field.element(np.sum(inners)) - - -def custom_norm(x): - norms = np.fromiter( - (xp.norm() for xp in x.parts), - dtype=x.space[0].dtype, count=len(x)) - - return float(np.linalg.norm(norms, ord=1)) - - -def custom_dist(x1, x2): - dists = np.fromiter( - (x1p.dist(x2p) for x1p, x2p in zip(x1.parts, x2.parts)), - dtype=x1.space[0].dtype, count=len(x1)) - - return float(np.linalg.norm(dists, ord=1)) - - -def test_custom_funcs(): - # Checking the standard 1-norm and standard inner product, just to - # see that the functions are handled correctly. +def test_pspace_inner(weighting): + """Test product space inner product implementation.""" r2 = odl.rn(2) - r2x = r2.element([1, -1]) - r2y = r2.element([-2, 3]) - # inner = -5, dist = 5, norms = (sqrt(2), sqrt(13)) - r3 = odl.rn(3) - r3x = r3.element([3, 4, 4]) - r3y = r3.element([1, -2, 1]) - # inner = -1, dist = 7, norms = (sqrt(41), sqrt(6)) - - pspace_2 = odl.ProductSpace(r2, r3, exponent=2.0) - x = pspace_2.element((r2x, r3x)) - y = pspace_2.element((r2y, r3y)) - - pspace_custom = odl.ProductSpace(r2, r3, inner=custom_inner) - xc = pspace_custom.element((r2x, r3x)) - yc = pspace_custom.element((r2y, r3y)) - assert x.inner(y) == pytest.approx(xc.inner(yc)) - - pspace_1 = odl.ProductSpace(r2, r3, exponent=1.0) - x = pspace_1.element((r2x, r3x)) - y = pspace_1.element((r2y, r3y)) - - pspace_custom = odl.ProductSpace(r2, r3, norm=custom_norm) - xc = pspace_custom.element((r2x, r3x)) - assert x.norm() == pytest.approx(xc.norm()) - - pspace_custom = odl.ProductSpace(r2, r3, dist=custom_dist) - xc = pspace_custom.element((r2x, r3x)) - yc = pspace_custom.element((r2y, r3y)) - assert x.dist(y) == pytest.approx(xc.dist(yc)) - - with pytest.raises(TypeError): - odl.ProductSpace(r2, r3, a=1) # extra keyword argument - - with pytest.raises(ValueError): - odl.ProductSpace(r2, r3, norm=custom_norm, inner=custom_inner) - - with pytest.raises(ValueError): - odl.ProductSpace(r2, r3, dist=custom_dist, inner=custom_inner) - - with pytest.raises(ValueError): - odl.ProductSpace(r2, r3, norm=custom_norm, dist=custom_dist) + pspace = odl.ProductSpace(r2, r3, weighting=weighting) - with pytest.raises(ValueError): - odl.ProductSpace(r2, r3, norm=custom_norm, exponent=1.0) - - with pytest.raises(ValueError): - odl.ProductSpace(r2, r3, norm=custom_norm, weighting=2.0) + (x1_arr, x2_arr), (x1, x2) = noise_elements(r2, 2) + (y1_arr, y2_arr), (y1, y2) = noise_elements(r3, 2) + z1 = pspace.element([x1, y1]) + z2 = pspace.element([x2, y2]) - with pytest.raises(ValueError): - odl.ProductSpace(r2, r3, dist=custom_dist, weighting=2.0) - - with pytest.raises(ValueError): - odl.ProductSpace(r2, r3, inner=custom_inner, weighting=2.0) - - -def test_power_RxR(): - H = odl.rn(2) - HxH = odl.ProductSpace(H, 2) - assert len(HxH) == 2 - - v1 = H.element([1, 2]) - v2 = H.element([3, 4]) - v = HxH.element([v1, v2]) - u = HxH.element([[1, 2], [3, 4]]) - - assert all_equal([v1, v2], v) - assert all_equal([v1, v2], u) + true_inner = _inner([x1_arr, y1_arr], [x2_arr, y2_arr], weighting) + assert pspace.inner(z1, z2) == pytest.approx(true_inner) def _test_shape(space, expected_shape): @@ -496,12 +289,6 @@ def _test_shape(space, expected_shape): assert space.shape == expected_shape assert space_el.shape == expected_shape - try: - arr = space_el.asarray() - except ValueError: - pass # could not convert to array - else: - assert arr.shape == expected_shape assert space.size == np.prod(expected_shape) assert space_el.size == np.prod(expected_shape) assert len(space) == expected_shape[0] @@ -519,7 +306,7 @@ def test_power_shape(): assert empty.size == empty2.size == 0 r2_3 = odl.ProductSpace(r2, 3) - _test_shape(r2_3, (3, 2)) + _test_shape(r2_3, (3,)) r2xr3 = odl.ProductSpace(r2, r3) _test_shape(r2xr3, (2,)) @@ -619,428 +406,7 @@ def test_getitem_fancy(): assert H[[0, 2]][1] is r3 -def test_element_equals(): - H = odl.ProductSpace(odl.rn(1), odl.rn(2)) - x = H.element([[0], [1, 2]]) - - assert x != 0 # test == not always true - assert x == x - - x_2 = H.element([[0], [1, 2]]) - assert x == x_2 - - x_3 = H.element([[3], [1, 2]]) - assert x != x_3 - - x_4 = H.element([[0], [1, 3]]) - assert x != x_4 - - -def test_element_getitem_int(): - """Test indexing of product space elements with one or several integers.""" - pspace = odl.ProductSpace(odl.rn(1), odl.rn(2)) - - # One level of product space - x0 = pspace[0].element([0]) - x1 = pspace[1].element([1, 2]) - x = pspace.element([x0, x1]) - - assert x[0] is x0 - assert x[1] is x1 - assert x[-2] is x0 - assert x[-1] is x1 - with pytest.raises(IndexError): - x[-3] - x[2] - assert x[0, 0] == 0 - assert x[1, 0] == 1 - - # Two levels of product spaces - pspace2 = odl.ProductSpace(pspace, 3) - z = pspace2.element([x, x, x]) - assert z[0] is x - assert z[1, 0] is x0 - assert z[1, 1, 1] == 2 - - -def test_element_getitem_slice(): - """Test indexing of product space elements with slices.""" - # One level of product space - pspace = odl.ProductSpace(odl.rn(1), odl.rn(2), odl.rn(3)) - - x0 = pspace[0].element([0]) - x1 = pspace[1].element([1, 2]) - x2 = pspace[2].element([3, 4, 5]) - x = pspace.element([x0, x1, x2]) - - assert x[:2].space == pspace[:2] - assert x[:2][0] is x0 - assert x[:2][1] is x1 - - -def test_element_getitem_fancy(): - pspace = odl.ProductSpace(odl.rn(1), odl.rn(2), odl.rn(3)) - - x0 = pspace[0].element([0]) - x1 = pspace[1].element([1, 2]) - x2 = pspace[2].element([3, 4, 5]) - x = pspace.element([x0, x1, x2]) - - assert x[[0, 2]].space == pspace[[0, 2]] - assert x[[0, 2]][0] is x0 - assert x[[0, 2]][1] is x2 - - -def test_element_getitem_multi(): - """Test element access with multiple indices.""" - pspace = odl.ProductSpace(odl.rn(1), odl.rn(2)) - pspace2 = odl.ProductSpace(pspace, 3) - pspace3 = odl.ProductSpace(pspace2, 2) - z = pspace3.element( - [[[[1], - [2, 3]], - [[4], - [5, 6]], - [[7], - [8, 9]]], - [[[10], - [12, 13]], - [[14], - [15, 16]], - [[17], - [18, 19]]] - ] - ) - - assert pspace3.shape == (2, 3, 2) - assert z[0, 0, 0, 0] == 1 - assert all_equal(z[0, 0, 1], [2, 3]) - assert all_equal(z[0, 0], [[1], [2, 3]]) - assert all_equal(z[0, 1:], [[[4], - [5, 6]], - [[7], - [8, 9]]]) - assert all_equal(z[0, 1:, 1], [[5, 6], - [8, 9]]) - assert all_equal(z[0, 1:, :, 0], [[[4], - [5]], - [[7], - [8]]]) - - -def test_element_setitem_single(): - """Test assignment of pspace parts with single indices.""" - pspace = odl.ProductSpace(odl.rn(1), odl.rn(2)) - - x0 = pspace[0].element([0]) - x1 = pspace[1].element([1, 2]) - x = pspace.element([x0, x1]) - old_x0 = x[0] - old_x1 = x[1] - - # Check that values are set, but identity is preserved - new_x0 = pspace[0].element([1]) - x[-2] = new_x0 - assert x[-2] == new_x0 - assert x[-2] is old_x0 - - new_x1 = pspace[1].element([3, 4]) - x[-1] = new_x1 - assert x[-1] == new_x1 - assert x[-1] is old_x1 - - # Set values with scalars - x[1] = -1 - assert all_equal(x[1], [-1, -1]) - assert x[1] is old_x1 - - # Check that out-of-bounds indices raise IndexError - with pytest.raises(IndexError): - x[-3] = x1 - with pytest.raises(IndexError): - x[2] = x0 - - -def test_element_setitem_slice(): - """Test assignment of pspace parts with slices.""" - pspace = odl.ProductSpace(odl.rn(1), odl.rn(2), odl.rn(3)) - - x0 = pspace[0].element([0]) - x1 = pspace[1].element([1, 2]) - x2 = pspace[2].element([3, 4, 5]) - x = pspace.element([x0, x1, x2]) - old_x0 = x[0] - old_x1 = x[1] - - # Check that values are set, but identity is preserved - new_x0 = pspace[0].element([6]) - new_x1 = pspace[1].element([7, 8]) - x[:2] = pspace[:2].element([new_x0, new_x1]) - assert x[:2][0] is old_x0 - assert x[:2][0] == new_x0 - assert x[:2][1] is old_x1 - assert x[:2][1] == new_x1 - - # Set values with sequences of scalars - x[:2] = [-1, -2] - assert x[:2][0] is old_x0 - assert all_equal(x[:2][0], [-1]) - assert x[:2][1] is old_x1 - assert all_equal(x[:2][1], [-2, -2]) - - -def test_element_setitem_fancy(): - """Test assignment of pspace parts with lists.""" - pspace = odl.ProductSpace(odl.rn(1), odl.rn(2), odl.rn(3)) - - x0 = pspace[0].element([0]) - x1 = pspace[1].element([1, 2]) - x2 = pspace[2].element([3, 4, 5]) - x = pspace.element([x0, x1, x2]) - old_x0 = x[0] - old_x2 = x[2] - - # Check that values are set, but identity is preserved - new_x0 = pspace[0].element([6]) - new_x2 = pspace[2].element([7, 8, 9]) - x[[0, 2]] = pspace[[0, 2]].element([new_x0, new_x2]) - assert x[[0, 2]][0] is old_x0 - assert x[[0, 2]][0] == new_x0 - assert x[[0, 2]][1] is old_x2 - assert x[[0, 2]][1] == new_x2 - - # Set values with sequences of scalars - x[[0, 2]] = [-1, -2] - assert x[[0, 2]][0] is old_x0 - assert all_equal(x[[0, 2]][0], [-1]) - assert x[[0, 2]][1] is old_x2 - assert all_equal(x[[0, 2]][1], [-2, -2, -2]) - - -def test_element_setitem_broadcast(): - """Test assignment of power space parts with broadcasting.""" - pspace = odl.ProductSpace(odl.rn(2), 3) - x0 = pspace[0].element([0, 1]) - x1 = pspace[1].element([2, 3]) - x2 = pspace[2].element([4, 5]) - x = pspace.element([x0, x1, x2]) - old_x0 = x[0] - old_x1 = x[1] - - # Set values with a single base space element - new_x0 = pspace[0].element([4, 5]) - x[:2] = new_x0 - assert x[0] is old_x0 - assert x[0] == new_x0 - assert x[1] is old_x1 - assert x[1] == new_x0 - - -def test_unary_ops(): - # Verify that the unary operators (`+x` and `-x`) work as expected - - space = odl.rn(3) - pspace = odl.ProductSpace(space, 2) - - for op in [operator.pos, operator.neg]: - x_arr, x = noise_elements(pspace) - - y_arr = op(x_arr) - y = op(x) - - assert all_almost_equal([x, y], [x_arr, y_arr]) - - -def test_operators(odl_arithmetic_op): - # Test of the operators `+`, `-`, etc work as expected by numpy - op = odl_arithmetic_op - - space = odl.rn(3) - pspace = odl.ProductSpace(space, 2) - - # Interactions with scalars - - for scalar in [-31.2, -1, 0, 1, 2.13]: - - # Left op - x_arr, x = noise_elements(pspace) - if scalar == 0 and op in [operator.truediv, operator.itruediv]: - # Check for correct zero division behaviour - with pytest.raises(ZeroDivisionError): - y = op(x, scalar) - else: - y_arr = op(x_arr, scalar) - y = op(x, scalar) - - assert all_almost_equal([x, y], [x_arr, y_arr]) - - # Right op - x_arr, x = noise_elements(pspace) - - y_arr = op(scalar, x_arr) - y = op(scalar, x) - - assert all_almost_equal([x, y], [x_arr, y_arr]) - - # Verify that the statement z=op(x, y) gives equivalent results to NumPy - x_arr, x = noise_elements(space, 1) - y_arr, y = noise_elements(pspace, 1) - - # non-aliased left - if op in [operator.iadd, operator.isub, operator.itruediv, operator.imul]: - # Check for correct error since in-place op is not possible here - with pytest.raises(TypeError): - z = op(x, y) - else: - z_arr = op(x_arr, y_arr) - z = op(x, y) - - assert all_almost_equal([x, y, z], [x_arr, y_arr, z_arr]) - - # non-aliased right - z_arr = op(y_arr, x_arr) - z = op(y, x) - - assert all_almost_equal([x, y, z], [x_arr, y_arr, z_arr]) - - # aliased operation - z_arr = op(y_arr, y_arr) - z = op(y, y) - - assert all_almost_equal([x, y, z], [x_arr, y_arr, z_arr]) - - -def test_ufuncs(): - # Cannot use fixture due to bug in pytest - H = odl.ProductSpace(odl.rn(1), odl.rn(2)) - - # one arg - x = H.element([[-1], [-2, -3]]) - - z = x.ufuncs.absolute() - assert all_almost_equal(z, [[1], [2, 3]]) - - # one arg with out - x = H.element([[-1], [-2, -3]]) - y = H.element() - - z = x.ufuncs.absolute(out=y) - assert y is z - assert all_almost_equal(z, [[1], [2, 3]]) - - # Two args - x = H.element([[1], [2, 3]]) - y = H.element([[4], [5, 6]]) - w = H.element() - - z = x.ufuncs.add(y) - assert all_almost_equal(z, [[5], [7, 9]]) - - # Two args with out - x = H.element([[1], [2, 3]]) - y = H.element([[4], [5, 6]]) - w = H.element() - - z = x.ufuncs.add(y, out=w) - assert w is z - assert all_almost_equal(z, [[5], [7, 9]]) - - -def test_reductions(): - H = odl.ProductSpace(odl.rn(1), odl.rn(2)) - x = H.element([[1], [2, 3]]) - assert x.ufuncs.sum() == 6.0 - assert x.ufuncs.prod() == 6.0 - assert x.ufuncs.min() == 1.0 - assert x.ufuncs.max() == 3.0 - - -def test_np_reductions(): - """Check that reductions via NumPy functions work.""" - H = odl.ProductSpace(odl.rn(2), 3) - x = 2 * H.one() - assert np.sum(x) == 2 * 6 - assert np.prod(x) == 2 ** 6 - - -def test_array_wrap_method(): - """Verify that the __array_wrap__ method for NumPy works.""" - space = odl.ProductSpace(odl.uniform_discr(0, 1, 10), 2) - x_arr, x = noise_elements(space) - y_arr = np.sin(x_arr) - y = np.sin(x) # Should yield again an ODL product space element - - assert y in space - assert all_equal(y, y_arr) - - -def test_real_imag_and_conj(): - """Verify that .real .imag and .conj() work for product space elements.""" - space = odl.ProductSpace(odl.uniform_discr(0, 1, 3, dtype=complex), - odl.cn(2)) - x = noise_element(space) - - # Test real - expected_result = space.real_space.element([x[0].real, x[1].real]) - assert x.real == expected_result - - # Test imag - expected_result = space.real_space.element([x[0].imag, x[1].imag]) - assert x.imag == expected_result - - # Test conj. Note that ProductSpace does not implement asarray if - # is_power_space is false. Hence the construction below - expected_result = space.element([x[0].conj(), x[1].conj()]) - x_conj = x.conj() - assert x_conj[0] == expected_result[0] - assert x_conj[1] == expected_result[1] - - -def test_real_setter_product_space(space, newpart): - """Verify that the setter for the real part of an element works.""" - x = noise_element(space) - x.real = newpart - - try: - # Catch the scalar - iter(newpart) - except TypeError: - expected_result = newpart * space.one() - else: - if newpart in space: - expected_result = newpart.real - elif np.shape(newpart) == (3,): - expected_result = [newpart, newpart] - else: - expected_result = newpart - - assert x in space - assert all_equal(x.real, expected_result) - - -def test_imag_setter_product_space(space, newpart): - """Verify that the setter for the imaginary part of an element works.""" - x = noise_element(space) - x.imag = newpart - - try: - # Catch the scalar - iter(newpart) - except TypeError: - expected_result = newpart * space.one() - else: - if newpart in space: - # The imaginary part is by definition real, and thus the new - # imaginary part is thus the real part of the element we try to set - # the value to - expected_result = newpart.real - elif np.shape(newpart) == (3,): - expected_result = [newpart, newpart] - else: - expected_result = newpart - - assert x in space - assert all_equal(x.imag, expected_result) +# TODO(kohr-h): ufunc tests if __name__ == '__main__': diff --git a/odl/test/space/space_utils_test.py b/odl/test/space/space_utils_test.py deleted file mode 100644 index 91c0fa67f87..00000000000 --- a/odl/test/space/space_utils_test.py +++ /dev/null @@ -1,80 +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 odl -from odl import vector -from odl.space.npy_tensors import NumpyTensor -from odl.util.testutils import all_equal - - -def test_vector_numpy(): - - # Rn - inp = [[1.0, 2.0, 3.0], - [4.0, 5.0, 6.0]] - - x = vector(inp) - assert isinstance(x, NumpyTensor) - assert x.dtype == np.dtype('float64') - assert all_equal(x, inp) - - x = vector([1.0, 2.0, float('inf')]) - assert x.dtype == np.dtype('float64') - assert isinstance(x, NumpyTensor) - - x = vector([1.0, 2.0, float('nan')]) - assert x.dtype == np.dtype('float64') - assert isinstance(x, NumpyTensor) - - x = vector([1, 2, 3], dtype='float32') - assert x.dtype == np.dtype('float32') - assert isinstance(x, NumpyTensor) - - # Cn - inp = [[1 + 1j, 2, 3 - 2j], - [4 + 1j, 5, 6 - 1j]] - - x = vector(inp) - assert isinstance(x, NumpyTensor) - assert x.dtype == np.dtype('complex128') - assert all_equal(x, inp) - - x = vector([1, 2, 3], dtype='complex64') - assert isinstance(x, NumpyTensor) - - # Generic TensorSpace - inp = [1, 2, 3] - x = vector(inp) - assert isinstance(x, NumpyTensor) - assert x.dtype == np.dtype('int') - assert all_equal(x, inp) - - inp = ['a', 'b', 'c'] - x = vector(inp) - assert isinstance(x, NumpyTensor) - assert np.issubdtype(x.dtype, np.str_) - assert all_equal(x, inp) - - x = vector([1, 2, 'inf']) # Becomes string type - assert isinstance(x, NumpyTensor) - assert np.issubdtype(x.dtype, np.str_) - assert all_equal(x, ['1', '2', 'inf']) - - # Scalar or empty input - x = vector(5.0) # becomes 1d, size 1 - assert x.shape == (1,) - - x = vector([]) # becomes 1d, size 0 - assert x.shape == (0,) - - -if __name__ == '__main__': - odl.util.test_file(__file__) diff --git a/odl/test/space/tensors_test.py b/odl/test/space/tensors_test.py index 4b1890e129f..d5dec829330 100644 --- a/odl/test/space/tensors_test.py +++ b/odl/test/space/tensors_test.py @@ -10,30 +10,15 @@ from __future__ import division -import operator -import sys - import numpy as np import pytest import odl -from odl.set.space import LinearSpaceTypeError -from odl.space.npy_tensors import ( - NumpyTensor, NumpyTensorSpace, NumpyTensorSpaceArrayWeighting, - NumpyTensorSpaceConstWeighting, NumpyTensorSpaceCustomDist, - NumpyTensorSpaceCustomInner, NumpyTensorSpaceCustomNorm) +from odl.space.npy_tensors import NumpyTensorSpace from odl.util.testutils import ( - all_almost_equal, all_equal, noise_array, noise_element, noise_elements, - simple_fixture) -from odl.util.ufuncs import UFUNCS - -# --- Test helpers --- # - -PYTHON2 = sys.version_info.major < 3 + all_almost_equal, all_equal, noise_array, noise_elements, simple_fixture) - -# Functions to return arrays and classes corresponding to impls. Extend -# when a new impl is available. +# --- Helpers --- # def _pos_array(space): @@ -49,45 +34,25 @@ def _array_cls(impl): assert False -def _odl_tensor_cls(impl): - """Return the ODL tensor class for given impl.""" - if impl == 'numpy': - return NumpyTensor - else: - assert False +def _inner(x1, x2, w): + return np.vdot(x2, w * x1) -def _weighting_cls(impl, kind): - """Return the weighting class for given impl and kind.""" - if impl == 'numpy': - if kind == 'array': - return NumpyTensorSpaceArrayWeighting - elif kind == 'const': - return NumpyTensorSpaceConstWeighting - elif kind == 'inner': - return NumpyTensorSpaceCustomInner - elif kind == 'norm': - return NumpyTensorSpaceCustomNorm - elif kind == 'dist': - return NumpyTensorSpaceCustomDist - else: - assert False +def _norm(x, p, w): + if p in {float('inf'), -float('inf')}: + return np.linalg.norm(x.ravel(), p) else: - assert False + return np.linalg.norm((w ** (1 / p) * x).ravel(), p) -# --- Pytest fixtures --- # +def _dist(x1, x2, p, w): + return _norm(x1 - x2, p, w) -exponent = simple_fixture('exponent', [2.0, 1.0, float('inf'), 0.5, 1.5]) -setitem_indices_params = [ - 0, [1], (1,), (0, 1), (0, 1, 2), slice(None), slice(None, None, 2), - (0, slice(None)), (slice(None), 0, slice(None, None, 2))] -setitem_indices = simple_fixture('indices', setitem_indices_params) +# --- pytest Fixtures --- # + -getitem_indices_params = (setitem_indices_params + - [([0, 1, 1, 0], [0, 1, 1, 2]), (Ellipsis, None)]) -getitem_indices = simple_fixture('indices', getitem_indices_params) +exponent = simple_fixture('exponent', [2.0, 1.0, float('inf'), 0.5, 1.5]) weight_params = [1.0, 0.5, _pos_array(odl.tensor_space((3, 4)))] weight_ids = [' weight=1.0 ', ' weight=0.5 ', ' weight= '] @@ -95,20 +60,22 @@ def _weighting_cls(impl, kind): # scope='module' removed due to pytest issue, see # https://github.com/pytest-dev/pytest/issues/6497 -# TODO: re-introduce when fixed +# TODO(kohr-h): re-introduce (fixed in pytest 5.4.0) @pytest.fixture(params=weight_params, ids=weight_ids) def weight(request): return request.param @pytest.fixture(scope='module') -def tspace(odl_floating_dtype, odl_tspace_impl): +def tspace(odl_floating_dtype, odl_tspace_impl, weight): impl = odl_tspace_impl dtype = odl_floating_dtype - return odl.tensor_space(shape=(3, 4), dtype=dtype, impl=impl) + return odl.tensor_space( + shape=(3, 4), dtype=dtype, impl=impl, weighting=weight + ) -# --- Space classes --- # +# --- Tests --- # def test_init_npy_tspace(): @@ -167,22 +134,7 @@ def test_init_tspace_weighting(weight, exponent, odl_tspace_impl): space = odl.tensor_space((3, 4), weighting=weight, exponent=exponent, impl=impl) - if impl == 'numpy': - if isinstance(weight, np.ndarray): - weighting_cls = _weighting_cls(impl, 'array') - else: - weighting_cls = _weighting_cls(impl, 'const') - else: - assert False - - weighting = weighting_cls(weight, exponent) - - assert space.weighting == weighting - - # Using a weighting instance - space = odl.tensor_space((3, 4), weighting=weighting, exponent=exponent, - impl=impl) - assert space.weighting is weighting + assert np.all(space.weighting == weight) # Errors for bad input with pytest.raises(ValueError): @@ -204,7 +156,6 @@ def test_properties(odl_tspace_impl): space = odl.tensor_space((3, 4), dtype='float32', exponent=1, weighting=2, impl=impl) x = space.element() - assert x.space is space assert x.ndim == space.ndim == 2 assert x.dtype == space.dtype == np.dtype('float32') assert x.size == space.size == 12 @@ -236,10 +187,10 @@ def test_element(tspace, odl_elem_order): order = odl_elem_order # From scratch elem = tspace.element(order=order) - assert elem.shape == elem.data.shape - assert elem.dtype == tspace.dtype == elem.data.dtype + assert elem.shape == tspace.shape + assert elem.dtype == tspace.dtype if order is not None: - assert elem.data.flags[order + '_CONTIGUOUS'] + assert elem.flags[order + '_CONTIGUOUS'] # From space elements other_elem = tspace.element(np.ones(tspace.shape)) @@ -248,43 +199,43 @@ def test_element(tspace, odl_elem_order): if order is None: assert elem is other_elem else: - assert elem.data.flags[order + '_CONTIGUOUS'] + assert elem.flags[order + '_CONTIGUOUS'] # From Numpy array (C order) arr_c = np.random.rand(*tspace.shape).astype(tspace.dtype) elem = tspace.element(arr_c, order=order) assert all_equal(elem, arr_c) - assert elem.shape == elem.data.shape - assert elem.dtype == tspace.dtype == elem.data.dtype + assert elem.shape == elem.shape + assert elem.dtype == tspace.dtype == elem.dtype if order is None or order == 'C': # None or same order should not lead to copy - assert np.may_share_memory(elem.data, arr_c) + assert np.may_share_memory(elem, arr_c) if order is not None: # Contiguousness in explicitly provided order should be guaranteed - assert elem.data.flags[order + '_CONTIGUOUS'] + assert elem.flags[order + '_CONTIGUOUS'] # From Numpy array (F order) arr_f = np.asfortranarray(arr_c) elem = tspace.element(arr_f, order=order) assert all_equal(elem, arr_f) - assert elem.shape == elem.data.shape - assert elem.dtype == tspace.dtype == elem.data.dtype + assert elem.shape == elem.shape + assert elem.dtype == tspace.dtype == elem.dtype if order is None or order == 'F': # None or same order should not lead to copy - assert np.may_share_memory(elem.data, arr_f) + assert np.may_share_memory(elem, arr_f) if order is not None: # Contiguousness in explicitly provided order should be guaranteed - assert elem.data.flags[order + '_CONTIGUOUS'] + assert elem.flags[order + '_CONTIGUOUS'] # From pointer arr_c_ptr = arr_c.ctypes.data elem = tspace.element(data_ptr=arr_c_ptr, order='C') assert all_equal(elem, arr_c) - assert np.may_share_memory(elem.data, arr_c) + assert np.may_share_memory(elem, arr_c) arr_f_ptr = arr_f.ctypes.data elem = tspace.element(data_ptr=arr_f_ptr, order='F') assert all_equal(elem, arr_f) - assert np.may_share_memory(elem.data, arr_f) + assert np.may_share_memory(elem, arr_f) # Check errors with pytest.raises(ValueError): @@ -311,25 +262,6 @@ def test_equals_space(odl_tspace_impl): assert hash(space) != hash(other_space) -def test_equals_elem(odl_tspace_impl): - """Test equality check of space elements.""" - impl = odl_tspace_impl - r3 = odl.rn(3, exponent=2, impl=impl) - r3_1 = odl.rn(3, exponent=1, impl=impl) - r4 = odl.rn(4, exponent=2, impl=impl) - r3_elem = r3.element([1, 2, 3]) - r3_same_elem = r3.element([1, 2, 3]) - r3_other_elem = r3.element([2, 2, 3]) - r3_1_elem = r3_1.element([1, 2, 3]) - r4_elem = r4.element([1, 2, 3, 4]) - - assert r3_elem == r3_elem - assert r3_elem == r3_same_elem - assert r3_elem != r3_other_elem - assert r3_elem != r3_1_elem - assert r3_elem != r4_elem - - def test_tspace_astype(odl_tspace_impl): """Test creation of a space counterpart with new dtype.""" impl = odl_tspace_impl @@ -367,7 +299,7 @@ def _test_lincomb(space, a, b, discontig): slc = tuple( [slice(None)] * (space.ndim - 1) + [slice(None, None, 2)] ) - res_space = space.element()[slc].space + res_space = space[slc] else: res_space = space @@ -430,6 +362,7 @@ def test_lincomb(tspace): _test_lincomb(tspace, a, b, discontig=False) +@pytest.mark.xfail(reason='need space indexing to test this') def test_lincomb_discontig(odl_tspace_impl): """Test lincomb with discontiguous input.""" impl = odl_tspace_impl @@ -451,27 +384,16 @@ def test_lincomb_discontig(odl_tspace_impl): _test_lincomb(tspace, a, b, discontig=True) -def test_lincomb_raise(tspace): - """Test if lincomb raises correctly for bad input.""" +def test_lincomb_exceptions(tspace): + """Test whether lincomb raises correctly for bad output element.""" other_space = odl.rn((4, 3), impl=tspace.impl) - other_x = other_space.zero() - x, y, z = tspace.zero(), tspace.zero(), tspace.zero() - - with pytest.raises(LinearSpaceTypeError): - tspace.lincomb(1, other_x, 1, y, z) - - with pytest.raises(LinearSpaceTypeError): - tspace.lincomb(1, y, 1, other_x, z) - - with pytest.raises(LinearSpaceTypeError): - tspace.lincomb(1, y, 1, z, other_x) - - with pytest.raises(LinearSpaceTypeError): - tspace.lincomb([], x, 1, y, z) + wrong_out = other_space.zero() + x, y = tspace.zero(), tspace.zero() - with pytest.raises(LinearSpaceTypeError): - tspace.lincomb(1, x, [], y, z) + with pytest.raises(TypeError): + # Only `out` must be an element of the space, thus raising TypeError + tspace.lincomb(1, x, 1, y, wrong_out) def test_multiply(tspace): @@ -483,193 +405,32 @@ def test_multiply(tspace): tspace.multiply(x, y, out) assert all_almost_equal([x_arr, y_arr, out_arr], [x, y, out]) - # member method - [x_arr, y_arr, out_arr], [x, y, out] = noise_elements(tspace, 3) - out_arr = x_arr * y_arr - - x.multiply(y, out=out) - assert all_almost_equal([x_arr, y_arr, out_arr], [x, y, out]) - def test_multiply_exceptions(tspace): - """Test if multiply raises correctly for bad input.""" + """Test if multiply raises correctly for bad output element.""" other_space = odl.rn((4, 3)) - other_x = other_space.zero() + wrong_out = other_space.zero() x, y = tspace.zero(), tspace.zero() - with pytest.raises(LinearSpaceTypeError): - tspace.multiply(other_x, x, y) - - with pytest.raises(LinearSpaceTypeError): - tspace.multiply(x, other_x, y) - - with pytest.raises(LinearSpaceTypeError): - tspace.multiply(x, y, other_x) - - -def test_power(tspace): - """Test ``**`` against direct array exponentiation.""" - [x_arr, y_arr], [x, y] = noise_elements(tspace, n=2) - y_pos = tspace.element(np.abs(y) + 0.1) - y_pos_arr = np.abs(y_arr) + 0.1 - - # Testing standard positive integer power out-of-place and in-place - assert all_almost_equal(x ** 2, x_arr ** 2) - y **= 2 - y_arr **= 2 - assert all_almost_equal(y, y_arr) - - # Real number and negative integer power - assert all_almost_equal(y_pos ** 1.3, y_pos_arr ** 1.3) - assert all_almost_equal(y_pos ** (-3), y_pos_arr ** (-3)) - y_pos **= 2.5 - y_pos_arr **= 2.5 - assert all_almost_equal(y_pos, y_pos_arr) - - # Array raised to the power of another array, entry-wise - assert all_almost_equal(y_pos ** x, y_pos_arr ** x_arr) - y_pos **= x.real - y_pos_arr **= x_arr.real - assert all_almost_equal(y_pos, y_pos_arr) - - -def test_unary_ops(tspace): - """Verify that the unary operators (`+x` and `-x`) work as expected.""" - for op in [operator.pos, operator.neg]: - x_arr, x = noise_elements(tspace) - - y_arr = op(x_arr) - y = op(x) - - assert all_almost_equal([x, y], [x_arr, y_arr]) - - -def test_scalar_operator(tspace, odl_arithmetic_op): - """Verify binary operations with scalars. - - Verifies that the statement y = op(x, scalar) gives equivalent results - to NumPy. - """ - op = odl_arithmetic_op - if op in (operator.truediv, operator.itruediv): - ndigits = int(-np.log10(np.finfo(tspace.dtype).resolution) // 2) - else: - ndigits = int(-np.log10(np.finfo(tspace.dtype).resolution)) - - for scalar in [-31.2, -1, 0, 1, 2.13]: - x_arr, x = noise_elements(tspace) - - # Left op - if scalar == 0 and op in [operator.truediv, operator.itruediv]: - # Check for correct zero division behaviour - with pytest.raises(ZeroDivisionError): - y = op(x, scalar) - else: - y_arr = op(x_arr, scalar) - y = op(x, scalar) - - assert all_almost_equal([x, y], [x_arr, y_arr], ndigits) - - # right op - x_arr, x = noise_elements(tspace) - - y_arr = op(scalar, x_arr) - y = op(scalar, x) - - assert all_almost_equal([x, y], [x_arr, y_arr], ndigits) - - -def test_binary_operator(tspace, odl_arithmetic_op): - """Verify binary operations with tensors. - - Verifies that the statement z = op(x, y) gives equivalent results - to NumPy. - """ - op = odl_arithmetic_op - if op in (operator.truediv, operator.itruediv): - ndigits = int(-np.log10(np.finfo(tspace.dtype).resolution) // 2) - else: - ndigits = int(-np.log10(np.finfo(tspace.dtype).resolution)) - - [x_arr, y_arr], [x, y] = noise_elements(tspace, 2) - - # non-aliased left - z_arr = op(x_arr, y_arr) - z = op(x, y) - - assert all_almost_equal([x, y, z], [x_arr, y_arr, z_arr], ndigits) - - # non-aliased right - z_arr = op(y_arr, x_arr) - z = op(y, x) - - assert all_almost_equal([x, y, z], [x_arr, y_arr, z_arr], ndigits) - - # aliased operation - z_arr = op(x_arr, x_arr) - z = op(x, x) - - assert all_almost_equal([x, y, z], [x_arr, y_arr, z_arr], ndigits) - - -def test_assign(tspace): - """Test the assign method using ``==`` comparison.""" - x = noise_element(tspace) - x_old = x - y = noise_element(tspace) - - y.assign(x) - - assert y == x - assert y is not x - assert x is x_old - - # test alignment - x *= 2 - assert y != x + with pytest.raises(TypeError): + tspace.multiply(x, y, wrong_out) def test_inner(tspace): """Test the inner method against numpy.vdot.""" - xd = noise_element(tspace) - yd = noise_element(tspace) - - # TODO: add weighting - correct_inner = np.vdot(yd, xd) - assert tspace.inner(xd, yd) == pytest.approx(correct_inner) - assert xd.inner(yd) == pytest.approx(correct_inner) - - -def test_inner_exceptions(tspace): - """Test if inner raises correctly for bad input.""" - other_space = odl.rn((4, 3)) - other_x = other_space.zero() - x = tspace.zero() - - with pytest.raises(LinearSpaceTypeError): - tspace.inner(other_x, x) - - with pytest.raises(LinearSpaceTypeError): - tspace.inner(x, other_x) + rel = 1e-2 if tspace.dtype == 'float16' else 1e-5 + (xarr, yarr), (x, y) = noise_elements(tspace, 2) + correct_inner = _inner(xarr, yarr, tspace.weighting) + assert tspace.inner(x, y) == pytest.approx(correct_inner, rel=rel) def test_norm(tspace): """Test the norm method against numpy.linalg.norm.""" + rel = 1e-2 if tspace.dtype == 'float16' else 1e-5 xarr, x = noise_elements(tspace) - - correct_norm = np.linalg.norm(xarr.ravel()) - assert tspace.norm(x) == pytest.approx(correct_norm) - assert x.norm() == pytest.approx(correct_norm) - - -def test_norm_exceptions(tspace): - """Test if norm raises correctly for bad input.""" - other_space = odl.rn((4, 3)) - other_x = other_space.zero() - - with pytest.raises(LinearSpaceTypeError): - tspace.norm(other_x) + correct_norm = _norm(xarr, tspace.exponent, tspace.weighting) + assert tspace.norm(x) == pytest.approx(correct_norm, rel=rel) def test_pnorm(exponent): @@ -677,32 +438,16 @@ def test_pnorm(exponent): for tspace in (odl.rn((3, 4), exponent=exponent), odl.cn((3, 4), exponent=exponent)): xarr, x = noise_elements(tspace) - correct_norm = np.linalg.norm(xarr.ravel(), ord=exponent) - + correct_norm = _norm(xarr, exponent, 1.0) assert tspace.norm(x) == pytest.approx(correct_norm) - assert x.norm() == pytest.approx(correct_norm) def test_dist(tspace): """Test the dist method against numpy.linalg.norm of the difference.""" - [xarr, yarr], [x, y] = noise_elements(tspace, n=2) - - correct_dist = np.linalg.norm((xarr - yarr).ravel()) - assert tspace.dist(x, y) == pytest.approx(correct_dist) - assert x.dist(y) == pytest.approx(correct_dist) - - -def test_dist_exceptions(tspace): - """Test if dist raises correctly for bad input.""" - other_space = odl.rn((4, 3)) - other_x = other_space.zero() - x = tspace.zero() - - with pytest.raises(LinearSpaceTypeError): - tspace.dist(other_x, x) - - with pytest.raises(LinearSpaceTypeError): - tspace.dist(x, other_x) + rel = 1e-2 if tspace.dtype == 'float16' else 1e-5 + (xarr, yarr), (x, y) = noise_elements(tspace, n=2) + correct_dist = _dist(xarr, yarr, tspace.exponent, tspace.weighting) + assert tspace.dist(x, y) == pytest.approx(correct_dist, rel=rel) def test_pdist(odl_tspace_impl, exponent): @@ -712,1002 +457,11 @@ def test_pdist(odl_tspace_impl, exponent): cls = odl.space.entry_points.tensor_space_impl(impl) if complex in cls.available_dtypes(): spaces.append(odl.cn((3, 4), exponent=exponent, impl=impl)) - for space in spaces: - [xarr, yarr], [x, y] = noise_elements(space, n=2) - - correct_dist = np.linalg.norm((xarr - yarr).ravel(), ord=exponent) - assert space.dist(x, y) == pytest.approx(correct_dist) - assert x.dist(y) == pytest.approx(correct_dist) - - -def test_element_getitem(odl_tspace_impl, getitem_indices): - """Check if getitem produces correct values, shape and other stuff.""" - impl = odl_tspace_impl - space = odl.tensor_space((2, 3, 4), dtype='float32', exponent=1, - weighting=2, impl=impl) - x_arr, x = noise_elements(space) - - x_arr_sliced = x_arr[getitem_indices] - sliced_shape = x_arr_sliced.shape - x_sliced = x[getitem_indices] - - if np.isscalar(x_arr_sliced): - assert x_arr_sliced == x_sliced - else: - assert x_sliced.shape == sliced_shape - assert all_equal(x_sliced, x_arr_sliced) - - # Check that the space properties are preserved - sliced_spc = x_sliced.space - assert sliced_spc.shape == sliced_shape - assert sliced_spc.dtype == space.dtype - assert sliced_spc.exponent == space.exponent - assert sliced_spc.weighting == space.weighting - - # Check that we have a view that manipulates the original array - # (or not, depending on indexing style) - x_arr_sliced[:] = 0 - x_sliced[:] = 0 - assert all_equal(x_arr, x) - - -def test_element_setitem(odl_tspace_impl, setitem_indices): - """Check if setitem produces the same result as NumPy.""" - impl = odl_tspace_impl - space = odl.tensor_space((2, 3, 4), dtype='float32', exponent=1, - weighting=2, impl=impl) - x_arr, x = noise_elements(space) - - x_arr_sliced = x_arr[setitem_indices] - sliced_shape = x_arr_sliced.shape - - # Setting values with scalars - x_arr[setitem_indices] = 2.3 - x[setitem_indices] = 2.3 - assert all_equal(x, x_arr) - - # Setting values with arrays - rhs_arr = np.ones(sliced_shape) - x_arr[setitem_indices] = rhs_arr - x[setitem_indices] = rhs_arr - assert all_equal(x, x_arr) - - # Using a list of lists - rhs_list = (-np.ones(sliced_shape)).tolist() - x_arr[setitem_indices] = rhs_list - x[setitem_indices] = rhs_list - assert all_equal(x, x_arr) - - -def test_element_getitem_bool_array(odl_tspace_impl): - """Check if getitem with boolean array yields the same result as NumPy.""" - impl = odl_tspace_impl - space = odl.tensor_space((2, 3, 4), dtype='float32', exponent=1, - weighting=2, impl=impl) - bool_space = odl.tensor_space((2, 3, 4), dtype=bool) - x_arr, x = noise_elements(space) - cond_arr, cond = noise_elements(bool_space) - - x_arr_sliced = x_arr[cond_arr] - x_sliced = x[cond] - assert all_equal(x_arr_sliced, x_sliced) - - # Check that the space properties are preserved - sliced_spc = x_sliced.space - assert sliced_spc.shape == x_arr_sliced.shape - assert sliced_spc.dtype == space.dtype - assert sliced_spc.exponent == space.exponent - assert sliced_spc.weighting == space.weighting - - -def test_element_setitem_bool_array(odl_tspace_impl): - """Check if setitem produces the same result as NumPy.""" - impl = odl_tspace_impl - space = odl.tensor_space((2, 3, 4), dtype='float32', exponent=1, - weighting=2, impl=impl) - bool_space = odl.tensor_space((2, 3, 4), dtype=bool) - x_arr, x = noise_elements(space) - cond_arr, cond = noise_elements(bool_space) - - x_arr_sliced = x_arr[cond_arr] - sliced_shape = x_arr_sliced.shape - - # Setting values with scalars - x_arr[cond_arr] = 2.3 - x[cond] = 2.3 - assert all_equal(x, x_arr) - - # Setting values with arrays - rhs_arr = np.ones(sliced_shape) - x_arr[cond_arr] = rhs_arr - x[cond] = rhs_arr - assert all_equal(x, x_arr) - - # Using a list of lists - rhs_list = (-np.ones(sliced_shape)).tolist() - x_arr[cond_arr] = rhs_list - x[cond] = rhs_list - assert all_equal(x, x_arr) - - -def test_transpose(odl_tspace_impl): - """Test the .T property of tensors against plain inner product.""" - impl = odl_tspace_impl - spaces = [odl.rn((3, 4), impl=impl)] - cls = odl.space.entry_points.tensor_space_impl(impl) - if complex in cls.available_dtypes(): - spaces.append(odl.cn((3, 4), impl=impl)) for space in spaces: - x = noise_element(space) - y = noise_element(space) - - # Assert linear operator - assert isinstance(x.T, odl.Operator) - assert x.T.is_linear - - # Check result - assert x.T(y) == pytest.approx(y.inner(x)) - assert all_equal(x.T.adjoint(1.0), x) - - # x.T.T returns self - assert x.T.T == x - - -def test_multiply_by_scalar(tspace): - """Verify that mult. with NumPy scalars preserves the element type.""" - x = tspace.zero() - assert x * 1.0 in tspace - assert x * np.float32(1.0) in tspace - assert 1.0 * x in tspace - assert np.float32(1.0) * x in tspace - - -def test_member_copy(odl_tspace_impl): - """Test copy method of elements.""" - impl = odl_tspace_impl - space = odl.tensor_space((3, 4), dtype='float32', exponent=1, weighting=2, - impl=impl) - x = noise_element(space) - - y = x.copy() - assert x == y - assert y is not x - - # Check that result is not aliased - x *= 2 - assert x != y - - -def test_python_copy(odl_tspace_impl): - """Test compatibility with the Python copy module.""" - import copy - impl = odl_tspace_impl - space = odl.tensor_space((3, 4), dtype='float32', exponent=1, weighting=2, - impl=impl) - x = noise_element(space) - - # Shallow copy - y = copy.copy(x) - assert x == y - assert y is not x - - # Check that result is not aliased - x *= 2 - assert x != y - - # Deep copy - z = copy.deepcopy(x) - assert x == z - assert z is not x - - # Check that result is not aliased - x *= 2 - assert x != z - - -def test_conversion_to_scalar(odl_tspace_impl): - """Test conversion of size-1 vectors/tensors to scalars.""" - impl = odl_tspace_impl - space = odl.rn(1, impl=impl) - # Size 1 real space - value = 1.5 - element = space.element(value) - - assert int(element) == int(value) - assert float(element) == float(value) - assert complex(element) == complex(value) - if PYTHON2: - assert long(element) == long(value) - - # Size 1 complex space - value = 1.5 + 0.5j - element = odl.cn(1).element(value) - assert complex(element) == complex(value) - - # Size 1 multi-dimensional space - value = 2.1 - element = odl.rn((1, 1, 1)).element(value) - assert float(element) == float(value) - - # Too large space - element = odl.rn(2).one() - - with pytest.raises(TypeError): - int(element) - with pytest.raises(TypeError): - float(element) - with pytest.raises(TypeError): - complex(element) - if PYTHON2: - with pytest.raises(TypeError): - long(element) - - -def test_bool_conversion(odl_tspace_impl): - """Verify that the __bool__ function works.""" - impl = odl_tspace_impl - space = odl.tensor_space(2, dtype='float32', impl=impl) - x = space.element([0, 1]) - - with pytest.raises(ValueError): - bool(x) - assert np.any(x) - assert any(x) - assert not np.all(x) - assert not all(x) - - space = odl.tensor_space(1, dtype='float32', impl=impl) - x = space.one() - - assert np.any(x) - assert any(x) - assert np.all(x) - assert all(x) - - -def test_numpy_array_interface(odl_tspace_impl): - """Verify that the __array__ interface for NumPy works.""" - impl = odl_tspace_impl - space = odl.tensor_space((3, 4), dtype='float32', exponent=1, weighting=2, - impl=impl) - x = space.one() - arr = x.__array__() - - assert isinstance(arr, np.ndarray) - assert np.array_equal(arr, np.ones(x.shape)) - - x_arr = np.array(x) - assert np.array_equal(x_arr, np.ones(x.shape)) - x_as_arr = np.asarray(x) - assert np.array_equal(x_as_arr, np.ones(x.shape)) - x_as_any_arr = np.asanyarray(x) - assert np.array_equal(x_as_any_arr, np.ones(x.shape)) - - -def test_array_wrap_method(odl_tspace_impl): - """Verify that the __array_wrap__ method for NumPy works.""" - impl = odl_tspace_impl - space = odl.tensor_space((3, 4), dtype='float32', exponent=1, weighting=2, - impl=impl) - x_arr, x = noise_elements(space) - y_arr = np.sin(x_arr) - y = np.sin(x) # Should yield again an ODL tensor - - assert all_equal(y, y_arr) - assert y in space - - -def test_conj(tspace): - """Test complex conjugation of tensors.""" - xarr, x = noise_elements(tspace) - - xconj = x.conj() - assert all_equal(xconj, xarr.conj()) - - y = tspace.element() - xconj = x.conj(out=y) - assert xconj is y - assert all_equal(y, xarr.conj()) - - -# --- Weightings (Numpy) --- # - - -def test_array_weighting_init(odl_tspace_impl, exponent): - """Test initialization of array weightings.""" - impl = odl_tspace_impl - space = odl.rn((3, 4), impl=impl) - weight_arr = _pos_array(space) - weight_elem = space.element(weight_arr) - - weighting_cls = _weighting_cls(impl, 'array') - weighting_arr = weighting_cls(weight_arr, exponent=exponent) - weighting_elem = weighting_cls(weight_elem, exponent=exponent) - - assert isinstance(weighting_arr.array, _array_cls(impl)) - assert isinstance(weighting_elem.array, _array_cls(impl)) - - -def test_array_weighting_array_is_valid(odl_tspace_impl): - """Test the is_valid method of array weightings.""" - impl = odl_tspace_impl - space = odl.rn((3, 4), impl=impl) - weight_arr = _pos_array(space) - - weighting_cls = _weighting_cls(impl, 'array') - weighting_arr = weighting_cls(weight_arr) - - assert weighting_arr.is_valid() - - # Invalid - weight_arr[0] = 0 - weighting_arr = NumpyTensorSpaceArrayWeighting(weight_arr) - assert not weighting_arr.is_valid() - - -def test_array_weighting_equals(odl_tspace_impl): - """Test the equality check method of array weightings.""" - impl = odl_tspace_impl - space = odl.rn(5, impl=impl) - weight_arr = _pos_array(space) - weight_elem = space.element(weight_arr) - - weighting_cls = _weighting_cls(impl, 'array') - weighting_arr = weighting_cls(weight_arr) - weighting_arr2 = weighting_cls(weight_arr) - weighting_elem = weighting_cls(weight_elem) - weighting_elem_copy = weighting_cls(weight_elem.copy()) - weighting_elem2 = weighting_cls(weight_elem) - weighting_other_arr = weighting_cls(weight_arr - 1) - weighting_other_exp = weighting_cls(weight_arr - 1, exponent=1) - - assert weighting_arr == weighting_arr2 - assert weighting_arr == weighting_elem - assert weighting_arr != weighting_elem_copy - assert weighting_elem == weighting_elem2 - assert weighting_arr != weighting_other_arr - assert weighting_arr != weighting_other_exp - - -def test_array_weighting_equiv(odl_tspace_impl): - """Test the equiv method of Numpy array weightings.""" - impl = odl_tspace_impl - space = odl.rn(5, impl=impl) - weight_arr = _pos_array(space) - weight_elem = space.element(weight_arr) - different_arr = weight_arr + 1 - - arr_weighting_cls = _weighting_cls(impl, 'array') - w_arr = arr_weighting_cls(weight_arr) - w_elem = arr_weighting_cls(weight_elem) - w_different_arr = arr_weighting_cls(different_arr) - - # Equal -> True - assert w_arr.equiv(w_arr) - assert w_arr.equiv(w_elem) - # Different array -> False - assert not w_arr.equiv(w_different_arr) - - # Test shortcuts in the implementation - const_arr = np.ones(space.shape) * 1.5 - - const_weighting_cls = _weighting_cls(impl, 'const') - w_const_arr = arr_weighting_cls(const_arr) - w_const = const_weighting_cls(1.5) - w_wrong_const = const_weighting_cls(1) - w_wrong_exp = const_weighting_cls(1.5, exponent=1) - - assert w_const_arr.equiv(w_const) - assert not w_const_arr.equiv(w_wrong_const) - assert not w_const_arr.equiv(w_wrong_exp) - - # Bogus input - assert not w_const_arr.equiv(True) - assert not w_const_arr.equiv(object) - assert not w_const_arr.equiv(None) - - -def test_array_weighting_inner(tspace): - """Test inner product in a weighted space.""" - [xarr, yarr], [x, y] = noise_elements(tspace, 2) - - weight_arr = _pos_array(tspace) - weighting = NumpyTensorSpaceArrayWeighting(weight_arr) - - true_inner = np.vdot(yarr, xarr * weight_arr) - assert weighting.inner(x, y) == pytest.approx(true_inner) - - # Exponent != 2 -> no inner product, should raise - with pytest.raises(NotImplementedError): - NumpyTensorSpaceArrayWeighting(weight_arr, exponent=1.0).inner(x, y) - - -def test_array_weighting_norm(tspace, exponent): - """Test norm in a weighted space.""" - rtol = np.sqrt(np.finfo(tspace.dtype).resolution) - xarr, x = noise_elements(tspace) - - weight_arr = _pos_array(tspace) - weighting = NumpyTensorSpaceArrayWeighting(weight_arr, exponent=exponent) - - if exponent == float('inf'): - true_norm = np.linalg.norm( - (weight_arr * xarr).ravel(), - ord=float('inf')) - else: - true_norm = np.linalg.norm( - (weight_arr ** (1 / exponent) * xarr).ravel(), - ord=exponent) - - assert weighting.norm(x) == pytest.approx(true_norm, rel=rtol) - - -def test_array_weighting_dist(tspace, exponent): - """Test dist product in a weighted space.""" - rtol = np.sqrt(np.finfo(tspace.dtype).resolution) - [xarr, yarr], [x, y] = noise_elements(tspace, n=2) - - weight_arr = _pos_array(tspace) - weighting = NumpyTensorSpaceArrayWeighting(weight_arr, exponent=exponent) - - if exponent == float('inf'): - true_dist = np.linalg.norm( - (weight_arr * (xarr - yarr)).ravel(), - ord=float('inf')) - else: - true_dist = np.linalg.norm( - (weight_arr ** (1 / exponent) * (xarr - yarr)).ravel(), - ord=exponent) - - assert weighting.dist(x, y) == pytest.approx(true_dist, rel=rtol) - - -def test_const_weighting_init(odl_tspace_impl, exponent): - """Test initialization of constant weightings.""" - impl = odl_tspace_impl - constant = 1.5 - - # Just test if the code runs - weighting_cls = _weighting_cls(impl, 'const') - weighting_cls(constant, exponent=exponent) - - with pytest.raises(ValueError): - weighting_cls(0) - with pytest.raises(ValueError): - weighting_cls(-1) - with pytest.raises(ValueError): - weighting_cls(float('inf')) - - -def test_const_weighting_comparison(odl_tspace_impl): - """Test equality to and equivalence with const weightings.""" - impl = odl_tspace_impl - constant = 1.5 - - const_weighting_cls = _weighting_cls(impl, 'const') - w_const = const_weighting_cls(constant) - w_const2 = const_weighting_cls(constant) - w_other_const = const_weighting_cls(constant + 1) - w_other_exp = const_weighting_cls(constant, exponent=1) - - const_arr = constant * np.ones((3, 4)) - - arr_weighting_cls = _weighting_cls(impl, 'array') - w_const_arr = arr_weighting_cls(const_arr) - other_const_arr = (constant + 1) * np.ones((3, 4)) - w_other_const_arr = arr_weighting_cls(other_const_arr) - - assert w_const == w_const - assert w_const == w_const2 - assert w_const2 == w_const - # Different but equivalent - assert w_const.equiv(w_const_arr) - assert w_const != w_const_arr - - # Not equivalent - assert not w_const.equiv(w_other_exp) - assert w_const != w_other_exp - assert not w_const.equiv(w_other_const) - assert w_const != w_other_const - assert not w_const.equiv(w_other_const_arr) - assert w_const != w_other_const_arr - - # Bogus input - assert not w_const.equiv(True) - assert not w_const.equiv(object) - assert not w_const.equiv(None) - - -def test_const_weighting_inner(tspace): - """Test inner product with const weighting.""" - [xarr, yarr], [x, y] = noise_elements(tspace, 2) - - constant = 1.5 - true_result_const = constant * np.vdot(yarr, xarr) - - w_const = NumpyTensorSpaceConstWeighting(constant) - assert w_const.inner(x, y) == pytest.approx(true_result_const) - - # Exponent != 2 -> no inner - w_const = NumpyTensorSpaceConstWeighting(constant, exponent=1) - with pytest.raises(NotImplementedError): - w_const.inner(x, y) - - -def test_const_weighting_norm(tspace, exponent): - """Test norm with const weighting.""" - xarr, x = noise_elements(tspace) - - constant = 1.5 - if exponent == float('inf'): - factor = constant - else: - factor = constant ** (1 / exponent) - true_norm = factor * np.linalg.norm(xarr.ravel(), ord=exponent) - - w_const = NumpyTensorSpaceConstWeighting(constant, exponent=exponent) - assert w_const.norm(x) == pytest.approx(true_norm) - - -def test_const_weighting_dist(tspace, exponent): - """Test dist with const weighting.""" - [xarr, yarr], [x, y] = noise_elements(tspace, 2) - - constant = 1.5 - if exponent == float('inf'): - factor = constant - else: - factor = constant ** (1 / exponent) - true_dist = factor * np.linalg.norm((xarr - yarr).ravel(), ord=exponent) - - w_const = NumpyTensorSpaceConstWeighting(constant, exponent=exponent) - assert w_const.dist(x, y) == pytest.approx(true_dist) - - -def test_custom_inner(tspace): - """Test weighting with a custom inner product.""" - rtol = np.sqrt(np.finfo(tspace.dtype).resolution) - - [xarr, yarr], [x, y] = noise_elements(tspace, 2) - - def inner(x, y): - return np.vdot(y, x) - - w = NumpyTensorSpaceCustomInner(inner) - w_same = NumpyTensorSpaceCustomInner(inner) - w_other = NumpyTensorSpaceCustomInner(np.dot) - - assert w == w - assert w == w_same - assert w != w_other - - true_inner = inner(xarr, yarr) - assert w.inner(x, y) == pytest.approx(true_inner) - - true_norm = np.linalg.norm(xarr.ravel()) - assert w.norm(x) == pytest.approx(true_norm) - - true_dist = np.linalg.norm((xarr - yarr).ravel()) - assert w.dist(x, y) == pytest.approx(true_dist, rel=rtol) - - with pytest.raises(TypeError): - NumpyTensorSpaceCustomInner(1) - - -def test_custom_norm(tspace): - """Test weighting with a custom norm.""" - [xarr, yarr], [x, y] = noise_elements(tspace, 2) - - norm = np.linalg.norm - - def other_norm(x): - return np.linalg.norm(x, ord=1) - - w = NumpyTensorSpaceCustomNorm(norm) - w_same = NumpyTensorSpaceCustomNorm(norm) - w_other = NumpyTensorSpaceCustomNorm(other_norm) - - assert w == w - assert w == w_same - assert w != w_other - - with pytest.raises(NotImplementedError): - w.inner(x, y) - - true_norm = np.linalg.norm(xarr.ravel()) - assert w.norm(x) == pytest.approx(true_norm) - - true_dist = np.linalg.norm((xarr - yarr).ravel()) - assert w.dist(x, y) == pytest.approx(true_dist) - - with pytest.raises(TypeError): - NumpyTensorSpaceCustomNorm(1) - - -def test_custom_dist(tspace): - """Test weighting with a custom dist.""" - [xarr, yarr], [x, y] = noise_elements(tspace, 2) - - def dist(x, y): - return np.linalg.norm(x - y) - - def other_dist(x, y): - return np.linalg.norm(x - y, ord=1) - - w = NumpyTensorSpaceCustomDist(dist) - w_same = NumpyTensorSpaceCustomDist(dist) - w_other = NumpyTensorSpaceCustomDist(other_dist) - - assert w == w - assert w == w_same - assert w != w_other - - with pytest.raises(NotImplementedError): - w.inner(x, y) - - with pytest.raises(NotImplementedError): - w.norm(x) - - true_dist = np.linalg.norm((xarr - yarr).ravel()) - assert w.dist(x, y) == pytest.approx(true_dist) - - with pytest.raises(TypeError): - NumpyTensorSpaceCustomDist(1) - - -# --- Ufuncs & Reductions --- # - - -def test_ufuncs(tspace, odl_ufunc): - """Test ufuncs in ``x.ufuncs`` against direct Numpy ufuncs.""" - name = odl_ufunc - - # Get the ufunc from numpy as reference, plus some additional info - npy_ufunc = getattr(np, name) - nin = npy_ufunc.nin - nout = npy_ufunc.nout - - if (np.issubsctype(tspace.dtype, np.floating) or - np.issubsctype(tspace.dtype, np.complexfloating) and - name in ['bitwise_and', - 'bitwise_or', - 'bitwise_xor', - 'invert', - 'left_shift', - 'right_shift']): - # Skip integer only methods for floating point data types - return - - if (np.issubsctype(tspace.dtype, np.complexfloating) and - name in ['remainder', - 'trunc', - 'signbit', - 'invert', - 'left_shift', - 'right_shift', - 'rad2deg', - 'deg2rad', - 'copysign', - 'mod', - 'modf', - 'fmod', - 'logaddexp2', - 'logaddexp', - 'hypot', - 'arctan2', - 'floor', - 'ceil']): - # Skip real-only methods for complex data types - return - - # Create some data - arrays, elements = noise_elements(tspace, nin + nout) - in_arrays = arrays[:nin] - out_arrays = arrays[nin:] - data_elem = elements[0] - - out_elems = elements[nin:] - if nout == 1: - out_arr_kwargs = {'out': out_arrays[0]} - out_elem_kwargs = {'out': out_elems[0]} - elif nout > 1: - out_arr_kwargs = {'out': out_arrays[:nout]} - out_elem_kwargs = {'out': out_elems[:nout]} - - # Get function to call, using both interfaces: - # - vec.ufunc(other_args) - # - np.ufunc(vec, other_args) - elem_fun_old = getattr(data_elem.ufuncs, name) - in_elems_old = elements[1:nin] - elem_fun_new = npy_ufunc - in_elems_new = elements[:nin] - - # Out-of-place - npy_result = npy_ufunc(*in_arrays) - odl_result_old = elem_fun_old(*in_elems_old) - assert all_almost_equal(npy_result, odl_result_old) - odl_result_new = elem_fun_new(*in_elems_new) - assert all_almost_equal(npy_result, odl_result_new) - - # Test type of output - if nout == 1: - assert isinstance(odl_result_old, tspace.element_type) - assert isinstance(odl_result_new, tspace.element_type) - elif nout > 1: - for i in range(nout): - assert isinstance(odl_result_old[i], tspace.element_type) - assert isinstance(odl_result_new[i], tspace.element_type) - - # In-place with ODL objects as `out` - npy_result = npy_ufunc(*in_arrays, **out_arr_kwargs) - odl_result_old = elem_fun_old(*in_elems_old, **out_elem_kwargs) - assert all_almost_equal(npy_result, odl_result_old) - # In-place will not work with Numpy < 1.13 - odl_result_new = elem_fun_new(*in_elems_new, **out_elem_kwargs) - assert all_almost_equal(npy_result, odl_result_new) - - # Check that returned stuff refers to given out - if nout == 1: - assert odl_result_old is out_elems[0] - assert odl_result_new is out_elems[0] - elif nout > 1: - for i in range(nout): - assert odl_result_old[i] is out_elems[i] - assert odl_result_new[i] is out_elems[i] - - # In-place with Numpy array as `out` for new interface - out_arrays_new = [np.empty_like(arr) for arr in out_arrays] - if nout == 1: - out_elem_kwargs_new = {'out': out_arrays_new[0]} - elif nout > 1: - out_elem_kwargs_new = {'out': out_arrays_new[:nout]} - - odl_result_elem_new = elem_fun_new(*in_elems_new, - **out_elem_kwargs_new) - assert all_almost_equal(npy_result, odl_result_elem_new) - - if nout == 1: - assert odl_result_elem_new is out_arrays_new[0] - elif nout > 1: - for i in range(nout): - assert odl_result_elem_new[i] is out_arrays_new[i] - - # Check `ufunc.at` - indices = ([0, 0, 1], - [0, 1, 2]) - - mod_array = in_arrays[0].copy() - mod_elem = in_elems_new[0].copy() - if nin == 1: - npy_result = npy_ufunc.at(mod_array, indices) - odl_result = npy_ufunc.at(mod_elem, indices) - elif nin == 2: - other_array = in_arrays[1][indices] - other_elem = in_elems_new[1][indices] - npy_result = npy_ufunc.at(mod_array, indices, other_array) - odl_result = npy_ufunc.at(mod_elem, indices, other_elem) - - assert all_almost_equal(odl_result, npy_result) - - # Check `ufunc.reduce` - if nin == 2 and nout == 1: - in_array = in_arrays[0] - in_elem = in_elems_new[0] - - # We only test along one axis since some binary ufuncs are not - # re-orderable, in which case Numpy raises a ValueError - npy_result = npy_ufunc.reduce(in_array) - odl_result = npy_ufunc.reduce(in_elem) - assert all_almost_equal(odl_result, npy_result) - odl_result_keepdims = npy_ufunc.reduce(in_elem, keepdims=True) - assert odl_result_keepdims.shape == (1,) + in_elem.shape[1:] - # In-place using `out` (with ODL vector and array) - out_elem = odl_result_keepdims.space.element() - out_array = np.empty(odl_result_keepdims.shape, - dtype=odl_result_keepdims.dtype) - npy_ufunc.reduce(in_elem, out=out_elem, keepdims=True) - npy_ufunc.reduce(in_elem, out=out_array, keepdims=True) - assert all_almost_equal(out_elem, odl_result_keepdims) - assert all_almost_equal(out_array, odl_result_keepdims) - # Using a specific dtype - npy_result = npy_ufunc.reduce(in_array, dtype=complex) - odl_result = npy_ufunc.reduce(in_elem, dtype=complex) - assert odl_result.dtype == npy_result.dtype - assert all_almost_equal(odl_result, npy_result) - - # Other ufunc method use the same interface, to we don't perform - # extra tests for them. - - -def test_ufunc_corner_cases(odl_tspace_impl): - """Check if some corner cases are handled correctly.""" - impl = odl_tspace_impl - space = odl.rn((2, 3), impl=impl) - x = space.element([[-1, 0, 1], - [1, 2, 3]]) - space_const_w = odl.rn((2, 3), weighting=2, impl=impl) - weights = [[1, 2, 1], - [3, 2, 1]] - space_arr_w = odl.rn((2, 3), weighting=weights, impl=impl) - - # --- Ufuncs with nin = 1, nout = 1 --- # - - with pytest.raises(ValueError): - # Too many arguments - x.__array_ufunc__(np.sin, '__call__', x, np.ones((2, 3))) - - # Check that `out=(None,)` is the same as not providing `out` - res = x.__array_ufunc__(np.sin, '__call__', x, out=(None,)) - assert all_almost_equal(res, np.sin(x.asarray())) - # Check that the result space is the same - assert res.space == space - - # Check usage of `order` argument - for order in ('C', 'F'): - res = x.__array_ufunc__(np.sin, '__call__', x, order=order) - assert all_almost_equal(res, np.sin(x.asarray())) - assert res.data.flags[order + '_CONTIGUOUS'] - - # Check usage of `dtype` argument - res = x.__array_ufunc__(np.sin, '__call__', x, dtype='float32') - assert all_almost_equal(res, np.sin(x.asarray(), dtype='float32')) - assert res.dtype == 'float32' - - # Check propagation of weightings - y = space_const_w.one() - res = y.__array_ufunc__(np.sin, '__call__', y) - assert res.space.weighting == space_const_w.weighting - y = space_arr_w.one() - res = y.__array_ufunc__(np.sin, '__call__', y) - assert res.space.weighting == space_arr_w.weighting - - # --- Ufuncs with nin = 2, nout = 1 --- # - - with pytest.raises(ValueError): - # Too few arguments - x.__array_ufunc__(np.add, '__call__', x) - - with pytest.raises(ValueError): - # Too many outputs - out1, out2 = np.empty_like(x), np.empty_like(x) - x.__array_ufunc__(np.add, '__call__', x, x, out=(out1, out2)) - - # Check that npy_array += odl_elem works - arr = np.ones((2, 3)) - arr += x - assert all_almost_equal(arr, x.asarray() + 1) - # For Numpy >= 1.13, this will be equivalent - arr = np.ones((2, 3)) - res = x.__array_ufunc__(np.add, '__call__', arr, x, out=(arr,)) - assert all_almost_equal(arr, x.asarray() + 1) - assert res is arr - - # --- `accumulate` --- # - - res = x.__array_ufunc__(np.add, 'accumulate', x) - assert all_almost_equal(res, np.add.accumulate(x.asarray())) - assert res.space == space - arr = np.empty_like(x) - res = x.__array_ufunc__(np.add, 'accumulate', x, out=(arr,)) - assert all_almost_equal(arr, np.add.accumulate(x.asarray())) - assert res is arr - - # `accumulate` with other dtype - res = x.__array_ufunc__(np.add, 'accumulate', x, dtype='float32') - assert res.dtype == 'float32' - - # Error scenarios - with pytest.raises(ValueError): - # Too many `out` arguments - out1, out2 = np.empty_like(x), np.empty_like(x) - x.__array_ufunc__(np.add, 'accumulate', x, out=(out1, out2)) - - # --- `reduce` --- # - - res = x.__array_ufunc__(np.add, 'reduce', x) - assert all_almost_equal(res, np.add.reduce(x.asarray())) - - # With `out` argument and `axis` - out_ax0 = np.empty(3) - res = x.__array_ufunc__(np.add, 'reduce', x, axis=0, out=(out_ax0,)) - assert all_almost_equal(out_ax0, np.add.reduce(x.asarray(), axis=0)) - assert res is out_ax0 - out_ax1 = odl.rn(2, impl=impl).element() - res = x.__array_ufunc__(np.add, 'reduce', x, axis=1, out=(out_ax1,)) - 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 - res = x.__array_ufunc__(np.add, 'reduce', x, axis=(0, 1)) - assert res == pytest.approx(np.add.reduce(x.asarray(), axis=(0, 1))) - - # Cannot propagate weightings in a meaningful way, check that there are - # none in the result - y = space_const_w.one() - res = y.__array_ufunc__(np.add, 'reduce', y, axis=0) - assert not res.space.is_weighted - y = space_arr_w.one() - res = y.__array_ufunc__(np.add, 'reduce', y, axis=0) - assert not res.space.is_weighted - - # Check that `exponent` is propagated - space_1 = odl.rn((2, 3), exponent=1) - z = space_1.one() - res = z.__array_ufunc__(np.add, 'reduce', z, axis=0) - assert res.space.exponent == 1 - - -def testodl_reduction(tspace, odl_reduction): - """Test reductions in x.ufunc against direct Numpy reduction.""" - name = odl_reduction - npy_reduction = getattr(np, name) - - x_arr, x = noise_elements(tspace, 1) - x_reduction = getattr(x.ufuncs, name) - - # Should be equal theoretically, but summation order, other stuff, ..., - # hence we use approx - - # Full reduction, produces scalar - result_npy = npy_reduction(x_arr) - result = x_reduction() - assert result == pytest.approx(result_npy) - result = x_reduction(axis=(0, 1)) - assert result == pytest.approx(result_npy) - - # Reduction along axes, produces element in reduced space - result_npy = npy_reduction(x_arr, axis=0) - result = x_reduction(axis=0) - assert isinstance(result, NumpyTensor) - assert result.shape == result_npy.shape - assert result.dtype == x.dtype - assert np.allclose(result, result_npy) - # Check reduced space properties - assert isinstance(result.space, NumpyTensorSpace) - assert result.space.exponent == x.space.exponent - assert result.space.weighting == x.space.weighting # holds true here - # Evaluate in-place - out = result.space.element() - x_reduction(axis=0, out=out) - assert np.allclose(out, result_npy) - - # Use keepdims parameter - result_npy = npy_reduction(x_arr, axis=1, keepdims=True) - result = x_reduction(axis=1, keepdims=True) - assert result.shape == result_npy.shape - assert np.allclose(result, result_npy) - # Evaluate in-place - out = result.space.element() - x_reduction(axis=1, keepdims=True, out=out) - assert np.allclose(out, result_npy) - - # Use dtype parameter - # These reductions have a `dtype` parameter - if name in ('cumprod', 'cumsum', 'mean', 'prod', 'std', 'sum', - 'trace', 'var'): - result_npy = npy_reduction(x_arr, axis=1, dtype='complex64') - result = x_reduction(axis=1, dtype='complex64') - assert result.dtype == np.dtype('complex64') - assert np.allclose(result, result_npy) - # Evaluate in-place - out = result.space.element() - x_reduction(axis=1, dtype='complex64', out=out) - assert np.allclose(out, result_npy) - - -def test_ufunc_reduction_docs_notempty(odl_tspace_impl): - """Check that the generated docstrings are not empty.""" - impl = odl_tspace_impl - x = odl.rn(3, impl=impl).element() - - for name, _, __, ___ in UFUNCS: - ufunc = getattr(x.ufuncs, name) - assert ufunc.__doc__.splitlines()[0] != '' - - for name in ['sum', 'prod', 'min', 'max']: - reduction = getattr(x.ufuncs, name) - assert reduction.__doc__.splitlines()[0] != '' + (xarr, yarr), (x, y) = noise_elements(space, n=2) + correct_dist = _dist(xarr, yarr, exponent, 1.0) + assert space.dist(x, y) == pytest.approx(correct_dist) if __name__ == '__main__': diff --git a/odl/test/tomo/backends/astra_cpu_test.py b/odl/test/tomo/backends/astra_cpu_test.py index 5726ea23a35..a7a43087ccd 100644 --- a/odl/test/tomo/backends/astra_cpu_test.py +++ b/odl/test/tomo/backends/astra_cpu_test.py @@ -1,4 +1,4 @@ -# Copyright 2014-2017 The ODL contributors +# Copyright 2014-2019 The ODL contributors # # This file is part of ODL. # @@ -9,13 +9,15 @@ """Test ASTRA backend using CPU.""" from __future__ import division + +import sys + import numpy as np import pytest -import sys import odl from odl.tomo.backends.astra_cpu import ( - astra_cpu_forward_projector, astra_cpu_back_projector) + astra_cpu_back_projector, astra_cpu_forward_projector) from odl.tomo.util.testutils import skip_if_no_astra # TODO: clean up and improve tests @@ -41,14 +43,18 @@ def test_astra_cpu_projector_parallel2d(): dtype='float32') # Forward evaluation - proj_data = astra_cpu_forward_projector(phantom, geom, proj_space) + proj_data = astra_cpu_forward_projector( + phantom, geom, reco_space, proj_space + ) assert proj_data.shape == proj_space.shape - assert proj_data.norm() > 0 + assert proj_space.norm(proj_data) > 0 # Backward evaluation - backproj = astra_cpu_back_projector(proj_data, geom, reco_space) + backproj = astra_cpu_back_projector( + proj_data, geom, reco_space, proj_space + ) assert backproj.shape == reco_space.shape - assert backproj.norm() > 0 + assert reco_space.norm(backproj) > 0 @skip_if_no_astra @@ -71,14 +77,18 @@ def test_astra_cpu_projector_fanflat(): dtype='float32') # Forward evaluation - proj_data = astra_cpu_forward_projector(phantom, geom, proj_space) + proj_data = astra_cpu_forward_projector( + phantom, geom, reco_space, proj_space + ) assert proj_data.shape == proj_space.shape - assert proj_data.norm() > 0 + assert proj_space.norm(proj_data) > 0 # Backward evaluation - backproj = astra_cpu_back_projector(proj_data, geom, reco_space) + backproj = astra_cpu_back_projector( + proj_data, geom, reco_space, proj_space + ) assert backproj.shape == reco_space.shape - assert backproj.norm() > 0 + assert reco_space.norm(backproj) > 0 if __name__ == '__main__': diff --git a/odl/test/tomo/backends/astra_cuda_test.py b/odl/test/tomo/backends/astra_cuda_test.py index ad684d62467..524c77e0a9f 100644 --- a/odl/test/tomo/backends/astra_cuda_test.py +++ b/odl/test/tomo/backends/astra_cuda_test.py @@ -96,15 +96,15 @@ def test_astra_cuda_projector(space_and_geometry): projector = AstraCudaProjectorImpl(geom, reco_space, proj_space) proj_data = projector.call_forward(phantom) assert proj_data in proj_space - assert proj_data.norm() > 0 - assert np.all(proj_data.asarray() >= 0) + assert proj_space.norm(proj_data) > 0 + assert np.all(proj_data >= 0) # Backward evaluation back_projector = AstraCudaBackProjectorImpl(geom, reco_space, proj_space) backproj = back_projector.call_backward(proj_data) assert backproj in reco_space - assert backproj.norm() > 0 - assert np.all(proj_data.asarray() >= 0) + assert reco_space.norm(backproj) > 0 + assert np.all(proj_data >= 0) if __name__ == '__main__': diff --git a/odl/test/tomo/backends/astra_setup_test.py b/odl/test/tomo/backends/astra_setup_test.py index d3502cf5bc2..239be56ba8a 100644 --- a/odl/test/tomo/backends/astra_setup_test.py +++ b/odl/test/tomo/backends/astra_setup_test.py @@ -27,43 +27,19 @@ pytestmark = pytest.mark.skipif("not odl.tomo.ASTRA_AVAILABLE") -def _discrete_domain(ndim): - """Create `DiscretizedSpace` space with isotropic grid stride. - - Parameters - ---------- - ndim : `int` - Number of space dimensions - - Returns - ------- - space : `DiscretizedSpace` - Returns a `DiscretizedSpace` instance - """ +def _space_iso(ndim): + """Return isotropic DiscretizedSpace with given ``ndim``.""" max_pt = np.arange(1, ndim + 1) min_pt = -max_pt shape = np.arange(1, ndim + 1) * 10 - return odl.uniform_discr(min_pt, max_pt, shape=shape, dtype='float32') -def _discrete_domain_anisotropic(ndim): - """Create `DiscretizedSpace` space with anisotropic grid stride. - - Parameters - ---------- - ndim : `int` - Number of space dimensions - - Returns - ------- - space : `DiscretizedSpace` - Returns a `DiscretizedSpace` instance - """ +def _space_aniso(ndim): + """Return anisotropic DiscretizedSpace with given ``ndim``.""" min_pt = [-1] * ndim max_pt = [1] * ndim shape = np.arange(1, ndim + 1) * 10 - return odl.uniform_discr(min_pt, max_pt, shape=shape, dtype='float32') @@ -73,7 +49,7 @@ def test_vol_geom_2d(): y_pts = 20 # y_pts = Columns # Isotropic voxel case - discr_dom = _discrete_domain(2) + vol_space = _space_iso(2) correct_dict = { 'GridColCount': y_pts, 'GridRowCount': x_pts, @@ -83,11 +59,11 @@ def test_vol_geom_2d(): 'WindowMinY': -1.0, # x_min 'WindowMaxY': 1.0}} # x_amx - vol_geom = astra_volume_geometry(discr_dom) + vol_geom = astra_volume_geometry(vol_space) assert vol_geom == correct_dict # Anisotropic voxel case - discr_dom = _discrete_domain_anisotropic(2) + vol_space = _space_aniso(2) correct_dict = { 'GridColCount': y_pts, 'GridRowCount': x_pts, @@ -98,11 +74,11 @@ def test_vol_geom_2d(): 'WindowMaxY': 1.0}} # x_amx if astra_supports('anisotropic_voxels_2d'): - vol_geom = astra_volume_geometry(discr_dom) + vol_geom = astra_volume_geometry(vol_space) assert vol_geom == correct_dict else: with pytest.raises(NotImplementedError): - astra_volume_geometry(discr_dom) + astra_volume_geometry(vol_space) def test_vol_geom_3d(): @@ -112,7 +88,7 @@ def test_vol_geom_3d(): z_pts = 30 # Isotropic voxel case - discr_dom = _discrete_domain(3) + vol_space = _space_iso(3) # x = columns, y = rows, z = slices correct_dict = { 'GridColCount': z_pts, @@ -126,10 +102,10 @@ def test_vol_geom_3d(): 'WindowMinZ': -1.0, # x_min 'WindowMaxZ': 1.0}} # x_amx - vol_geom = astra_volume_geometry(discr_dom) + vol_geom = astra_volume_geometry(vol_space) assert vol_geom == correct_dict - discr_dom = _discrete_domain_anisotropic(3) + vol_space = _space_aniso(3) # x = columns, y = rows, z = slices correct_dict = { 'GridColCount': z_pts, @@ -144,16 +120,15 @@ def test_vol_geom_3d(): 'WindowMaxZ': 1.0}} # x_amx if astra_supports('anisotropic_voxels_3d'): - vol_geom = astra_volume_geometry(discr_dom) + vol_geom = astra_volume_geometry(vol_space) assert vol_geom == correct_dict else: with pytest.raises(NotImplementedError): - astra_volume_geometry(discr_dom) + astra_volume_geometry(vol_space) def test_proj_geom_parallel_2d(): """Create ASTRA 2D projection geometry.""" - apart = odl.uniform_partition(0, 2, 5) dpart = odl.uniform_partition(-1, 1, 10) geom = odl.tomo.Parallel2dGeometry(apart, dpart) @@ -228,14 +203,14 @@ def test_astra_projection_geometry(): def test_volume_data_2d(): - """Create ASTRA data structure in 2D.""" + """Verify ASTRA data structure creation in 2D.""" # From scratch data_id = astra_data(VOL_GEOM_2D, 'volume', ndim=2) data_out = astra.data2d.get_shared(data_id) assert data_out.shape == (10, 20) # From existing - discr_dom = _discrete_domain(2) + discr_dom = _space_iso(2) data_in = discr_dom.element(np.ones((10, 20), dtype='float32')) data_id = astra_data(VOL_GEOM_2D, 'volume', data=data_in) data_out = astra.data2d.get_shared(data_id) @@ -248,15 +223,14 @@ def test_volume_data_2d(): def test_volume_data_3d(): - """Create ASTRA data structure in 2D.""" - + """Verify ASTRA data structure creation in 3D.""" # From scratch data_id = astra_data(VOL_GEOM_3D, 'volume', ndim=3) data_out = astra.data3d.get_shared(data_id) assert data_out.shape == (10, 20, 30) # From existing - discr_dom = _discrete_domain(3) + discr_dom = _space_iso(3) data_in = discr_dom.element(np.ones((10, 20, 30), dtype='float32')) data_id = astra_data(VOL_GEOM_3D, 'volume', data=data_in) data_out = astra.data3d.get_shared(data_id) @@ -276,7 +250,7 @@ def test_volume_data_3d(): def test_parallel_2d_projector(): - """Create ASTRA 2D projectors.""" + """Verify that ASTRA 2D projectors can be created.""" # We can just test if it runs astra_projector('line', VOL_GEOM_2D, PROJ_GEOM_2D, ndim=2) astra_projector('linear', VOL_GEOM_2D, PROJ_GEOM_2D, ndim=2) diff --git a/odl/test/tomo/backends/skimage_test.py b/odl/test/tomo/backends/skimage_test.py index 44622a0659f..17b6136ce8f 100644 --- a/odl/test/tomo/backends/skimage_test.py +++ b/odl/test/tomo/backends/skimage_test.py @@ -1,4 +1,4 @@ -# Copyright 2014-2017 The ODL contributors +# Copyright 2014-2019 The ODL contributors # # This file is part of ODL. # @@ -9,11 +9,12 @@ """Test skimage back-end.""" from __future__ import division + import numpy as np import odl from odl.tomo.backends.skimage_radon import ( - skimage_radon_forward_projector, skimage_radon_back_projector) + skimage_radon_back_projector, skimage_radon_forward_projector) from odl.tomo.util.testutils import skip_if_no_skimage @@ -34,14 +35,18 @@ def test_skimage_radon_projector_parallel2d(): proj_space = odl.uniform_discr_frompartition(geom.partition) # Forward evaluation - proj_data = skimage_radon_forward_projector(phantom, geom, proj_space) + proj_data = skimage_radon_forward_projector( + phantom, geom, reco_space, proj_space + ) assert proj_data.shape == proj_space.shape - assert proj_data.norm() > 0 + assert proj_space.norm(proj_data) > 0 # Backward evaluation - backproj = skimage_radon_back_projector(proj_data, geom, reco_space) + backproj = skimage_radon_back_projector( + proj_data, geom, reco_space, proj_space + ) assert backproj.shape == reco_space.shape - assert backproj.norm() > 0 + assert reco_space.norm(backproj) > 0 if __name__ == '__main__': diff --git a/odl/test/tomo/operators/ray_trafo_test.py b/odl/test/tomo/operators/ray_trafo_test.py index dd7af502cd5..8eadda6ba48 100644 --- a/odl/test/tomo/operators/ray_trafo_test.py +++ b/odl/test/tomo/operators/ray_trafo_test.py @@ -228,7 +228,7 @@ def test_projector(projector, in_place): # We expect maximum value to be along diagonal expected_max = projector.domain.partition.extent[0] * np.sqrt(2) - assert proj.ufuncs.max() == pytest.approx(expected_max, rel=rtol) + assert np.max(proj) == pytest.approx(expected_max, rel=rtol) def test_adjoint(projector): @@ -251,8 +251,8 @@ def test_adjoint(projector): backproj = projector.adjoint(proj) # Verified the identity = - result_AxAx = proj.inner(proj) - result_xAtAx = backproj.inner(vol) + result_AxAx = projector.range.inner(proj, proj) + result_xAtAx = projector.domain.inner(backproj, vol) assert result_AxAx == pytest.approx(result_xAtAx, rel=rtol) @@ -285,7 +285,7 @@ def test_angles(projector): lambda x: np.exp(-(2 * x[0] - 10 + x[1]) ** 2)) # Create projection - result = projector(vol).asarray() + result = projector(vol) # Find the angle where the projection has a maximum (along the line). # TODO: center of mass would be more robust @@ -367,8 +367,8 @@ def test_anisotropic_voxels(geometry): # Just check that this doesn't crash and computes something nonzero data = ray_trafo(vol_one) backproj = ray_trafo.adjoint(data_one) - assert data.norm() > 0 - assert backproj.norm() > 0 + assert ray_trafo.range.norm(data) > 0 + assert ray_trafo.domain.norm(backproj) > 0 else: assert False diff --git a/odl/test/trafos/backends/pyfftw_bindings_test.py b/odl/test/trafos/backends/pyfftw_bindings_test.py index d380ea280af..fe239f50b48 100644 --- a/odl/test/trafos/backends/pyfftw_bindings_test.py +++ b/odl/test/trafos/backends/pyfftw_bindings_test.py @@ -275,7 +275,7 @@ def test_pyfftw_call_plan_preserve_input(planning): for shape in [(10,), (3, 4)]: arr = _random_array(shape, dtype='complex128') - arr_cpy = arr.copy() + arr_cpy = np.copy(arr) idft_scaling = np.prod(shape) true_idft = np.fft.ifftn(arr) * idft_scaling @@ -346,7 +346,7 @@ def test_pyfftw_call_forward_with_plan(): for shape in [(10,), (3, 4, 5)]: arr = _random_array(shape, dtype='complex128') - arr_cpy = arr.copy() + arr_cpy = np.copy(arr) true_dft = np.fft.fftn(arr) # First run, create plan @@ -367,7 +367,7 @@ def test_pyfftw_call_backward_with_plan(): for shape in [(10,), (3, 4, 5)]: arr = _random_array(shape, dtype='complex128') - arr_cpy = arr.copy() + arr_cpy = np.copy(arr) idft_scaling = np.prod(shape) true_idft = np.fft.ifftn(arr) * idft_scaling diff --git a/odl/test/trafos/fourier_test.py b/odl/test/trafos/fourier_test.py index bdf2b39ff4d..7c57788889e 100644 --- a/odl/test/trafos/fourier_test.py +++ b/odl/test/trafos/fourier_test.py @@ -238,7 +238,7 @@ def test_dft_call(impl): rand_arr = noise_element(dft_dom) rand_arr_dft = dft(rand_arr, flags=('FFTW_ESTIMATE',)) rand_arr_idft = idft(rand_arr_dft, flags=('FFTW_ESTIMATE',)) - assert (rand_arr_idft - rand_arr).norm() < 1e-6 + assert dft_dom.norm(rand_arr_idft - rand_arr) < 1e-6 # 2d, halfcomplex, first axis shape = (4, 5) @@ -267,7 +267,7 @@ def test_dft_call(impl): rand_arr = noise_element(dft_dom) rand_arr_dft = dft(rand_arr, flags=('FFTW_ESTIMATE',)) rand_arr_idft = idft(rand_arr_dft, flags=('FFTW_ESTIMATE',)) - assert (rand_arr_idft - rand_arr).norm() < 1e-6 + assert dft_dom.norm(rand_arr_idft - rand_arr) < 1e-6 def test_dft_sign(impl): @@ -335,8 +335,7 @@ def test_dft_init_plan(impl): dft.init_fftw_plan() # Make sure plan can be used - dft._fftw_plan(dft.domain.element().asarray(), - dft.range.element().asarray()) + dft._fftw_plan(dft.domain.element(), dft.range.element()) dft.clear_fftw_plan() assert dft._fftw_plan is None @@ -411,8 +410,7 @@ def test_fourier_trafo_init_plan(impl, odl_floating_dtype): ft.init_fftw_plan() # Make sure plan can be used - ft._fftw_plan(ft.domain.element().asarray(), - ft.range.element().asarray()) + ft._fftw_plan(ft.domain.element(), ft.range.element()) ft.clear_fftw_plan() assert ft._fftw_plan is None @@ -427,8 +425,7 @@ def test_fourier_trafo_init_plan(impl, odl_floating_dtype): ft.init_fftw_plan() # Make sure plan can be used - ft._fftw_plan(ft.domain.element().asarray(), - ft.range.element().asarray()) + ft._fftw_plan(ft.domain.element(), ft.range.element()) ft.clear_fftw_plan() assert ft._fftw_plan is None @@ -442,8 +439,7 @@ def test_fourier_trafo_init_plan(impl, odl_floating_dtype): ft.init_fftw_plan() # Make sure plan can be used - ft._fftw_plan(ft.domain.element().asarray(), - ft.range.element().asarray()) + ft._fftw_plan(ft.domain.element(), ft.range.element()) ft.clear_fftw_plan() assert ft._fftw_plan is None @@ -516,7 +512,7 @@ def char_interval_ft(x): for dft in [dft_base, dft_complex, dft_complex_shift]: func_true_ft = dft.range.element(char_interval_ft) func_dft = dft(char_interval) - assert (func_dft - func_true_ft).norm() < 5e-6 + assert dft.range.norm(func_dft - func_true_ft) < 5e-6 def test_fourier_trafo_scaling(): @@ -536,7 +532,7 @@ def char_interval_ft(x): for factor in (2, 1j, -2.5j, 1 - 4j): func_true_ft = factor * dft.range.element(char_interval_ft) func_dft = dft(factor * discr.element(char_interval)) - assert (func_dft - func_true_ft).norm() < 1e-6 + assert dft.range.norm(func_dft - func_true_ft) < 1e-6 def test_fourier_trafo_sign(impl): @@ -632,7 +628,7 @@ def hat_func_ft(x): dft = FourierTransform(discr) func_true_ft = dft.range.element(hat_func_ft) func_dft = dft(hat_func) - assert (func_dft - func_true_ft).norm() < 0.001 + assert dft.range.norm(func_dft - func_true_ft) < 0.001 def test_fourier_trafo_complex_sum(): @@ -662,7 +658,7 @@ def char_interval_ft(x): + 1j * dft.range.element(char_interval_ft) ) func_dft = dft(func) - assert (func_dft - func_true_ft).norm() < 0.001 + assert dft.range.norm(func_dft - func_true_ft) < 0.001 def test_fourier_trafo_gaussian_1d(): @@ -675,7 +671,7 @@ def gaussian(x): dft = FourierTransform(discr) func_true_ft = dft.range.element(gaussian) func_dft = dft(gaussian) - assert (func_dft - func_true_ft).norm() < 0.001 + assert dft.range.norm(func_dft - func_true_ft) < 0.001 def test_fourier_trafo_freq_shifted_charfun_1d(): @@ -693,7 +689,7 @@ def fshift_char_interval_ft(x): dft = FourierTransform(discr) func_true_ft = dft.range.element(fshift_char_interval_ft) func_dft = dft(fshift_char_interval) - assert (func_dft - func_true_ft).norm() < 0.001 + assert dft.range.norm(func_dft - func_true_ft) < 0.001 def test_dft_with_known_pairs_2d(): @@ -720,7 +716,7 @@ def fshift_char_rect_ft(x): dft = FourierTransform(discr) func_true_ft = dft.range.element(fshift_char_rect_ft) func_dft = dft(fshift_char_rect) - assert (func_dft - func_true_ft).norm() < 0.001 + assert dft.range.norm(func_dft - func_true_ft) < 0.001 def test_fourier_trafo_completely(): diff --git a/odl/tomo/analytic/filtered_back_projection.py b/odl/tomo/analytic/filtered_back_projection.py index 2002eded301..86d792aa0ee 100644 --- a/odl/tomo/analytic/filtered_back_projection.py +++ b/odl/tomo/analytic/filtered_back_projection.py @@ -77,7 +77,9 @@ def _fbp_filter(norm_freq, filter_type, frequency_scaling): ... filter_type='Hann', ... frequency_scaling=0.8) """ - filter_type, filter_type_in = str(filter_type).lower(), filter_type + if not callable(filter_type): + filter_type, filter_type_in = str(filter_type).lower(), filter_type + if callable(filter_type): filt = filter_type(norm_freq) elif filter_type == 'ram-lak': @@ -93,8 +95,7 @@ def _fbp_filter(norm_freq, filter_type, frequency_scaling): filt = norm_freq * ( np.cos(norm_freq * np.pi / (2 * frequency_scaling)) ** 2) else: - raise ValueError('unknown `filter_type` ({})' - ''.format(filter_type_in)) + raise ValueError('unknown `filter_type` ({})'.format(filter_type_in)) indicator = (norm_freq <= frequency_scaling) filt *= indicator @@ -566,7 +567,9 @@ def fbp_op(ray_trafo, padding=True, filter_type='Ram-Lak', # Crete and show TD window td_window = tam_danielson_window(ray_trafo, smoothing_width=0) - td_window.show('Tam-Danielson window', coords=[0, None, None]) + ray_trafo.range.show( + td_window, 'Tam-Danielson window', coords=[0, None, None] + ) # Show the Parker weighting @@ -577,7 +580,7 @@ def fbp_op(ray_trafo, padding=True, filter_type='Ram-Lak', # Crete and show parker weighting parker_weighting = parker_weighting(ray_trafo) - parker_weighting.show('Parker weighting') + ray_trafo.range.show(parker_weighting, 'Parker weighting') # Also run the doctests run_doctests() diff --git a/odl/tomo/backends/astra_cpu.py b/odl/tomo/backends/astra_cpu.py index 7f1c70321eb..54dfb727a0c 100644 --- a/odl/tomo/backends/astra_cpu.py +++ b/odl/tomo/backends/astra_cpu.py @@ -12,7 +12,7 @@ import numpy as np -from odl.discr import DiscretizedSpace, DiscretizedSpaceElement +from odl.discr import DiscretizedSpace from odl.tomo.backends.astra_setup import ( astra_algorithm, astra_data, astra_projection_geometry, astra_projector, astra_volume_geometry) @@ -66,8 +66,8 @@ def default_astra_proj_type(geom): ) -def astra_cpu_forward_projector(vol_data, geometry, proj_space, out=None, - astra_proj_type=None): +def astra_cpu_forward_projector(vol_data, geometry, vol_space, proj_space, + out=None, astra_proj_type=None): """Run an ASTRA forward projection on the given data using the CPU. Parameters @@ -93,15 +93,18 @@ 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, 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}" - "".format(vol_data.space.impl)) + if not isinstance(vol_data, np.ndarray): + raise TypeError('`vol_data` must be a `numpy.ndarray`, got {}' + ''.format(type(vol_data))) if not isinstance(geometry, Geometry): raise TypeError('geometry {!r} is not a Geometry instance' ''.format(geometry)) + if not isinstance(vol_space, DiscretizedSpace): + raise TypeError('`vol_space` {!r} is not a DiscreteLp ' + 'instance.'.format(vol_space)) + if vol_space.impl != 'numpy': + raise TypeError("`vol_space.impl` must be 'numpy', got {!r}" + "".format(vol_space.impl)) if not isinstance(proj_space, DiscretizedSpace): raise TypeError('`proj_space` {!r} is not a DiscretizedSpace ' 'instance.'.format(proj_space)) @@ -122,7 +125,7 @@ def astra_cpu_forward_projector(vol_data, geometry, proj_space, out=None, ndim = vol_data.ndim # Create astra geometries - vol_geom = astra_volume_geometry(vol_data.space) + vol_geom = astra_volume_geometry(vol_space) proj_geom = astra_projection_geometry(geometry) # Create projector @@ -154,8 +157,8 @@ def astra_cpu_forward_projector(vol_data, geometry, proj_space, out=None, return out -def astra_cpu_back_projector(proj_data, geometry, vol_space, out=None, - astra_proj_type=None): +def astra_cpu_back_projector(proj_data, geometry, vol_space, proj_space, + out=None, astra_proj_type=None): """Run an ASTRA back-projection on the given data using the CPU. Parameters @@ -182,28 +185,29 @@ 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, 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}" - "".format(proj_data.space.impl)) + if not isinstance(proj_data, np.ndarray): + raise TypeError('projection data {!r} is not a DiscreteLpElement ' + 'instance'.format(proj_data)) if not isinstance(geometry, Geometry): raise TypeError('geometry {!r} is not a Geometry instance' ''.format(geometry)) if not isinstance(vol_space, DiscretizedSpace): - raise TypeError('volume space {!r} is not a DiscretizedSpace ' + raise TypeError('reconstruction 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}" + raise TypeError("`reco_space.impl` must be 'numpy', got {!r}" "".format(vol_space.impl)) if vol_space.ndim != geometry.ndim: raise ValueError('dimensions {} of reconstruction space and {} of ' 'geometry do not match'.format( vol_space.ndim, geometry.ndim)) + if not isinstance(proj_space, DiscretizedSpace): + raise TypeError('reconstruction space {!r} is not a DiscretizedSpace ' + 'instance'.format(proj_space)) + if proj_space.impl != 'numpy': + raise TypeError("`reco_space.impl` must be 'numpy', got {!r}" + "".format(proj_space.impl)) + if out is None: out = vol_space.element() else: @@ -238,8 +242,8 @@ def astra_cpu_back_projector(proj_data, geometry, vol_space, out=None, astra.algorithm.run(algo_id) # Weight the adjoint by appropriate weights - scaling_factor = float(proj_data.space.weighting.const) - scaling_factor /= float(vol_space.weighting.const) + scaling_factor = float(proj_space.weighting) + scaling_factor /= float(vol_space.weighting) out *= scaling_factor diff --git a/odl/tomo/backends/astra_cuda.py b/odl/tomo/backends/astra_cuda.py index 69627888b23..b317b3e5189 100644 --- a/odl/tomo/backends/astra_cuda.py +++ b/odl/tomo/backends/astra_cuda.py @@ -98,9 +98,9 @@ def call_forward(self, vol_data, out=None): # Copy data to GPU memory if self.geometry.ndim == 2: - astra.data2d.store(self.vol_id, vol_data.asarray()) + astra.data2d.store(self.vol_id, np.asarray(vol_data)) elif self.geometry.ndim == 3: - astra.data3d.store(self.vol_id, vol_data.asarray()) + astra.data3d.store(self.vol_id, np.asarray(vol_data)) else: raise RuntimeError('unknown ndim') @@ -255,10 +255,10 @@ def call_backward(self, proj_data, out=None): # Copy data to GPU memory if self.geometry.ndim == 2: - astra.data2d.store(self.sino_id, proj_data.asarray()) + astra.data2d.store(self.sino_id, np.asarray(proj_data)) elif self.geometry.ndim == 3: shape = (-1,) + self.geometry.det_partition.shape - reshaped_proj_data = proj_data.asarray().reshape(shape) + reshaped_proj_data = np.asarray(proj_data).reshape(shape) swapped_proj_data = np.ascontiguousarray( np.swapaxes(reshaped_proj_data, 0, 1)) astra.data3d.store(self.sino_id, swapped_proj_data) diff --git a/odl/tomo/backends/astra_setup.py b/odl/tomo/backends/astra_setup.py index c847f1f2574..0c65321548f 100644 --- a/odl/tomo/backends/astra_setup.py +++ b/odl/tomo/backends/astra_setup.py @@ -29,7 +29,7 @@ import numpy as np -from odl.discr import DiscretizedSpace, DiscretizedSpaceElement +from odl.discr import DiscretizedSpace from odl.tomo.geometry import ( DivergentBeamGeometry, Flat1dDetector, Flat2dDetector, Geometry, ParallelBeamGeometry) @@ -570,7 +570,7 @@ 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, (DiscretizedSpaceElement, np.ndarray)): + if isinstance(data, np.ndarray): ndim = data.ndim else: raise TypeError('`data` {!r} is neither DiscretizedSpaceElement ' @@ -604,8 +604,6 @@ def astra_data(astra_geom, datatype, data=None, ndim=2, allow_copy=False): else: if isinstance(data, np.ndarray): return link(astra_dtype_str, astra_geom, data) - elif data.tensor.impl == 'numpy': - return link(astra_dtype_str, astra_geom, data.asarray()) else: # Something else than NumPy data representation raise NotImplementedError('ASTRA supports data wrapping only ' diff --git a/odl/tomo/backends/skimage_radon.py b/odl/tomo/backends/skimage_radon.py index 86d5d64ac67..c3b80efd7ef 100644 --- a/odl/tomo/backends/skimage_radon.py +++ b/odl/tomo/backends/skimage_radon.py @@ -52,7 +52,8 @@ def _interpolator(x, out=None): return _interpolator -def skimage_radon_forward_projector(volume, geometry, proj_space, out=None): +def skimage_radon_forward_projector(volume, geometry, vol_space, proj_space, + out=None): """Calculate forward projection using skimage. Parameters @@ -79,12 +80,10 @@ def skimage_radon_forward_projector(volume, geometry, proj_space, out=None): assert volume.shape[0] == volume.shape[1] theta = np.degrees(geometry.angles) - skimage_range = skimage_proj_space(geometry, volume.space, proj_space) + skimage_range = skimage_proj_space(geometry, vol_space, proj_space) # Rotate volume from (x, y) to (rows, cols), then project - sino_arr = radon( - np.rot90(volume.asarray(), 1), theta=theta, circle=False - ) + sino_arr = radon(np.rot90(volume, 1), theta=theta, circle=False) sinogram = skimage_range.element(sino_arr.T) if out is None: @@ -97,13 +96,14 @@ def skimage_radon_forward_projector(volume, geometry, proj_space, out=None): out=out_arr, ) - scale = volume.space.cell_sides[0] + scale = vol_space.cell_sides[0] out *= scale return out -def skimage_radon_back_projector(sinogram, geometry, vol_space, out=None): +def skimage_radon_back_projector(sinogram, geometry, vol_space, proj_space, + out=None): """Calculate forward projection using skimage. Parameters @@ -127,12 +127,12 @@ def skimage_radon_back_projector(sinogram, geometry, vol_space, out=None): from skimage.transform import iradon theta = np.degrees(geometry.angles) - skimage_range = skimage_proj_space(geometry, vol_space, sinogram.space) + skimage_range = skimage_proj_space(geometry, vol_space, proj_space) skimage_sinogram = skimage_range.element() with writable_array(skimage_sinogram) as sino_arr: point_collocation( - clamped_interpolation(sinogram.space, sinogram), + clamped_interpolation(proj_space, sinogram), skimage_range.grid.meshgrid, out=sino_arr, ) @@ -145,7 +145,7 @@ def skimage_radon_back_projector(sinogram, geometry, vol_space, out=None): # Rotate back from (rows, cols) to (x, y), then back-project (no filter) backproj = iradon( - skimage_sinogram.asarray().T, + skimage_sinogram.T, theta, output_size=vol_space.shape[0], filter=None, @@ -158,11 +158,11 @@ def skimage_radon_back_projector(sinogram, geometry, vol_space, out=None): # Correct in case of non-weighted spaces proj_volume = np.prod(sinogram.space.partition.extent) - proj_size = sinogram.space.partition.size + proj_size = proj_space.partition.size proj_weighting = proj_volume / proj_size - scaling_factor *= sinogram.space.weighting.const / proj_weighting - scaling_factor /= vol_space.weighting.const / vol_space.cell_volume + scaling_factor *= proj_space.weighting / proj_weighting + scaling_factor /= vol_space.weighting / vol_space.cell_volume # Correctly scale the output out *= scaling_factor diff --git a/odl/tomo/operators/ray_trafo.py b/odl/tomo/operators/ray_trafo.py index f5e3346ed09..255e3b3dc08 100644 --- a/odl/tomo/operators/ray_trafo.py +++ b/odl/tomo/operators/ray_trafo.py @@ -16,7 +16,6 @@ from odl.discr import DiscretizedSpace from odl.operator import Operator -from odl.space.weighting import ConstWeighting from odl.tomo.backends import ( ASTRA_AVAILABLE, ASTRA_CUDA_AVAILABLE, ASTRA_VERSION, SKIMAGE_AVAILABLE, AstraCudaBackProjectorImpl, AstraCudaProjectorImpl, @@ -218,9 +217,10 @@ def __init__(self, reco_space, geometry, variant, **kwargs): if not reco_space.is_weighted: weighting = None - elif (isinstance(reco_space.weighting, ConstWeighting) and - np.isclose(reco_space.weighting.const, - reco_space.cell_volume)): + elif ( + reco_space.weighting_type == 'const' + and np.isclose(reco_space.weighting, reco_space.cell_volume) + ): # Approximate cell volume # TODO: find a way to treat angles and detector differently # regarding weighting. While the detector should be uniformly @@ -406,8 +406,13 @@ def _call_real(self, x_real, out_real, **kwargs): if data_impl == 'cpu': return astra_cpu_forward_projector( - x_real, self.geometry, self.range.real_space, out_real, - **kwargs) + x_real, + self.geometry, + self.domain.real_space, + self.range.real_space, + out_real, + **kwargs, + ) elif data_impl == 'cuda': if self._astra_wrapper is None: @@ -426,8 +431,13 @@ def _call_real(self, x_real, out_real, **kwargs): elif self.impl == 'skimage': return skimage_radon_forward_projector( - x_real, self.geometry, self.range.real_space, out_real, - **kwargs) + x_real, + self.geometry, + self.domain.real_space, + self.range.real_space, + out_real, + **kwargs, + ) else: # Should never happen raise RuntimeError('bad `impl` {!r}'.format(self.impl)) @@ -443,7 +453,7 @@ def adjoint(self): if self._adjoint is not None: return self._adjoint - kwargs = self._extra_kwargs.copy() + kwargs = dict(self._extra_kwargs) kwargs['domain'] = self.range self._adjoint = RayBackProjection(self.domain, self.geometry, impl=self.impl, @@ -521,8 +531,13 @@ def _call_real(self, x_real, out_real, **kwargs): backend, data_impl = self.impl.split('_') if data_impl == 'cpu': return astra_cpu_back_projector( - x_real, self.geometry, self.range.real_space, out_real, - **kwargs) + x_real, + self.geometry, + self.range.real_space, + self.domain.real_space, + out_real, + **kwargs, + ) elif data_impl == 'cuda': if self._astra_wrapper is None: astra_wrapper = AstraCudaBackProjectorImpl( @@ -540,8 +555,13 @@ def _call_real(self, x_real, out_real, **kwargs): elif self.impl == 'skimage': return skimage_radon_back_projector( - x_real, self.geometry, self.range.real_space, out_real, - **kwargs) + x_real, + self.geometry, + self.range.real_space, + self.domain.real_space, + out_real, + **kwargs, + ) else: # Should never happen raise RuntimeError('bad `impl` {!r}'.format(self.impl)) @@ -557,7 +577,7 @@ def adjoint(self): if self._adjoint is not None: return self._adjoint - kwargs = self._extra_kwargs.copy() + kwargs = dict(self._extra_kwargs) kwargs['range'] = self.domain self._adjoint = RayTransform(self.range, self.geometry, impl=self.impl, diff --git a/odl/trafos/fourier.py b/odl/trafos/fourier.py index 03af04f02ff..5bf06936107 100644 --- a/odl/trafos/fourier.py +++ b/odl/trafos/fourier.py @@ -168,11 +168,12 @@ def _call(self, x, out, **kwargs): odl.trafos.backends.pyfftw_bindings.pyfftw_call : Call pyfftw backend directly """ - # TODO: Implement zero padding if self.impl == 'numpy': - out[:] = self._call_numpy(x.asarray()) + out[:] = self._call_numpy(np.asarray(x)) else: - out[:] = self._call_pyfftw(x.asarray(), out.asarray(), **kwargs) + out[:] = self._call_pyfftw( + np.asarray(x), np.asarray(out), **kwargs + ) @property def impl(self): @@ -285,9 +286,15 @@ def _call_pyfftw(self, x, out, **kwargs): direction = 'forward' if self.sign == '-' else 'backward' self._fftw_plan = pyfftw_call( - x, out, direction=direction, axes=self.axes, - halfcomplex=self.halfcomplex, planning_effort=effort, - fftw_plan=self._fftw_plan, normalise_idft=False) + np.asarray(x), + np.asarray(out), + direction=direction, + axes=self.axes, + halfcomplex=self.halfcomplex, + planning_effort=effort, + fftw_plan=self._fftw_plan, + normalise_idft=False, + ) return out @@ -333,9 +340,14 @@ def init_fftw_plan(self, planning_effort='measure', **kwargs): direction = 'forward' if self.sign == '-' else 'backward' self._fftw_plan = pyfftw_call( - x.asarray(), y.asarray(), direction=direction, - halfcomplex=self.halfcomplex, axes=self.axes, - planning_effort=planning_effort, **kwargs) + np.asarray(x), + np.asarray(y), + direction=direction, + halfcomplex=self.halfcomplex, + axes=self.axes, + planning_effort=planning_effort, + **kwargs, + ) def clear_fftw_plan(self): """Delete the FFTW plan of this transform. @@ -612,6 +624,8 @@ def _call_numpy(self, x): out : `numpy.ndarray` Result of the transform """ + assert isinstance(x, np.ndarray) + if self.halfcomplex: return np.fft.irfftn(x, axes=self.axes) else: @@ -654,6 +668,9 @@ def _call_pyfftw(self, x, out, **kwargs): .. _pyfftw API documentation: https://pyfftw.readthedocs.io """ + assert isinstance(x, np.ndarray) + assert isinstance(out, np.ndarray) + kwargs.pop('normalise_idft', None) # Using `True` here kwargs.pop('axes', None) kwargs.pop('halfcomplex', None) @@ -762,13 +779,13 @@ def __init__(self, inverse, domain, range=None, impl=None, **kwargs): Other Parameters ---------------- - tmp_r : `DiscretizedSpaceElement` or `numpy.ndarray`, optional + tmp_r : `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 : `DiscretizedSpaceElement` or `numpy.ndarray`, optional + tmp_f : `numpy.ndarray`, optional Temporary for calculations in the frequency (reciprocal) space. It is shared with the inverse. @@ -873,9 +890,9 @@ def __init__(self, inverse, domain, range=None, impl=None, **kwargs): self._fftw_plan = None if tmp_r is not None: - tmp_r = domain.element(tmp_r).asarray() + tmp_r = domain.element(tmp_r) if tmp_f is not None: - tmp_f = range.element(tmp_f).asarray() + tmp_f = range.element(tmp_f) self._tmp_r = tmp_r self._tmp_f = tmp_f @@ -901,12 +918,13 @@ def _call(self, x, out, **kwargs): odl.trafos.backends.pyfftw_bindings.pyfftw_call : Call pyfftw backend directly """ - # TODO: Implement zero padding if self.impl == 'numpy': - out[:] = self._call_numpy(x.asarray()) + out[:] = self._call_numpy(np.asarray(x)) else: # 0-overhead assignment if asarray() does not copy - out[:] = self._call_pyfftw(x.asarray(), out.asarray(), **kwargs) + out[:] = self._call_pyfftw( + np.asarray(x), np.asarray(out), **kwargs + ) def _call_numpy(self, x): """Return ``self(x)`` for numpy back-end. @@ -1036,9 +1054,9 @@ def create_temporaries(self, r=True, f=True): fspace = self.range if r: - self._tmp_r = rspace.element().asarray() + self._tmp_r = np.asarray(rspace.element()) if f: - self._tmp_f = fspace.element().asarray() + self._tmp_f = np.asarray(fspace.element()) def clear_temporaries(self): """Set the temporaries to ``None``.""" @@ -1098,18 +1116,18 @@ def init_fftw_plan(self, planning_effort='measure', **kwargs): elif self._tmp_f is not None: arr_in = arr_out = self._tmp_f else: - arr_in = arr_out = rspace.element().asarray() + arr_in = arr_out = np.asarray(rspace.element()) elif self.halfcomplex: # R2HC / HC2R: Use 'r' and 'f' temporary distinctly if initialized if self._tmp_r is not None: arr_r = self._tmp_r else: - arr_r = rspace.element().asarray() + arr_r = np.asarray(rspace.element()) if self._tmp_f is not None: arr_f = self._tmp_f else: - arr_f = fspace.element().asarray() + arr_f = np.asarray(fspace.element()) if inverse: arr_in, arr_out = arr_f, arr_r @@ -1121,7 +1139,7 @@ def init_fftw_plan(self, planning_effort='measure', **kwargs): if self._tmp_f is not None: arr_in = arr_out = self._tmp_f else: - arr_in = arr_out = fspace.element().asarray() + arr_in = arr_out = np.asarray(fspace.element()) kwargs.pop('planning_timelimit', None) @@ -1310,6 +1328,8 @@ def _call_numpy(self, x): out : `numpy.ndarray` Result of the transform """ + assert isinstance(x, np.ndarray) + # Pre-processing before calculating the DFT # Note: since the FFT call is out-of-place, it does not matter if # preprocess produces real or complex output in the R2C variant. @@ -1358,6 +1378,9 @@ def _call_pyfftw(self, x, out, **kwargs): Result of the transform. The returned object is a reference to the input parameter ``out``. """ + assert isinstance(x, np.ndarray) + assert isinstance(out, np.ndarray) + # We pop some kwargs options here so that we always use the ones # given during init or implicitly assumed. kwargs.pop('axes', None) @@ -1551,6 +1574,8 @@ def _call_numpy(self, x): out : `numpy.ndarray` Result of the transform """ + assert isinstance(x, np.ndarray) + # Pre-processing before calculating the DFT preproc = self._preprocess(x) @@ -1603,6 +1628,8 @@ def _call_pyfftw(self, x, out, **kwargs): Result of the transform. If ``out`` was given, the returned object is a reference to it. """ + assert isinstance(x, np.ndarray) + assert isinstance(out, np.ndarray) # We pop some kwargs options here so that we always use the ones # given during init or implicitly assumed. diff --git a/odl/trafos/util/ft_utils.py b/odl/trafos/util/ft_utils.py index 2e2f3059773..3901a5d6f2e 100644 --- a/odl/trafos/util/ft_utils.py +++ b/odl/trafos/util/ft_utils.py @@ -104,11 +104,11 @@ def reciprocal_grid(grid, shift=True, axes=None, halfcomplex=False): param_conv=bool) # Full-length vectors - stride = grid.stride.copy() + stride = np.copy(grid.stride) stride[stride == 0] = 1 shape = np.array(grid.shape) - rmin = grid.min_pt.copy() - rmax = grid.max_pt.copy() + rmin = np.copy(grid.min_pt) + rmax = np.copy(grid.max_pt) rshape = list(shape) # Shifted axes (full length to avoid ugly double indexing) diff --git a/odl/ufunc_ops/ufunc_ops.py b/odl/ufunc_ops/ufunc_ops.py deleted file mode 100644 index af88b1f408a..00000000000 --- a/odl/ufunc_ops/ufunc_ops.py +++ /dev/null @@ -1,442 +0,0 @@ -# Copyright 2014-2017 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/. - -"""Ufunc operators for ODL vectors.""" - -from __future__ import print_function, division, absolute_import -import numpy as np - -from odl.set import LinearSpace, RealNumbers, Field -from odl.space import ProductSpace, tensor_space -from odl.operator import Operator, MultiplyOperator -from odl.solvers import (Functional, ScalingFunctional, FunctionalQuotient, - ConstantFunctional) -from odl.util.ufuncs import UFUNCS - -__all__ = () - -SUPP_TYPECODES = '?bhilqpBHILQPefdgFDG' -SUPP_TYPECODES_TO_DTYPES = {tc: np.dtype(tc) for tc in SUPP_TYPECODES} - - -def find_min_signature(ufunc, dtypes_in): - """Determine the minimum matching ufunc signature for given dtypes. - - Parameters - ---------- - ufunc : str or numpy.ufunc - Ufunc whose signatures are to be considered. - dtypes_in : - Sequence of objects specifying input dtypes. Its length must match - the number of inputs of ``ufunc``, and its entries must be understood - by `numpy.dtype`. - - Returns - ------- - signature : str - Minimum matching ufunc signature, see, e.g., ``np.add.types`` - for examples. - - Raises - ------ - TypeError - If no valid signature is found. - """ - if not isinstance(ufunc, np.ufunc): - ufunc = getattr(np, str(ufunc)) - - dtypes_in = [np.dtype(dt_in) for dt_in in dtypes_in] - tcs_in = [dt.base.char for dt in dtypes_in] - - if len(tcs_in) != ufunc.nin: - raise ValueError('expected {} input dtype(s) for {}, got {}' - ''.format(ufunc.nin, ufunc, len(tcs_in))) - - valid_sigs = [] - for sig in ufunc.types: - sig_tcs_in, sig_tcs_out = sig.split('->') - if all(np.dtype(tc_in) <= np.dtype(sig_tc_in) and - sig_tc_in in SUPP_TYPECODES - for tc_in, sig_tc_in in zip(tcs_in, sig_tcs_in)): - valid_sigs.append(sig) - - if not valid_sigs: - raise TypeError('no valid signature found for {} and input dtypes {}' - ''.format(ufunc, tuple(dt.name for dt in dtypes_in))) - - def in_dtypes(sig): - """Comparison key function for input dtypes of a signature.""" - sig_tcs_in = sig.split('->')[0] - return tuple(np.dtype(tc) for tc in sig_tcs_in) - - return min(valid_sigs, key=in_dtypes) - - -def dtypes_out(ufunc, dtypes_in): - """Return the result dtype(s) of ``ufunc`` with inputs of given dtypes.""" - sig = find_min_signature(ufunc, dtypes_in) - tcs_out = sig.split('->')[1] - return tuple(np.dtype(tc) for tc in tcs_out) - - -def _is_integer_only_ufunc(name): - return 'shift' in name or 'bitwise' in name or name == 'invert' - - -LINEAR_UFUNCS = ['negative', 'rad2deg', 'deg2rad', 'add', 'subtract'] - - -RAW_EXAMPLES_DOCSTRING = """ -Examples --------- ->>> import odl ->>> space = odl.{space!r} ->>> op = odl.ufunc_ops.{name}(space) ->>> print(op({arg})) -{result!s} -""" - - -def gradient_factory(name): - """Create gradient `Functional` for some ufuncs.""" - - if name == 'sin': - def gradient(self): - """Return the gradient operator.""" - return cos(self.domain) - elif name == 'cos': - def gradient(self): - """Return the gradient operator.""" - return -sin(self.domain) - elif name == 'tan': - def gradient(self): - """Return the gradient operator.""" - return 1 + square(self.domain) * self - elif name == 'sqrt': - def gradient(self): - """Return the gradient operator.""" - return FunctionalQuotient(ConstantFunctional(self.domain, 0.5), - self) - elif name == 'square': - def gradient(self): - """Return the gradient operator.""" - return ScalingFunctional(self.domain, 2.0) - elif name == 'log': - def gradient(self): - """Return the gradient operator.""" - return reciprocal(self.domain) - elif name == 'exp': - def gradient(self): - """Return the gradient operator.""" - return self - elif name == 'reciprocal': - def gradient(self): - """Return the gradient operator.""" - return FunctionalQuotient(ConstantFunctional(self.domain, -1.0), - square(self.domain)) - elif name == 'sinh': - def gradient(self): - """Return the gradient operator.""" - return cosh(self.domain) - elif name == 'cosh': - def gradient(self): - """Return the gradient operator.""" - return sinh(self.domain) - else: - # Fallback to default - gradient = Functional.gradient - - return gradient - - -def derivative_factory(name): - """Create derivative function for some ufuncs.""" - - if name == 'sin': - def derivative(self, point): - """Return the derivative operator.""" - return MultiplyOperator(cos(self.domain)(point)) - elif name == 'cos': - def derivative(self, point): - """Return the derivative operator.""" - point = self.domain.element(point) - return MultiplyOperator(-sin(self.domain)(point)) - elif name == 'tan': - def derivative(self, point): - """Return the derivative operator.""" - return MultiplyOperator(1 + self(point) ** 2) - elif name == 'sqrt': - def derivative(self, point): - """Return the derivative operator.""" - return MultiplyOperator(0.5 / self(point)) - elif name == 'square': - def derivative(self, point): - """Return the derivative operator.""" - point = self.domain.element(point) - return MultiplyOperator(2.0 * point) - elif name == 'log': - def derivative(self, point): - """Return the derivative operator.""" - point = self.domain.element(point) - return MultiplyOperator(1.0 / point) - elif name == 'exp': - def derivative(self, point): - """Return the derivative operator.""" - return MultiplyOperator(self(point)) - elif name == 'reciprocal': - def derivative(self, point): - """Return the derivative operator.""" - point = self.domain.element(point) - return MultiplyOperator(-self(point) ** 2) - elif name == 'sinh': - def derivative(self, point): - """Return the derivative operator.""" - point = self.domain.element(point) - return MultiplyOperator(cosh(self.domain)(point)) - elif name == 'cosh': - def derivative(self, point): - """Return the derivative operator.""" - return MultiplyOperator(sinh(self.domain)(point)) - else: - # Fallback to default - derivative = Operator.derivative - - return derivative - - -def ufunc_class_factory(name, nargin, nargout, docstring): - """Create a Ufunc `Operator` from a given specification.""" - - assert 0 <= nargin <= 2 - - def __init__(self, space): - """Initialize an instance. - - Parameters - ---------- - space : `TensorSpace` - The domain of the operator. - """ - if not isinstance(space, LinearSpace): - raise TypeError('`space` {!r} not a `LinearSpace`'.format(space)) - - if nargin == 1: - domain = space0 = space - dtypes = [space.dtype] - elif nargin == len(space) == 2 and isinstance(space, ProductSpace): - domain = space - space0 = space[0] - dtypes = [space[0].dtype, space[1].dtype] - else: - domain = ProductSpace(space, nargin) - space0 = space - dtypes = [space.dtype, space.dtype] - - dts_out = dtypes_out(name, dtypes) - - if nargout == 1: - range = space0.astype(dts_out[0]) - else: - range = ProductSpace(space0.astype(dts_out[0]), - space0.astype(dts_out[1])) - - linear = name in LINEAR_UFUNCS - Operator.__init__(self, domain=domain, range=range, linear=linear) - - def _call(self, x, out=None): - """Return ``self(x)``.""" - # TODO: use `__array_ufunc__` when implemented on `ProductSpace`, - # or try both - if out is None: - if nargin == 1: - return getattr(x.ufuncs, name)() - else: - return getattr(x[0].ufuncs, name)(*x[1:]) - else: - if nargin == 1: - return getattr(x.ufuncs, name)(out=out) - else: - return getattr(x[0].ufuncs, name)(*x[1:], out=out) - - def __repr__(self): - """Return ``repr(self)``.""" - return '{}({!r})'.format(name, self.domain) - - # Create example (also functions as doctest) - if 'shift' in name or 'bitwise' in name or name == 'invert': - dtype = int - else: - dtype = float - - space = tensor_space(3, dtype=dtype) - if nargin == 1: - vec = space.element([-1, 1, 2]) - arg = '{}'.format(vec) - with np.errstate(all='ignore'): - result = getattr(vec.ufuncs, name)() - else: - vec = space.element([-1, 1, 2]) - vec2 = space.element([3, 4, 5]) - arg = '[{}, {}]'.format(vec, vec2) - with np.errstate(all='ignore'): - result = getattr(vec.ufuncs, name)(vec2) - - if nargout == 2: - result_space = ProductSpace(vec.space, 2) - result = repr(result_space.element(result)) - - examples_docstring = RAW_EXAMPLES_DOCSTRING.format(space=space, name=name, - arg=arg, result=result) - full_docstring = docstring + examples_docstring - - attributes = {"__init__": __init__, - "_call": _call, - "derivative": derivative_factory(name), - "__repr__": __repr__, - "__doc__": full_docstring} - - full_name = name + '_op' - - return type(full_name, (Operator,), attributes) - - -def ufunc_functional_factory(name, nargin, nargout, docstring): - """Create a ufunc `Functional` from a given specification.""" - - assert 0 <= nargin <= 2 - - def __init__(self, field): - """Initialize an instance. - - Parameters - ---------- - field : `Field` - The domain of the functional. - """ - if not isinstance(field, Field): - raise TypeError('`field` {!r} not a `Field`'.format(space)) - - if _is_integer_only_ufunc(name): - raise ValueError("ufunc '{}' only defined with integral dtype" - "".format(name)) - - linear = name in LINEAR_UFUNCS - Functional.__init__(self, space=field, linear=linear) - - def _call(self, x): - """Return ``self(x)``.""" - if nargin == 1: - return getattr(np, name)(x) - else: - return getattr(np, name)(*x) - - def __repr__(self): - """Return ``repr(self)``.""" - return '{}({!r})'.format(name, self.domain) - - # Create example (also functions as doctest) - - if nargin != 1: - raise NotImplementedError('Currently not suppored') - - if nargout != 1: - raise NotImplementedError('Currently not suppored') - - space = RealNumbers() - val = 1.0 - arg = '{}'.format(val) - with np.errstate(all='ignore'): - result = np.float64(getattr(np, name)(val)) - - examples_docstring = RAW_EXAMPLES_DOCSTRING.format(space=space, name=name, - arg=arg, result=result) - full_docstring = docstring + examples_docstring - - attributes = {"__init__": __init__, - "_call": _call, - "gradient": property(gradient_factory(name)), - "__repr__": __repr__, - "__doc__": full_docstring} - - full_name = name + '_op' - - return type(full_name, (Functional,), attributes) - - -RAW_UFUNC_FACTORY_DOCSTRING = """{docstring} -Notes ------ -This creates a `Operator`/`Functional` that applies a ufunc pointwise. - -Examples --------- -{operator_example} -{functional_example} -""" - -RAW_UFUNC_FACTORY_FUNCTIONAL_DOCSTRING = """ -Create functional with domain/range as real numbers: - ->>> func = odl.ufunc_ops.{name}() -""" - -RAW_UFUNC_FACTORY_OPERATOR_DOCSTRING = """ -Create operator that acts pointwise on a `TensorSpace` - ->>> space = odl.rn(3) ->>> op = odl.ufunc_ops.{name}(space) -""" - - -# Create an operator for each ufunc -for name, nargin, nargout, docstring in UFUNCS: - def indirection(name, docstring): - # Indirection is needed since name should be saved but is changed - # in the loop. - - def ufunc_factory(domain=RealNumbers()): - # Create a `Operator` or `Functional` depending on arguments - try: - if isinstance(domain, Field): - return globals()[name + '_func'](domain) - else: - return globals()[name + '_op'](domain) - except KeyError: - raise ValueError('ufunc not available for {}'.format(domain)) - return ufunc_factory - - globals()[name + '_op'] = ufunc_class_factory(name, nargin, - nargout, docstring) - if not _is_integer_only_ufunc(name): - operator_example = RAW_UFUNC_FACTORY_OPERATOR_DOCSTRING.format( - name=name) - else: - operator_example = "" - - if not _is_integer_only_ufunc(name) and nargin == 1 and nargout == 1: - globals()[name + '_func'] = ufunc_functional_factory( - name, nargin, nargout, docstring) - functional_example = RAW_UFUNC_FACTORY_FUNCTIONAL_DOCSTRING.format( - name=name) - else: - functional_example = "" - - ufunc_factory = indirection(name, docstring) - - ufunc_factory.__doc__ = RAW_UFUNC_FACTORY_DOCSTRING.format( - docstring=docstring, name=name, - functional_example=functional_example, - operator_example=operator_example) - - globals()[name] = ufunc_factory - __all__ += (name,) - - -if __name__ == '__main__': - from odl.util.testutils import run_doctests - run_doctests() diff --git a/odl/util/numerics.py b/odl/util/numerics.py index 7e2a0269fec..bd88736272e 100644 --- a/odl/util/numerics.py +++ b/odl/util/numerics.py @@ -117,9 +117,9 @@ def apply_on_boundary(array, func, only_once=True, which_boundaries=None, ''.format(len(axis_order), array.ndim)) if out is None: - out = array.copy() + out = np.copy(array) else: - out[:] = array # Self assignment is free, in case out is array + out[:] = array # Self-assignment is free, in case `out is array` # The 'only_once' functionality is implemented by storing for each axis # if the left and right boundaries have been processed. This information @@ -490,7 +490,7 @@ def resize_array(arr, newshp, offset=None, pad_mode='constant', pad_const=0, else: # Apply adjoint padding to a copy of the input and copy the inner # part when finished - tmp = arr.copy() + tmp = np.copy(arr) _apply_padding(tmp, out, offset, pad_mode, 'adjoint') _assign_intersection(out, tmp, offset) diff --git a/odl/util/pytest_config.py b/odl/util/pytest_config.py index 059420f4212..d7b8a02c2d3 100644 --- a/odl/util/pytest_config.py +++ b/odl/util/pytest_config.py @@ -103,21 +103,18 @@ def pytest_ignore_collect(path, config): floating_dtypes = np.sctypes['float'] + np.sctypes['complex'] floating_dtype_params = [np.dtype(dt) for dt in floating_dtypes] -odl_floating_dtype = simple_fixture(name='dtype', - params=floating_dtype_params, - fmt=' {name} = np.{value.name} ') +odl_floating_dtype = simple_fixture( + name='dtype', params=floating_dtype_params, fmt=' {name}=np.{value.name} ' +) scalar_dtypes = floating_dtype_params + np.sctypes['int'] + np.sctypes['uint'] scalar_dtype_params = [np.dtype(dt) for dt in floating_dtypes] -odl_scalar_dtype = simple_fixture(name='dtype', - params=scalar_dtype_params, - fmt=' {name} = np.{value.name} ') +odl_scalar_dtype = simple_fixture( + name='dtype', params=scalar_dtype_params, fmt=' {name}=np.{value.name} ' +) odl_elem_order = simple_fixture(name='order', params=[None, 'C', 'F']) -odl_ufunc = simple_fixture('ufunc', [p[0] for p in odl.util.ufuncs.UFUNCS]) -odl_reduction = simple_fixture('reduction', ['sum', 'prod', 'min', 'max']) - # More complicated ones with non-trivial documentation arithmetic_op_par = [operator.add, operator.truediv, @@ -127,7 +124,7 @@ def pytest_ignore_collect(path, config): operator.itruediv, operator.imul, operator.isub] -arithmetic_op_ids = [" op = '{}' ".format(op) +arithmetic_op_ids = [" op='{}' ".format(op) for op in ['+', '/', '*', '-', '+=', '/=', '*=', '-=']] diff --git a/odl/util/testutils.py b/odl/util/testutils.py index 6463c7f8b6e..489863a1705 100644 --- a/odl/util/testutils.py +++ b/odl/util/testutils.py @@ -111,8 +111,10 @@ def all_equal(iter1, iter2): """Return ``True`` if all elements in ``a`` and ``b`` are equal.""" # Direct comparison for scalars, tuples or lists try: - if iter1 == iter2: - return True + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + if iter1 is iter2 or iter1 == iter2: + return True except ValueError: # Raised by NumPy when comparing arrays pass @@ -148,16 +150,27 @@ def all_equal(iter1, iter2): def all_almost_equal_array(v1, v2, ndigits): - return np.allclose(v1, v2, - rtol=10 ** -ndigits, atol=10 ** -ndigits, - equal_nan=True) + if v1.dtype == object and v2.dtype == object: + return all( + all_almost_equal_array(v1_i, v2_i, ndigits) + for v1_i, v2_i in zip(v1, v2) + ) + + try: + return np.allclose( + v1, v2, rtol=10 ** -ndigits, atol=10 ** -ndigits, equal_nan=True + ) + except TypeError: + return False def all_almost_equal(iter1, iter2, ndigits=None): """Return ``True`` if all elements in ``a`` and ``b`` are almost equal.""" try: - if iter1 is iter2 or iter1 == iter2: - return True + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + if iter1 is iter2 or iter1 == iter2: + return True except ValueError: pass @@ -166,7 +179,7 @@ def all_almost_equal(iter1, iter2, ndigits=None): if hasattr(iter1, '__array__') and hasattr(iter2, '__array__'): # Only get default ndigits if comparing arrays, need to keep `None` - # otherwise for recursive calls. + # otherwise for recursive calls if ndigits is None: ndigits = _ndigits(iter1, iter2, None) return all_almost_equal_array(iter1, iter2, ndigits) @@ -295,8 +308,10 @@ def simple_fixture(name, params, fmt=None): # Helpers to generate data + +# TODO(kohr-h): rename to noise_np_array def noise_array(space): - """Generate a white noise array that is compatible with ``space``. + """Generate a white noise array for ``space``. The array contains white noise with standard deviation 1 in the case of floating point dtypes and uniformly spaced values between -10 and 10 in @@ -316,9 +331,8 @@ def noise_array(space): Returns ------- - noise_array : `numpy.ndarray` element - Array with white noise such that ``space.element``'s can be created - from it. + noise_array : numpy.ndarray + Array containing white noise. Examples -------- @@ -335,26 +349,27 @@ def noise_array(space): typical to the space. """ from odl.space import ProductSpace + if isinstance(space, ProductSpace): - return np.array([noise_array(si) for si in space]) + return np.array([noise_element(spc_i) for spc_i in space]) + + if space.dtype == bool: + arr = np.random.randint(0, 2, size=space.shape, dtype=bool) + elif np.issubdtype(space.dtype, np.unsignedinteger): + arr = np.random.randint(0, 10, space.shape) + elif np.issubdtype(space.dtype, np.signedinteger): + arr = np.random.randint(-10, 10, space.shape) + elif np.issubdtype(space.dtype, np.floating): + arr = np.random.randn(*space.shape) + elif np.issubdtype(space.dtype, np.complexfloating): + arr = ( + np.random.randn(*space.shape) + + 1j * np.random.randn(*space.shape) + ) / np.sqrt(2.0) else: - if space.dtype == bool: - arr = np.random.randint(0, 2, size=space.shape, dtype=bool) - elif np.issubdtype(space.dtype, np.unsignedinteger): - arr = np.random.randint(0, 10, space.shape) - elif np.issubdtype(space.dtype, np.signedinteger): - arr = np.random.randint(-10, 10, space.shape) - elif np.issubdtype(space.dtype, np.floating): - arr = np.random.randn(*space.shape) - elif np.issubdtype(space.dtype, np.complexfloating): - arr = ( - np.random.randn(*space.shape) - + 1j * np.random.randn(*space.shape) - ) / np.sqrt(2.0) - else: - raise ValueError('bad dtype {}'.format(space.dtype)) + raise ValueError('bad dtype {}'.format(space.dtype)) - return arr.astype(space.dtype, copy=False) + return arr.astype(space.dtype, copy=False) def noise_element(space): @@ -448,9 +463,7 @@ def noise_elements(space, n=1): noise_element """ arrs = tuple(noise_array(space) for _ in range(n)) - - # Make space elements from arrays - elems = tuple(space.element(arr.copy()) for arr in arrs) + elems = tuple(space.copy(arr) for arr in arrs) if n == 1: return tuple(arrs + elems) diff --git a/odl/util/ufuncs.py b/odl/util/ufuncs.py deleted file mode 100644 index 6926e642501..00000000000 --- a/odl/util/ufuncs.py +++ /dev/null @@ -1,303 +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/. - -"""Universal functions (ufuncs) for ODL-wrapped arrays. - -These functions are internal and should only be used as methods on -`Tensor`-like classes. - -See `numpy.ufuncs -`_ -for more information. - -Notes ------ -The default implementation of these methods uses the ``__array_ufunc__`` -dispatch machinery `introduced in Numpy 1.13 -`_. -""" - -from __future__ import print_function, division, absolute_import -from builtins import object -import numpy as np -import re - - -__all__ = ('TensorSpaceUfuncs', 'ProductSpaceUfuncs') - - -# Some are ignored since they don't cooperate with dtypes, needs fix -RAW_UFUNCS = ['absolute', 'add', 'arccos', 'arccosh', 'arcsin', 'arcsinh', - 'arctan', 'arctan2', 'arctanh', 'bitwise_and', 'bitwise_or', - 'bitwise_xor', 'ceil', 'conj', 'copysign', 'cos', 'cosh', - 'deg2rad', 'divide', 'equal', 'exp', 'exp2', 'expm1', 'floor', - 'floor_divide', 'fmax', 'fmin', 'fmod', 'greater', - 'greater_equal', 'hypot', 'invert', 'isfinite', 'isinf', 'isnan', - 'left_shift', 'less', 'less_equal', 'log', 'log10', 'log1p', - 'log2', 'logaddexp', 'logaddexp2', 'logical_and', 'logical_not', - 'logical_or', 'logical_xor', 'maximum', 'minimum', 'mod', 'modf', - 'multiply', 'negative', 'not_equal', 'power', - 'rad2deg', 'reciprocal', 'remainder', 'right_shift', 'rint', - 'sign', 'signbit', 'sin', 'sinh', 'sqrt', 'square', 'subtract', - 'tan', 'tanh', 'true_divide', 'trunc'] -# ,'isreal', 'iscomplex', 'ldexp', 'frexp' - -# Add some standardized information -UFUNCS = [] -for name in RAW_UFUNCS: - ufunc = getattr(np, name) - n_in, n_out = ufunc.nin, ufunc.nout - descr = ufunc.__doc__.splitlines()[2] - # Numpy occasionally uses single ticks for doc, we only use them for links - descr = re.sub('`+', '``', descr) - doc = descr + """ - -See Also --------- -numpy.{} -""".format(name) - UFUNCS.append((name, n_in, n_out, doc)) - -# TODO: add the following reductions (to the CUDA implementation): -# ['var', 'trace', 'tensordot', 'std', 'ptp', 'mean', 'diff', 'cumsum', -# 'cumprod', 'average'] - - -# --- Wrappers for `Tensor` --- # - - -def wrap_ufunc_base(name, n_in, n_out, doc): - """Return ufunc wrapper for implementation-agnostic ufunc classes.""" - ufunc = getattr(np, name) - if n_in == 1: - if n_out == 1: - def wrapper(self, out=None, **kwargs): - if out is None or isinstance(out, (type(self.elem), - type(self.elem.data))): - out = (out,) - - return self.elem.__array_ufunc__( - ufunc, '__call__', self.elem, out=out, **kwargs) - - elif n_out == 2: - def wrapper(self, out=None, **kwargs): - if out is None: - out = (None, None) - - return self.elem.__array_ufunc__( - ufunc, '__call__', self.elem, out=out, **kwargs) - - else: - raise NotImplementedError - - elif n_in == 2: - if n_out == 1: - def wrapper(self, x2, out=None, **kwargs): - return self.elem.__array_ufunc__( - ufunc, '__call__', self.elem, x2, out=(out,), **kwargs) - - else: - raise NotImplementedError - else: - raise NotImplementedError - - wrapper.__name__ = wrapper.__qualname__ = name - wrapper.__doc__ = doc - return wrapper - - -class TensorSpaceUfuncs(object): - - """Ufuncs for `Tensor` objects. - - Internal object, should not be created except in `Tensor`. - """ - - def __init__(self, elem): - """Create ufunc wrapper for elem.""" - self.elem = elem - - # Reductions for backwards compatibility - - def sum(self, axis=None, dtype=None, out=None, keepdims=False): - """Return the sum of ``self``. - - See Also - -------- - numpy.sum - prod - """ - return self.elem.__array_ufunc__( - np.add, 'reduce', self.elem, - axis=axis, dtype=dtype, out=(out,), keepdims=keepdims) - - def prod(self, axis=None, dtype=None, out=None, keepdims=False): - """Return the product of ``self``. - - See Also - -------- - numpy.prod - sum - """ - return self.elem.__array_ufunc__( - np.multiply, 'reduce', self.elem, - axis=axis, dtype=dtype, out=(out,), keepdims=keepdims) - - def min(self, axis=None, dtype=None, out=None, keepdims=False): - """Return the minimum of ``self``. - - See Also - -------- - numpy.amin - max - """ - return self.elem.__array_ufunc__( - np.minimum, 'reduce', self.elem, - axis=axis, dtype=dtype, out=(out,), keepdims=keepdims) - - def max(self, axis=None, dtype=None, out=None, keepdims=False): - """Return the maximum of ``self``. - - See Also - -------- - numpy.amax - min - """ - return self.elem.__array_ufunc__( - np.maximum, 'reduce', self.elem, - axis=axis, dtype=dtype, out=(out,), keepdims=keepdims) - - -# Add ufunc methods to ufunc class -for name, n_in, n_out, doc in UFUNCS: - method = wrap_ufunc_base(name, n_in, n_out, doc) - setattr(TensorSpaceUfuncs, name, method) - - -# --- Wrappers for `ProductSpaceElement` --- # - - -def wrap_ufunc_productspace(name, n_in, n_out, doc): - """Return ufunc wrapper for `ProductSpaceUfuncs`.""" - if n_in == 1: - if n_out == 1: - def wrapper(self, out=None, **kwargs): - if out is None: - result = [getattr(x.ufuncs, name)(**kwargs) - for x in self.elem] - return self.elem.space.element(result) - else: - for x, out_x in zip(self.elem, out): - getattr(x.ufuncs, name)(out=out_x, **kwargs) - return out - - elif n_out == 2: - def wrapper(self, out1=None, out2=None, **kwargs): - if out1 is None: - out1 = self.elem.space.element() - if out2 is None: - out2 = self.elem.space.element() - for x, out1_x, out2_x in zip(self.elem, out1, out2): - getattr(x.ufuncs, name)(out1=out1_x, out2=out2_x, **kwargs) - return out1, out2 - - else: - raise NotImplementedError - - elif n_in == 2: - if n_out == 1: - def wrapper(self, x2, out=None, **kwargs): - if x2 in self.elem.space: - if out is None: - result = [getattr(x.ufuncs, name)(x2p, **kwargs) - for x, x2p in zip(self.elem, x2)] - return self.elem.space.element(result) - else: - for x, x2p, outp in zip(self.elem, x2, out): - getattr(x.ufuncs, name)(x2p, out=outp, **kwargs) - return out - else: - if out is None: - result = [getattr(x.ufuncs, name)(x2, **kwargs) - for x in self.elem] - return self.elem.space.element(result) - else: - for x, outp in zip(self.elem, out): - getattr(x.ufuncs, name)(x2, out=outp, **kwargs) - return out - - else: - raise NotImplementedError - else: - raise NotImplementedError - - wrapper.__name__ = wrapper.__qualname__ = name - wrapper.__doc__ = doc - return wrapper - - -class ProductSpaceUfuncs(object): - - """Ufuncs for `ProductSpaceElement` objects. - - Internal object, should not be created except in `ProductSpaceElement`. - """ - def __init__(self, elem): - """Create ufunc wrapper for ``elem``.""" - self.elem = elem - - def sum(self): - """Return the sum of ``self``. - - See Also - -------- - numpy.sum - prod - """ - results = [x.ufuncs.sum() for x in self.elem] - return np.sum(results) - - def prod(self): - """Return the product of ``self``. - - See Also - -------- - numpy.prod - sum - """ - results = [x.ufuncs.prod() for x in self.elem] - return np.prod(results) - - def min(self): - """Return the minimum of ``self``. - - See Also - -------- - numpy.amin - max - """ - results = [x.ufuncs.min() for x in self.elem] - return np.min(results) - - def max(self): - """Return the maximum of ``self``. - - See Also - -------- - numpy.amax - min - """ - results = [x.ufuncs.max() for x in self.elem] - return np.max(results) - - -# Add ufunc methods to ufunc class -for name, n_in, n_out, doc in UFUNCS: - method = wrap_ufunc_productspace(name, n_in, n_out, doc) - setattr(ProductSpaceUfuncs, name, method) diff --git a/odl/util/utility.py b/odl/util/utility.py index 9b5732d91e8..fca0ea4b0a0 100644 --- a/odl/util/utility.py +++ b/odl/util/utility.py @@ -196,7 +196,7 @@ def npy_printoptions(**extra_opts): orig_opts = np.get_printoptions() try: - new_opts = orig_opts.copy() + new_opts = dict(orig_opts) new_opts.update(extra_opts) np.set_printoptions(**new_opts) yield @@ -597,7 +597,7 @@ def writable_array(obj, **kwargs): >>> with writable_array(x) as arr: ... arr += [1, 1, 1] >>> x - uniform_discr(0.0, 1.0, 3).element([ 2., 3., 4.]) + array([ 2., 3., 4.]) Additional keyword arguments are passed to `numpy.asarray`: