File: core.py

package info (click to toggle)
python-astropy 1.3-8~bpo8%2B2
  • links: PTS, VCS
  • area: main
  • in suites: jessie-backports
  • size: 44,292 kB
  • sloc: ansic: 160,360; python: 137,322; sh: 11,493; lex: 7,638; yacc: 4,956; xml: 1,796; makefile: 474; cpp: 364
file content (370 lines) | stat: -rw-r--r-- 11,750 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""
This module contains the convolution and filter functionalities of astropy.

A few conceptual notes:
A filter kernel is mainly characterized by its response function. In the 1D
case we speak of "impulse response function", in the 2D case we call it "point
spread function". This response function is given for every kernel by an
astropy `FittableModel`, which is evaluated on a grid to obtain a filter array,
which can then be applied to binned data.

The model is centered on the array and should have an amplitude such that the array
integrates to one per default.

Currently only symmetric 2D kernels are supported.
"""
from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

import warnings
import copy

import numpy as np
from ..utils.exceptions import AstropyUserWarning
from .utils import (discretize_model, add_kernel_arrays_1D,
                    add_kernel_arrays_2D)

MAX_NORMALIZATION = 100

__all__ = ['Kernel', 'Kernel1D', 'Kernel2D', 'kernel_arithmetics']


class Kernel(object):
    """
    Convolution kernel base class.

    Parameters
    ----------
    array : `~numpy.ndarray`
        Kernel array.
    """
    _separable = False
    _is_bool = True
    _model = None

    def __init__(self, array):
        self._array = np.asanyarray(array)

    @property
    def truncation(self):
        """
        Deviation from the normalization to one.
        """
        return self._truncation

    @property
    def is_bool(self):
        """
        Indicates if kernel is bool.

        If the kernel is bool the multiplication in the convolution could
        be omitted, to increase the performance.
        """
        return self._is_bool

    @property
    def model(self):
        """
        Kernel response model.
        """
        return self._model

    @property
    def dimension(self):
        """
        Kernel dimension.
        """
        return self.array.ndim

    @property
    def center(self):
        """
        Index of the kernel center.
        """
        return [axes_size // 2 for axes_size in self._array.shape]

    def normalize(self, mode='integral'):
        """
        Normalize the filter kernel.

        Parameters
        ----------
        mode : {'integral', 'peak'}
            One of the following modes:
                * 'integral' (default)
                    Kernel is normalized such that its integral = 1.
                * 'peak'
                    Kernel is normalized such that its peak = 1.
        """

        if mode == 'integral':
            normalization = self._array.sum()
        elif mode == 'peak':
            normalization = self._array.max()
        else:
            raise ValueError("invalid mode, must be 'integral' or 'peak'")

        # Warn the user for kernels that sum to zero
        if normalization == 0:
            warnings.warn('The kernel cannot be normalized because it '
                          'sums to zero.', AstropyUserWarning)
        else:
            np.divide(self._array, normalization, self._array)

        self._kernel_sum = self._array.sum()

    @property
    def shape(self):
        """
        Shape of the kernel array.
        """
        return self._array.shape

    @property
    def separable(self):
        """
        Indicates if the filter kernel is separable.

        A 2D filter is separable, when its filter array can be written as the
        outer product of two 1D arrays.

        If a filter kernel is separable, higher dimension convolutions will be
        performed by applying the 1D filter array consecutively on every dimension.
        This is significantly faster, than using a filter array with the same
        dimension.
        """
        return self._separable

    @property
    def array(self):
        """
        Filter kernel array.
        """
        return self._array

    def __add__(self, kernel):
        """
        Add two filter kernels.
        """
        return kernel_arithmetics(self, kernel, 'add')

    def __sub__(self,  kernel):
        """
        Subtract two filter kernels.
        """
        return kernel_arithmetics(self, kernel, 'sub')

    def __mul__(self, value):
        """
        Multiply kernel with number or convolve two kernels.
        """
        return kernel_arithmetics(self, value, "mul")

    def __rmul__(self, value):
        """
        Multiply kernel with number or convolve two kernels.
        """
        return kernel_arithmetics(self, value, "mul")

    def __array__(self):
        """
        Array representation of the kernel.
        """
        return self._array

    def __array_wrap__(self, array, context=None):
        """
        Wrapper for multiplication with numpy arrays.
        """
        if type(context[0]) == np.ufunc:
            return NotImplemented
        else:
            return array


class Kernel1D(Kernel):
    """
    Base class for 1D filter kernels.

    Parameters
    ----------
    model : `~astropy.modeling.FittableModel`
        Model to be evaluated.
    x_size : odd int, optional
        Size of the kernel array. Default = 8 * width.
    array : `~numpy.ndarray`
        Kernel array.
    width : number
        Width of the filter kernel.
    mode : str, optional
        One of the following discretization modes:
            * 'center' (default)
                Discretize model by taking the value
                at the center of the bin.
            * 'linear_interp'
                Discretize model by linearly interpolating
                between the values at the corners of the bin.
            * 'oversample'
                Discretize model by taking the average
                on an oversampled grid.
            * 'integrate'
                Discretize model by integrating the
                model over the bin.
    factor : number, optional
        Factor of oversampling. Default factor = 10.
    """
    def __init__(self, model=None, x_size=None, array=None, **kwargs):
        # Initialize from model
        if array is None:
            if self._model is None:
                raise TypeError("Must specify either array or model.")

            if x_size is None:
                x_size = self._default_size
            elif x_size != int(x_size):
                raise TypeError("x_size should be an integer")

            # Set ranges where to evaluate the model

            if x_size % 2 == 0:  # even kernel
                x_range = (-(int(x_size)) // 2 + 0.5, (int(x_size)) // 2 + 0.5)
            else:  # odd kernel
                x_range = (-(int(x_size) - 1) // 2, (int(x_size) - 1) // 2 + 1)

            array = discretize_model(self._model, x_range, **kwargs)

        # Initialize from array
        elif array is not None:
            self._model = None

        super(Kernel1D, self).__init__(array)


class Kernel2D(Kernel):
    """
    Base class for 2D filter kernels.

    Parameters
    ----------
    model : `~astropy.modeling.FittableModel`
        Model to be evaluated.
    x_size : odd int, optional
        Size in x direction of the kernel array. Default = 8 * width.
    y_size : odd int, optional
        Size in y direction of the kernel array. Default = 8 * width.
    array : `~numpy.ndarray`
        Kernel array.
    mode : str, optional
        One of the following discretization modes:
            * 'center' (default)
                Discretize model by taking the value
                at the center of the bin.
            * 'linear_interp'
                Discretize model by performing a bilinear interpolation
                between the values at the corners of the bin.
            * 'oversample'
                Discretize model by taking the average
                on an oversampled grid.
            * 'integrate'
                Discretize model by integrating the
                model over the bin.
    width : number
        Width of the filter kernel.
    factor : number, optional
        Factor of oversampling. Default factor = 10.
    """
    def __init__(self, model=None, x_size=None, y_size=None, array=None, **kwargs):

        # Initialize from model
        if array is None:
            if self._model is None:
                raise TypeError("Must specify either array or model.")

            if x_size is None:
                x_size = self._default_size
            elif x_size != int(x_size):
                raise TypeError("x_size should be an integer")

            if y_size is None:
                y_size = x_size
            elif y_size != int(y_size):
                raise TypeError("y_size should be an integer")

            # Set ranges where to evaluate the model

            if x_size % 2 == 0:  # even kernel
                x_range = (-(int(x_size)) // 2 + 0.5, (int(x_size)) // 2 + 0.5)
            else:  # odd kernel
                x_range = (-(int(x_size) - 1) // 2, (int(x_size) - 1) // 2 + 1)

            if y_size % 2 == 0:  # even kernel
                y_range = (-(int(y_size)) // 2 + 0.5, (int(y_size)) // 2 + 0.5)
            else:  # odd kernel
                y_range = (-(int(y_size) - 1) // 2, (int(y_size) - 1) // 2 + 1)

            array = discretize_model(self._model, x_range, y_range, **kwargs)

        # Initialize from array
        elif array is not None:
            self._model = None

        super(Kernel2D, self).__init__(array)


def kernel_arithmetics(kernel, value, operation):
    """
    Add, subtract or multiply two kernels.

    Parameters
    ----------
    kernel : `astropy.convolution.Kernel`
        Kernel instance
    value : kernel, float or int
        Value to operate with
    operation : {'add', 'sub', 'mul'}
        One of the following operations:
            * 'add'
                Add two kernels
            * 'sub'
                Subtract two kernels
            * 'mul'
                Multiply kernel with number or convolve two kernels.
    """
    # 1D kernels
    if isinstance(kernel, Kernel1D) and isinstance(value, Kernel1D):
        if operation == "add":
            new_array = add_kernel_arrays_1D(kernel.array, value.array)
        if operation == "sub":
            new_array = add_kernel_arrays_1D(kernel.array, -value.array)
        if operation == "mul":
            raise Exception("Kernel operation not supported. Maybe you want "
                            "to use convolve(kernel1, kernel2) instead.")
        new_kernel = Kernel1D(array=new_array)
        new_kernel._separable = kernel._separable and value._separable
        new_kernel._is_bool = kernel._is_bool or value._is_bool

    # 2D kernels
    elif isinstance(kernel, Kernel2D) and isinstance(value, Kernel2D):
        if operation == "add":
            new_array = add_kernel_arrays_2D(kernel.array, value.array)
        if operation == "sub":
            new_array = add_kernel_arrays_2D(kernel.array, -value.array)
        if operation == "mul":
            raise Exception("Kernel operation not supported. Maybe you want "
                            "to use convolve(kernel1, kernel2) instead.")
        new_kernel = Kernel2D(array=new_array)
        new_kernel._separable = kernel._separable and value._separable
        new_kernel._is_bool = kernel._is_bool or value._is_bool

    # kernel and number
    elif ((isinstance(kernel, Kernel1D) or isinstance(kernel, Kernel2D))
        and np.isscalar(value)):
        if operation == "mul":
            new_kernel = copy.copy(kernel)
            new_kernel._array *= value
        else:
            raise Exception("Kernel operation not supported.")
    else:
        raise Exception("Kernel operation not supported.")
    return new_kernel