{"action":"created","comment":{"url":"https://api.github.com/repos/odlgroup/odl/pulls/comments/69097598","id":69097598,"diff_hunk":"@@ -0,0 +1,804 @@\n+# Copyright 2014-2016 The ODL development group\n+#\n+# This file is part of ODL.\n+#\n+# ODL is free software: you can redistribute it and/or modify\n+# it under the terms of the GNU General Public License as published by\n+# the Free Software Foundation, either version 3 of the License, or\n+# (at your option) any later version.\n+#\n+# ODL is distributed in the hope that it will be useful,\n+# but WITHOUT ANY WARRANTY; without even the implied warranty of\n+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n+# GNU General Public License for more details.\n+#\n+# You should have received a copy of the GNU General Public License\n+# along with ODL.  If not, see <http://www.gnu.org/licenses/>.\n+\n+\"\"\"Operators based on (integral) transformations.\"\"\"\n+\n+# Imports for common Python 2/3 codebase\n+from __future__ import print_function, division, absolute_import\n+from future import standard_library\n+standard_library.install_aliases()\n+from builtins import super\n+\n+import numpy as np\n+import scipy.signal as signal\n+\n+from odl.discr.discr_ops import Resampling\n+from odl.discr.lp_discr import (\n+    DiscreteLp, DiscreteLpVector, uniform_discr, uniform_discr_fromdiscr)\n+from odl.operator.operator import Operator\n+from odl.space.ntuples import Ntuples\n+from odl.space.cu_ntuples import CudaNtuples\n+from odl.set.sets import ComplexNumbers\n+from odl.trafos.fourier import FourierTransform\n+from odl.util.normalize import normalized_scalar_param_list, safe_int_conv\n+\n+\n+__all__ = ('Convolution', 'FourierSpaceConvolution', 'RealSpaceConvolution')\n+\n+\n+_REAL_CONV_SUPPORTED_IMPL = ('scipy_convolve',)\n+_FOURIER_CONV_SUPPORTED_IMPL = ('default', 'pyfftw')\n+_FOURIER_CONV_SUPPORTED_KER_MODES = ('real', 'fourier')\n+\n+\n+class Convolution(Operator):\n+\n+    \"\"\"Discretization of the convolution integral as an operator.\n+\n+    This class provides common attributes and methods for subclasses\n+    implementing different variants.\n+    \"\"\"\n+\n+    def kernel(self):\n+        \"\"\"Return the kernel of this convolution operator.\"\"\"\n+        raise NotImplementedError('abstract method')\n+\n+\n+class FourierSpaceConvolution(Convolution):\n+\n+    \"\"\"Convolution with a kernel, implemented in Fourier space.\n+\n+    This operator implements a discrete approximation to the continuous\n+    convolution with a fixed kernel. It supports real-space and\n+    Fourier based back-ends.\n+    \"\"\"\n+\n+    def __init__(self, domain, kernel, **kwargs):\n+        \"\"\"Initialize a new instance.\n+\n+        Parameters\n+        ----------\n+        domain : `DiscreteLp`\n+            Domain of the operator.\n+        kernel :\n+            Convolution kernel of this operator. The kernel can be\n+            specified in several ways:\n+\n+            domain `element-like` : The object is interpreted as the\n+            real-space kernel representation (mode ``'real'``).\n+            Valid for ``impl``: ``'numpy_ft', 'pyfftw_ft',\n+            'scipy_convolve'``\n+\n+            `element-like` for the range of `FourierTransform` defined\n+            on ``dom`` : The object is interpreted as the Fourier\n+            transform of a real-space kernel (mode ``ft`` or ``'ft_hc'``).\n+            The correct space can be calculated with `reciprocal_space`.\n+            Valid for ``impl``: ``'default_ft', 'pyfftw_ft'``\n+\n+            `array-like`, arbitrary length : The object is interpreted as\n+            real-space kernel (mode ``'real'``) and can be shorter than\n+            the convolved function.\n+            Valid for ``impl``: ``'scipy_convolve'``\n+\n+        kernel_mode : {'real', 'fourier'}, optional\n+            Specifies the way the kernel is to be interpreted. If not\n+            provided, the kernel is tried to be converted into an element\n+            of a reasonable space derived from ``domain``.\n+\n+            'real' : real-space kernel (default)\n+\n+            'fourier' : fourier-space kernel\n+\n+        impl : `str`, optional\n+            Implementation of the convolution. Available options are:\n+\n+            'default' : Fourier transform using the default FFT\n+            implementation\n+\n+            'pyfftw' : Fourier transform using pyFFTW (faster than\n+            the default FT)\n+\n+        axes : sequence of `int`, optional\n+            Dimensions in which to convolve. Default: all axes\n+\n+        scale : `bool`, optional\n+            If `True`, scale the discrete convolution by\n+            ``2*pi ** (ndim/2)``, such that it corresponds to a\n+            continuous convolution.\n+            Default: `True`\n+\n+        kwargs :\n+            Extra arguments are passed to the transform when the\n+            kernel FT is calculated.\n+\n+        See also\n+        --------\n+        FourierTransform : discretization of the continuous FT\n+        scipy.signal.convolve : real-space convolution\n+\n+        Notes\n+        -----\n+        - The continuous convolution of two functions\n+          :math:`f` and :math:`g` defined on :math:`\\mathbb{R^d}` is\n+\n+              :math:`[f \\\\ast g](x) =\n+              \\int_{\\mathbb{R^d}} f(x - y) g(y) \\mathrm{d}y`.\n+\n+          With the help of the Fourier transform, this operation can be\n+          expressed as a multiplication,\n+\n+              :math:`\\\\big[\\mathcal{F}(f \\\\ast g)\\\\big](\\\\xi)\n+              = (2\\pi)^{\\\\frac{d}{2}}\\,\n+              \\\\big[\\mathcal{F}(f)\\\\big](\\\\xi) \\cdot\n+              \\\\big[\\mathcal{F}(g)\\\\big](\\\\xi)`.\n+\n+          This implementation covers the case of a fixed convolution\n+          kernel :math:`k`, i.e. the linear operator\n+\n+              :math:`\\\\big[\\mathcal{C}_k(f)\\\\big](x) = [k \\\\ast f](x)`.\n+        \"\"\"\n+        # Basic checks\n+        if not isinstance(domain, DiscreteLp):\n+            raise TypeError('`domain` {!r} is not a `DiscreteLp` instance.'\n+                            ''.format(domain))\n+\n+\n+        if ran is not None:\n+            # TODO\n+            raise NotImplementedError('custom range not implemented')\n+        else:\n+            ran = dom\n+\n+        super().__init__(dom, ran, linear=True)\n+\n+        # Handle kernel mode and impl\n+        impl = kwargs.pop('impl', 'default_ft')\n+        impl, impl_in = str(impl).lower(), impl\n+        if impl not in _CONV_SUPPORTED_IMPL:\n+            raise ValueError(\"implementation '{}' not understood.\"\n+                             ''.format(impl_in))\n+        self._impl = impl\n+\n+        ker_mode = kwargs.pop('kernel_mode', 'real')\n+        ker_mode, ker_mode_in = str(ker_mode).lower(), ker_mode\n+        if ker_mode not in _CONV_SUPPORTED_KER_MODES:\n+            raise ValueError(\"kernel mode '{}' not understood.\"\n+                             ''.format(ker_mode_in))\n+\n+        self._kernel_mode = ker_mode\n+\n+        use_own_ft = (self.impl in ('default_ft', 'pyfftw_ft'))\n+        if not use_own_ft and self.kernel_mode != 'real':\n+            raise ValueError(\"kernel mode 'real' is required for impl \"\n+                             \"{}.\".format(impl_in))\n+\n+        self._axes = list(kwargs.pop('axes', (range(self.domain.ndim))))\n+        if ker_mode == 'real':\n+            halfcomplex = True  # efficient\n+        else:\n+            halfcomplex = ker_mode.endswith('hc')\n+\n+        if use_own_ft:\n+            fft_impl = self.impl.split('_')[0]\n+            self._transform = FourierTransform(\n+                self.domain, axes=self.axes, halfcomplex=halfcomplex,\n+                impl=fft_impl)\n+        else:\n+            self._transform = None\n+\n+        scale = kwargs.pop('scale', True)\n+\n+        # TODO: handle case if axes are given, but the kernel is the same\n+        # for each point along the other axes\n+        if ker_mode == 'real':\n+            # Kernel given as real space element\n+            if use_own_ft:\n+                self._kernel = None\n+                self._kernel_transform = self.transform(kernel, **kwargs)\n+                if scale:\n+                    self._kernel_transform *= (np.sqrt(2 * np.pi) **\n+                                               self.domain.ndim)\n+            else:\n+                if not self.domain.partition.is_regular:\n+                    raise NotImplementedError(\n+                        'real-space convolution not implemented for '\n+                        'irregular sampling.')\n+                try:\n+                    # Function or other element-like as input. All axes\n+                    # must be used, otherwise we get an error.\n+                    # TODO: make a zero-centered space by default and\n+                    # adapt the range otherwise\n+                    self._kernel = self._kernel_elem(\n+                        self.domain.element(kernel).asarray(), self.axes)\n+                except (TypeError, ValueError):\n+                    # Got an array-like, axes can be used\n+                    self._kernel = self._kernel_elem(kernel, self.axes)\n+                finally:\n+                    self._kernel_transform = None\n+                    if scale:\n+                        self._kernel *= self.domain.cell_volume\n+        else:\n+            # Kernel given as Fourier space element\n+            self._kernel = None\n+            self._kernel_transform = self.transform.range.element(kernel)\n+            if scale:\n+                self._kernel_transform *= (np.sqrt(2 * np.pi) **\n+                                           self.domain.ndim)\n+\n+    def _kernel_elem(self, kernel, axes):\n+        \"\"\"Return kernel with adapted shape for real space convolution.\"\"\"\n+        kernel = np.asarray(kernel)\n+        extra_dims = self.domain.ndim - kernel.ndim\n+\n+        if extra_dims == 0:\n+            return self.domain.element(kernel)\n+        else:\n+            if len(axes) != extra_dims:\n+                raise ValueError('kernel dim {} + number of axes {} '\n+                                 '!= space dimension {}.'\n+                                 ''.format(kernel.ndim, len(axes),\n+                                           self.domain.ndim))\n+\n+            # Sparse kernel (less dimensions), blow up\n+            slc = [None] * self.domain.ndim\n+            for ax in axes:\n+                slc[ax] = slice(None)\n+\n+            kernel = kernel[slc]\n+            # Assuming uniform discretization\n+            min_corner = -self.domain.cell_sides * self.domain.shape / 2\n+            max_corner = self.domain.cell_sides * self.domain.shape / 2\n+            if isinstance(self.domain.dspace, Ntuples):\n+                impl = 'numpy'\n+            elif isinstance(self.domain.dspace, CudaNtuples):\n+                impl = 'cuda'\n+            else:\n+                raise RuntimeError\n+\n+            space = uniform_discr(min_corner, max_corner, kernel.shape,\n+                                  self.domain.exponent, self.domain.interp,\n+                                  impl, dtype=self.domain.dspace.dtype,\n+                                  order=self.domain.order,\n+                                  weighting=self.domain.weighting)\n+            return space.element(kernel)\n+\n+    @property\n+    def impl(self):\n+        \"\"\"Implementation of this operator.\"\"\"\n+        return self._impl\n+\n+    @property\n+    def kernel_mode(self):\n+        \"\"\"The way in which the kernel is specified.\"\"\"\n+        return self._kernel_mode\n+\n+    @property\n+    def transform(self):\n+        \"\"\"Fourier transform operator back-end if used, else `None`.\"\"\"\n+        return self._transform\n+\n+    @property\n+    def axes(self):\n+        \"\"\"Axes along which the convolution is taken.\"\"\"\n+        return self._axes\n+\n+    @property\n+    def kernel(self):\n+        \"\"\"Real-space kernel if used, else `None`.\n+\n+        Note that this is the scaled version ``kernel * cell_volume``\n+        if ``scale=True`` was specified. Scaling here is more efficient\n+        than scaling the result.\n+        \"\"\"\n+        return self._kernel\n+\n+    @property\n+    def kernel_space(self):\n+        \"\"\"Space of the convolution kernel.\"\"\"\n+        return getattr(self.kernel, 'space', None)\n+\n+    @property\n+    def kernel_transform(self):\n+        \"\"\"Fourier-space kernel if used, else `None`.\n+\n+        Note that this is the scaled version\n+        ``kernel_ft * (2*pi) ** (ndim/2)`` of the FT of the input\n+        kernel if ``scale=True`` was specified. Scaling here is more\n+        efficient than scaling the result.\n+        \"\"\"\n+        return self._kernel_transform\n+\n+    def _call(self, x, out, **kwargs):\n+        \"\"\"Implement ``self(x, out[, **kwargs])``.\n+\n+        Keyword arguments are passed on to the transform.\n+        \"\"\"\n+        if self.kernel is not None:\n+            # Scipy based convolution\n+            out[:] = signal.convolve(x, self.kernel, mode='same')\n+        elif self.kernel_transform is not None:\n+            # Convolution based on our own transforms\n+            if self.domain.field == ComplexNumbers():\n+                # Use out as a temporary, has the same size\n+                # TODO: won't work for CUDA\n+                tmp = self.transform.range.element(out.asarray())\n+            else:\n+                # No temporary since out has reduced size (halfcomplex)\n+                tmp = None\n+\n+            x_trafo = self.transform(x, out=tmp, **kwargs)\n+            x_trafo *= self.kernel_transform\n+\n+            self.transform.inverse(x_trafo, out=out, **kwargs)\n+        else:\n+            raise RuntimeError('both kernel and kernel_transform are None.')\n+\n+    def _adj_kernel(self, kernel, axes):\n+        \"\"\"Return adjoint kernel with adapted shape.\"\"\"\n+        kernel = np.asarray(kernel).conj()\n+        extra_dims = self.domain.ndim - kernel.ndim\n+\n+        if extra_dims == 0:\n+            slc = [slice(None, None, -1)] * self.domain.ndim\n+            return kernel[slc]\n+        else:\n+            if len(axes) != extra_dims:\n+                raise ValueError('kernel dim ({}) + number of axes ({}) '\n+                                 'does not add up to the space dimension '\n+                                 '({}).'.format(kernel.ndim, len(axes),\n+                                                self.domain.ndim))\n+\n+            # Sparse kernel (less dimensions), blow up\n+            slc = [None] * self.domain.ndim\n+            for ax in axes:\n+                slc[ax] = slice(None, None, -1)\n+            return kernel[slc]\n+\n+    @property\n+    def adjoint(self):\n+        \"\"\"Adjoint operator.\"\"\"\n+        if self.kernel is not None:\n+            # TODO: this could be expensive. Move to init?\n+            adj_kernel = self._adj_kernel(self.kernel, self.axes)\n+            return Convolution(dom=self.domain,\n+                               kernel=adj_kernel, kernel_mode='real',\n+                               impl=self.impl, axes=self.axes)\n+\n+        elif self.kernel_transform is not None:\n+            # TODO: this could be expensive. Move to init?\n+            adj_kernel_ft = self.kernel_transform.conj()\n+\n+            if self.transform.halfcomplex:\n+                kernel_mode = 'ft_hc'\n+            else:\n+                kernel_mode = 'ft'\n+            return Convolution(dom=self.domain,\n+                               kernel=adj_kernel_ft, kernel_mode=kernel_mode,\n+                               impl=self.impl, axes=self.axes)\n+        else:\n+            raise RuntimeError('both kernel and kernel_transform are None.')\n+\n+\n+class RealSpaceConvolution(Convolution):\n+\n+    \"\"\"Convolution implemented in real space.\"\"\"\n+\n+    def __init__(self, domain, kernel, range=None, impl='scipy_convolve',\n+                 **kwargs):\n+        \"\"\"Initialize a new instance.\n+\n+        Parameters\n+        ----------\n+        domain : `DiscreteLp`\n+            Domain of the operator\n+        kernel :\n+            The kernel can be specified in several ways, depending on\n+            the choice of ``impl``:\n+\n+            `DiscreteLpVector` :\n+            Valid for ``impl``: ``'scipy_convolve'``\n+\n+            `array-like`, arbitrary shape : The object is interpreted as\n+            real-space kernel (mode ``'real'``) and can be differently\n+            sized than the convolved function. It is assumed to be an\n+            element of a space with same `DiscreteLp.cell_sides` as\n+            ``domain``. Note that the ``scipy_convolve`` back-end does\n+            not allow the kernel to be larger.\n+            Valid for ``impl``: ``'scipy_convolve'``\n+\n+        range : `DiscreteLp`, optional\n+            Range of the operator. By default, the range is calculated\n+            from the kernel.\n+            Note: custom range is currently not supported.\n+\n+        impl : string, optional\n+            Implementation of the convolution. Available options are:\n+\n+            'scipy_convolve': Real-space convolution using\n+            `scipy.signal.convolve` (default, fast for short kernels)\n+\n+        scale : `bool`, optional\n+            If `True`, scale the discrete convolution with\n+            `DiscreteLp.cell_volume`, such that it\n+            corresponds to a continuous convolution.\n+            Default: `True`\n+\n+        resample : string or sequence of strings\n+            If ``kernel`` and ``domain`` have different cell sizes,\n+            this option defines the behavior of the evaluation. The\n+            following values can be given (per axis in the case of a\n+            sequence):\n+\n+            'down' : Use the larger one of the two cell sizes (default).\n+\n+            'up' : Use the smaller one of the two cell sizes. Note that\n+            this can be computationally expensive.\n+\n+        kernel_kwargs : dict, optional\n+            Keyword arguments passed to the call of the kernel function\n+            if given as a callable.\n+\n+        See also\n+        --------\n+        scipy.signal.convolve : real-space convolution\n+\n+        Notes\n+        -----\n+        - The continuous convolution of two functions\n+          :math:`f` and :math:`g` defined on :math:`\\mathbb{R^d}` is\n+\n+              :math:`[f \\\\ast g](x) =\n+              \\int_{\\mathbb{R^d}} f(x - y) g(y) \\mathrm{d}y`.\n+\n+          This implementation covers the case of a fixed convolution\n+          kernel :math:`k`, i.e. the linear operator :math:`\\mathcal{C}_k`\n+          given by\n+\n+              :math:`\\mathcal{C}_k(f): x \\mapsto [k \\\\ast f](x)`.\n+\n+        - If the convolution kernel :math:`k` is defined on a rectangular\n+          domain :math:`\\Omega_0` with midpoint :math:`m_0`, then the\n+          convolution :math:`k \\\\ast f` of a function\n+          :math:`f:\\Omega \\\\to \\mathbb{R}` with :math:`k` has a support\n+          that is a superset of :math:`\\Omega + m_0`. Therefore, we\n+          choose to keep the size of the domain and take\n+          :math:`\\Omega + m_0` as the domain of definition of functions\n+          in the range of the convolution operator.\n+\n+          In fact, the support of the convolution is contained in the\n+          `Minkowski sum\n+          <https://en.wikipedia.org/wiki/Minkowski_addition>`_\n+          :math:`\\Omega + \\Omega_0`.\n+\n+          For example, if :math:`\\Omega = [0, 5]` and\n+          :math:`\\Omega_0 = [1, 3]`, i.e. :math:`m_0 = 2`, then\n+          :math:`\\Omega + \\Omega_0 = [1, 8]`. However, we choose the\n+          shifted interval :math:`\\Omega + m_0 = [2, 7]` for the range\n+          of the convolution since it contains the \"main mass\" of the\n+          result.\n+        \"\"\"\n+        # Basic checks\n+        if not isinstance(domain, DiscreteLp):\n+            raise TypeError('domain {!r} is not a DiscreteLp instance'\n+                            ''.format(domain))\n+        if not domain.is_uniform:\n+            raise ValueError('irregular sampling not supported')\n+\n+        if range is not None:\n+            # TODO\n+            raise NotImplementedError('custom range not implemented')\n+        else:\n+            kernel_kwargs = kwargs.pop('kernel_kwargs', {})\n+            self._kernel, range = self._compute_kernel_and_range(\n+                kernel, domain, kernel_kwargs)\n+\n+        super().__init__(domain, range, linear=True)\n+\n+        # Handle impl\n+        impl, impl_in = str(impl).lower(), impl\n+        if impl not in _REAL_CONV_SUPPORTED_IMPL:\n+            raise ValueError(\"implementation '{}' not understood\"\n+                             ''.format(impl_in))\n+        self._impl = impl\n+\n+        # Handle the `resample` input parameter\n+        resample = normalized_scalar_param_list(kwargs.pop('resample', 'down'),\n+                                                length=self.domain.ndim,\n+                                                param_conv=str)\n+        for i, r in enumerate(resample):\n+            if r.lower() not in ('up', 'down'):\n+                raise ValueError(\"in axis {}: `resample` '{}' not understood\"\n+                                 ''.format(i, r))\n+            else:\n+                resample[i] = r.lower()\n+        self._resample = resample\n+\n+        # Initialize resampling operators\n+        new_kernel_csides, new_domain_csides = self._get_resamp_csides(\n+            self.resample, self._kernel.space.cell_sides,\n+            self.domain.cell_sides)\n+\n+        domain_resamp_space = uniform_discr_fromdiscr(\n+            self.domain, cell_sides=new_domain_csides)\n+        self._domain_resampling_op = Resampling(self.domain,\n+                                                domain_resamp_space)\n+\n+        kernel_resamp_space = uniform_discr_fromdiscr(\n+            self._kernel.space, cell_sides=new_kernel_csides)\n+        self._kernel_resampling_op = Resampling(self._kernel.space,\n+                                                kernel_resamp_space)\n+\n+        # Scale the kernel if desired\n+        self._kernel_scaling = np.prod(new_kernel_csides)\n+        self._scale = bool(kwargs.pop('scale', True))\n+        if self._scale:\n+            # Don't modify the input\n+            self._kernel = self._kernel * self._kernel_scaling\n+\n+    @staticmethod","path":"odl/trafos/convolution.py","position":552,"original_position":552,"commit_id":"f482c00ba819df8eb3019f01753366f069defb44","original_commit_id":"f482c00ba819df8eb3019f01753366f069defb44","user":{"login":"adler-j","id":2202312,"avatar_url":"https://avatars.githubusercontent.com/u/2202312?v=3","gravatar_id":"","url":"https://api.github.com/users/adler-j","html_url":"https://github.com/adler-j","followers_url":"https://api.github.com/users/adler-j/followers","following_url":"https://api.github.com/users/adler-j/following{/other_user}","gists_url":"https://api.github.com/users/adler-j/gists{/gist_id}","starred_url":"https://api.github.com/users/adler-j/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/adler-j/subscriptions","organizations_url":"https://api.github.com/users/adler-j/orgs","repos_url":"https://api.github.com/users/adler-j/repos","events_url":"https://api.github.com/users/adler-j/events{/privacy}","received_events_url":"https://api.github.com/users/adler-j/received_events","type":"User","site_admin":false},"body":"Static methods should in general be free. Only reason to have static methods is if users need to ask the class something, in general before initialization, e.g.\r\n\r\n```python\r\nfor impl im Convolution.valid_impls():\r\n    conv = Convolution(impl=impl)\r\n```","created_at":"2016-06-30T09:01:59Z","updated_at":"2016-06-30T09:01:59Z","html_url":"https://github.com/odlgroup/odl/pull/472#discussion_r69097598","pull_request_url":"https://api.github.com/repos/odlgroup/odl/pulls/472","_links":{"self":{"href":"https://api.github.com/repos/odlgroup/odl/pulls/comments/69097598"},"html":{"href":"https://github.com/odlgroup/odl/pull/472#discussion_r69097598"},"pull_request":{"href":"https://api.github.com/repos/odlgroup/odl/pulls/472"}}},"pull_request":{"url":"https://api.github.com/repos/odlgroup/odl/pulls/472","id":75526102,"html_url":"https://github.com/odlgroup/odl/pull/472","diff_url":"https://github.com/odlgroup/odl/pull/472.diff","patch_url":"https://github.com/odlgroup/odl/pull/472.patch","issue_url":"https://api.github.com/repos/odlgroup/odl/issues/472","number":472,"state":"open","locked":false,"title":"WIP: Implement convolution operator(s)","user":{"login":"kohr-h","id":5030250,"avatar_url":"https://avatars.githubusercontent.com/u/5030250?v=3","gravatar_id":"","url":"https://api.github.com/users/kohr-h","html_url":"https://github.com/kohr-h","followers_url":"https://api.github.com/users/kohr-h/followers","following_url":"https://api.github.com/users/kohr-h/following{/other_user}","gists_url":"https://api.github.com/users/kohr-h/gists{/gist_id}","starred_url":"https://api.github.com/users/kohr-h/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/kohr-h/subscriptions","organizations_url":"https://api.github.com/users/kohr-h/orgs","repos_url":"https://api.github.com/users/kohr-h/repos","events_url":"https://api.github.com/users/kohr-h/events{/privacy}","received_events_url":"https://api.github.com/users/kohr-h/received_events","type":"User","site_admin":false},"body":"The real-space convolution is fully working now and could use a design review. Here's a quick summary of its behavior with respect to the different possibilities a kernel can be provided:\r\n\r\n1. **As a `DiscreteLpVector`.** The kernel has information about its own function domain, which results in the convolution operator to map to a translated space, where the shift is the midpoint of the kernel domain. The reason for this is that mathematically, the region shifted in this way contains the main \"mass\" of the convolved function.\r\n2. **As a `callable`.** In this case, a \"standard\" kernel space is created, which is a zero-centered version of the operator domain (same cell sizes and shape). The operator range is the same as the domain.\r\n3. **As an array-like.** Similarly to case 2, a standard zero-centered kernel space is created, but this time with the same shape as the given array. This is actually the most useful (=efficient) case when one wants to calculate a convolution with a very small kernel.\r\n\r\nThe Fourier-based convolution is currently not functioning, ignore it for now.","created_at":"2016-06-28T18:25:09Z","updated_at":"2016-06-30T09:01:59Z","closed_at":null,"merged_at":null,"merge_commit_sha":"2fc849e6000be194f1c4c682d9dff9ba2d69b9bd","assignee":null,"assignees":[],"milestone":null,"commits_url":"https://api.github.com/repos/odlgroup/odl/pulls/472/commits","review_comments_url":"https://api.github.com/repos/odlgroup/odl/pulls/472/comments","review_comment_url":"https://api.github.com/repos/odlgroup/odl/pulls/comments{/number}","comments_url":"https://api.github.com/repos/odlgroup/odl/issues/472/comments","statuses_url":"https://api.github.com/repos/odlgroup/odl/statuses/f482c00ba819df8eb3019f01753366f069defb44","head":{"label":"odlgroup:issue-209__convolution","ref":"issue-209__convolution","sha":"f482c00ba819df8eb3019f01753366f069defb44","user":{"login":"odlgroup","id":15667171,"avatar_url":"https://avatars.githubusercontent.com/u/15667171?v=3","gravatar_id":"","url":"https://api.github.com/users/odlgroup","html_url":"https://github.com/odlgroup","followers_url":"https://api.github.com/users/odlgroup/followers","following_url":"https://api.github.com/users/odlgroup/following{/other_user}","gists_url":"https://api.github.com/users/odlgroup/gists{/gist_id}","starred_url":"https://api.github.com/users/odlgroup/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/odlgroup/subscriptions","organizations_url":"https://api.github.com/users/odlgroup/orgs","repos_url":"https://api.github.com/users/odlgroup/repos","events_url":"https://api.github.com/users/odlgroup/events{/privacy}","received_events_url":"https://api.github.com/users/odlgroup/received_events","type":"Organization","site_admin":false},"repo":{"id":45596393,"name":"odl","full_name":"odlgroup/odl","owner":{"login":"odlgroup","id":15667171,"avatar_url":"https://avatars.githubusercontent.com/u/15667171?v=3","gravatar_id":"","url":"https://api.github.com/users/odlgroup","html_url":"https://github.com/odlgroup","followers_url":"https://api.github.com/users/odlgroup/followers","following_url":"https://api.github.com/users/odlgroup/following{/other_user}","gists_url":"https://api.github.com/users/odlgroup/gists{/gist_id}","starred_url":"https://api.github.com/users/odlgroup/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/odlgroup/subscriptions","organizations_url":"https://api.github.com/users/odlgroup/orgs","repos_url":"https://api.github.com/users/odlgroup/repos","events_url":"https://api.github.com/users/odlgroup/events{/privacy}","received_events_url":"https://api.github.com/users/odlgroup/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/odlgroup/odl","description":"Operator Discretization Library","fork":false,"url":"https://api.github.com/repos/odlgroup/odl","forks_url":"https://api.github.com/repos/odlgroup/odl/forks","keys_url":"https://api.github.com/repos/odlgroup/odl/keys{/key_id}","collaborators_url":"https://api.github.com/repos/odlgroup/odl/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/odlgroup/odl/teams","hooks_url":"https://api.github.com/repos/odlgroup/odl/hooks","issue_events_url":"https://api.github.com/repos/odlgroup/odl/issues/events{/number}","events_url":"https://api.github.com/repos/odlgroup/odl/events","assignees_url":"https://api.github.com/repos/odlgroup/odl/assignees{/user}","branches_url":"https://api.github.com/repos/odlgroup/odl/branches{/branch}","tags_url":"https://api.github.com/repos/odlgroup/odl/tags","blobs_url":"https://api.github.com/repos/odlgroup/odl/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/odlgroup/odl/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/odlgroup/odl/git/refs{/sha}","trees_url":"https://api.github.com/repos/odlgroup/odl/git/trees{/sha}","statuses_url":"https://api.github.com/repos/odlgroup/odl/statuses/{sha}","languages_url":"https://api.github.com/repos/odlgroup/odl/languages","stargazers_url":"https://api.github.com/repos/odlgroup/odl/stargazers","contributors_url":"https://api.github.com/repos/odlgroup/odl/contributors","subscribers_url":"https://api.github.com/repos/odlgroup/odl/subscribers","subscription_url":"https://api.github.com/repos/odlgroup/odl/subscription","commits_url":"https://api.github.com/repos/odlgroup/odl/commits{/sha}","git_commits_url":"https://api.github.com/repos/odlgroup/odl/git/commits{/sha}","comments_url":"https://api.github.com/repos/odlgroup/odl/comments{/number}","issue_comment_url":"https://api.github.com/repos/odlgroup/odl/issues/comments{/number}","contents_url":"https://api.github.com/repos/odlgroup/odl/contents/{+path}","compare_url":"https://api.github.com/repos/odlgroup/odl/compare/{base}...{head}","merges_url":"https://api.github.com/repos/odlgroup/odl/merges","archive_url":"https://api.github.com/repos/odlgroup/odl/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/odlgroup/odl/downloads","issues_url":"https://api.github.com/repos/odlgroup/odl/issues{/number}","pulls_url":"https://api.github.com/repos/odlgroup/odl/pulls{/number}","milestones_url":"https://api.github.com/repos/odlgroup/odl/milestones{/number}","notifications_url":"https://api.github.com/repos/odlgroup/odl/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/odlgroup/odl/labels{/name}","releases_url":"https://api.github.com/repos/odlgroup/odl/releases{/id}","deployments_url":"https://api.github.com/repos/odlgroup/odl/deployments","created_at":"2015-11-05T07:57:55Z","updated_at":"2016-06-16T12:20:53Z","pushed_at":"2016-06-29T16:06:00Z","git_url":"git://github.com/odlgroup/odl.git","ssh_url":"git@github.com:odlgroup/odl.git","clone_url":"https://github.com/odlgroup/odl.git","svn_url":"https://github.com/odlgroup/odl","homepage":null,"size":18623,"stargazers_count":8,"watchers_count":8,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":4,"mirror_url":null,"open_issues_count":123,"forks":4,"open_issues":123,"watchers":8,"default_branch":"master"}},"base":{"label":"odlgroup:master","ref":"master","sha":"b8b4eb46b17861f37a61fc9e24512fe91ad45765","user":{"login":"odlgroup","id":15667171,"avatar_url":"https://avatars.githubusercontent.com/u/15667171?v=3","gravatar_id":"","url":"https://api.github.com/users/odlgroup","html_url":"https://github.com/odlgroup","followers_url":"https://api.github.com/users/odlgroup/followers","following_url":"https://api.github.com/users/odlgroup/following{/other_user}","gists_url":"https://api.github.com/users/odlgroup/gists{/gist_id}","starred_url":"https://api.github.com/users/odlgroup/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/odlgroup/subscriptions","organizations_url":"https://api.github.com/users/odlgroup/orgs","repos_url":"https://api.github.com/users/odlgroup/repos","events_url":"https://api.github.com/users/odlgroup/events{/privacy}","received_events_url":"https://api.github.com/users/odlgroup/received_events","type":"Organization","site_admin":false},"repo":{"id":45596393,"name":"odl","full_name":"odlgroup/odl","owner":{"login":"odlgroup","id":15667171,"avatar_url":"https://avatars.githubusercontent.com/u/15667171?v=3","gravatar_id":"","url":"https://api.github.com/users/odlgroup","html_url":"https://github.com/odlgroup","followers_url":"https://api.github.com/users/odlgroup/followers","following_url":"https://api.github.com/users/odlgroup/following{/other_user}","gists_url":"https://api.github.com/users/odlgroup/gists{/gist_id}","starred_url":"https://api.github.com/users/odlgroup/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/odlgroup/subscriptions","organizations_url":"https://api.github.com/users/odlgroup/orgs","repos_url":"https://api.github.com/users/odlgroup/repos","events_url":"https://api.github.com/users/odlgroup/events{/privacy}","received_events_url":"https://api.github.com/users/odlgroup/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/odlgroup/odl","description":"Operator Discretization Library","fork":false,"url":"https://api.github.com/repos/odlgroup/odl","forks_url":"https://api.github.com/repos/odlgroup/odl/forks","keys_url":"https://api.github.com/repos/odlgroup/odl/keys{/key_id}","collaborators_url":"https://api.github.com/repos/odlgroup/odl/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/odlgroup/odl/teams","hooks_url":"https://api.github.com/repos/odlgroup/odl/hooks","issue_events_url":"https://api.github.com/repos/odlgroup/odl/issues/events{/number}","events_url":"https://api.github.com/repos/odlgroup/odl/events","assignees_url":"https://api.github.com/repos/odlgroup/odl/assignees{/user}","branches_url":"https://api.github.com/repos/odlgroup/odl/branches{/branch}","tags_url":"https://api.github.com/repos/odlgroup/odl/tags","blobs_url":"https://api.github.com/repos/odlgroup/odl/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/odlgroup/odl/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/odlgroup/odl/git/refs{/sha}","trees_url":"https://api.github.com/repos/odlgroup/odl/git/trees{/sha}","statuses_url":"https://api.github.com/repos/odlgroup/odl/statuses/{sha}","languages_url":"https://api.github.com/repos/odlgroup/odl/languages","stargazers_url":"https://api.github.com/repos/odlgroup/odl/stargazers","contributors_url":"https://api.github.com/repos/odlgroup/odl/contributors","subscribers_url":"https://api.github.com/repos/odlgroup/odl/subscribers","subscription_url":"https://api.github.com/repos/odlgroup/odl/subscription","commits_url":"https://api.github.com/repos/odlgroup/odl/commits{/sha}","git_commits_url":"https://api.github.com/repos/odlgroup/odl/git/commits{/sha}","comments_url":"https://api.github.com/repos/odlgroup/odl/comments{/number}","issue_comment_url":"https://api.github.com/repos/odlgroup/odl/issues/comments{/number}","contents_url":"https://api.github.com/repos/odlgroup/odl/contents/{+path}","compare_url":"https://api.github.com/repos/odlgroup/odl/compare/{base}...{head}","merges_url":"https://api.github.com/repos/odlgroup/odl/merges","archive_url":"https://api.github.com/repos/odlgroup/odl/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/odlgroup/odl/downloads","issues_url":"https://api.github.com/repos/odlgroup/odl/issues{/number}","pulls_url":"https://api.github.com/repos/odlgroup/odl/pulls{/number}","milestones_url":"https://api.github.com/repos/odlgroup/odl/milestones{/number}","notifications_url":"https://api.github.com/repos/odlgroup/odl/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/odlgroup/odl/labels{/name}","releases_url":"https://api.github.com/repos/odlgroup/odl/releases{/id}","deployments_url":"https://api.github.com/repos/odlgroup/odl/deployments","created_at":"2015-11-05T07:57:55Z","updated_at":"2016-06-16T12:20:53Z","pushed_at":"2016-06-29T16:06:00Z","git_url":"git://github.com/odlgroup/odl.git","ssh_url":"git@github.com:odlgroup/odl.git","clone_url":"https://github.com/odlgroup/odl.git","svn_url":"https://github.com/odlgroup/odl","homepage":null,"size":18623,"stargazers_count":8,"watchers_count":8,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":4,"mirror_url":null,"open_issues_count":123,"forks":4,"open_issues":123,"watchers":8,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/odlgroup/odl/pulls/472"},"html":{"href":"https://github.com/odlgroup/odl/pull/472"},"issue":{"href":"https://api.github.com/repos/odlgroup/odl/issues/472"},"comments":{"href":"https://api.github.com/repos/odlgroup/odl/issues/472/comments"},"review_comments":{"href":"https://api.github.com/repos/odlgroup/odl/pulls/472/comments"},"review_comment":{"href":"https://api.github.com/repos/odlgroup/odl/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/odlgroup/odl/pulls/472/commits"},"statuses":{"href":"https://api.github.com/repos/odlgroup/odl/statuses/f482c00ba819df8eb3019f01753366f069defb44"}}}}
{"actor":{"display_login":"adler-j"}}