summaryrefslogtreecommitdiff
path: root/src/silx/math
diff options
context:
space:
mode:
Diffstat (limited to 'src/silx/math')
-rw-r--r--src/silx/math/__init__.py39
-rw-r--r--src/silx/math/_colormap.pyx571
-rw-r--r--src/silx/math/calibration.py180
-rw-r--r--src/silx/math/chistogramnd.pyx1251
-rw-r--r--src/silx/math/chistogramnd_lut.pyx435
-rw-r--r--src/silx/math/colormap.py450
-rw-r--r--src/silx/math/combo.pyx329
-rw-r--r--src/silx/math/fft/__init__.py8
-rw-r--r--src/silx/math/fft/basefft.py146
-rw-r--r--src/silx/math/fft/clfft.py286
-rw-r--r--src/silx/math/fft/cufft.py253
-rw-r--r--src/silx/math/fft/fft.py96
-rw-r--r--src/silx/math/fft/fftw.py214
-rw-r--r--src/silx/math/fft/npfft.py124
-rw-r--r--src/silx/math/fft/setup.py41
-rw-r--r--src/silx/math/fft/test/__init__.py23
-rw-r--r--src/silx/math/fft/test/test_fft.py257
-rw-r--r--src/silx/math/fit/__init__.py39
-rw-r--r--src/silx/math/fit/bgtheories.py440
-rw-r--r--src/silx/math/fit/filters.pyx416
-rw-r--r--src/silx/math/fit/filters/include/filters.h45
-rw-r--r--src/silx/math/fit/filters/src/smoothnd.c317
-rw-r--r--src/silx/math/fit/filters/src/snip1d.c149
-rw-r--r--src/silx/math/fit/filters/src/snip2d.c96
-rw-r--r--src/silx/math/fit/filters/src/snip3d.c186
-rw-r--r--src/silx/math/fit/filters/src/strip.c118
-rw-r--r--src/silx/math/fit/filters_wrapper.pxd71
-rw-r--r--src/silx/math/fit/fitmanager.py1087
-rw-r--r--src/silx/math/fit/fittheories.py1374
-rw-r--r--src/silx/math/fit/fittheory.py161
-rw-r--r--src/silx/math/fit/functions.pyx985
-rw-r--r--src/silx/math/fit/functions/include/functions.h68
-rw-r--r--src/silx/math/fit/functions/src/funs.c1265
-rw-r--r--src/silx/math/fit/functions_wrapper.pxd170
-rw-r--r--src/silx/math/fit/leastsq.py901
-rw-r--r--src/silx/math/fit/peaks.pyx175
-rw-r--r--src/silx/math/fit/peaks/include/peaks.h32
-rw-r--r--src/silx/math/fit/peaks/src/peaks.c255
-rw-r--r--src/silx/math/fit/peaks_wrapper.pxd41
-rw-r--r--src/silx/math/fit/setup.py85
-rw-r--r--src/silx/math/fit/test/__init__.py23
-rw-r--r--src/silx/math/fit/test/test_bgtheories.py154
-rw-r--r--src/silx/math/fit/test/test_filters.py122
-rw-r--r--src/silx/math/fit/test/test_fit.py373
-rw-r--r--src/silx/math/fit/test/test_fitmanager.py498
-rw-r--r--src/silx/math/fit/test/test_functions.py259
-rw-r--r--src/silx/math/fit/test/test_peaks.py132
-rw-r--r--src/silx/math/histogram.py593
-rw-r--r--src/silx/math/histogramnd/include/histogramnd_c.h313
-rw-r--r--src/silx/math/histogramnd/include/msvc/stdint.h247
-rw-r--r--src/silx/math/histogramnd/include/templates.h30
-rw-r--r--src/silx/math/histogramnd/src/histogramnd_c.c301
-rw-r--r--src/silx/math/histogramnd/src/histogramnd_template.c260
-rw-r--r--src/silx/math/histogramnd_c.pxd299
-rw-r--r--src/silx/math/include/math_compatibility.h53
-rw-r--r--src/silx/math/interpolate.pyx165
-rw-r--r--src/silx/math/marchingcubes.pyx246
-rw-r--r--src/silx/math/marchingcubes/mc.hpp724
-rw-r--r--src/silx/math/marchingcubes/mc_lut.cpp316
-rw-r--r--src/silx/math/math_compatibility.pxd35
-rw-r--r--src/silx/math/mc.pxd51
-rw-r--r--src/silx/math/medianfilter/__init__.py30
-rw-r--r--src/silx/math/medianfilter/include/median_filter.hpp284
-rw-r--r--src/silx/math/medianfilter/median_filter.pxd42
-rw-r--r--src/silx/math/medianfilter/medianfilter.pyx496
-rw-r--r--src/silx/math/medianfilter/setup.py59
-rw-r--r--src/silx/math/medianfilter/test/__init__.py23
-rw-r--r--src/silx/math/medianfilter/test/benchmark.py122
-rw-r--r--src/silx/math/medianfilter/test/test_medianfilter.py722
-rw-r--r--src/silx/math/setup.py99
-rw-r--r--src/silx/math/test/__init__.py23
-rw-r--r--src/silx/math/test/benchmark_combo.py192
-rw-r--r--src/silx/math/test/histo_benchmarks.py269
-rw-r--r--src/silx/math/test/test_HistogramndLut_nominal.py571
-rw-r--r--src/silx/math/test/test_calibration.py145
-rw-r--r--src/silx/math/test/test_colormap.py269
-rw-r--r--src/silx/math/test/test_combo.py207
-rw-r--r--src/silx/math/test/test_histogramnd_error.py519
-rw-r--r--src/silx/math/test/test_histogramnd_nominal.py937
-rw-r--r--src/silx/math/test/test_histogramnd_vs_np.py826
-rw-r--r--src/silx/math/test/test_interpolate.py125
-rw-r--r--src/silx/math/test/test_marchingcubes.py174
82 files changed, 24507 insertions, 0 deletions
diff --git a/src/silx/math/__init__.py b/src/silx/math/__init__.py
new file mode 100644
index 0000000..d8b7d81
--- /dev/null
+++ b/src/silx/math/__init__.py
@@ -0,0 +1,39 @@
+# coding: utf-8
+# /*##########################################################################
+# Copyright (C) 2016-2018 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
+"""This package provides some processing functions for 1D, 2D, 3D or nD arrays.
+
+For additional processing functions dedicated to 2D images,
+see the silx.image package.
+For OpenCL-based processing functions see the silx.opencl package.
+
+See silx documentation: http://www.silx.org/doc/silx/latest/
+"""
+
+__authors__ = ["D. Naudet", "V.A. Sole", "P. Knobel"]
+__license__ = "MIT"
+__date__ = "11/05/2017"
+
+from .histogram import Histogramnd # noqa
+from .histogram import HistogramndLut # noqa
+from .medianfilter import medfilt, medfilt1d, medfilt2d
diff --git a/src/silx/math/_colormap.pyx b/src/silx/math/_colormap.pyx
new file mode 100644
index 0000000..70857f0
--- /dev/null
+++ b/src/silx/math/_colormap.pyx
@@ -0,0 +1,571 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2018-2021 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
+"""This module provides :func:`cmap` which applies a colormap to a dataset.
+"""
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "16/05/2018"
+
+
+import os
+cimport cython
+from cython.parallel import prange
+cimport numpy as cnumpy
+from libc.math cimport frexp, sinh, sqrt
+from .math_compatibility cimport asinh, isnan, isfinite, lrint, INFINITY, NAN
+
+import logging
+import numbers
+
+import numpy
+
+__all__ = ['cmap']
+
+_logger = logging.getLogger(__name__)
+
+
+cdef int DEFAULT_NUM_THREADS
+if hasattr(os, 'sched_getaffinity'):
+ DEFAULT_NUM_THREADS = min(4, len(os.sched_getaffinity(0)))
+elif os.cpu_count() is not None:
+ DEFAULT_NUM_THREADS = min(4, os.cpu_count())
+else: # Fallback
+ DEFAULT_NUM_THREADS = 1
+# Number of threads to use for the computation (initialized to up to 4)
+
+cdef int USE_OPENMP_THRESHOLD = 1000
+"""OpenMP is not used for arrays with less elements than this threshold"""
+
+# Supported data types
+ctypedef fused data_types:
+ cnumpy.uint8_t
+ cnumpy.int8_t
+ cnumpy.uint16_t
+ cnumpy.int16_t
+ cnumpy.uint32_t
+ cnumpy.int32_t
+ cnumpy.uint64_t
+ cnumpy.int64_t
+ float
+ double
+ long double
+
+
+# Data types using a LUT to apply the colormap
+ctypedef fused lut_types:
+ cnumpy.uint8_t
+ cnumpy.int8_t
+ cnumpy.uint16_t
+ cnumpy.int16_t
+
+
+# Data types using default colormap implementation
+ctypedef fused default_types:
+ cnumpy.uint32_t
+ cnumpy.int32_t
+ cnumpy.uint64_t
+ cnumpy.int64_t
+ float
+ double
+ long double
+
+
+# Supported colors/output types
+ctypedef fused image_types:
+ cnumpy.uint8_t
+ float
+
+
+# Normalization
+
+ctypedef double (*NormalizationFunction)(double) nogil
+
+
+cdef class Normalization:
+ """Base class for colormap normalization"""
+
+ def apply(self, data, double vmin, double vmax):
+ """Apply normalization.
+
+ :param Union[float,numpy.ndarray] data:
+ :param float vmin: Lower bound of the range
+ :param float vmax: Upper bound of the range
+ :rtype: Union[float,numpy.ndarray]
+ """
+ cdef int length
+ cdef double[:] result
+
+ if isinstance(data, numbers.Real):
+ return self.apply_double(<double> data, vmin, vmax)
+ else:
+ data = numpy.array(data, copy=False)
+ length = <int> data.size
+ result = numpy.empty(length, dtype=numpy.float64)
+ data1d = numpy.ravel(data)
+ for index in range(length):
+ result[index] = self.apply_double(
+ <double> data1d[index], vmin, vmax)
+ return numpy.array(result).reshape(data.shape)
+
+ def revert(self, data, double vmin, double vmax):
+ """Revert normalization.
+
+ :param Union[float,numpy.ndarray] data:
+ :param float vmin: Lower bound of the range
+ :param float vmax: Upper bound of the range
+ :rtype: Union[float,numpy.ndarray]
+ """
+ cdef int length
+ cdef double[:] result
+
+ if isinstance(data, numbers.Real):
+ return self.revert_double(<double> data, vmin, vmax)
+ else:
+ data = numpy.array(data, copy=False)
+ length = <int> data.size
+ result = numpy.empty(length, dtype=numpy.float64)
+ data1d = numpy.ravel(data)
+ for index in range(length):
+ result[index] = self.revert_double(
+ <double> data1d[index], vmin, vmax)
+ return numpy.array(result).reshape(data.shape)
+
+ cdef double apply_double(self, double value, double vmin, double vmax) nogil:
+ """Apply normalization to a floating point value
+
+ Override in subclass
+
+ :param float value:
+ :param float vmin: Lower bound of the range
+ :param float vmax: Upper bound of the range
+ """
+ return value
+
+ cdef double revert_double(self, double value, double vmin, double vmax) nogil:
+ """Apply inverse of normalization to a floating point value
+
+ Override in subclass
+
+ :param float value:
+ :param float vmin: Lower bound of the range
+ :param float vmax: Upper bound of the range
+ """
+ return value
+
+
+cdef class LinearNormalization(Normalization):
+ """Linear normalization"""
+
+ cdef double apply_double(self, double value, double vmin, double vmax) nogil:
+ return value
+
+ cdef double revert_double(self, double value, double vmin, double vmax) nogil:
+ return value
+
+
+cdef class LogarithmicNormalization(Normalization):
+ """Logarithmic normalization using a fast log approximation"""
+ cdef:
+ readonly int lutsize
+ readonly double[::1] lut # LUT used for fast log approximation
+
+ def __cinit__(self, int lutsize=4096):
+ # Initialize log approximation LUT
+ self.lutsize = lutsize
+ self.lut = numpy.log2(
+ numpy.linspace(0.5, 1., lutsize + 1,
+ endpoint=True).astype(numpy.float64))
+ # index_lut can overflow of 1
+ self.lut[lutsize] = self.lut[lutsize - 1]
+
+ def __dealloc__(self):
+ self.lut = None
+
+ @cython.wraparound(False)
+ @cython.boundscheck(False)
+ @cython.nonecheck(False)
+ @cython.cdivision(True)
+ cdef double apply_double(self, double value, double vmin, double vmax) nogil:
+ """Return log10(value) fast approximation based on LUT"""
+ cdef double result = NAN # if value < 0.0 or value == NAN
+ cdef int exponent, index_lut
+ cdef double mantissa # in [0.5, 1) unless value == 0 NaN or +/-inf
+
+ if value <= 0.0 or not isfinite(value):
+ if value == 0.0:
+ result = - INFINITY
+ elif value > 0.0: # i.e., value = +INFINITY
+ result = value # i.e. +INFINITY
+ else:
+ mantissa = frexp(value, &exponent)
+ index_lut = lrint(self.lutsize * 2 * (mantissa - 0.5))
+ # 1/log2(10) = 0.30102999566398114
+ result = 0.30102999566398114 * (<double> exponent +
+ self.lut[index_lut])
+ return result
+
+ cdef double revert_double(self, double value, double vmin, double vmax) nogil:
+ return 10**value
+
+
+cdef class ArcsinhNormalization(Normalization):
+ """Inverse hyperbolic sine normalization"""
+
+ cdef double apply_double(self, double value, double vmin, double vmax) nogil:
+ return asinh(value)
+
+ cdef double revert_double(self, double value, double vmin, double vmax) nogil:
+ return sinh(value)
+
+
+cdef class SqrtNormalization(Normalization):
+ """Square root normalization"""
+
+ cdef double apply_double(self, double value, double vmin, double vmax) nogil:
+ return sqrt(value)
+
+ cdef double revert_double(self, double value, double vmin, double vmax) nogil:
+ return value**2
+
+
+cdef class PowerNormalization(Normalization):
+ """Gamma correction:
+
+ Linear normalization to [0, 1] followed by power normalization.
+
+ :param gamma: Gamma correction factor
+ """
+
+ cdef:
+ readonly double gamma
+
+ def __cinit__(self, double gamma):
+ self.gamma = gamma
+
+ def __init__(self, gamma):
+ # Needed for multiple inheritance to work
+ pass
+
+ cdef double apply_double(self, double value, double vmin, double vmax) nogil:
+ if vmin == vmax:
+ return 0.
+ elif value <= vmin:
+ return 0.
+ elif value >= vmax:
+ return 1.
+ else:
+ return ((value - vmin) / (vmax - vmin))**self.gamma
+
+ cdef double revert_double(self, double value, double vmin, double vmax) nogil:
+ if value <= 0.:
+ return vmin
+ elif value >= 1.:
+ return vmax
+ else:
+ return vmin + (vmax - vmin) * value**(1.0/self.gamma)
+
+
+# Colormap
+
+@cython.wraparound(False)
+@cython.boundscheck(False)
+@cython.nonecheck(False)
+@cython.cdivision(True)
+cdef image_types[:, ::1] compute_cmap(
+ default_types[:] data,
+ image_types[:, ::1] colors,
+ Normalization normalization,
+ double vmin,
+ double vmax,
+ image_types[::1] nan_color):
+ """Apply colormap to data.
+
+ :param data: Input data
+ :param colors: Colors look-up-table
+ :param vmin: Lower bound of the colormap range
+ :param vmax: Upper bound of the colormap range
+ :param nan_color: Color to use for NaN value
+ :param normalization: Normalization to apply
+ :return: Data converted to colors
+ """
+ cdef image_types[:, ::1] output
+ cdef double scale, value, normalized_vmin, normalized_vmax
+ cdef int length, nb_channels, nb_colors
+ cdef int channel, index, lut_index, num_threads
+
+ nb_colors = <int> colors.shape[0]
+ nb_channels = <int> colors.shape[1]
+ length = <int> data.size
+
+ output = numpy.empty((length, nb_channels),
+ dtype=numpy.array(colors, copy=False).dtype)
+
+ normalized_vmin = normalization.apply_double(vmin, vmin, vmax)
+ normalized_vmax = normalization.apply_double(vmax, vmin, vmax)
+
+ if not isfinite(normalized_vmin) or not isfinite(normalized_vmax):
+ raise ValueError('Colormap range is not valid')
+
+ if normalized_vmin == normalized_vmax:
+ scale = 0.
+ else:
+ scale = nb_colors / (normalized_vmax - normalized_vmin)
+
+ if length < USE_OPENMP_THRESHOLD:
+ num_threads = 1
+ else:
+ num_threads = min(
+ DEFAULT_NUM_THREADS,
+ int(os.environ.get("OMP_NUM_THREADS", DEFAULT_NUM_THREADS)))
+
+ with nogil:
+ for index in prange(length, num_threads=num_threads):
+ value = normalization.apply_double(
+ <double> data[index], vmin, vmax)
+
+ # Handle NaN
+ if isnan(value):
+ for channel in range(nb_channels):
+ output[index, channel] = nan_color[channel]
+ continue
+
+ if value <= normalized_vmin:
+ lut_index = 0
+ elif value >= normalized_vmax:
+ lut_index = nb_colors - 1
+ else:
+ lut_index = <int>((value - normalized_vmin) * scale)
+ # Index can overflow of 1
+ if lut_index >= nb_colors:
+ lut_index = nb_colors - 1
+
+ for channel in range(nb_channels):
+ output[index, channel] = colors[lut_index, channel]
+
+ return output
+
+@cython.wraparound(False)
+@cython.boundscheck(False)
+@cython.nonecheck(False)
+@cython.cdivision(True)
+cdef image_types[:, ::1] compute_cmap_with_lut(
+ lut_types[:] data,
+ image_types[:, ::1] colors,
+ Normalization normalization,
+ double vmin,
+ double vmax,
+ image_types[::1] nan_color):
+ """Convert data to colors using look-up table to speed the process.
+
+ Only supports data of types: uint8, uint16, int8, int16.
+
+ :param data: Input data
+ :param colors: Colors look-up-table
+ :param vmin: Lower bound of the colormap range
+ :param vmax: Upper bound of the colormap range
+ :param nan_color: Color to use for NaN values
+ :param normalization: Normalization to apply
+ :return: The generated image
+ """
+ cdef image_types[:, ::1] output
+ cdef double[:] values
+ cdef image_types[:, ::1] lut
+ cdef int type_min, type_max
+ cdef int nb_channels, length
+ cdef int channel, index, lut_index, num_threads
+
+ length = <int> data.size
+ nb_channels = <int> colors.shape[1]
+
+ if lut_types is cnumpy.int8_t:
+ type_min = -128
+ type_max = 127
+ elif lut_types is cnumpy.uint8_t:
+ type_min = 0
+ type_max = 255
+ elif lut_types is cnumpy.int16_t:
+ type_min = -32768
+ type_max = 32767
+ else: # uint16_t
+ type_min = 0
+ type_max = 65535
+
+ colors_dtype = numpy.array(colors).dtype
+
+ values = numpy.arange(type_min, type_max + 1, dtype=numpy.float64)
+ lut = compute_cmap(
+ values, colors, normalization, vmin, vmax, nan_color)
+
+ output = numpy.empty((length, nb_channels), dtype=colors_dtype)
+
+ if length < USE_OPENMP_THRESHOLD:
+ num_threads = 1
+ else:
+ num_threads = min(
+ DEFAULT_NUM_THREADS,
+ int(os.environ.get("OMP_NUM_THREADS", DEFAULT_NUM_THREADS)))
+
+ with nogil:
+ # Apply LUT
+ for index in prange(length, num_threads=num_threads):
+ lut_index = data[index] - type_min
+ for channel in range(nb_channels):
+ output[index, channel] = lut[lut_index, channel]
+
+ return output
+
+
+# Normalizations without parameters
+_BASIC_NORMALIZATIONS = {
+ 'linear': LinearNormalization(),
+ 'log': LogarithmicNormalization(),
+ 'arcsinh': ArcsinhNormalization(),
+ 'sqrt': SqrtNormalization(),
+ }
+
+
+@cython.wraparound(False)
+@cython.boundscheck(False)
+@cython.nonecheck(False)
+@cython.cdivision(True)
+def _cmap(data_types[:] data,
+ image_types[:, ::1] colors,
+ Normalization normalization,
+ double vmin,
+ double vmax,
+ image_types[::1] nan_color):
+ """Implementation of colormap.
+
+ Use :func:`cmap`.
+
+ :param data: Input data
+ :param colors: Colors look-up-table
+ :param normalization: Normalization object to apply
+ :param vmin: Lower bound of the colormap range
+ :param vmax: Upper bound of the colormap range
+ :param nan_color: Color to use for NaN value.
+ :return: The generated image
+ """
+ cdef image_types[:, ::1] output
+
+ # Proxy for calling the right implementation depending on data type
+ if data_types in lut_types: # Use LUT implementation
+ output = compute_cmap_with_lut(
+ data, colors, normalization, vmin, vmax, nan_color)
+
+ elif data_types in default_types: # Use default implementation
+ output = compute_cmap(
+ data, colors, normalization, vmin, vmax, nan_color)
+
+ else:
+ raise ValueError('Unsupported data type')
+
+ return numpy.array(output, copy=False)
+
+
+def cmap(data not None,
+ colors not None,
+ double vmin,
+ double vmax,
+ normalization='linear',
+ nan_color=None):
+ """Convert data to colors with provided colors look-up table.
+
+ :param numpy.ndarray data: The input data
+ :param numpy.ndarray colors: Color look-up table as a 2D array.
+ It MUST be of type uint8 or float32
+ :param vmin: Data value to map to the beginning of colormap.
+ :param vmax: Data value to map to the end of the colormap.
+ :param Union[str,Normalization] normalization:
+ Either a :class:`Normalization` instance or a str in:
+
+ - 'linear' (default)
+ - 'log'
+ - 'arcsinh'
+ - 'sqrt'
+ - 'gamma'
+
+ :param nan_color: Color to use for NaN value.
+ Default: A color with all channels set to 0
+ :return: Array of colors. The shape of the
+ returned array is that of data array + the last dimension of colors.
+ The dtype of the returned array is that of the colors array.
+ :rtype: numpy.ndarray
+ :raises ValueError: If data of colors dtype is not supported
+ """
+ cdef int nb_channels
+ cdef Normalization norm
+
+ # Make data a numpy array of native endian type (no need for contiguity)
+ data = numpy.array(data, copy=False)
+ if data.dtype.kind not in ('b', 'i', 'u', 'f'):
+ raise ValueError("Unsupported data dtype: %s" % data.dtype)
+ native_endian_dtype = data.dtype.newbyteorder('N')
+ if native_endian_dtype.kind == 'f' and native_endian_dtype.itemsize == 2:
+ native_endian_dtype = "=f4" # Use native float32 instead of float16
+ data = numpy.array(data, copy=False, dtype=native_endian_dtype)
+
+ # Make colors a contiguous array of native endian type
+ colors = numpy.array(colors, copy=False)
+ if colors.dtype.kind == 'f':
+ colors_dtype = numpy.dtype('float32')
+ elif colors.dtype.kind in ('b', 'i', 'u'):
+ colors_dtype = numpy.dtype('uint8')
+ else:
+ raise ValueError("Unsupported colors dtype: %s" % colors.dtype)
+ if (colors_dtype.kind != colors.dtype.kind or
+ colors_dtype.itemsize != colors.dtype.itemsize):
+ # Do not warn if only endianness has changed
+ _logger.warning("Casting colors from %s to %s", colors.dtype, colors_dtype)
+ nb_channels = colors.shape[colors.ndim - 1]
+ colors = numpy.ascontiguousarray(colors, dtype=colors_dtype)
+
+ # Make normalization a Normalization object
+ if isinstance(normalization, str):
+ norm = _BASIC_NORMALIZATIONS.get(normalization, None)
+ if norm is None:
+ raise ValueError('Unsupported normalization %s' % normalization)
+ else:
+ norm = normalization
+
+ # Check nan_color
+ if nan_color is None:
+ nan_color = numpy.zeros((nb_channels,), dtype=colors.dtype)
+ else:
+ nan_color = numpy.ascontiguousarray(
+ nan_color, dtype=colors.dtype).reshape(-1)
+ assert nan_color.shape == (nb_channels,)
+
+ image = _cmap(
+ data.reshape(-1),
+ colors.reshape(-1, nb_channels),
+ norm,
+ vmin,
+ vmax,
+ nan_color)
+ image.shape = data.shape + (nb_channels,)
+
+ return image
diff --git a/src/silx/math/calibration.py b/src/silx/math/calibration.py
new file mode 100644
index 0000000..658e2dc
--- /dev/null
+++ b/src/silx/math/calibration.py
@@ -0,0 +1,180 @@
+# coding: utf-8
+# /*##########################################################################
+# Copyright (C) 2018 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
+"""
+This module provides classes to calibrate data.
+
+Classes
+-------
+
+- :class:`NoCalibration`
+- :class:`LinearCalibration`
+- :class:`ArrayCalibration`
+
+"""
+import numpy
+
+
+class AbstractCalibration(object):
+ """A calibration is a transformation to be applied to an axis (i.e. a 1D array).
+
+ """
+ def __init__(self):
+ super(AbstractCalibration, self).__init__()
+
+ def __call__(self, x):
+ """Apply calibration to an axis or to a value.
+
+ :param x: Axis (1-D array), or value"""
+ raise NotImplementedError(
+ "AbstractCalibration can not be used directly. " +
+ "You must subclass it and implement __call__")
+
+ def is_affine(self):
+ """Returns True for an affine calibration of the form
+ :math:`x \\mapsto a + b * x`, or else False.
+ """
+ return False
+
+ def get_slope(self):
+ raise NotImplementedError(
+ "get_slope is implemented only for affine calibrations")
+
+
+class NoCalibration(AbstractCalibration):
+ """No calibration :math:`x \\mapsto x`
+ """
+ def __init__(self):
+ super(NoCalibration, self).__init__()
+
+ def __call__(self, x):
+ return x
+
+ def is_affine(self):
+ return True
+
+ def get_slope(self):
+ return 1.
+
+
+class LinearCalibration(AbstractCalibration):
+ """Linear calibration :math:`x \\mapsto a + b x`,
+ where *a* is the y-intercept and *b* is the slope.
+
+ :param y_intercept: y-intercept
+ :param slope: Slope of the affine transformation
+ """
+ def __init__(self, y_intercept, slope):
+ super(LinearCalibration, self).__init__()
+ self.constant = y_intercept
+ self.slope = slope
+
+ def __call__(self, x):
+ return self.constant + self.slope * x
+
+ def is_affine(self):
+ return True
+
+ def get_slope(self):
+ return self.slope
+
+
+class ArrayCalibration(AbstractCalibration):
+ """One-to-one mapping calibration, defined by an array *x'*,
+ such as :math:`x \\mapsto x'`.
+
+ This calibration can only be applied to x arrays of the same length as the
+ calibration array *x'*.
+ It is typically applied to an axis of indices or
+ channels (:math:`0, 1, ..., n-1`).
+
+ :param x1: Calibration array"""
+ def __init__(self, x1):
+ super(ArrayCalibration, self).__init__()
+ if not isinstance(x1, (list, tuple)) and not hasattr(x1, "shape"):
+ raise TypeError(
+ "The calibration array must be a sequence (list, dataset, array)")
+ self.calibration_array = numpy.array(x1)
+ self._is_affine = None
+
+ def __call__(self, x):
+ # calibrate the entire axis
+ if isinstance(x, (list, tuple, numpy.ndarray)) and \
+ len(self.calibration_array) == len(x):
+ return self.calibration_array
+ # calibrate one value, by index
+ if isinstance(x, int) and x < len(self.calibration_array):
+ return self.calibration_array[x]
+ raise ValueError("ArrayCalibration must be applied to array of same size "
+ "or to index.")
+
+ def is_affine(self):
+ """If all values in the calibration array are regularly spaced,
+ return True."""
+ if self._is_affine is None:
+ delta_x = self.calibration_array[1:] - self.calibration_array[:-1]
+ # use a less strict relative tolerance to account for rounding errors
+ # e.g. when using float64 into float32 (see #1823)
+ if not numpy.isclose(delta_x, delta_x[0], rtol=1e-4).all():
+ self._is_affine = False
+ else:
+ self._is_affine = True
+ return self._is_affine
+
+ def get_slope(self):
+ """If the calibration array is regularly spaced, return the spacing."""
+ if not self.is_affine():
+ raise AttributeError(
+ "get_slope only makes sense for affine transformations"
+ )
+ return self.calibration_array[1] - self.calibration_array[0]
+
+
+class FunctionCalibration(AbstractCalibration):
+ """Calibration defined by a function *f*, such as :math:`x \\mapsto f(x)`*.
+
+ :param function: Calibration function"""
+ def __init__(self, function, is_affine=False):
+ super(FunctionCalibration, self).__init__()
+ if not hasattr(function, "__call__"):
+ raise TypeError("The calibration function must be a callable")
+ self.function = function
+ self._is_affine = is_affine
+
+ def __call__(self, x):
+ return self.function(x)
+
+ def is_affine(self):
+ """Return True if calibration is affine.
+ This is False by default, unless the object is instantiated with
+ ``is_affine=True``."""
+ return self._is_affine
+
+ def get_slope(self):
+ """If the calibration array is regularly spaced, return the spacing."""
+ if not self.is_affine():
+ raise AttributeError(
+ "get_slope only makes sense for affine transformations"
+ )
+ # fixme: what if function is not defined at x=1 or x=2?
+ return self.function(2) - self.function(1)
diff --git a/src/silx/math/chistogramnd.pyx b/src/silx/math/chistogramnd.pyx
new file mode 100644
index 0000000..8484f35
--- /dev/null
+++ b/src/silx/math/chistogramnd.pyx
@@ -0,0 +1,1251 @@
+# coding: utf-8
+# /*##########################################################################
+# Copyright (C) 2016-2018 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
+
+__authors__ = ["D. Naudet"]
+__license__ = "MIT"
+__date__ = "02/10/2017"
+
+cimport numpy as cnumpy # noqa
+cimport cython
+import numpy as np
+
+cimport silx.math.histogramnd_c as histogramnd_c
+
+
+def chistogramnd(sample,
+ histo_range,
+ n_bins,
+ weights=None,
+ weight_min=None,
+ weight_max=None,
+ last_bin_closed=False,
+ histo=None,
+ weighted_histo=None,
+ wh_dtype=None):
+ """Computes the multidimensional histogram of some data.
+
+ :param sample:
+ The data to be histogrammed.
+ Its shape must be either
+ (N,) if it contains one dimensional coordinates,
+ or an (N,D) array where the rows are the
+ coordinates of points in a D dimensional space.
+ The following dtypes are supported : :class:`numpy.float64`,
+ :class:`numpy.float32`, :class:`numpy.int32`.
+
+ .. warning:: if sample is not a C_CONTIGUOUS ndarray (e.g : a non
+ contiguous slice) then histogramnd will have to do make an internal
+ copy.
+ :type sample: :class:`numpy.array`
+
+ :param histo_range:
+ A (N, 2) array containing the histogram range along each dimension,
+ where N is the sample's number of dimensions.
+ :type histo_range: array_like
+
+ :param n_bins:
+ The number of bins :
+ * a scalar (same number of bins for all dimensions)
+ * a D elements array (number of bins for each dimensions)
+ :type n_bins: scalar or array_like
+
+ :param weights:
+ A N elements numpy array of values associated with
+ each sample.
+ The values of the *weighted_histo* array
+ returned by the function are equal to the sum of
+ the weights associated with the samples falling
+ into each bin.
+ The following dtypes are supported : :class:`numpy.float64`,
+ :class:`numpy.float32`, :class:`numpy.int32`.
+
+ .. note:: If None, the weighted histogram returned will be None.
+ :type weights: *optional*, :class:`numpy.array`
+
+ :param weight_min:
+ Use this parameter to filter out all samples whose
+ weights are lower than this value.
+
+ .. note:: This value will be cast to the same type
+ as *weights*.
+ :type weight_min: *optional*, scalar
+
+ :param weight_max:
+ Use this parameter to filter out all samples whose
+ weights are higher than this value.
+
+ .. note:: This value will be cast to the same type
+ as *weights*.
+
+ :type weight_max: *optional*, scalar
+
+ :param last_bin_closed:
+ By default the last bin is half
+ open (i.e.: [x,y) ; x included, y
+ excluded), like all the other bins.
+ Set this parameter to true if you want
+ the LAST bin to be closed.
+ :type last_bin_closed: *optional*, :class:`python.boolean`
+
+ :param histo:
+ Use this parameter if you want to pass your
+ own histogram array instead of the one
+ created by this function. New values
+ will be added to this array. The returned array
+ will then be this one (same reference).
+
+ .. warning:: If the histo array was created by a previous
+ call to histogramnd then the user is
+ responsible for providing the same parameters
+ (*n_bins*, *histo_range*, ...).
+ :type histo: *optional*, :class:`numpy.array`
+
+ :param weighted_histo:
+ Use this parameter if you want to pass your
+ own weighted histogram array instead of
+ the created by this function. New
+ values will be added to this array. The returned array
+ will then be this one (same reference).
+
+ .. warning:: If the weighted_histo array was created by a previous
+ call to histogramnd then the user is
+ responsible for providing the same parameters
+ (*n_bins*, *histo_range*, ...).
+
+ .. warning:: if weighted_histo is not a C_CONTIGUOUS ndarray (e.g : a
+ non contiguous slice) then histogramnd will have to do make an
+ internal copy.
+ :type weighted_histo: *optional*, :class:`numpy.array`
+
+ :param wh_dtype: type of the weighted histogram array. This parameter is
+ ignored if *weighted_histo* is provided. If not provided, the
+ weighted histogram array will contain values of the same type as
+ *weights*. Allowed values are : `numpu.double` and `numpy.float32`.
+ :type wh_dtype: *optional*, numpy data type
+
+ :return: Histogram (bin counts, always returned), weighted histogram of
+ the sample (or *None* if weights is *None*) and bin edges for each
+ dimension.
+ :rtype: *tuple* (:class:`numpy.array`, :class:`numpy.array`, `tuple`) or
+ (:class:`numpy.array`, None, `tuple`)
+ """
+
+ if wh_dtype is None:
+ wh_dtype = np.double
+ elif wh_dtype not in (np.double, np.float32):
+ raise ValueError('<wh_dtype> type not supported : {0}.'.format(wh_dtype))
+
+ if (weighted_histo is not None and
+ weighted_histo.flags['C_CONTIGUOUS'] is False):
+ raise ValueError('<weighted_histo> must be a C_CONTIGUOUS numpy array.')
+
+ if histo is not None and histo.flags['C_CONTIGUOUS'] is False:
+ raise ValueError('<histo> must be a C_CONTIGUOUS numpy array.')
+
+ s_shape = sample.shape
+
+ n_dims = 1 if len(s_shape) == 1 else s_shape[1]
+
+ if weights is not None:
+ w_shape = weights.shape
+
+ # making sure the sample and weights sizes are coherent
+ # 2 different cases : 2D sample (N,M) and 1D (N)
+ if len(w_shape) != 1 or w_shape[0] != s_shape[0]:
+ raise ValueError('<weights> must be an array whose length '
+ 'is equal to the number of samples.')
+
+ weights_type = weights.dtype
+ else:
+ weights_type = None
+
+ # just in case those arent numpy arrays
+ # (this allows the user to provide native python lists,
+ # => easier for testing)
+ i_histo_range = histo_range
+ histo_range = np.array(histo_range)
+ err_histo_range = False
+
+ if n_dims == 1:
+ if histo_range.shape == (2,):
+ pass
+ elif histo_range.shape == (1, 2):
+ histo_range.shape = -1
+ else:
+ err_histo_range = True
+ elif n_dims != 1 and histo_range.shape != (n_dims, 2):
+ err_histo_range = True
+
+ if err_histo_range:
+ raise ValueError('<histo_range> error : expected {n_dims} sets of '
+ 'lower and upper bin edges, '
+ 'got the following instead : {histo_range}. '
+ '(provided <sample> contains '
+ '{n_dims}D values)'
+ ''.format(histo_range=i_histo_range,
+ n_dims=n_dims))
+
+ # check range value
+ if np.inf in histo_range:
+ raise ValueError('Range parameter should be finite value')
+ if np.nan in histo_range:
+ raise ValueError('Range value can\'t be nan')
+
+ # checking n_bins size
+ n_bins = np.array(n_bins, ndmin=1)
+ if len(n_bins) == 1:
+ n_bins = np.tile(n_bins, n_dims)
+ elif n_bins.shape != (n_dims,):
+ raise ValueError('n_bins must be either a scalar (same number '
+ 'of bins for all dimensions) or '
+ 'an array (number of bins for each '
+ 'dimension).')
+
+ # checking if None is in n_bins, otherwise a rather cryptic
+ # exception is thrown when calling np.zeros
+ # also testing for negative/null values
+ if np.any(np.equal(n_bins, None)) or np.any(n_bins <= 0):
+ raise ValueError('<n_bins> : only positive values allowed.')
+
+ output_shape = tuple(n_bins)
+
+ # checking the histo array, if provided
+ if histo is None:
+ histo = np.zeros(output_shape, dtype=np.uint32)
+ else:
+ if histo.shape != output_shape:
+ raise ValueError('Provided <histo> array doesn\'t have '
+ 'a shape compatible with <n_bins> '
+ ': should be {0} instead of {1}.'
+ ''.format(output_shape, histo.shape))
+ if histo.dtype != np.uint32:
+ raise ValueError('Provided <histo> array doesn\'t have '
+ 'the expected type '
+ ': should be {0} instead of {1}.'
+ ''.format(np.uint32, histo.dtype))
+
+ # checking the weighted_histo array, if provided
+ if weights_type is None:
+ # no weights provided, not creating the weighted_histo array
+ weighted_histo = None
+ elif weighted_histo is None:
+ # weights provided, but no weighted_histo, creating it
+ if wh_dtype is None:
+ wh_dtype = weights_type
+ weighted_histo = np.zeros(output_shape, dtype=wh_dtype)
+ else:
+ # weighted_histo provided, checking shape/dtype
+ if weighted_histo.shape != output_shape:
+ raise ValueError('Provided <weighted_histo> array doesn\'t have '
+ 'a shape compatible with <n_bins> '
+ ': should be {0} instead of {1}.'
+ ''.format(output_shape, weighted_histo.shape))
+ if (weighted_histo.dtype != np.float64 and
+ weighted_histo.dtype != np.float32):
+ raise ValueError('Provided <weighted_histo> array doesn\'t have '
+ 'the expected type '
+ ': should be {0} or {1} instead of {2}.'
+ ''.format(np.double,
+ np.float32,
+ weighted_histo.dtype))
+
+ option_flags = 0
+
+ if weight_min is not None:
+ option_flags |= histogramnd_c.HISTO_WEIGHT_MIN
+ else:
+ weight_min = 0
+
+ if weight_max is not None:
+ option_flags |= histogramnd_c.HISTO_WEIGHT_MAX
+ else:
+ weight_max = 0
+
+ if last_bin_closed is not None and last_bin_closed:
+ option_flags |= histogramnd_c.HISTO_LAST_BIN_CLOSED
+
+ sample_type = sample.dtype
+ sample_type = sample_type.newbyteorder('N')
+
+ n_elem = sample.size // n_dims
+
+ bin_edges = np.zeros(n_bins.sum() + n_bins.size, dtype=np.double)
+
+ # wanted to store the functions in a dict (with the supported types
+ # as keys, but I couldn't find a way to make it work with cdef
+ # functions. so I have to explicitly list them all...
+
+ def raise_unsupported_type():
+ raise TypeError('Case not supported - sample:{0} '
+ 'and weights:{1}.'
+ ''.format(sample_type, weights_type))
+
+ sample_c = np.ascontiguousarray(sample.reshape((sample.size,)),
+ dtype=sample_type)
+
+ weights_c = (np.ascontiguousarray(weights.reshape((weights.size,)),
+ dtype=weights.dtype.newbyteorder('N'))
+ if weights is not None else None)
+
+ histo_range_c = np.ascontiguousarray(histo_range.reshape((histo_range.size,)),
+ dtype=np.double)
+
+ n_bins_c = np.ascontiguousarray(n_bins.reshape((n_bins.size,)),
+ dtype=np.int32)
+
+ histo_c = histo.reshape((histo.size,))
+
+ if weighted_histo is not None:
+ cumul_c = weighted_histo.reshape((weighted_histo.size,))
+ else:
+ cumul_c = None
+
+ bin_edges_c = np.ascontiguousarray(bin_edges.reshape((bin_edges.size,)),
+ dtype=bin_edges.dtype.newbyteorder('N'))
+
+ rc = 0
+
+ if weighted_histo is None or weighted_histo.dtype == np.double:
+
+ if sample_type == np.float64:
+
+ if weights_type == np.float64 or weights_type is None:
+
+ rc = _histogramnd_double_double_double(sample_c,
+ weights_c,
+ n_dims,
+ n_elem,
+ histo_range_c,
+ n_bins_c,
+ histo_c,
+ cumul_c,
+ bin_edges_c,
+ option_flags,
+ weight_min=weight_min,
+ weight_max=weight_max)
+
+ elif weights_type == np.float32:
+
+ rc = _histogramnd_double_float_double(sample_c,
+ weights_c,
+ n_dims,
+ n_elem,
+ histo_range_c,
+ n_bins_c,
+ histo_c,
+ cumul_c,
+ bin_edges_c,
+ option_flags,
+ weight_min=weight_min,
+ weight_max=weight_max)
+
+ elif weights_type == np.int32:
+
+ rc = _histogramnd_double_int32_t_double(sample_c,
+ weights_c,
+ n_dims,
+ n_elem,
+ histo_range_c,
+ n_bins_c,
+ histo_c,
+ cumul_c,
+ bin_edges_c,
+ option_flags,
+ weight_min=weight_min,
+ weight_max=weight_max)
+
+ else:
+ raise_unsupported_type()
+
+ # endif sample_type == np.float64
+ elif sample_type == np.float32:
+
+ if weights_type == np.float64 or weights_type is None:
+
+ rc = _histogramnd_float_double_double(sample_c,
+ weights_c,
+ n_dims,
+ n_elem,
+ histo_range_c,
+ n_bins_c,
+ histo_c,
+ cumul_c,
+ bin_edges_c,
+ option_flags,
+ weight_min=weight_min,
+ weight_max=weight_max)
+
+ elif weights_type == np.float32:
+
+ rc = _histogramnd_float_float_double(sample_c,
+ weights_c,
+ n_dims,
+ n_elem,
+ histo_range_c,
+ n_bins_c,
+ histo_c,
+ cumul_c,
+ bin_edges_c,
+ option_flags,
+ weight_min=weight_min,
+ weight_max=weight_max)
+
+ elif weights_type == np.int32:
+
+ rc = _histogramnd_float_int32_t_double(sample_c,
+ weights_c,
+ n_dims,
+ n_elem,
+ histo_range_c,
+ n_bins_c,
+ histo_c,
+ cumul_c,
+ bin_edges_c,
+ option_flags,
+ weight_min=weight_min,
+ weight_max=weight_max)
+
+ else:
+ raise_unsupported_type()
+
+ # endif sample_type == np.float32
+ elif sample_type == np.int32:
+
+ if weights_type == np.float64 or weights_type is None:
+
+ rc = _histogramnd_int32_t_double_double(sample_c,
+ weights_c,
+ n_dims,
+ n_elem,
+ histo_range_c,
+ n_bins_c,
+ histo_c,
+ cumul_c,
+ bin_edges_c,
+ option_flags,
+ weight_min=weight_min,
+ weight_max=weight_max)
+
+ elif weights_type == np.float32:
+
+ rc = _histogramnd_int32_t_float_double(sample_c,
+ weights_c,
+ n_dims,
+ n_elem,
+ histo_range_c,
+ n_bins_c,
+ histo_c,
+ cumul_c,
+ bin_edges_c,
+ option_flags,
+ weight_min=weight_min,
+ weight_max=weight_max)
+
+ elif weights_type == np.int32:
+
+ rc = _histogramnd_int32_t_int32_t_double(sample_c,
+ weights_c,
+ n_dims,
+ n_elem,
+ histo_range_c,
+ n_bins_c,
+ histo_c,
+ cumul_c,
+ bin_edges_c,
+ option_flags,
+ weight_min=weight_min,
+ weight_max=weight_max)
+
+ else:
+ raise_unsupported_type()
+
+ # endif sample_type == np.int32:
+ else:
+ raise_unsupported_type()
+
+ # endif weighted_histo is None or weighted_histo.dtype == np.double:
+ elif weighted_histo.dtype == np.float32:
+
+ if sample_type == np.float64:
+
+ if weights_type == np.float64 or weights_type is None:
+
+ rc = _histogramnd_double_double_float(sample_c,
+ weights_c,
+ n_dims,
+ n_elem,
+ histo_range_c,
+ n_bins_c,
+ histo_c,
+ cumul_c,
+ bin_edges_c,
+ option_flags,
+ weight_min=weight_min,
+ weight_max=weight_max)
+
+ elif weights_type == np.float32:
+
+ rc = _histogramnd_double_float_float(sample_c,
+ weights_c,
+ n_dims,
+ n_elem,
+ histo_range_c,
+ n_bins_c,
+ histo_c,
+ cumul_c,
+ bin_edges_c,
+ option_flags,
+ weight_min=weight_min,
+ weight_max=weight_max)
+
+ elif weights_type == np.int32:
+
+ rc = _histogramnd_double_int32_t_float(sample_c,
+ weights_c,
+ n_dims,
+ n_elem,
+ histo_range_c,
+ n_bins_c,
+ histo_c,
+ cumul_c,
+ bin_edges_c,
+ option_flags,
+ weight_min=weight_min,
+ weight_max=weight_max)
+
+ else:
+ raise_unsupported_type()
+
+ # endif sample_type == np.float64
+ elif sample_type == np.float32:
+
+ if weights_type == np.float64 or weights_type is None:
+
+ rc = _histogramnd_float_double_float(sample_c,
+ weights_c,
+ n_dims,
+ n_elem,
+ histo_range_c,
+ n_bins_c,
+ histo_c,
+ cumul_c,
+ bin_edges_c,
+ option_flags,
+ weight_min=weight_min,
+ weight_max=weight_max)
+
+ elif weights_type == np.float32:
+
+ rc = _histogramnd_float_float_float(sample_c,
+ weights_c,
+ n_dims,
+ n_elem,
+ histo_range_c,
+ n_bins_c,
+ histo_c,
+ cumul_c,
+ bin_edges_c,
+ option_flags,
+ weight_min=weight_min,
+ weight_max=weight_max)
+
+ elif weights_type == np.int32:
+
+ rc = _histogramnd_float_int32_t_float(sample_c,
+ weights_c,
+ n_dims,
+ n_elem,
+ histo_range_c,
+ n_bins_c,
+ histo_c,
+ cumul_c,
+ bin_edges_c,
+ option_flags,
+ weight_min=weight_min,
+ weight_max=weight_max)
+
+ else:
+ raise_unsupported_type()
+
+ # endif sample_type == np.float32
+ elif sample_type == np.int32:
+
+ if weights_type == np.float64 or weights_type is None:
+
+ rc = _histogramnd_int32_t_double_float(sample_c,
+ weights_c,
+ n_dims,
+ n_elem,
+ histo_range_c,
+ n_bins_c,
+ histo_c,
+ cumul_c,
+ bin_edges_c,
+ option_flags,
+ weight_min=weight_min,
+ weight_max=weight_max)
+
+ elif weights_type == np.float32:
+
+ rc = _histogramnd_int32_t_float_float(sample_c,
+ weights_c,
+ n_dims,
+ n_elem,
+ histo_range_c,
+ n_bins_c,
+ histo_c,
+ cumul_c,
+ bin_edges_c,
+ option_flags,
+ weight_min=weight_min,
+ weight_max=weight_max)
+
+ elif weights_type == np.int32:
+
+ rc = _histogramnd_int32_t_int32_t_float(sample_c,
+ weights_c,
+ n_dims,
+ n_elem,
+ histo_range_c,
+ n_bins_c,
+ histo_c,
+ cumul_c,
+ bin_edges_c,
+ option_flags,
+ weight_min=weight_min,
+ weight_max=weight_max)
+
+ else:
+ raise_unsupported_type()
+
+ # endif sample_type == np.int32:
+ else:
+ raise_unsupported_type()
+
+ # end elseif weighted_histo.dtype == np.float32:
+ else:
+ # this isnt supposed to happen since weighted_histo type was checked earlier
+ raise_unsupported_type()
+
+ if rc != histogramnd_c.HISTO_OK:
+ if rc == histogramnd_c.HISTO_ERR_ALLOC:
+ raise MemoryError('histogramnd failed to allocate memory.')
+ else:
+ raise Exception('histogramnd returned an error : {0}'
+ ''.format(rc))
+
+ edges = []
+ offset = 0
+ for i_dim in range(n_dims):
+ edges.append(bin_edges[offset:offset + n_bins[i_dim] + 1])
+ offset += n_bins[i_dim] + 1
+
+ return histo, weighted_histo, tuple(edges)
+
+# =====================
+# double sample, double cumul
+# =====================
+
+
+@cython.wraparound(False)
+@cython.boundscheck(False)
+@cython.initializedcheck(False)
+@cython.nonecheck(False)
+cdef int _histogramnd_double_double_double(double[:] sample,
+ double[:] weights,
+ int n_dims,
+ int n_elem,
+ double[:] histo_range,
+ int[:] n_bins,
+ cnumpy.uint32_t[:] histo,
+ double[:] cumul,
+ double[:] bin_edges,
+ int option_flags,
+ double weight_min,
+ double weight_max) nogil:
+
+ return histogramnd_c.histogramnd_double_double_double(&sample[0],
+ &weights[0],
+ n_dims,
+ n_elem,
+ &histo_range[0],
+ &n_bins[0],
+ &histo[0],
+ &cumul[0],
+ &bin_edges[0],
+ option_flags,
+ weight_min,
+ weight_max)
+
+
+@cython.wraparound(False)
+@cython.boundscheck(False)
+@cython.initializedcheck(False)
+@cython.nonecheck(False)
+cdef int _histogramnd_double_float_double(double[:] sample,
+ float[:] weights,
+ int n_dims,
+ int n_elem,
+ double[:] histo_range,
+ int[:] n_bins,
+ cnumpy.uint32_t[:] histo,
+ double[:] cumul,
+ double[:] bin_edges,
+ int option_flags,
+ float weight_min,
+ float weight_max) nogil:
+
+ return histogramnd_c.histogramnd_double_float_double(&sample[0],
+ &weights[0],
+ n_dims,
+ n_elem,
+ &histo_range[0],
+ &n_bins[0],
+ &histo[0],
+ &cumul[0],
+ &bin_edges[0],
+ option_flags,
+ weight_min,
+ weight_max)
+
+
+@cython.wraparound(False)
+@cython.boundscheck(False)
+@cython.initializedcheck(False)
+@cython.nonecheck(False)
+cdef int _histogramnd_double_int32_t_double(double[:] sample,
+ cnumpy.int32_t[:] weights,
+ int n_dims,
+ int n_elem,
+ double[:] histo_range,
+ int[:] n_bins,
+ cnumpy.uint32_t[:] histo,
+ double[:] cumul,
+ double[:] bin_edges,
+ int option_flags,
+ cnumpy.int32_t weight_min,
+ cnumpy.int32_t weight_max) nogil:
+
+ return histogramnd_c.histogramnd_double_int32_t_double(&sample[0],
+ &weights[0],
+ n_dims,
+ n_elem,
+ &histo_range[0],
+ &n_bins[0],
+ &histo[0],
+ &cumul[0],
+ &bin_edges[0],
+ option_flags,
+ weight_min,
+ weight_max)
+
+
+# =====================
+# float sample, double cumul
+# =====================
+
+
+@cython.wraparound(False)
+@cython.boundscheck(False)
+@cython.initializedcheck(False)
+@cython.nonecheck(False)
+cdef int _histogramnd_float_double_double(float[:] sample,
+ double[:] weights,
+ int n_dims,
+ int n_elem,
+ double[:] histo_range,
+ int[:] n_bins,
+ cnumpy.uint32_t[:] histo,
+ double[:] cumul,
+ double[:] bin_edges,
+ int option_flags,
+ double weight_min,
+ double weight_max) nogil:
+
+ return histogramnd_c.histogramnd_float_double_double(&sample[0],
+ &weights[0],
+ n_dims,
+ n_elem,
+ &histo_range[0],
+ &n_bins[0],
+ &histo[0],
+ &cumul[0],
+ &bin_edges[0],
+ option_flags,
+ weight_min,
+ weight_max)
+
+
+@cython.wraparound(False)
+@cython.boundscheck(False)
+@cython.initializedcheck(False)
+@cython.nonecheck(False)
+cdef int _histogramnd_float_float_double(float[:] sample,
+ float[:] weights,
+ int n_dims,
+ int n_elem,
+ double[:] histo_range,
+ int[:] n_bins,
+ cnumpy.uint32_t[:] histo,
+ double[:] cumul,
+ double[:] bin_edges,
+ int option_flags,
+ float weight_min,
+ float weight_max) nogil:
+
+ return histogramnd_c.histogramnd_float_float_double(&sample[0],
+ &weights[0],
+ n_dims,
+ n_elem,
+ &histo_range[0],
+ &n_bins[0],
+ &histo[0],
+ &cumul[0],
+ &bin_edges[0],
+ option_flags,
+ weight_min,
+ weight_max)
+
+
+@cython.wraparound(False)
+@cython.boundscheck(False)
+@cython.initializedcheck(False)
+@cython.nonecheck(False)
+cdef int _histogramnd_float_int32_t_double(float[:] sample,
+ cnumpy.int32_t[:] weights,
+ int n_dims,
+ int n_elem,
+ double[:] histo_range,
+ int[:] n_bins,
+ cnumpy.uint32_t[:] histo,
+ double[:] cumul,
+ double[:] bin_edges,
+ int option_flags,
+ cnumpy.int32_t weight_min,
+ cnumpy.int32_t weight_max) nogil:
+
+ return histogramnd_c.histogramnd_float_int32_t_double(&sample[0],
+ &weights[0],
+ n_dims,
+ n_elem,
+ &histo_range[0],
+ &n_bins[0],
+ &histo[0],
+ &cumul[0],
+ &bin_edges[0],
+ option_flags,
+ weight_min,
+ weight_max)
+
+
+# =====================
+# numpy.int32_t sample, double cumul
+# =====================
+
+
+@cython.wraparound(False)
+@cython.boundscheck(False)
+@cython.initializedcheck(False)
+@cython.nonecheck(False)
+cdef int _histogramnd_int32_t_double_double(cnumpy.int32_t[:] sample,
+ double[:] weights,
+ int n_dims,
+ int n_elem,
+ double[:] histo_range,
+ int[:] n_bins,
+ cnumpy.uint32_t[:] histo,
+ double[:] cumul,
+ double[:] bin_edges,
+ int option_flags,
+ double weight_min,
+ double weight_max) nogil:
+
+ return histogramnd_c.histogramnd_int32_t_double_double(&sample[0],
+ &weights[0],
+ n_dims,
+ n_elem,
+ &histo_range[0],
+ &n_bins[0],
+ &histo[0],
+ &cumul[0],
+ &bin_edges[0],
+ option_flags,
+ weight_min,
+ weight_max)
+
+
+@cython.wraparound(False)
+@cython.boundscheck(False)
+@cython.initializedcheck(False)
+@cython.nonecheck(False)
+cdef int _histogramnd_int32_t_float_double(cnumpy.int32_t[:] sample,
+ float[:] weights,
+ int n_dims,
+ int n_elem,
+ double[:] histo_range,
+ int[:] n_bins,
+ cnumpy.uint32_t[:] histo,
+ double[:] cumul,
+ double[:] bin_edges,
+ int option_flags,
+ float weight_min,
+ float weight_max) nogil:
+
+ return histogramnd_c.histogramnd_int32_t_float_double(&sample[0],
+ &weights[0],
+ n_dims,
+ n_elem,
+ &histo_range[0],
+ &n_bins[0],
+ &histo[0],
+ &cumul[0],
+ &bin_edges[0],
+ option_flags,
+ weight_min,
+ weight_max)
+
+
+@cython.wraparound(False)
+@cython.boundscheck(False)
+@cython.initializedcheck(False)
+@cython.nonecheck(False)
+cdef int _histogramnd_int32_t_int32_t_double(cnumpy.int32_t[:] sample,
+ cnumpy.int32_t[:] weights,
+ int n_dims,
+ int n_elem,
+ double[:] histo_range,
+ int[:] n_bins,
+ cnumpy.uint32_t[:] histo,
+ double[:] cumul,
+ double[:] bin_edges,
+ int option_flags,
+ cnumpy.int32_t weight_min,
+ cnumpy.int32_t weight_max) nogil:
+
+ return histogramnd_c.histogramnd_int32_t_int32_t_double(&sample[0],
+ &weights[0],
+ n_dims,
+ n_elem,
+ &histo_range[0],
+ &n_bins[0],
+ &histo[0],
+ &cumul[0],
+ &bin_edges[0],
+ option_flags,
+ weight_min,
+ weight_max)
+
+
+# =====================
+# double sample, float cumul
+# =====================
+
+
+@cython.wraparound(False)
+@cython.boundscheck(False)
+@cython.initializedcheck(False)
+@cython.nonecheck(False)
+cdef int _histogramnd_double_double_float(double[:] sample,
+ double[:] weights,
+ int n_dims,
+ int n_elem,
+ double[:] histo_range,
+ int[:] n_bins,
+ cnumpy.uint32_t[:] histo,
+ float[:] cumul,
+ double[:] bin_edges,
+ int option_flags,
+ double weight_min,
+ double weight_max) nogil:
+
+ return histogramnd_c.histogramnd_double_double_float(&sample[0],
+ &weights[0],
+ n_dims,
+ n_elem,
+ &histo_range[0],
+ &n_bins[0],
+ &histo[0],
+ &cumul[0],
+ &bin_edges[0],
+ option_flags,
+ weight_min,
+ weight_max)
+
+
+@cython.wraparound(False)
+@cython.boundscheck(False)
+@cython.initializedcheck(False)
+@cython.nonecheck(False)
+cdef int _histogramnd_double_float_float(double[:] sample,
+ float[:] weights,
+ int n_dims,
+ int n_elem,
+ double[:] histo_range,
+ int[:] n_bins,
+ cnumpy.uint32_t[:] histo,
+ float[:] cumul,
+ double[:] bin_edges,
+ int option_flags,
+ float weight_min,
+ float weight_max) nogil:
+
+ return histogramnd_c.histogramnd_double_float_float(&sample[0],
+ &weights[0],
+ n_dims,
+ n_elem,
+ &histo_range[0],
+ &n_bins[0],
+ &histo[0],
+ &cumul[0],
+ &bin_edges[0],
+ option_flags,
+ weight_min,
+ weight_max)
+
+
+@cython.wraparound(False)
+@cython.boundscheck(False)
+@cython.initializedcheck(False)
+@cython.nonecheck(False)
+cdef int _histogramnd_double_int32_t_float(double[:] sample,
+ cnumpy.int32_t[:] weights,
+ int n_dims,
+ int n_elem,
+ double[:] histo_range,
+ int[:] n_bins,
+ cnumpy.uint32_t[:] histo,
+ float[:] cumul,
+ double[:] bin_edges,
+ int option_flags,
+ cnumpy.int32_t weight_min,
+ cnumpy.int32_t weight_max) nogil:
+
+ return histogramnd_c.histogramnd_double_int32_t_float(&sample[0],
+ &weights[0],
+ n_dims,
+ n_elem,
+ &histo_range[0],
+ &n_bins[0],
+ &histo[0],
+ &cumul[0],
+ &bin_edges[0],
+ option_flags,
+ weight_min,
+ weight_max)
+
+
+# =====================
+# float sample, float cumul
+# =====================
+
+
+@cython.wraparound(False)
+@cython.boundscheck(False)
+@cython.initializedcheck(False)
+@cython.nonecheck(False)
+cdef int _histogramnd_float_double_float(float[:] sample,
+ double[:] weights,
+ int n_dims,
+ int n_elem,
+ double[:] histo_range,
+ int[:] n_bins,
+ cnumpy.uint32_t[:] histo,
+ float[:] cumul,
+ double[:] bin_edges,
+ int option_flags,
+ double weight_min,
+ double weight_max) nogil:
+
+ return histogramnd_c.histogramnd_float_double_float(&sample[0],
+ &weights[0],
+ n_dims,
+ n_elem,
+ &histo_range[0],
+ &n_bins[0],
+ &histo[0],
+ &cumul[0],
+ &bin_edges[0],
+ option_flags,
+ weight_min,
+ weight_max)
+
+
+@cython.wraparound(False)
+@cython.boundscheck(False)
+@cython.initializedcheck(False)
+@cython.nonecheck(False)
+cdef int _histogramnd_float_float_float(float[:] sample,
+ float[:] weights,
+ int n_dims,
+ int n_elem,
+ double[:] histo_range,
+ int[:] n_bins,
+ cnumpy.uint32_t[:] histo,
+ float[:] cumul,
+ double[:] bin_edges,
+ int option_flags,
+ float weight_min,
+ float weight_max) nogil:
+
+ return histogramnd_c.histogramnd_float_float_float(&sample[0],
+ &weights[0],
+ n_dims,
+ n_elem,
+ &histo_range[0],
+ &n_bins[0],
+ &histo[0],
+ &cumul[0],
+ &bin_edges[0],
+ option_flags,
+ weight_min,
+ weight_max)
+
+
+@cython.wraparound(False)
+@cython.boundscheck(False)
+@cython.initializedcheck(False)
+@cython.nonecheck(False)
+cdef int _histogramnd_float_int32_t_float(float[:] sample,
+ cnumpy.int32_t[:] weights,
+ int n_dims,
+ int n_elem,
+ double[:] histo_range,
+ int[:] n_bins,
+ cnumpy.uint32_t[:] histo,
+ float[:] cumul,
+ double[:] bin_edges,
+ int option_flags,
+ cnumpy.int32_t weight_min,
+ cnumpy.int32_t weight_max) nogil:
+
+ return histogramnd_c.histogramnd_float_int32_t_float(&sample[0],
+ &weights[0],
+ n_dims,
+ n_elem,
+ &histo_range[0],
+ &n_bins[0],
+ &histo[0],
+ &cumul[0],
+ &bin_edges[0],
+ option_flags,
+ weight_min,
+ weight_max)
+
+
+# =====================
+# numpy.int32_t sample, float cumul
+# =====================
+
+
+@cython.wraparound(False)
+@cython.boundscheck(False)
+@cython.initializedcheck(False)
+@cython.nonecheck(False)
+cdef int _histogramnd_int32_t_double_float(cnumpy.int32_t[:] sample,
+ double[:] weights,
+ int n_dims,
+ int n_elem,
+ double[:] histo_range,
+ int[:] n_bins,
+ cnumpy.uint32_t[:] histo,
+ float[:] cumul,
+ double[:] bin_edges,
+ int option_flags,
+ double weight_min,
+ double weight_max) nogil:
+
+ return histogramnd_c.histogramnd_int32_t_double_float(&sample[0],
+ &weights[0],
+ n_dims,
+ n_elem,
+ &histo_range[0],
+ &n_bins[0],
+ &histo[0],
+ &cumul[0],
+ &bin_edges[0],
+ option_flags,
+ weight_min,
+ weight_max)
+
+
+@cython.wraparound(False)
+@cython.boundscheck(False)
+@cython.initializedcheck(False)
+@cython.nonecheck(False)
+cdef int _histogramnd_int32_t_float_float(cnumpy.int32_t[:] sample,
+ float[:] weights,
+ int n_dims,
+ int n_elem,
+ double[:] histo_range,
+ int[:] n_bins,
+ cnumpy.uint32_t[:] histo,
+ float[:] cumul,
+ double[:] bin_edges,
+ int option_flags,
+ float weight_min,
+ float weight_max) nogil:
+
+ return histogramnd_c.histogramnd_int32_t_float_float(&sample[0],
+ &weights[0],
+ n_dims,
+ n_elem,
+ &histo_range[0],
+ &n_bins[0],
+ &histo[0],
+ &cumul[0],
+ &bin_edges[0],
+ option_flags,
+ weight_min,
+ weight_max)
+
+
+@cython.wraparound(False)
+@cython.boundscheck(False)
+@cython.initializedcheck(False)
+@cython.nonecheck(False)
+cdef int _histogramnd_int32_t_int32_t_float(cnumpy.int32_t[:] sample,
+ cnumpy.int32_t[:] weights,
+ int n_dims,
+ int n_elem,
+ double[:] histo_range,
+ int[:] n_bins,
+ cnumpy.uint32_t[:] histo,
+ float[:] cumul,
+ double[:] bin_edges,
+ int option_flags,
+ cnumpy.int32_t weight_min,
+ cnumpy.int32_t weight_max) nogil:
+
+ return histogramnd_c.histogramnd_int32_t_int32_t_float(&sample[0],
+ &weights[0],
+ n_dims,
+ n_elem,
+ &histo_range[0],
+ &n_bins[0],
+ &histo[0],
+ &cumul[0],
+ &bin_edges[0],
+ option_flags,
+ weight_min,
+ weight_max)
diff --git a/src/silx/math/chistogramnd_lut.pyx b/src/silx/math/chistogramnd_lut.pyx
new file mode 100644
index 0000000..3a3f05e
--- /dev/null
+++ b/src/silx/math/chistogramnd_lut.pyx
@@ -0,0 +1,435 @@
+# coding: utf-8
+# /*##########################################################################
+# Copyright (C) 2016-2018 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
+
+__authors__ = ["D. Naudet"]
+__license__ = "MIT"
+__date__ = "15/05/2016"
+
+
+cimport numpy as cnumpy # noqa
+cimport cython
+import numpy as np
+
+ctypedef fused sample_t:
+ cnumpy.float64_t
+ cnumpy.float32_t
+ cnumpy.int32_t
+ cnumpy.int64_t
+
+ctypedef fused cumul_t:
+ cnumpy.float64_t
+ cnumpy.float32_t
+ cnumpy.int32_t
+ cnumpy.int64_t
+
+ctypedef fused weights_t:
+ cnumpy.float64_t
+ cnumpy.float32_t
+ cnumpy.int32_t
+ cnumpy.int64_t
+
+ctypedef fused lut_t:
+ cnumpy.int64_t
+ cnumpy.int32_t
+ cnumpy.int16_t
+
+
+def histogramnd_get_lut(sample,
+ histo_range,
+ n_bins,
+ last_bin_closed=False):
+ """TBD
+
+ :param sample:
+ The data to be histogrammed.
+ Its shape must be either (N,) if it contains one dimensional
+ coordinates, or an (N, D) array where the rows are the
+ coordinates of points in a D dimensional space.
+ The following dtypes are supported : :class:`numpy.float64`,
+ :class:`numpy.float32`, :class:`numpy.int32`.
+ :type sample: :class:`numpy.array`
+
+ :param histo_range:
+ A (N, 2) array containing the histogram range along each dimension,
+ where N is the sample's number of dimensions.
+ :type histo_range: array_like
+
+ :param n_bins:
+ The number of bins :
+ * a scalar (same number of bins for all dimensions)
+ * a D elements array (number of bins for each dimensions)
+ :type n_bins: scalar or array_like
+
+ :param last_bin_closed:
+ By default the last bin is half
+ open (i.e.: [x,y) ; x included, y
+ excluded), like all the other bins.
+ Set this parameter to true if you want
+ the LAST bin to be closed.
+ :type last_bin_closed: *optional*, :class:`python.boolean`
+
+ :return: The indices for each sample and the histogram (bin counts).
+ :rtype: tuple : (:class:`numpy.array`, :class:`numpy.array`)
+ """
+
+ s_shape = sample.shape
+
+ n_dims = 1 if len(s_shape) == 1 else s_shape[1]
+
+ # just in case those arent numpy arrays
+ # (this allows the user to provide native python lists,
+ # => easier for testing)
+ i_histo_range = histo_range
+ histo_range = np.array(histo_range)
+ err_histo_range = False
+
+ if n_dims == 1:
+ if histo_range.shape == (2,):
+ pass
+ elif histo_range.shape == (1, 2):
+ histo_range.reshape(-1)
+ else:
+ err_histo_range = True
+ elif n_dims != 1 and histo_range.shape != (n_dims, 2):
+ err_histo_range = True
+
+ if err_histo_range:
+ raise ValueError('<histo_range> error : expected {n_dims} sets of '
+ 'lower and upper bin edges, '
+ 'got the following instead : {histo_range}. '
+ '(provided <sample> contains '
+ '{n_dims}D values)'
+ ''.format(histo_range=i_histo_range,
+ n_dims=n_dims))
+
+ histo_range = np.double(histo_range)
+
+ # checking n_bins size
+ n_bins = np.array(n_bins, ndmin=1)
+ if len(n_bins) == 1:
+ n_bins = np.tile(n_bins, n_dims)
+ elif n_bins.shape != (n_dims,):
+ raise ValueError('n_bins must be either a scalar (same number '
+ 'of bins for all dimensions) or '
+ 'an array (number of bins for each '
+ 'dimension).')
+
+ # checking if None is in n_bins, otherwise a rather cryptic
+ # exception is thrown when calling np.zeros
+ # also testing for negative/null values
+ if np.any(np.equal(n_bins, None)) or np.any(n_bins <= 0):
+ raise ValueError('<n_bins> : only positive values allowed.')
+
+ sample_type = sample.dtype
+
+ n_elem = sample.size // n_dims
+
+ if n_bins.prod(dtype=np.uint64) < 2**15:
+ lut_dtype = np.int16
+ elif n_bins.prod(dtype=np.uint64) < 2**31:
+ lut_dtype = np.int32
+
+ else:
+ lut_dtype = np.int64
+
+ # allocating the output arrays
+ lut = np.zeros(n_elem, dtype=lut_dtype)
+ histo = np.zeros(n_bins, dtype=np.uint32)
+
+ dtype = sample.dtype.newbyteorder("N")
+ sample_c = np.ascontiguousarray(sample.reshape((sample.size,)),
+ dtype=dtype)
+
+ histo_range_c = np.ascontiguousarray(histo_range.reshape((histo_range.size,)),
+ dtype=histo_range.dtype.newbyteorder("N"))
+
+ n_bins_c = np.ascontiguousarray(n_bins.reshape((n_bins.size,)),
+ dtype=np.int32)
+
+ lut_c = np.ascontiguousarray(lut.reshape((lut.size,)))
+ histo_c = np.ascontiguousarray(histo.reshape((histo.size,)),
+ dtype=histo.dtype.newbyteorder('N'))
+
+ rc = 0
+
+ try:
+ rc = _histogramnd_get_lut_fused(sample_c,
+ n_dims,
+ n_elem,
+ histo_range_c,
+ n_bins_c,
+ lut_c,
+ histo_c,
+ last_bin_closed)
+ except TypeError as ex:
+ raise TypeError('Type not supported - sample : {0}'
+ ''.format(sample_type))
+
+ if rc != 0:
+ raise Exception('histogramnd returned an error : {0}'
+ ''.format(rc))
+
+ edges = []
+ histo_range = histo_range.reshape(-1)
+ for i_dim in range(n_dims):
+ dim_edges = np.zeros(n_bins[i_dim] + 1)
+ rng_min = histo_range[2 * i_dim]
+ rng_max = histo_range[2 * i_dim + 1]
+ dim_edges[:-1] = (rng_min + np.arange(n_bins[i_dim]) *
+ ((rng_max - rng_min) / n_bins[i_dim]))
+ dim_edges[-1] = rng_max
+ edges.append(dim_edges)
+
+ return lut, histo, tuple(edges)
+
+
+# =====================
+# =====================
+
+
+def histogramnd_from_lut(weights,
+ histo_lut,
+ histo=None,
+ weighted_histo=None,
+ shape=None,
+ dtype=None,
+ weight_min=None,
+ weight_max=None):
+ """
+ dtype ignored if weighted_histo provided
+ """
+
+ if histo is None and weighted_histo is None:
+ if shape is None:
+ raise ValueError('At least one of the following parameters has to '
+ 'be provided : <shape> or <histo> or '
+ '<weighted_histo>')
+
+ if shape is not None:
+ if histo is not None and list(histo.shape) != list(shape):
+ raise ValueError('The <shape> value does not match'
+ 'the <histo> shape.')
+
+ if(weighted_histo is not None and
+ list(weighted_histo.shape) != list(shape)):
+ raise ValueError('The <shape> value does not match'
+ 'the <weighted_histo> shape.')
+ else:
+ if histo is not None:
+ shape = histo.shape
+ else:
+ shape = weighted_histo.shape
+
+ if histo is not None:
+ if histo.dtype != np.uint32:
+ raise ValueError('Provided <histo> array doesn\'t have '
+ 'the expected type '
+ ': should be {0} instead of {1}.'
+ ''.format(np.uint32, histo.dtype))
+
+ if weighted_histo is not None:
+ if histo.shape != weighted_histo.shape:
+ raise ValueError('The <histo> shape does not match'
+ 'the <weighted_histo> shape.')
+ else:
+ histo = np.zeros(shape, dtype=np.uint32)
+
+ w_dtype = weights.dtype
+
+ if dtype is None:
+ if weighted_histo is None:
+ dtype = w_dtype
+ else:
+ dtype = weighted_histo.dtype
+ elif weighted_histo is not None:
+ if weighted_histo.dtype != dtype:
+ raise ValueError('Provided <dtype> and <weighted_histo>\'s dtype'
+ ' do not match.')
+ dtype = weighted_histo.dtype
+
+ if weighted_histo is None:
+ weighted_histo = np.zeros(shape, dtype=dtype)
+
+ if histo_lut.size != weights.size:
+ raise ValueError('The LUT and weights arrays must have the same '
+ 'number of elements.')
+
+ w_c = np.ascontiguousarray(weights.reshape((weights.size,)),
+ dtype=weights.dtype.newbyteorder('N'))
+
+ h_c = np.ascontiguousarray(histo.reshape((histo.size,)),
+ dtype=histo.dtype.newbyteorder('N'))
+
+ w_h_c = np.ascontiguousarray(weighted_histo.reshape((weighted_histo.size,)),
+ dtype=weighted_histo.dtype.newbyteorder('N')) # noqa
+
+ h_lut_c = np.ascontiguousarray(histo_lut.reshape((histo_lut.size,)),
+ histo_lut.dtype.newbyteorder('N'))
+
+ rc = 0
+
+ if weight_min is None:
+ weight_min = 0
+ filt_min_weights = False
+ else:
+ filt_min_weights = True
+
+ if weight_max is None:
+ weight_max = 0
+ filt_max_weights = False
+ else:
+ filt_max_weights = True
+
+ try:
+ _histogramnd_from_lut_fused(w_c,
+ h_lut_c,
+ h_c,
+ w_h_c,
+ weights.size,
+ filt_min_weights,
+ w_dtype.type(weight_min),
+ filt_max_weights,
+ w_dtype.type(weight_max))
+ except TypeError as ex:
+ print(ex)
+ raise TypeError('Case not supported - weights:{0} '
+ 'and histo:{1}.'
+ ''.format(weights.dtype, histo.dtype))
+
+ return histo, weighted_histo
+
+
+# =====================
+# =====================
+
+
+@cython.wraparound(False)
+@cython.boundscheck(False)
+@cython.initializedcheck(False)
+@cython.nonecheck(False)
+@cython.cdivision(True)
+def _histogramnd_from_lut_fused(weights_t[:] i_weights,
+ lut_t[:] i_lut,
+ cnumpy.uint32_t[:] o_histo,
+ cumul_t[:] o_weighted_histo,
+ int i_n_elems,
+ bint i_filt_min_weights,
+ weights_t i_weight_min,
+ bint i_filt_max_weights,
+ weights_t i_weight_max):
+ with nogil:
+ for i in range(i_n_elems):
+ if (i_lut[i] >= 0):
+ if i_filt_min_weights and i_weights[i] < i_weight_min:
+ continue
+ if i_filt_max_weights and i_weights[i] > i_weight_max:
+ continue
+ o_histo[i_lut[i]] += 1
+ o_weighted_histo[i_lut[i]] += <cumul_t>i_weights[i] # noqa
+
+
+# =====================
+# =====================
+
+
+@cython.wraparound(False)
+@cython.boundscheck(False)
+@cython.initializedcheck(False)
+@cython.nonecheck(False)
+@cython.cdivision(True)
+def _histogramnd_get_lut_fused(sample_t[:] i_sample,
+ int i_n_dims,
+ int i_n_elems,
+ double[:] i_histo_range,
+ int[:] i_n_bins,
+ lut_t[:] o_lut,
+ cnumpy.uint32_t[:] o_histo,
+ bint last_bin_closed):
+
+ cdef:
+ int i = 0
+ long elem_idx = 0
+ long max_idx = 0
+ long lut_idx = -1
+
+ # computed bin index (i_sample -> grid)
+ long bin_idx = 0
+
+ sample_t elem_coord = 0
+
+ double[50] g_min
+ double[50] g_max
+ double[50] bins_range
+
+ for i in range(i_n_dims):
+ g_min[i] = i_histo_range[2*i]
+ g_max[i] = i_histo_range[2*i+1]
+ bins_range[i] = g_max[i] - g_min[i]
+
+ elem_idx = 0 - i_n_dims
+ max_idx = i_n_elems * i_n_dims - i_n_dims
+
+ with nogil:
+ while elem_idx < max_idx:
+ elem_idx += i_n_dims
+ lut_idx += 1
+
+ bin_idx = 0
+
+ for i in range(i_n_dims):
+ elem_coord = i_sample[elem_idx+i]
+ # =====================
+ # Element is rejected if any of the following is NOT true :
+ # 1. coordinate is >= than the minimum value
+ # 2. coordinate is <= than the maximum value
+ # 3. coordinate==maximum value and last_bin_closed is True
+ # =====================
+ if elem_coord < g_min[i]:
+ bin_idx = -1
+ break
+
+ # Here we make the assumption that most of the time
+ # there will be more coordinates inside the grid interval
+ # (one test)
+ # than coordinates higher or equal to the max
+ # (two tests)
+ if elem_coord < g_max[i]:
+ bin_idx = <long>(bin_idx * i_n_bins[i] + # noqa
+ (((elem_coord - g_min[i]) * i_n_bins[i]) /
+ bins_range[i]))
+ else:
+ # if equal and the last bin is closed :
+ # put it in the last bin
+ # else : discard
+ if last_bin_closed and elem_coord == g_max[i]:
+ bin_idx = (bin_idx + 1) * i_n_bins[i] - 1
+ else:
+ bin_idx = -1
+ break
+
+ o_lut[lut_idx] = bin_idx
+ if bin_idx >= 0:
+ o_histo[bin_idx] += 1
+
+ return 0
diff --git a/src/silx/math/colormap.py b/src/silx/math/colormap.py
new file mode 100644
index 0000000..43b8949
--- /dev/null
+++ b/src/silx/math/colormap.py
@@ -0,0 +1,450 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2018-2021 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
+"""This module provides helper functions for applying colormaps to datasets"""
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "25/08/2021"
+
+
+import collections
+import warnings
+import numpy
+
+from ..resources import resource_filename as _resource_filename
+from .combo import min_max as _min_max
+from . import _colormap
+from ._colormap import cmap # noqa
+
+
+__all__ = ["apply_colormap", "cmap"]
+
+
+_LUT_DESCRIPTION = collections.namedtuple("_LUT_DESCRIPTION", ["source", "cursor_color"])
+"""Description of a LUT for internal purpose."""
+
+
+_AVAILABLE_LUTS = collections.OrderedDict([
+ ('gray', _LUT_DESCRIPTION('builtin', '#ff66ff')),
+ ('reversed gray', _LUT_DESCRIPTION('builtin', '#ff66ff')),
+ ('red', _LUT_DESCRIPTION('builtin', '#00ff00')),
+ ('green', _LUT_DESCRIPTION('builtin', '#ff66ff')),
+ ('blue', _LUT_DESCRIPTION('builtin', '#ffff00')),
+ ('viridis', _LUT_DESCRIPTION('resource', '#ff66ff')),
+ ('cividis', _LUT_DESCRIPTION('resource', '#ff66ff')),
+ ('magma', _LUT_DESCRIPTION('resource', '#00ff00')),
+ ('inferno', _LUT_DESCRIPTION('resource', '#00ff00')),
+ ('plasma', _LUT_DESCRIPTION('resource', '#00ff00')),
+ ('temperature', _LUT_DESCRIPTION('builtin', '#ff66ff')),
+])
+"""Description for internal porpose of all the default LUT provided by the library."""
+
+
+# Colormap loader
+
+_COLORMAP_CACHE = {}
+"""Cache already used colormaps as name: color LUT"""
+
+
+def array_to_rgba8888(colors):
+ """Convert colors from a numpy array using float (0..1) int or uint
+ (0..255) to uint8 RGBA.
+
+ :param numpy.ndarray colors: Array of float int or uint colors to convert
+ :return: colors as uint8
+ :rtype: numpy.ndarray
+ """
+ assert len(colors.shape) == 2
+ assert colors.shape[1] in (3, 4)
+
+ if colors.dtype == numpy.uint8:
+ pass
+ elif colors.dtype.kind == 'f':
+ # Each bin is [N, N+1[ except the last one: [255, 256]
+ colors = numpy.clip(colors.astype(numpy.float64) * 256, 0., 255.)
+ colors = colors.astype(numpy.uint8)
+ elif colors.dtype.kind in 'iu':
+ colors = numpy.clip(colors, 0, 255)
+ colors = colors.astype(numpy.uint8)
+
+ if colors.shape[1] == 3:
+ tmp = numpy.empty((len(colors), 4), dtype=numpy.uint8)
+ tmp[:, 0:3] = colors
+ tmp[:, 3] = 255
+ colors = tmp
+
+ return colors
+
+
+def _create_colormap_lut(name):
+ """Returns the color LUT corresponding to a colormap name
+
+ :param str name: Name of the colormap to load
+ :returns: Corresponding table of colors
+ :rtype: numpy.ndarray
+ :raise ValueError: If no colormap corresponds to name
+ """
+ description = _AVAILABLE_LUTS.get(name)
+ if description is not None:
+ if description.source == "builtin":
+ # Build colormap LUT
+ lut = numpy.zeros((256, 4), dtype=numpy.uint8)
+ lut[:, 3] = 255
+
+ if name == 'gray':
+ lut[:, :3] = numpy.arange(256, dtype=numpy.uint8).reshape(-1, 1)
+ elif name == 'reversed gray':
+ lut[:, :3] = numpy.arange(255, -1, -1, dtype=numpy.uint8).reshape(-1, 1)
+ elif name == 'red':
+ lut[:, 0] = numpy.arange(256, dtype=numpy.uint8)
+ elif name == 'green':
+ lut[:, 1] = numpy.arange(256, dtype=numpy.uint8)
+ elif name == 'blue':
+ lut[:, 2] = numpy.arange(256, dtype=numpy.uint8)
+ elif name == 'temperature':
+ # Red
+ lut[128:192, 0] = numpy.arange(2, 255, 4, dtype=numpy.uint8)
+ lut[192:, 0] = 255
+ # Green
+ lut[:64, 1] = numpy.arange(0, 255, 4, dtype=numpy.uint8)
+ lut[64:192, 1] = 255
+ lut[192:, 1] = numpy.arange(252, -1, -4, dtype=numpy.uint8)
+ # Blue
+ lut[:64, 2] = 255
+ lut[64:128, 2] = numpy.arange(254, 0, -4, dtype=numpy.uint8)
+ else:
+ raise RuntimeError("Built-in colormap not implemented")
+ return lut
+
+ elif description.source == "resource":
+ # Load colormap LUT
+ colors = numpy.load(_resource_filename("gui/colormaps/%s.npy" % name))
+ # Convert to uint8 and add alpha channel
+ lut = array_to_rgba8888(colors)
+ return lut
+
+ else:
+ raise RuntimeError("Internal LUT source '%s' unsupported" % description.source)
+
+ raise ValueError("Unknown colormap '%s'" % name)
+
+
+def register_colormap(name, lut, cursor_color='#000000'):
+ """Register a custom colormap LUT
+
+ It can override existing LUT names.
+
+ :param str name: Name of the LUT as defined to configure colormaps
+ :param numpy.ndarray lut: The custom LUT to register.
+ Nx3 or Nx4 numpy array of RGB(A) colors,
+ either uint8 or float in [0, 1].
+ :param str cursor_color: Color used to display overlay over images using
+ colormap with this LUT.
+ """
+ description = _LUT_DESCRIPTION('user', cursor_color)
+ colors = array_to_rgba8888(lut)
+ _AVAILABLE_LUTS[name] = description
+
+ # Register the cache as the LUT was already loaded
+ _COLORMAP_CACHE[name] = colors
+
+
+def get_registered_colormaps():
+ """Returns currently registered colormap names"""
+ return tuple(_AVAILABLE_LUTS.keys())
+
+
+def get_colormap_cursor_color(name):
+ """Get a color suitable for overlay over a colormap.
+
+ :param str name: The name of the colormap.
+ :return: Name of the color.
+ :rtype: str
+ """
+ description = _AVAILABLE_LUTS.get(name, None)
+ if description is not None:
+ color = description.cursor_color
+ if color is not None:
+ return color
+ return 'black'
+
+
+def get_colormap_lut(name):
+ """Returns the color LUT corresponding to a colormap name
+
+ :param str name: Name of the colormap to load
+ :returns: Corresponding table of colors
+ :rtype: numpy.ndarray
+ :raise ValueError: If no colormap corresponds to name
+ """
+ name = str(name)
+ if name not in _COLORMAP_CACHE:
+ lut = _create_colormap_lut(name)
+ _COLORMAP_CACHE[name] = lut
+ return _COLORMAP_CACHE[name]
+
+
+# Normalizations
+
+class _NormalizationMixIn:
+ """Colormap normalization mix-in class"""
+
+ DEFAULT_RANGE = 0, 1
+ """Fallback for (vmin, vmax)"""
+
+ def is_valid(self, value):
+ """Check if a value is in the valid range for this normalization.
+
+ Override in subclass.
+
+ :param Union[float,numpy.ndarray] value:
+ :rtype: Union[bool,numpy.ndarray]
+ """
+ if isinstance(value, collections.abc.Iterable):
+ return numpy.ones_like(value, dtype=numpy.bool_)
+ else:
+ return True
+
+ def autoscale(self, data, mode):
+ """Returns range for given data and autoscale mode.
+
+ :param Union[None,numpy.ndarray] data:
+ :param str mode: Autoscale mode: 'minmax' or 'stddev3'
+ :returns: Range as (min, max)
+ :rtype: Tuple[float,float]
+ """
+ data = None if data is None else numpy.array(data, copy=False)
+ if data is None or data.size == 0:
+ return self.DEFAULT_RANGE
+
+ if mode == "minmax":
+ vmin, vmax = self.autoscale_minmax(data)
+ elif mode == "stddev3":
+ dmin, dmax = self.autoscale_minmax(data)
+ stdmin, stdmax = self.autoscale_mean3std(data)
+ if dmin is None:
+ vmin = stdmin
+ elif stdmin is None:
+ vmin = dmin
+ else:
+ vmin = max(dmin, stdmin)
+
+ if dmax is None:
+ vmax = stdmax
+ elif stdmax is None:
+ vmax = dmax
+ else:
+ vmax = min(dmax, stdmax)
+
+ else:
+ raise ValueError('Unsupported mode: %s' % mode)
+
+ # Check returned range and handle fallbacks
+ if vmin is None or not numpy.isfinite(vmin):
+ vmin = self.DEFAULT_RANGE[0]
+ if vmax is None or not numpy.isfinite(vmax):
+ vmax = self.DEFAULT_RANGE[1]
+ if vmax < vmin:
+ vmax = vmin
+ return float(vmin), float(vmax)
+
+ def autoscale_minmax(self, data):
+ """Autoscale using min/max
+
+ :param numpy.ndarray data:
+ :returns: (vmin, vmax)
+ :rtype: Tuple[float,float]
+ """
+ data = data[self.is_valid(data)]
+ if data.size == 0:
+ return None, None
+ result = _min_max(data, min_positive=False, finite=True)
+ return result.minimum, result.maximum
+
+ def autoscale_mean3std(self, data):
+ """Autoscale using mean+/-3std
+
+ This implementation only works for normalization that do NOT
+ use the data range.
+ Override this method for normalization using the range.
+
+ :param numpy.ndarray data:
+ :returns: (vmin, vmax)
+ :rtype: Tuple[float,float]
+ """
+ # Use [0, 1] as data range for normalization not using range
+ normdata = self.apply(data, 0., 1.)
+ if normdata.dtype.kind == 'f': # Replaces inf by NaN
+ normdata[numpy.isfinite(normdata) == False] = numpy.nan
+ if normdata.size == 0: # Fallback
+ return None, None
+
+ with warnings.catch_warnings():
+ warnings.simplefilter('ignore', category=RuntimeWarning)
+ # Ignore nanmean "Mean of empty slice" warning and
+ # nanstd "Degrees of freedom <= 0 for slice" warning
+ mean, std = numpy.nanmean(normdata), numpy.nanstd(normdata)
+
+ return self.revert(mean - 3 * std, 0., 1.), self.revert(mean + 3 * std, 0., 1.)
+
+
+class _LinearNormalizationMixIn(_NormalizationMixIn):
+ """Colormap normalization mix-in class specific to autoscale taken from initial range"""
+
+ def autoscale_mean3std(self, data):
+ """Autoscale using mean+/-3std
+
+ Do the autoscale on the data itself, not the normalized data.
+
+ :param numpy.ndarray data:
+ :returns: (vmin, vmax)
+ :rtype: Tuple[float,float]
+ """
+ if data.dtype.kind == 'f': # Replaces inf by NaN
+ data = numpy.array(data, copy=True) # Work on a copy
+ data[numpy.isfinite(data) == False] = numpy.nan
+ if data.size == 0: # Fallback
+ return None, None
+ with warnings.catch_warnings():
+ warnings.simplefilter('ignore', category=RuntimeWarning)
+ # Ignore nanmean "Mean of empty slice" warning and
+ # nanstd "Degrees of freedom <= 0 for slice" warning
+ mean, std = numpy.nanmean(data), numpy.nanstd(data)
+ return mean - 3 * std, mean + 3 * std
+
+
+class LinearNormalization(_colormap.LinearNormalization, _LinearNormalizationMixIn):
+ """Linear normalization"""
+ def __init__(self):
+ _colormap.LinearNormalization.__init__(self)
+ _LinearNormalizationMixIn.__init__(self)
+
+
+class LogarithmicNormalization(_colormap.LogarithmicNormalization, _NormalizationMixIn):
+ """Logarithm normalization"""
+
+ DEFAULT_RANGE = 1, 10
+
+ def __init__(self):
+ _colormap.LogarithmicNormalization.__init__(self)
+ _NormalizationMixIn.__init__(self)
+
+ def is_valid(self, value):
+ return value > 0.
+
+ def autoscale_minmax(self, data):
+ result = _min_max(data, min_positive=True, finite=True)
+ return result.min_positive, result.maximum
+
+
+class SqrtNormalization(_colormap.SqrtNormalization, _NormalizationMixIn):
+ """Square root normalization"""
+
+ DEFAULT_RANGE = 0, 1
+
+ def __init__(self):
+ _colormap.SqrtNormalization.__init__(self)
+ _NormalizationMixIn.__init__(self)
+
+ def is_valid(self, value):
+ return value >= 0.
+
+
+class GammaNormalization(_colormap.PowerNormalization, _LinearNormalizationMixIn):
+ """Gamma correction normalization:
+
+ Linear normalization to [0, 1] followed by power normalization.
+
+ :param gamma: Gamma correction factor
+ """
+ def __init__(self, gamma):
+ _colormap.PowerNormalization.__init__(self, gamma)
+ _LinearNormalizationMixIn.__init__(self)
+
+
+# Backward compatibility
+PowerNormalization = GammaNormalization
+
+
+class ArcsinhNormalization(_colormap.ArcsinhNormalization, _NormalizationMixIn):
+ """Inverse hyperbolic sine normalization"""
+
+ def __init__(self):
+ _colormap.ArcsinhNormalization.__init__(self)
+ _NormalizationMixIn.__init__(self)
+
+
+# Colormap function
+
+_BASIC_NORMALIZATIONS = {
+ "linear": LinearNormalization(),
+ "log": LogarithmicNormalization(),
+ "sqrt": SqrtNormalization(),
+ "arcsinh": ArcsinhNormalization(),
+}
+
+_DEFAULT_NAN_COLOR = 255, 255, 255, 0
+
+def apply_colormap(data,
+ colormap: str,
+ norm: str="linear",
+ autoscale: str="minmax",
+ vmin=None,
+ vmax=None,
+ gamma=1.0):
+ """Apply colormap to data with given normalization and autoscale.
+
+ :param numpy.ndarray data: Data on which to apply the colormap
+ :param str colormap: Name of the colormap to use
+ :param str norm: Normalization to use
+ :param str autoscale: Autoscale mode: "minmax" (default) or "stddev3"
+ :param vmin: Lower bound, None (default) to autoscale
+ :param vmax: Upper bound, None (default) to autoscale
+ :param float gamma:
+ Gamma correction parameter (used only for "gamma" normalization)
+ :returns: Array of colors
+ """
+ colors = get_colormap_lut(colormap)
+
+ if norm == "gamma":
+ normalizer = GammaNormalization(gamma)
+ else:
+ normalizer = _BASIC_NORMALIZATIONS[norm]
+
+ if vmin is None or vmax is None:
+ auto_vmin, auto_vmax = normalizer.autoscale(data, autoscale)
+ if vmin is None: # Set vmin respecting provided vmax
+ vmin = auto_vmin if vmax is None else min(auto_vmin, vmax)
+ if vmax is None:
+ vmax = max(auto_vmax, vmin) # Handle max_ <= 0 for log scale
+
+ return _colormap.cmap(
+ data,
+ colors,
+ vmin,
+ vmax,
+ normalizer,
+ _DEFAULT_NAN_COLOR,
+ )
diff --git a/src/silx/math/combo.pyx b/src/silx/math/combo.pyx
new file mode 100644
index 0000000..e24edda
--- /dev/null
+++ b/src/silx/math/combo.pyx
@@ -0,0 +1,329 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+"""This module provides combination of statistics as single operation.
+
+For now it provides min/max (and optionally positive min) and indices
+of first occurrences (i.e., argmin/argmax) in a single pass.
+"""
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "24/04/2018"
+
+cimport cython
+from .math_compatibility cimport isnan, isfinite, INFINITY
+
+
+import numpy
+
+
+# All supported types
+ctypedef fused _number:
+ float
+ double
+ long double
+ signed char
+ signed short
+ signed int
+ signed long
+ signed long long
+ unsigned char
+ unsigned short
+ unsigned int
+ unsigned long
+ unsigned long long
+
+# All supported floating types:
+# cython.floating + long double
+ctypedef fused _floating:
+ float
+ double
+ long double
+
+
+class _MinMaxResult(object):
+ """Object storing result from :func:`min_max`"""
+
+ def __init__(self, minimum, min_pos, maximum,
+ argmin, argmin_pos, argmax):
+ self._minimum = minimum
+ self._min_positive = min_pos
+ self._maximum = maximum
+
+ self._argmin = argmin
+ self._argmin_positive = argmin_pos
+ self._argmax = argmax
+
+ minimum = property(
+ lambda self: self._minimum,
+ doc="Minimum value of the array")
+ maximum = property(
+ lambda self: self._maximum,
+ doc="Maximum value of the array")
+
+ argmin = property(
+ lambda self: self._argmin,
+ doc="Index of the first occurrence of the minimum value")
+ argmax = property(
+ lambda self: self._argmax,
+ doc="Index of the first occurrence of the maximum value")
+
+ min_positive = property(
+ lambda self: self._min_positive,
+ doc="""Strictly positive minimum value
+
+ It is None if no value is strictly positive.
+ """)
+ argmin_positive = property(
+ lambda self: self._argmin_positive,
+ doc="""Index of the strictly positive minimum value.
+
+ It is None if no value is strictly positive.
+ It is the index of the first occurrence.""")
+
+ def __getitem__(self, key):
+ if key == 0:
+ return self.minimum
+ elif key == 1:
+ return self.maximum
+ else:
+ raise IndexError("Index out of range")
+
+
+@cython.initializedcheck(False)
+@cython.boundscheck(False)
+@cython.wraparound(False)
+def _min_max(_number[::1] data, bint min_positive=False):
+ """:func:`min_max` implementation including infinite values
+
+ See :func:`min_max` for documentation.
+ """
+ cdef:
+ _number value, minimum, min_pos, maximum
+ unsigned int length
+ unsigned int index = 0
+ unsigned int min_index = 0
+ unsigned int min_pos_index = 0
+ unsigned int max_index = 0
+
+ length = len(data)
+
+ if length == 0:
+ raise ValueError('Zero-size array')
+
+ with nogil:
+ # Init starting values
+ value = data[0]
+ minimum = value
+ maximum = value
+ if min_positive and value > 0:
+ min_pos = value
+ else:
+ min_pos = 0
+
+ if _number in _floating:
+ # For floating, loop until first not NaN value
+ for index in range(length):
+ value = data[index]
+ if not isnan(value):
+ minimum = value
+ min_index = index
+ maximum = value
+ max_index = index
+ break
+
+ if not min_positive:
+ for index in range(index, length):
+ value = data[index]
+ if value > maximum:
+ maximum = value
+ max_index = index
+ elif value < minimum:
+ minimum = value
+ min_index = index
+
+ else:
+ # Loop until min_pos is defined
+ for index in range(index, length):
+ value = data[index]
+ if value > maximum:
+ maximum = value
+ max_index = index
+ elif value < minimum:
+ minimum = value
+ min_index = index
+
+ if value > 0:
+ min_pos = value
+ min_pos_index = index
+ break
+
+ # Loop until the end
+ for index in range(index + 1, length):
+ value = data[index]
+ if value > maximum:
+ maximum = value
+ max_index = index
+ else:
+ if value < minimum:
+ minimum = value
+ min_index = index
+
+ if 0 < value < min_pos:
+ min_pos = value
+ min_pos_index = index
+
+ return _MinMaxResult(minimum,
+ min_pos if min_pos > 0 else None,
+ maximum,
+ min_index,
+ min_pos_index if min_pos > 0 else None,
+ max_index)
+
+
+@cython.initializedcheck(False)
+@cython.boundscheck(False)
+@cython.wraparound(False)
+def _finite_min_max(_floating[::1] data, bint min_positive=False):
+ """:func:`min_max` implementation for floats skipping infinite values
+
+ See :func:`min_max` for documentation.
+ """
+ cdef:
+ _floating value, minimum, min_pos, maximum
+ unsigned int length
+ unsigned int index = 0
+ unsigned int min_index = 0
+ unsigned int min_pos_index = 0
+ unsigned int max_index = 0
+
+ length = len(data)
+
+ if length == 0:
+ raise ValueError('Zero-size array')
+
+ with nogil:
+ minimum = INFINITY
+ maximum = -INFINITY
+ min_pos = INFINITY
+
+ if not min_positive:
+ for index in range(length):
+ value = data[index]
+ if isfinite(value):
+ if value > maximum:
+ maximum = value
+ max_index = index
+ if value < minimum:
+ minimum = value
+ min_index = index
+
+ else:
+ for index in range(index, length):
+ value = data[index]
+ if isfinite(value):
+ if value > maximum:
+ maximum = value
+ max_index = index
+ if value < minimum:
+ minimum = value
+ min_index = index
+
+ if 0. < value < min_pos:
+ min_pos = value
+ min_pos_index = index
+
+ return _MinMaxResult(minimum if isfinite(minimum) else None,
+ min_pos if isfinite(min_pos) else None,
+ maximum if isfinite(maximum) else None,
+ min_index if isfinite(minimum) else None,
+ min_pos_index if isfinite(min_pos) else None,
+ max_index if isfinite(maximum) else None)
+
+
+def min_max(data not None, bint min_positive=False, bint finite=False):
+ """Returns min, max and optionally strictly positive min of data.
+
+ It also computes the indices of first occurrence of min/max.
+
+ NaNs are ignored while computing min/max unless all data is NaNs,
+ in which case returned min/max are NaNs.
+
+ The result data type is that of the input data, except for the following cases.
+ For input using non-native bytes order, the result is returned as native
+ floating-point or integers. For input using 16-bits floating-point,
+ the result is returned as 32-bits floating-point.
+
+ Examples:
+
+ >>> import numpy
+ >>> data = numpy.arange(10)
+
+ Usage as a function returning min and max:
+
+ >>> min_, max_ = min_max(data)
+
+ Usage as a function returning a result object to access all information:
+
+ >>> result = min_max(data) # Do not get positive min
+ >>> result.minimum, result.argmin
+ 0, 0
+ >>> result.maximum, result.argmax
+ 9, 10
+ >>> result.min_positive, result.argmin_positive # Not computed
+ None, None
+
+ Getting strictly positive min information:
+
+ >>> result = min_max(data, min_positive=True)
+ >>> result.min_positive, result.argmin_positive # Computed
+ 1, 1
+
+ If *finite* is True, min/max information is computed only from finite data.
+ Then, all result fields (include minimum and maximum) can be None
+ when all data is infinity or NaN.
+
+ :param data: Array-like dataset
+ :param bool min_positive: True to compute the positive min and argmin
+ Default: False.
+ :param bool finite: True to compute min/max from finite data only
+ Default: False.
+ :returns: An object with minimum, maximum and min_positive attributes
+ and the indices of first occurrence in the flattened data:
+ argmin, argmax and argmin_positive attributes.
+ If all data is <= 0 or min_positive argument is False, then
+ min_positive and argmin_positive are None.
+ :raises: ValueError if data is empty
+ """
+ data = numpy.array(data, copy=False)
+ native_endian_dtype = data.dtype.newbyteorder('N')
+ if native_endian_dtype.kind == 'f' and native_endian_dtype.itemsize == 2:
+ # Use native float32 instead of float16
+ native_endian_dtype = "=f4"
+ data = numpy.ascontiguousarray(data, dtype=native_endian_dtype).ravel()
+ if finite and data.dtype.kind == 'f':
+ return _finite_min_max(data, min_positive)
+ else:
+ return _min_max(data, min_positive)
diff --git a/src/silx/math/fft/__init__.py b/src/silx/math/fft/__init__.py
new file mode 100644
index 0000000..ea12cd6
--- /dev/null
+++ b/src/silx/math/fft/__init__.py
@@ -0,0 +1,8 @@
+#!/usr/bin/env python
+# coding: utf-8
+
+__authors__ = ["P. Paleo"]
+__license__ = "MIT"
+__date__ = "12/12/2018"
+
+from .fft import FFT
diff --git a/src/silx/math/fft/basefft.py b/src/silx/math/fft/basefft.py
new file mode 100644
index 0000000..854ca37
--- /dev/null
+++ b/src/silx/math/fft/basefft.py
@@ -0,0 +1,146 @@
+#!/usr/bin/env python
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2018 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+import numpy as np
+from pkg_resources import parse_version
+
+
+def check_version(package, required_version):
+ """
+ Check whether a given package version is superior or equal to required_version.
+ """
+ try:
+ ver = getattr(package, "__version__")
+ except AttributeError:
+ try:
+ ver = getattr(package, "version")
+ except Exception:
+ return False
+ req_v = parse_version(required_version)
+ ver_v = parse_version(ver)
+ return ver_v >= req_v
+
+
+class BaseFFT(object):
+ """
+ Base class for all FFT backends.
+ """
+ def __init__(self, **kwargs):
+ self.__get_args(**kwargs)
+
+ if self.shape is None and self.dtype is None and self.template is None:
+ raise ValueError("Please provide either (shape and dtype) or template")
+ if self.template is not None:
+ self.shape = self.template.shape
+ self.dtype = self.template.dtype
+ self.user_data = self.template
+ self.data_allocated = False
+ self.__calc_axes()
+ self.__set_dtypes()
+ self.__calc_shape()
+
+ def __get_args(self, **kwargs):
+ expected_args = {
+ "shape": None,
+ "dtype": None,
+ "template": None,
+ "shape_out": None,
+ "axes": None,
+ "normalize": "rescale",
+ }
+ for arg_name, default_val in expected_args.items():
+ if arg_name not in kwargs:
+ # Base class was not instantiated properly
+ raise ValueError("Please provide argument %s" % arg_name)
+ setattr(self, arg_name, default_val)
+ for arg_name, arg_val in kwargs.items():
+ setattr(self, arg_name, arg_val)
+
+ def __set_dtypes(self):
+ dtypes_mapping = {
+ np.dtype("float32"): np.complex64,
+ np.dtype("float64"): np.complex128,
+ np.dtype("complex64"): np.complex64,
+ np.dtype("complex128"): np.complex128
+ }
+ dp = {
+ np.dtype("float32"): np.float64,
+ np.dtype("complex64"): np.complex128
+ }
+ self.dtype_in = np.dtype(self.dtype)
+ if self.dtype_in not in dtypes_mapping:
+ raise ValueError("Invalid input data type: got %s" %
+ self.dtype_in
+ )
+ self.dtype_out = dtypes_mapping[self.dtype_in]
+
+ def __calc_shape(self):
+ # TODO allow for C2C even for real input data (?)
+ if self.dtype_in in [np.float32, np.float64]:
+ last_dim = self.shape[-1]//2 + 1
+ # FFTW convention
+ self.shape_out = self.shape[:-1] + (self.shape[-1]//2 + 1,)
+ else:
+ self.shape_out = self.shape
+
+ def __calc_axes(self):
+ default_axes = tuple(range(len(self.shape)))
+ if self.axes is None:
+ self.axes = default_axes
+ self.user_axes = None
+ else:
+ self.user_axes = self.axes
+ # Handle possibly negative axes
+ self.axes = tuple(np.array(default_axes)[np.array(self.user_axes)])
+
+ def _allocate(self, shape, dtype):
+ raise ValueError("This should be implemented by back-end FFT")
+
+ def set_data(self, dst, src, shape, dtype, copy=True):
+ raise ValueError("This should be implemented by back-end FFT")
+
+ def allocate_arrays(self):
+ if not(self.data_allocated):
+ self.data_in = self._allocate(self.shape, self.dtype_in)
+ self.data_out = self._allocate(self.shape_out, self.dtype_out)
+ self.data_allocated = True
+
+ def set_input_data(self, data, copy=True):
+ if data is None:
+ return self.data_in
+ else:
+ return self.set_data(self.data_in, data, self.shape, self.dtype_in, copy=copy, name="data_in")
+
+ def set_output_data(self, data, copy=True):
+ if data is None:
+ return self.data_out
+ else:
+ return self.set_data(self.data_out, data, self.shape_out, self.dtype_out, copy=copy, name="data_out")
+
+ def fft(self, array, **kwargs):
+ raise ValueError("This should be implemented by back-end FFT")
+
+ def ifft(self, array, **kwargs):
+ raise ValueError("This should be implemented by back-end FFT")
diff --git a/src/silx/math/fft/clfft.py b/src/silx/math/fft/clfft.py
new file mode 100644
index 0000000..dad8ec1
--- /dev/null
+++ b/src/silx/math/fft/clfft.py
@@ -0,0 +1,286 @@
+#!/usr/bin/env python
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2018-2019 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+import numpy as np
+
+from .basefft import BaseFFT, check_version
+try:
+ import pyopencl as cl
+ import pyopencl.array as parray
+ import gpyfft
+ from gpyfft.fft import FFT as cl_fft
+ from ...opencl.common import ocl
+ __have_clfft__ = True
+except ImportError:
+ __have_clfft__ = False
+
+
+# Check gpyfft version
+__required_gpyfft_version__ = "0.3.0"
+if __have_clfft__:
+ __have_clfft__ = check_version(gpyfft, __required_gpyfft_version__)
+
+
+class CLFFT(BaseFFT):
+ """Initialize a clfft plan.
+
+ Please see FFT class for parameters help.
+
+ CLFFT-specific parameters
+ --------------------------
+
+ :param pyopencl.Context ctx:
+ If set to other than None, an existing pyopencl context is used.
+ :param bool fast_math:
+ If set to True, computations will be done with "fast math" mode,
+ i.e., more speed but less accuracy.
+ :param bool choose_best_device:
+ Whether to automatically choose the best available OpenCL device.
+ """
+ def __init__(
+ self,
+ shape=None,
+ dtype=None,
+ template=None,
+ shape_out=None,
+ axes=None,
+ normalize="rescale",
+ ctx=None,
+ fast_math=False,
+ choose_best_device=True,
+ ):
+ if not(__have_clfft__) or not(__have_clfft__):
+ raise ImportError("Please install pyopencl and gpyfft >= %s to use the OpenCL back-end" % __required_gpyfft_version__)
+
+ super(CLFFT, self).__init__(
+ shape=shape,
+ dtype=dtype,
+ template=template,
+ shape_out=shape_out,
+ axes=axes,
+ normalize=normalize,
+ )
+ self.ctx = ctx
+ self.choose_best_device = choose_best_device
+ self.fast_math = fast_math
+ self.backend = "clfft"
+
+ self.fix_axes()
+ self.init_context_queue()
+ self.allocate_arrays()
+ self.real_transform = np.isrealobj(self.data_in)
+ self.compute_forward_plan()
+ self.compute_inverse_plan()
+ self.refs = {
+ "data_in": self.data_in,
+ "data_out": self.data_out,
+ }
+ # TODO
+ # Either pyopencl ElementWiseKernel, or built-in clfft callbacks
+ if self.normalize != "rescale":
+ raise NotImplementedError(
+ "Normalization modes other than rescale are not implemented with OpenCL backend yet."
+ )
+
+ def fix_axes(self):
+ """
+ "Fix" axes.
+
+ clfft does not have the same convention as FFTW/cuda/numpy.
+ """
+ self.axes = self.axes[::-1]
+
+ def _allocate(self, shape, dtype):
+ ary = parray.empty(self.queue, shape, dtype=dtype)
+ ary.fill(0)
+ return ary
+
+
+ def check_array(self, array, shape, dtype, copy=True):
+ if array.shape != shape:
+ raise ValueError("Invalid data shape: expected %s, got %s" %
+ (shape, array.shape)
+ )
+ if array.dtype != dtype:
+ raise ValueError("Invalid data type: expected %s, got %s" %
+ (dtype, array.dtype)
+ )
+
+
+ def set_data(self, dst, src, shape, dtype, copy=True, name=None):
+ """
+ dst is a device array owned by the current instance
+ (either self.data_in or self.data_out).
+
+ copy is ignored for device<-> arrays.
+ """
+ self.check_array(src, shape, dtype)
+ if isinstance(src, np.ndarray):
+ if name == "data_out":
+ # Makes little sense to provide output=numpy_array
+ return dst
+ if not(src.flags["C_CONTIGUOUS"]):
+ src = np.ascontiguousarray(src, dtype=dtype)
+ # working on underlying buffer is notably faster
+ #~ dst[:] = src[:]
+ evt = cl.enqueue_copy(self.queue, dst.data, src)
+ evt.wait()
+ elif isinstance(src, parray.Array):
+ # No copy, use the data as self.d_input or self.d_output
+ # (this prevents the use of in-place transforms, however).
+ # We have to keep their old references.
+ if name is None:
+ # This should not happen
+ raise ValueError("Please provide either copy=True or name != None")
+ assert id(self.refs[name]) == id(dst) # DEBUG
+ setattr(self, name, src)
+ return src
+ else:
+ raise ValueError(
+ "Invalid array type %s, expected numpy.ndarray or pyopencl.array" %
+ type(src)
+ )
+ return dst
+
+
+ def recover_array_references(self):
+ self.data_in = self.refs["data_in"]
+ self.data_out = self.refs["data_out"]
+
+
+ def init_context_queue(self):
+ if self.ctx is None:
+ if self.choose_best_device:
+ self.ctx = ocl.create_context()
+ else:
+ self.ctx = cl.create_some_context()
+ self.queue = cl.CommandQueue(self.ctx)
+
+
+ def compute_forward_plan(self):
+ self.plan_forward = cl_fft(
+ self.ctx,
+ self.queue,
+ self.data_in,
+ out_array=self.data_out,
+ axes=self.axes,
+ fast_math=self.fast_math,
+ real=self.real_transform,
+ )
+
+
+ def compute_inverse_plan(self):
+ self.plan_inverse = cl_fft(
+ self.ctx,
+ self.queue,
+ self.data_out,
+ out_array=self.data_in,
+ axes=self.axes,
+ fast_math=self.fast_math,
+ real=self.real_transform,
+ )
+
+
+ def update_forward_plan_arrays(self):
+ self.plan_forward.data = self.data_in
+ self.plan_forward.result = self.data_out
+
+
+ def update_inverse_plan_arrays(self):
+ self.plan_inverse.data = self.data_out
+ self.plan_inverse.result = self.data_in
+
+
+ def copy_output_if_numpy(self, dst, src):
+ if isinstance(dst, parray.Array):
+ return
+ # working on underlying buffer is notably faster
+ #~ dst[:] = src[:]
+ evt = cl.enqueue_copy(self.queue, dst, src.data)
+ evt.wait()
+
+
+ def fft(self, array, output=None, do_async=False):
+ """
+ Perform a (forward) Fast Fourier Transform.
+
+ :param Union[numpy.ndarray,pyopencl.array] array:
+ Input data. Must be consistent with the current context.
+ :param Union[numpy.ndarray,pyopencl.array] output:
+ Output data. By default, output is a numpy.ndarray.
+ :param bool do_async:
+ Whether to perform operation in asynchronous mode.
+ Default is False, meaning that we wait for transform to complete.
+ """
+ self.set_input_data(array, copy=False)
+ self.set_output_data(output, copy=False)
+ self.update_forward_plan_arrays()
+ event, = self.plan_forward.enqueue()
+ if not(do_async):
+ event.wait()
+ if output is not None:
+ self.copy_output_if_numpy(output, self.data_out)
+ res = output
+ else:
+ res = self.data_out.get()
+ self.recover_array_references()
+ return res
+
+
+ def ifft(self, array, output=None, do_async=False):
+ """
+ Perform a (inverse) Fast Fourier Transform.
+
+ :param Union[numpy.ndarray,pyopencl.array] array:
+ Input data. Must be consistent with the current context.
+ :param Union[numpy.ndarray,pyopencl.array] output:
+ Output data. By default, output is a numpy.ndarray.
+ :param bool do_async:
+ Whether to perform operation in asynchronous mode.
+ Default is False, meaning that we wait for transform to complete.
+ """
+ self.set_output_data(array, copy=False)
+ self.set_input_data(output, copy=False)
+ self.update_inverse_plan_arrays()
+ event, = self.plan_inverse.enqueue(forward=False)
+ if not(do_async):
+ event.wait()
+ if output is not None:
+ self.copy_output_if_numpy(output, self.data_in)
+ res = output
+ else:
+ res = self.data_in.get()
+ self.recover_array_references()
+ return res
+
+
+ def __del__(self):
+ # It seems that gpyfft underlying clFFT destructors are not called.
+ # This results in the following warning:
+ # Warning: Program terminating, but clFFT resources not freed.
+ # Please consider explicitly calling clfftTeardown( )
+ del self.plan_forward
+ del self.plan_inverse
+
diff --git a/src/silx/math/fft/cufft.py b/src/silx/math/fft/cufft.py
new file mode 100644
index 0000000..848f3e6
--- /dev/null
+++ b/src/silx/math/fft/cufft.py
@@ -0,0 +1,253 @@
+#!/usr/bin/env python
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2018-2019 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+import numpy as np
+
+from .basefft import BaseFFT
+try:
+ import pycuda.gpuarray as gpuarray
+ from skcuda.fft import Plan
+ from skcuda.fft import fft as cu_fft
+ from skcuda.fft import ifft as cu_ifft
+ __have_cufft__ = True
+except ImportError:
+ __have_cufft__ = False
+
+
+class CUFFT(BaseFFT):
+ """Initialize a cufft plan
+
+ Please see FFT class for parameters help.
+
+ CUFFT-specific parameters
+ --------------------------
+
+ :param pycuda.driver.Stream stream:
+ Stream with which to associate the plan. If no stream is specified,
+ the default stream is used.
+ """
+ def __init__(
+ self,
+ shape=None,
+ dtype=None,
+ template=None,
+ shape_out=None,
+ axes=None,
+ normalize="rescale",
+ stream=None,
+ ):
+ if not(__have_cufft__) or not(__have_cufft__):
+ raise ImportError("Please install pycuda and scikit-cuda to use the CUDA back-end")
+
+ super(CUFFT, self).__init__(
+ shape=shape,
+ dtype=dtype,
+ template=template,
+ shape_out=shape_out,
+ axes=axes,
+ normalize=normalize,
+ )
+ self.cufft_stream = stream
+ self.backend = "cufft"
+
+ self.configure_batched_transform()
+ self.allocate_arrays()
+ self.real_transform = np.isrealobj(self.data_in)
+ self.compute_forward_plan()
+ self.compute_inverse_plan()
+ self.refs = {
+ "data_in": self.data_in,
+ "data_out": self.data_out,
+ }
+ self.configure_normalization()
+
+ def _allocate(self, shape, dtype):
+ return gpuarray.zeros(shape, dtype)
+
+ # TODO support batched transform where batch is other than dimension 0
+ def configure_batched_transform(self):
+ self.cufft_batch_size = 1
+ self.cufft_shape = self.shape
+ if (self.axes is not None) and (len(self.axes) < len(self.shape)):
+ # In the easiest case, the transform is computed along the fastest dimensions:
+ # - 1D transforms of lines of 2D data
+ # - 2D transforms of images of 3D data (stacked along slow dim)
+ # - 1D transforms of 3D data along fastest dim
+ # Otherwise, we have to configure cuda "advanced memory layout",
+ # which is not implemented yet.
+
+ data_ndims = len(self.shape)
+ supported_axes = {
+ 2: [(1,)],
+ 3: [(1, 2), (2, 1), (1,), (2,)],
+ }
+ if self.axes not in supported_axes[data_ndims]:
+ raise NotImplementedError("With the CUDA backend, batched transform is only supported along fastest dimensions")
+ self.cufft_batch_size = self.shape[0]
+ self.cufft_shape = self.shape[1:]
+ if data_ndims == 3 and len(self.axes) == 1:
+ # 1D transform on 3D data: here only supported along fast dim,
+ # so batch_size is Nx*Ny
+ self.cufft_batch_size = np.prod(self.shape[:2])
+ self.cufft_shape = (self.shape[-1],)
+ if len(self.cufft_shape) == 1:
+ self.cufft_shape = self.cufft_shape[0]
+
+ def configure_normalization(self):
+ # TODO
+ if self.normalize == "ortho":
+ raise NotImplementedError(
+ "Normalization mode 'ortho' is not implemented with CUDA backend yet."
+ )
+ self.cufft_scale_inverse = (self.normalize == "rescale")
+
+ def check_array(self, array, shape, dtype, copy=True):
+ if array.shape != shape:
+ raise ValueError("Invalid data shape: expected %s, got %s" %
+ (shape, array.shape))
+ if array.dtype != dtype:
+ raise ValueError("Invalid data type: expected %s, got %s" %
+ (dtype, array.dtype))
+
+ def set_data(self, dst, src, shape, dtype, copy=True, name=None):
+ """
+ dst is a device array owned by the current instance
+ (either self.data_in or self.data_out).
+
+ copy is ignored for device<-> arrays.
+ """
+ self.check_array(src, shape, dtype)
+ if isinstance(src, np.ndarray):
+ if name == "data_out":
+ # Makes little sense to provide output=numpy_array
+ return dst
+ if not(src.flags["C_CONTIGUOUS"]):
+ src = np.ascontiguousarray(src, dtype=dtype)
+ dst[:] = src[:]
+ elif isinstance(src, gpuarray.GPUArray):
+ # No copy, use the data as self.d_input or self.d_output
+ # (this prevents the use of in-place transforms, however).
+ # We have to keep their old references.
+ if name is None:
+ # This should not happen
+ raise ValueError("Please provide either copy=True or name != None")
+ assert id(self.refs[name]) == id(dst) # DEBUG
+ setattr(self, name, src)
+ return src
+ else:
+ raise ValueError(
+ "Invalid array type %s, expected numpy.ndarray or pycuda.gpuarray" %
+ type(src)
+ )
+ return dst
+
+ def recover_array_references(self):
+ self.data_in = self.refs["data_in"]
+ self.data_out = self.refs["data_out"]
+
+ def compute_forward_plan(self):
+ self.plan_forward = Plan(
+ self.cufft_shape,
+ self.dtype,
+ self.dtype_out,
+ batch=self.cufft_batch_size,
+ stream=self.cufft_stream,
+ # cufft extensible plan API is only supported after 0.5.1
+ # (commit 65288d28ca0b93e1234133f8d460dc6becb65121)
+ # but there is still no official 0.5.2
+ #~ auto_allocate=True # cufft extensible plan API
+ )
+
+ def compute_inverse_plan(self):
+ self.plan_inverse = Plan(
+ self.cufft_shape, # not shape_out
+ self.dtype_out,
+ self.dtype,
+ batch=self.cufft_batch_size,
+ stream=self.cufft_stream,
+ # cufft extensible plan API is only supported after 0.5.1
+ # (commit 65288d28ca0b93e1234133f8d460dc6becb65121)
+ # but there is still no official 0.5.2
+ #~ auto_allocate=True
+ )
+
+ def copy_output_if_numpy(self, dst, src):
+ if isinstance(dst, gpuarray.GPUArray):
+ return
+ dst[:] = src[:]
+
+ def fft(self, array, output=None):
+ """
+ Perform a (forward) Fast Fourier Transform.
+
+ :param Union[numpy.ndarray,pycuda.gpuarray] array:
+ Input data. Must be consistent with the current context.
+ :param Union[numpy.ndarray,pycuda.gpuarray] output:
+ Output data. By default, output is a numpy.ndarray.
+ """
+ data_in = self.set_input_data(array, copy=False)
+ data_out = self.set_output_data(output, copy=False)
+
+ cu_fft(
+ data_in,
+ data_out,
+ self.plan_forward,
+ scale=False
+ )
+
+ if output is not None:
+ self.copy_output_if_numpy(output, self.data_out)
+ res = output
+ else:
+ res = self.data_out.get()
+ self.recover_array_references()
+ return res
+
+ def ifft(self, array, output=None):
+ """
+ Perform a (inverse) Fast Fourier Transform.
+
+ :param Union[numpy.ndarray,pycuda.gpuarray] array:
+ Input data. Must be consistent with the current context.
+ :param Union[numpy.ndarray,pycuda.gpuarray] output:
+ Output data. By default, output is a numpy.ndarray.
+ """
+ data_in = self.set_output_data(array, copy=False)
+ data_out = self.set_input_data(output, copy=False)
+
+ cu_ifft(
+ data_in,
+ data_out,
+ self.plan_inverse,
+ scale=self.cufft_scale_inverse,
+ )
+
+ if output is not None:
+ self.copy_output_if_numpy(output, self.data_in)
+ res = output
+ else:
+ res = self.data_in.get()
+ self.recover_array_references()
+ return res
diff --git a/src/silx/math/fft/fft.py b/src/silx/math/fft/fft.py
new file mode 100644
index 0000000..eb0d73b
--- /dev/null
+++ b/src/silx/math/fft/fft.py
@@ -0,0 +1,96 @@
+#!/usr/bin/env python
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2018-2019 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+from .fftw import FFTW
+from .clfft import CLFFT
+from .npfft import NPFFT
+from .cufft import CUFFT
+
+
+def FFT(
+ shape=None,
+ dtype=None,
+ template=None,
+ shape_out=None,
+ axes=None,
+ normalize="rescale",
+ backend="numpy",
+ **kwargs
+):
+ """
+ Initialize a FFT plan.
+
+ :param List[int] shape:
+ Shape of the input data.
+ :param numpy.dtype dtype:
+ Data type of the input data.
+ :param numpy.ndarray template:
+ Optional data, replacement for "shape" and "dtype".
+ If provided, the arguments "shape" and "dtype" are ignored,
+ and are instead inferred from it.
+ :param List[int] shape_out:
+ Optional shape of output data.
+ By default, the data has the same shape as the input
+ data (in case of C2C transform), or a shape with the last dimension halved
+ (in case of R2C transform). If shape_out is provided, it must be greater
+ or equal than the shape of input data. In this case, FFT is performed
+ with zero-padding.
+ :param List[int] axes:
+ Axes along which FFT is computed.
+ * For 2D transform: axes=(1,0)
+ * For batched 1D transform of 2D image: axes=(0,)
+ :param str normalize:
+ Whether to normalize FFT and IFFT. Possible values are:
+ * "rescale": in this case, Fourier data is divided by "N"
+ before IFFT, so that (FFT(data)) = data
+ * "ortho": in this case, FFT and IFFT are adjoint of eachother,
+ the transform is unitary. Both FFT and IFFT are scaled with 1/sqrt(N).
+ * "none": no normalizatio is done : IFFT(FFT(data)) = data*N
+ :param str backend:
+ FFT Backend to use. Value can be "numpy", "fftw", "opencl", "cuda".
+ """
+ backends = {
+ "numpy": NPFFT,
+ "np": NPFFT,
+ "fftw": FFTW,
+ "opencl": CLFFT,
+ "clfft": CLFFT,
+ "cuda": CUFFT,
+ "cufft": CUFFT,
+ }
+
+ backend = backend.lower()
+ if backend not in backends:
+ raise ValueError("Unknown backend %s, available are %s" % (backend, backends))
+ F = backends[backend](
+ shape=shape,
+ dtype=dtype,
+ template=template,
+ shape_out=shape_out,
+ axes=axes,
+ normalize=normalize,
+ **kwargs
+ )
+ return F
diff --git a/src/silx/math/fft/fftw.py b/src/silx/math/fft/fftw.py
new file mode 100644
index 0000000..ff6966c
--- /dev/null
+++ b/src/silx/math/fft/fftw.py
@@ -0,0 +1,214 @@
+#!/usr/bin/env python
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2018-2019 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+import numpy as np
+
+from .basefft import BaseFFT, check_version
+try:
+ import pyfftw
+ __have_fftw__ = True
+except ImportError:
+ __have_fftw__ = False
+
+
+# Check pyfftw version
+__required_pyfftw_version__ = "0.10.0"
+if __have_fftw__:
+ __have_fftw__ = check_version(pyfftw, __required_pyfftw_version__)
+
+
+class FFTW(BaseFFT):
+ """Initialize a FFTW plan.
+
+ Please see FFT class for parameters help.
+
+ FFTW-specific parameters
+ -------------------------
+
+ :param bool check_alignment:
+ If set to True and "data" is provided, this will enforce the input data
+ to be "byte aligned", which might imply extra memory usage.
+ :param int num_threads:
+ Number of threads for computing FFT.
+ """
+ def __init__(
+ self,
+ shape=None,
+ dtype=None,
+ template=None,
+ shape_out=None,
+ axes=None,
+ normalize="rescale",
+ check_alignment=False,
+ num_threads=1,
+ ):
+ if not(__have_fftw__):
+ raise ImportError("Please install pyfftw >= %s to use the FFTW back-end" % __required_pyfftw_version__)
+ super(FFTW, self).__init__(
+ shape=shape,
+ dtype=dtype,
+ template=template,
+ shape_out=shape_out,
+ axes=axes,
+ normalize=normalize,
+ )
+ self.check_alignment = check_alignment
+ self.num_threads = num_threads
+ self.backend = "fftw"
+
+ self.allocate_arrays()
+ self.set_fftw_flags()
+ self.compute_forward_plan()
+ self.compute_inverse_plan()
+ self.refs = {
+ "data_in": self.data_in,
+ "data_out": self.data_out,
+ }
+
+ def set_fftw_flags(self):
+ self.fftw_flags = ('FFTW_MEASURE', ) # TODO
+ self.fftw_planning_timelimit = None # TODO
+ self.fftw_norm_modes = {
+ "rescale": {"ortho": False, "normalize": True},
+ "ortho": {"ortho": True, "normalize": False},
+ "none": {"ortho": False, "normalize": False},
+ }
+ if self.normalize not in self.fftw_norm_modes:
+ raise ValueError("Unknown normalization mode %s. Possible values are %s" %
+ (self.normalize, self.fftw_norm_modes.keys())
+ )
+ self.fftw_norm_mode = self.fftw_norm_modes[self.normalize]
+
+ def _allocate(self, shape, dtype):
+ return pyfftw.zeros_aligned(shape, dtype=dtype)
+
+ def check_array(self, array, shape, dtype, copy=True):
+ if array.shape != shape:
+ raise ValueError("Invalid data shape: expected %s, got %s" %
+ (shape, array.shape)
+ )
+ if array.dtype != dtype:
+ raise ValueError("Invalid data type: expected %s, got %s" %
+ (dtype, array.dtype)
+ )
+
+ def set_data(self, self_array, array, shape, dtype, copy=True, name=None):
+ """
+ :param self_array: array owned by the current instance
+ (either self.data_in or self.data_out).
+ :type: numpy.ndarray
+ :param self_array: data to set
+ :type: numpy.ndarray
+ :type tuple shape: shape of the array
+ :param dtype: type of the array
+ :type: numpy.dtype
+ :param bool copy: should we copy the array
+ :param str name: name of the array
+
+ Copies are avoided when possible.
+ """
+ self.check_array(array, shape, dtype)
+ if id(self.refs[name]) == id(array):
+ # nothing to do: fft is performed on self.data_in or self.data_out
+ arr_to_use = self.refs[name]
+ if self.check_alignment and not(pyfftw.is_byte_aligned(array)):
+ # If the array is not properly aligned,
+ # create a temp. array copy it to self.data_in or self.data_out
+ self_array[:] = array[:]
+ arr_to_use = self_array
+ else:
+ # If the array is properly aligned, use it directly
+ if copy:
+ arr_to_use = np.copy(array)
+ else:
+ arr_to_use = array
+ return arr_to_use
+
+ def compute_forward_plan(self):
+ self.plan_forward = pyfftw.FFTW(
+ self.data_in,
+ self.data_out,
+ axes=self.axes,
+ direction='FFTW_FORWARD',
+ flags=self.fftw_flags,
+ threads=self.num_threads,
+ planning_timelimit=self.fftw_planning_timelimit,
+ # the following seems to be taken into account only when using __call__
+ ortho=self.fftw_norm_mode["ortho"],
+ normalise_idft=self.fftw_norm_mode["normalize"],
+ )
+
+ def compute_inverse_plan(self):
+ self.plan_inverse = pyfftw.FFTW(
+ self.data_out,
+ self.data_in,
+ axes=self.axes,
+ direction='FFTW_BACKWARD',
+ flags=self.fftw_flags,
+ threads=self.num_threads,
+ planning_timelimit=self.fftw_planning_timelimit,
+ # the following seem to be taken into account only when using __call__
+ ortho=self.fftw_norm_mode["ortho"],
+ normalise_idft=self.fftw_norm_mode["normalize"],
+ )
+
+ def fft(self, array, output=None):
+ """
+ Perform a (forward) Fast Fourier Transform.
+
+ :param numpy.ndarray array:
+ Input data. Must be consistent with the current context.
+ :param numpy.ndarray output:
+ Optional output data.
+ """
+ data_in = self.set_input_data(array, copy=False)
+ data_out = self.set_output_data(output, copy=False)
+ self.plan_forward.update_arrays(data_in, data_out)
+ # execute.__call__ does both update_arrays() and normalization
+ self.plan_forward(
+ ortho=self.fftw_norm_mode["ortho"],
+ )
+ self.plan_forward.update_arrays(self.refs["data_in"], self.refs["data_out"])
+ return data_out
+
+ def ifft(self, array, output=None):
+ """
+ Perform a (inverse) Fast Fourier Transform.
+
+ :param numpy.ndarray array:
+ Input data. Must be consistent with the current context.
+ :param numpy.ndarray output:
+ Optional output data.
+ """
+ data_in = self.set_output_data(array, copy=False)
+ data_out = self.set_input_data(output, copy=False)
+ self.plan_inverse.update_arrays(data_in, data_out)
+ # execute.__call__ does both update_arrays() and normalization
+ self.plan_inverse(
+ ortho=self.fftw_norm_mode["ortho"],
+ normalise_idft=self.fftw_norm_mode["normalize"]
+ )
+ self.plan_inverse.update_arrays(self.refs["data_out"], self.refs["data_in"])
+ return data_out
diff --git a/src/silx/math/fft/npfft.py b/src/silx/math/fft/npfft.py
new file mode 100644
index 0000000..20351de
--- /dev/null
+++ b/src/silx/math/fft/npfft.py
@@ -0,0 +1,124 @@
+#!/usr/bin/env python
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2018-2019 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+import numpy as np
+
+from .basefft import BaseFFT
+
+
+class NPFFT(BaseFFT):
+ """Initialize a numpy plan.
+
+ Please see FFT class for parameters help.
+ """
+ def __init__(
+ self,
+ shape=None,
+ dtype=None,
+ template=None,
+ shape_out=None,
+ axes=None,
+ normalize="rescale",
+ ):
+ super(NPFFT, self).__init__(
+ shape=shape,
+ dtype=dtype,
+ template=template,
+ shape_out=shape_out,
+ axes=axes,
+ normalize=normalize,
+ )
+ self.backend = "numpy"
+ self.real_transform = False
+ if template is not None and np.isrealobj(template):
+ self.real_transform = True
+ # For numpy functions.
+ # TODO Issue warning if user wants ifft(fft(data)) = N*data ?
+ if normalize != "ortho":
+ self.normalize = None
+ self.set_fft_functions()
+ #~ self.allocate_arrays() # not needed for this backend
+ self.compute_plans()
+
+
+ def set_fft_functions(self):
+ # (fwd, inv) = _fft_functions[is_real][ndim]
+ self._fft_functions = {
+ True: {
+ 1: (np.fft.rfft, np.fft.irfft),
+ 2: (np.fft.rfft2, np.fft.irfft2),
+ 3: (np.fft.rfftn, np.fft.irfftn),
+ },
+ False: {
+ 1: (np.fft.fft, np.fft.ifft),
+ 2: (np.fft.fft2, np.fft.ifft2),
+ 3: (np.fft.fftn, np.fft.ifftn),
+ }
+ }
+
+
+ def _allocate(self, shape, dtype):
+ return np.zeros(self.queue, shape, dtype=dtype)
+
+
+ def compute_plans(self):
+ ndim = len(self.shape)
+ funcs = self._fft_functions[self.real_transform][np.minimum(ndim, 3)]
+ if np.version.version[:4] in ["1.8.", "1.9."]:
+ # norm keyword was introduced in 1.10 and we support numpy >= 1.8
+ self.numpy_args = {}
+ else:
+ self.numpy_args = {"norm": self.normalize}
+ # Batched transform
+ if (self.user_axes is not None) and len(self.user_axes) < ndim:
+ funcs = self._fft_functions[self.real_transform][np.minimum(ndim-1, 3)]
+ self.numpy_args["axes"] = self.user_axes
+ # Special case of batched 1D transform on 2D data
+ if ndim == 2:
+ assert len(self.user_axes) == 1
+ self.numpy_args["axis"] = self.user_axes[0]
+ self.numpy_args.pop("axes")
+ self.numpy_funcs = funcs
+
+
+ def fft(self, array):
+ """
+ Perform a (forward) Fast Fourier Transform.
+
+ :param numpy.ndarray array:
+ Input data. Must be consistent with the current context.
+ """
+ return self.numpy_funcs[0](array, **self.numpy_args)
+
+
+ def ifft(self, array):
+ """
+ Perform a (inverse) Fast Fourier Transform.
+
+ :param numpy.ndarray array:
+ Input data. Must be consistent with the current context.
+ """
+ return self.numpy_funcs[1](array, **self.numpy_args)
+
diff --git a/src/silx/math/fft/setup.py b/src/silx/math/fft/setup.py
new file mode 100644
index 0000000..76bb864
--- /dev/null
+++ b/src/silx/math/fft/setup.py
@@ -0,0 +1,41 @@
+# coding: utf-8
+# /*##########################################################################
+# Copyright (C) 2016-2017 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
+
+__authors__ = ["P. Naudet"]
+__license__ = "MIT"
+__date__ = "12/12/2018"
+
+import numpy
+from numpy.distutils.misc_util import Configuration
+
+
+def configuration(parent_package='', top_path=None):
+ config = Configuration('fft', parent_package, top_path)
+ config.add_subpackage('test')
+ return config
+
+
+if __name__ == "__main__":
+ from numpy.distutils.core import setup
+ setup(configuration=configuration)
diff --git a/src/silx/math/fft/test/__init__.py b/src/silx/math/fft/test/__init__.py
new file mode 100644
index 0000000..ad9836c
--- /dev/null
+++ b/src/silx/math/fft/test/__init__.py
@@ -0,0 +1,23 @@
+# coding: utf-8
+# /*##########################################################################
+# Copyright (C) 2016-2019 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
diff --git a/src/silx/math/fft/test/test_fft.py b/src/silx/math/fft/test/test_fft.py
new file mode 100644
index 0000000..19becb8
--- /dev/null
+++ b/src/silx/math/fft/test/test_fft.py
@@ -0,0 +1,257 @@
+#!/usr/bin/env python
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2018-2021 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+"""Test of the FFT module"""
+
+import numpy as np
+import unittest
+import logging
+import pytest
+try:
+ from scipy.misc import ascent
+ __have_scipy = True
+except ImportError:
+ __have_scipy = False
+from silx.utils.testutils import ParametricTestCase
+from silx.math.fft.fft import FFT
+from silx.math.fft.clfft import __have_clfft__
+from silx.math.fft.cufft import __have_cufft__
+from silx.math.fft.fftw import __have_fftw__
+
+
+logger = logging.getLogger(__name__)
+
+
+class TransformInfos(object):
+ def __init__(self):
+ self.dimensions = [
+ "1D",
+ "batched_1D",
+ "2D",
+ "batched_2D",
+ "3D",
+ ]
+ self.modes = {
+ "R2C": np.float32,
+ "R2C_double": np.float64,
+ "C2C": np.complex64,
+ "C2C_double": np.complex128,
+ }
+ self.sizes = {
+ "1D": [(128,), (127,)],
+ "2D": [(128, 128), (128, 127), (127, 128), (127, 127)],
+ "3D": [(64, 64, 64), (64, 64, 63), (64, 63, 64), (63, 64, 64),
+ (64, 63, 63), (63, 64, 63), (63, 63, 64), (63, 63, 63)]
+ }
+ self.axes = {
+ "1D": None,
+ "batched_1D": (-1,),
+ "2D": None,
+ "batched_2D": (-2, -1),
+ "3D": None,
+ }
+ self.sizes["batched_1D"] = self.sizes["2D"]
+ self.sizes["batched_2D"] = self.sizes["3D"]
+
+
+class Data(object):
+ def __init__(self):
+ self.data = ascent().astype("float32")
+ self.data1d = self.data[:, 0] # non-contiguous data
+ self.data3d = np.tile(self.data[:64, :64], (64, 1, 1))
+ self.data_refs = {
+ 1: self.data1d,
+ 2: self.data,
+ 3: self.data3d,
+ }
+
+
+@unittest.skipUnless(__have_scipy, "scipy is missing")
+@pytest.mark.usefixtures("test_options_class_attr")
+class TestFFT(ParametricTestCase):
+ """Test cuda/opencl/fftw backends of FFT"""
+
+ def setUp(self):
+ self.tol = {
+ np.dtype("float32"): 1e-3,
+ np.dtype("float64"): 1e-9,
+ np.dtype("complex64"): 1e-3,
+ np.dtype("complex128"): 1e-9,
+ }
+ self.transform_infos = TransformInfos()
+ self.test_data = Data()
+
+ @staticmethod
+ def calc_mae(arr1, arr2):
+ """
+ Compute the Max Absolute Error between two arrays
+ """
+ return np.max(np.abs(arr1 - arr2))
+
+ @unittest.skipIf(not __have_cufft__,
+ "cuda back-end requires pycuda and scikit-cuda")
+ def test_cuda(self):
+ import pycuda.autoinit
+
+ # Error is higher when using cuda. fast_math mode ?
+ self.tol[np.dtype("float32")] *= 2
+
+ self.__run_tests(backend="cuda")
+
+ @unittest.skipIf(not __have_clfft__,
+ "opencl back-end requires pyopencl and gpyfft")
+ def test_opencl(self):
+ from silx.opencl.common import ocl
+ if ocl is not None:
+ self.__run_tests(backend="opencl", ctx=ocl.create_context())
+
+ @unittest.skipIf(not __have_fftw__,
+ "fftw back-end requires pyfftw")
+ def test_fftw(self):
+ self.__run_tests(backend="fftw")
+
+ def __run_tests(self, backend, **extra_args):
+ """Run all tests with the given backend
+
+ :param str backend:
+ :param dict extra_args: Additional arguments to provide to FFT
+ """
+ for trdim in self.transform_infos.dimensions:
+ for mode in self.transform_infos.modes:
+ for size in self.transform_infos.sizes[trdim]:
+ with self.subTest(trdim=trdim, mode=mode, size=size):
+ self.__test(backend, trdim, mode, size, **extra_args)
+
+ def __test(self, backend, trdim, mode, size, **extra_args):
+ """Compare given backend with numpy for given conditions"""
+ logger.debug("backend: %s, trdim: %s, mode: %s, size: %s",
+ backend, trdim, mode, str(size))
+ if size == "3D" and self.test_options.TEST_LOW_MEM:
+ self.skipTest("low mem")
+
+ ndim = len(size)
+ input_data = self.test_data.data_refs[ndim].astype(
+ self.transform_infos.modes[mode])
+ tol = self.tol[np.dtype(input_data.dtype)]
+ if trdim == "3D":
+ tol *= 10 # Error is relatively high in high dimensions
+ # It seems that cuda has problems with C2D batched 1D
+ if trdim == "batched_1D" and backend == "cuda" and mode == "C2C":
+ tol *= 10
+
+ # Python < 3.5 does not want to mix **extra_args with existing kwargs
+ fft_args = {
+ "template": input_data,
+ "axes": self.transform_infos.axes[trdim],
+ "backend": backend,
+ }
+ fft_args.update(extra_args)
+ F = FFT(
+ **fft_args
+ )
+ F_np = FFT(
+ template=input_data,
+ axes=self.transform_infos.axes[trdim],
+ backend="numpy"
+ )
+
+ # Forward FFT
+ res = F.fft(input_data)
+ res_np = F_np.fft(input_data)
+ mae = self.calc_mae(res, res_np)
+ all_close = np.allclose(res, res_np, atol=tol, rtol=tol),
+ self.assertTrue(
+ all_close,
+ "FFT %s:%s, MAE(%s, numpy) = %f (tol = %.2e)" % (mode, trdim, backend, mae, tol)
+ )
+
+ # Inverse FFT
+ res2 = F.ifft(res)
+ mae = self.calc_mae(res2, input_data)
+ self.assertTrue(
+ mae < tol,
+ "IFFT %s:%s, MAE(%s, numpy) = %f" % (mode, trdim, backend, mae)
+ )
+
+
+@unittest.skipUnless(__have_scipy, "scipy is missing")
+class TestNumpyFFT(ParametricTestCase):
+ """
+ Test the Numpy backend individually.
+ """
+
+ def setUp(self):
+ transforms = {
+ "1D": {
+ True: (np.fft.rfft, np.fft.irfft),
+ False: (np.fft.fft, np.fft.ifft),
+ },
+ "2D": {
+ True: (np.fft.rfft2, np.fft.irfft2),
+ False: (np.fft.fft2, np.fft.ifft2),
+ },
+ "3D": {
+ True: (np.fft.rfftn, np.fft.irfftn),
+ False: (np.fft.fftn, np.fft.ifftn),
+ },
+ }
+ transforms["batched_1D"] = transforms["1D"]
+ transforms["batched_2D"] = transforms["2D"]
+ self.transforms = transforms
+ self.transform_infos = TransformInfos()
+ self.test_data = Data()
+
+ def test(self):
+ """Test the numpy backend against native fft.
+
+ Results should be exactly the same.
+ """
+ for trdim in self.transform_infos.dimensions:
+ for mode in self.transform_infos.modes:
+ for size in self.transform_infos.sizes[trdim]:
+ with self.subTest(trdim=trdim, mode=mode, size=size):
+ self.__test(trdim, mode, size)
+
+ def __test(self, trdim, mode, size):
+ logger.debug("trdim: %s, mode: %s, size: %s", trdim, mode, str(size))
+ ndim = len(size)
+ input_data = self.test_data.data_refs[ndim].astype(
+ self.transform_infos.modes[mode])
+ np_fft, np_ifft = self.transforms[trdim][np.isrealobj(input_data)]
+
+ F = FFT(
+ template=input_data,
+ axes=self.transform_infos.axes[trdim],
+ backend="numpy"
+ )
+ # Test FFT
+ res = F.fft(input_data)
+ ref = np_fft(input_data)
+ self.assertTrue(np.allclose(res, ref))
+
+ # Test IFFT
+ res2 = F.ifft(res)
+ ref2 = np_ifft(ref)
+ self.assertTrue(np.allclose(res2, ref2))
diff --git a/src/silx/math/fit/__init__.py b/src/silx/math/fit/__init__.py
new file mode 100644
index 0000000..29e6a9e
--- /dev/null
+++ b/src/silx/math/fit/__init__.py
@@ -0,0 +1,39 @@
+# coding: utf-8
+# /*##########################################################################
+# Copyright (C) 2016 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
+
+__authors__ = ["P. Knobel"]
+__license__ = "MIT"
+__date__ = "22/06/2016"
+
+
+from .leastsq import leastsq, chisq_alpha_beta
+from .leastsq import \
+ CFREE, CPOSITIVE, CQUOTED, CFIXED, \
+ CFACTOR, CDELTA, CSUM
+
+from .functions import *
+from .filters import *
+from .peaks import peak_search, guess_fwhm
+from .fitmanager import FitManager
+from .fittheory import FitTheory
diff --git a/src/silx/math/fit/bgtheories.py b/src/silx/math/fit/bgtheories.py
new file mode 100644
index 0000000..631c43e
--- /dev/null
+++ b/src/silx/math/fit/bgtheories.py
@@ -0,0 +1,440 @@
+# coding: utf-8
+#/*##########################################################################
+#
+# Copyright (c) 2004-2020 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+########################################################################### */
+"""This modules defines a set of background model functions and associated
+estimation functions in a format that can be imported into a
+:class:`silx.math.fit.FitManager` object.
+
+A background function is a function that you want to add to a regular fit
+function prior to fitting the sum of both functions. This is useful, for
+instance, if you need to fit multiple gaussian peaks in an array of
+measured data points when the measurement is polluted by a background signal.
+
+The models include common background models such as a constant value or a
+linear background.
+
+It also includes background computation filters - *strip* and *snip* - that
+can extract a more complex low-curvature background signal from a signal with
+peaks having higher curvatures.
+
+The source code of this module can serve as a template for defining your
+own fit background theories. The minimal skeleton of such a theory definition
+file is::
+
+ from silx.math.fit.fittheory import FitTheory
+
+ def bgfunction1(x, y0, …):
+ bg_signal = …
+ return bg_signal
+
+ def estimation_function1(x, y):
+ …
+ estimated_params = …
+ constraints = …
+ return estimated_params, constraints
+
+ THEORY = {
+ 'bg_theory_name1': FitTheory(
+ description='Description of theory 1',
+ function=bgfunction1,
+ parameters=('param name 1', 'param name 2', …),
+ estimate=estimation_function1,
+ configure=configuration_function1,
+ derivative=derivative_function1,
+ is_background=True),
+ 'theory_name_2': …,
+ }
+"""
+__authors__ = ["P. Knobel"]
+__license__ = "MIT"
+__date__ = "16/01/2017"
+
+from collections import OrderedDict
+import numpy
+from silx.math.fit.filters import strip, snip1d,\
+ savitsky_golay
+from silx.math.fit.fittheory import FitTheory
+
+CONFIG = {
+ "SmoothingFlag": False,
+ "SmoothingWidth": 5,
+ "AnchorsFlag": False,
+ "AnchorsList": [],
+ "StripWidth": 2,
+ "StripIterations": 5000,
+ "StripThresholdFactor": 1.0,
+ "SnipWidth": 16,
+ "EstimatePolyOnStrip": True
+}
+
+# to avoid costly computations when parameters stay the same
+_BG_STRIP_OLDY = numpy.array([])
+_BG_STRIP_OLDPARS = [0, 0]
+_BG_STRIP_OLDBG = numpy.array([])
+
+_BG_SNIP_OLDY = numpy.array([])
+_BG_SNIP_OLDWIDTH = None
+_BG_SNIP_OLDBG = numpy.array([])
+
+
+_BG_OLD_ANCHORS = []
+_BG_OLD_ANCHORS_FLAG = None
+
+_BG_SMOOTH_OLDWIDTH = None
+_BG_SMOOTH_OLDFLAG = None
+
+
+def _convert_anchors_to_indices(x):
+ """Anchors stored in CONFIG["AnchorsList"] are abscissa.
+ Convert then to indices (take first index where x >= anchor),
+ then return the list of indices.
+
+ :param x: Original array of abscissa
+ :return: List of indices of anchors in x array.
+ If CONFIG['AnchorsFlag'] is False or None, or if the list
+ of indices is empty, return None.
+ """
+ # convert anchor X abscissa to index
+ if CONFIG['AnchorsFlag'] and CONFIG['AnchorsList'] is not None:
+ anchors_indices = []
+ for anchor_x in CONFIG['AnchorsList']:
+ if anchor_x <= x[0]:
+ continue
+ # take the first index where x > anchor_x
+ indices = numpy.nonzero(x >= anchor_x)[0]
+ if len(indices):
+ anchors_indices.append(min(indices))
+ if not len(anchors_indices):
+ anchors_indices = None
+ else:
+ anchors_indices = None
+
+ return anchors_indices
+
+
+def strip_bg(x, y0, width, niter):
+ """Extract and return the strip bg from y0.
+
+ Use anchors coordinates in CONFIG["AnchorsList"] if flag
+ CONFIG["AnchorsFlag"] is True. Convert anchors from x coordinate
+ to array index prior to passing it to silx.math.fit.filters.strip
+
+ :param x: Abscissa array
+ :param x: Ordinate array (data values at x positions)
+ :param width: strip width
+ :param niter: strip niter
+ """
+ global _BG_STRIP_OLDY
+ global _BG_STRIP_OLDPARS
+ global _BG_STRIP_OLDBG
+ global _BG_SMOOTH_OLDWIDTH
+ global _BG_SMOOTH_OLDFLAG
+ global _BG_OLD_ANCHORS
+ global _BG_OLD_ANCHORS_FLAG
+
+ parameters_changed =\
+ _BG_STRIP_OLDPARS != [width, niter] or\
+ _BG_SMOOTH_OLDWIDTH != CONFIG["SmoothingWidth"] or\
+ _BG_SMOOTH_OLDFLAG != CONFIG["SmoothingFlag"] or\
+ _BG_OLD_ANCHORS_FLAG != CONFIG["AnchorsFlag"] or\
+ _BG_OLD_ANCHORS != CONFIG["AnchorsList"]
+
+ # same parameters
+ if not parameters_changed:
+ # same data
+ if numpy.array_equal(_BG_STRIP_OLDY, y0):
+ # same result
+ return _BG_STRIP_OLDBG
+
+ _BG_STRIP_OLDY = y0
+ _BG_STRIP_OLDPARS = [width, niter]
+ _BG_SMOOTH_OLDWIDTH = CONFIG["SmoothingWidth"]
+ _BG_SMOOTH_OLDFLAG = CONFIG["SmoothingFlag"]
+ _BG_OLD_ANCHORS = CONFIG["AnchorsList"]
+ _BG_OLD_ANCHORS_FLAG = CONFIG["AnchorsFlag"]
+
+ y1 = savitsky_golay(y0, CONFIG["SmoothingWidth"]) if CONFIG["SmoothingFlag"] else y0
+
+ anchors_indices = _convert_anchors_to_indices(x)
+
+ background = strip(y1,
+ w=width,
+ niterations=niter,
+ factor=CONFIG["StripThresholdFactor"],
+ anchors=anchors_indices)
+
+ _BG_STRIP_OLDBG = background
+
+ return background
+
+
+def snip_bg(x, y0, width):
+ """Compute the snip bg for y0"""
+ global _BG_SNIP_OLDY
+ global _BG_SNIP_OLDWIDTH
+ global _BG_SNIP_OLDBG
+ global _BG_SMOOTH_OLDWIDTH
+ global _BG_SMOOTH_OLDFLAG
+ global _BG_OLD_ANCHORS
+ global _BG_OLD_ANCHORS_FLAG
+
+ parameters_changed =\
+ _BG_SNIP_OLDWIDTH != width or\
+ _BG_SMOOTH_OLDWIDTH != CONFIG["SmoothingWidth"] or\
+ _BG_SMOOTH_OLDFLAG != CONFIG["SmoothingFlag"] or\
+ _BG_OLD_ANCHORS_FLAG != CONFIG["AnchorsFlag"] or\
+ _BG_OLD_ANCHORS != CONFIG["AnchorsList"]
+
+ # same parameters
+ if not parameters_changed:
+ # same data
+ if numpy.sum(_BG_SNIP_OLDY == y0) == len(y0):
+ # same result
+ return _BG_SNIP_OLDBG
+
+ _BG_SNIP_OLDY = y0
+ _BG_SNIP_OLDWIDTH = width
+ _BG_SMOOTH_OLDWIDTH = CONFIG["SmoothingWidth"]
+ _BG_SMOOTH_OLDFLAG = CONFIG["SmoothingFlag"]
+ _BG_OLD_ANCHORS = CONFIG["AnchorsList"]
+ _BG_OLD_ANCHORS_FLAG = CONFIG["AnchorsFlag"]
+
+ y1 = savitsky_golay(y0, CONFIG["SmoothingWidth"]) if CONFIG["SmoothingFlag"] else y0
+
+ anchors_indices = _convert_anchors_to_indices(x)
+
+ if anchors_indices is None or not len(anchors_indices):
+ anchors_indices = [0, len(y1) - 1]
+
+ background = numpy.zeros_like(y1)
+ previous_anchor = 0
+ for anchor_index in anchors_indices:
+ if (anchor_index > previous_anchor) and (anchor_index < len(y1)):
+ background[previous_anchor:anchor_index] =\
+ snip1d(y1[previous_anchor:anchor_index],
+ width)
+ previous_anchor = anchor_index
+
+ if previous_anchor < len(y1):
+ background[previous_anchor:] = snip1d(y1[previous_anchor:],
+ width)
+
+ _BG_SNIP_OLDBG = background
+
+ return background
+
+
+def estimate_linear(x, y):
+ """
+ Estimate the linear parameters (constant, slope) of a y signal.
+
+ Strip peaks, then perform a linear regression.
+ """
+ bg = strip_bg(x, y,
+ width=CONFIG["StripWidth"],
+ niter=CONFIG["StripIterations"])
+ n = float(len(bg))
+ Sy = numpy.sum(bg)
+ Sx = float(numpy.sum(x))
+ Sxx = float(numpy.sum(x * x))
+ Sxy = float(numpy.sum(x * bg))
+
+ deno = n * Sxx - (Sx * Sx)
+ if deno != 0:
+ bg = (Sxx * Sy - Sx * Sxy) / deno
+ slope = (n * Sxy - Sx * Sy) / deno
+ else:
+ bg = 0.0
+ slope = 0.0
+ estimated_par = [bg, slope]
+ # code = 0: FREE
+ constraints = [[0, 0, 0], [0, 0, 0]]
+ return estimated_par, constraints
+
+
+def estimate_strip(x, y):
+ """Estimation function for strip parameters.
+
+ Return parameters as defined in CONFIG dict,
+ set constraints to FIXED.
+ """
+ estimated_par = [CONFIG["StripWidth"],
+ CONFIG["StripIterations"]]
+ constraints = numpy.zeros((len(estimated_par), 3), numpy.float64)
+ # code = 3: FIXED
+ constraints[0][0] = 3
+ constraints[1][0] = 3
+ return estimated_par, constraints
+
+
+def estimate_snip(x, y):
+ """Estimation function for snip parameters.
+
+ Return parameters as defined in CONFIG dict,
+ set constraints to FIXED.
+ """
+ estimated_par = [CONFIG["SnipWidth"]]
+ constraints = numpy.zeros((len(estimated_par), 3), numpy.float64)
+ # code = 3: FIXED
+ constraints[0][0] = 3
+ return estimated_par, constraints
+
+
+def poly(x, y, *pars):
+ """Order n polynomial.
+ The order of the polynomial is defined by the number of
+ coefficients (``*pars``).
+
+ """
+ p = numpy.poly1d(pars)
+ return p(x)
+
+
+def estimate_poly(x, y, deg=2):
+ """Estimate polynomial coefficients.
+
+ """
+ # extract bg signal with strip, to estimate polynomial on background
+ if CONFIG["EstimatePolyOnStrip"]:
+ y = strip_bg(x, y,
+ CONFIG["StripWidth"],
+ CONFIG["StripIterations"])
+ pcoeffs = numpy.polyfit(x, y, deg)
+ cons = numpy.zeros((deg + 1, 3), numpy.float64)
+ return pcoeffs, cons
+
+
+def estimate_quadratic_poly(x, y):
+ """Estimate quadratic polynomial coefficients.
+ """
+ return estimate_poly(x, y, deg=2)
+
+
+def estimate_cubic_poly(x, y):
+ """Estimate cubic polynomial coefficients.
+ """
+ return estimate_poly(x, y, deg=3)
+
+
+def estimate_quartic_poly(x, y):
+ """Estimate degree 4 polynomial coefficients.
+ """
+ return estimate_poly(x, y, deg=4)
+
+
+def estimate_quintic_poly(x, y):
+ """Estimate degree 5 polynomial coefficients.
+ """
+ return estimate_poly(x, y, deg=5)
+
+
+def configure(**kw):
+ """Update the CONFIG dict
+ """
+ # inspect **kw to find known keys, update them in CONFIG
+ for key in CONFIG:
+ if key in kw:
+ CONFIG[key] = kw[key]
+
+ return CONFIG
+
+
+THEORY = OrderedDict(
+ (('No Background',
+ FitTheory(
+ description="No background function",
+ function=lambda x, y0: numpy.zeros_like(x),
+ parameters=[],
+ is_background=True)),
+ ('Constant',
+ FitTheory(
+ description='Constant background',
+ function=lambda x, y0, c: c * numpy.ones_like(x),
+ parameters=['Constant', ],
+ estimate=lambda x, y: ([min(y)], [[0, 0, 0]]),
+ is_background=True)),
+ ('Linear',
+ FitTheory(
+ description="Linear background, parameters 'Constant' and"
+ " 'Slope'",
+ function=lambda x, y0, a, b: a + b * x,
+ parameters=['Constant', 'Slope'],
+ estimate=estimate_linear,
+ configure=configure,
+ is_background=True)),
+ ('Strip',
+ FitTheory(
+ description="Compute background using a strip filter\n"
+ "Parameters 'StripWidth', 'StripIterations'",
+ function=strip_bg,
+ parameters=['StripWidth', 'StripIterations'],
+ estimate=estimate_strip,
+ configure=configure,
+ is_background=True)),
+ ('Snip',
+ FitTheory(
+ description="Compute background using a snip filter\n"
+ "Parameter 'SnipWidth'",
+ function=snip_bg,
+ parameters=['SnipWidth'],
+ estimate=estimate_snip,
+ configure=configure,
+ is_background=True)),
+ ('Degree 2 Polynomial',
+ FitTheory(
+ description="Quadratic polynomial background, Parameters "
+ "'a', 'b' and 'c'\ny = a*x^2 + b*x +c",
+ function=poly,
+ parameters=['a', 'b', 'c'],
+ estimate=estimate_quadratic_poly,
+ configure=configure,
+ is_background=True)),
+ ('Degree 3 Polynomial',
+ FitTheory(
+ description="Cubic polynomial background, Parameters "
+ "'a', 'b', 'c' and 'd'\n"
+ "y = a*x^3 + b*x^2 + c*x + d",
+ function=poly,
+ parameters=['a', 'b', 'c', 'd'],
+ estimate=estimate_cubic_poly,
+ configure=configure,
+ is_background=True)),
+ ('Degree 4 Polynomial',
+ FitTheory(
+ description="Quartic polynomial background\n"
+ "y = a*x^4 + b*x^3 + c*x^2 + d*x + e",
+ function=poly,
+ parameters=['a', 'b', 'c', 'd', 'e'],
+ estimate=estimate_quartic_poly,
+ configure=configure,
+ is_background=True)),
+ ('Degree 5 Polynomial',
+ FitTheory(
+ description="Quaintic polynomial background\n"
+ "y = a*x^5 + b*x^4 + c*x^3 + d*x^2 + e*x + f",
+ function=poly,
+ parameters=['a', 'b', 'c', 'd', 'e', 'f'],
+ estimate=estimate_quintic_poly,
+ configure=configure,
+ is_background=True))))
diff --git a/src/silx/math/fit/filters.pyx b/src/silx/math/fit/filters.pyx
new file mode 100644
index 0000000..da1f6f5
--- /dev/null
+++ b/src/silx/math/fit/filters.pyx
@@ -0,0 +1,416 @@
+# coding: utf-8
+#/*##########################################################################
+# Copyright (C) 2016-2018 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+#############################################################################*/
+"""This module provides background extraction functions and smoothing
+functions. These functions are extracted from PyMca module SpecFitFuns.
+
+Index of background extraction functions:
+------------------------------------------
+
+ - :func:`strip`
+ - :func:`snip1d`
+ - :func:`snip2d`
+ - :func:`snip3d`
+
+Smoothing functions:
+--------------------
+
+ - :func:`savitsky_golay`
+ - :func:`smooth1d`
+ - :func:`smooth2d`
+ - :func:`smooth3d`
+
+References:
+-----------
+
+.. [Morhac97] Miroslav Morháč et al.
+ Background elimination methods for multidimensional coincidence γ-ray spectra.
+ Nucl. Instruments and Methods in Physics Research A401 (1997) 113-132.
+ https://doi.org/10.1016/S0168-9002(97)01023-1
+
+.. [Ryan88] C.G. Ryan et al.
+ SNIP, a statistics-sensitive background treatment for the quantitative analysis of PIXE spectra in geoscience applications.
+ Nucl. Instruments and Methods in Physics Research B34 (1988) 396-402*.
+ https://doi.org/10.1016/0168-583X(88)90063-8
+
+API documentation:
+-------------------
+
+"""
+
+__authors__ = ["P. Knobel"]
+__license__ = "MIT"
+__date__ = "15/05/2017"
+
+import logging
+import numpy
+
+_logger = logging.getLogger(__name__)
+
+cimport cython
+cimport silx.math.fit.filters_wrapper as filters_wrapper
+
+
+def strip(data, w=1, niterations=1000, factor=1.0, anchors=None):
+ """Extract background from data using the strip algorithm, as explained at
+ http://pymca.sourceforge.net/stripbackground.html.
+
+ In its simplest implementation it is just as an iterative procedure
+ depending on two parameters. These parameters are the strip background
+ width ``w``, and the number of iterations. At each iteration, if the
+ contents of channel ``i``, ``y(i)``, is above the average of the contents
+ of the channels at ``w`` channels of distance, ``y(i-w)`` and
+ ``y(i+w)``, ``y(i)`` is replaced by the average.
+ At the end of the process we are left with something that resembles a spectrum
+ in which the peaks have been stripped.
+
+ :param data: Data array
+ :type data: numpy.ndarray
+ :param w: Strip width
+ :param niterations: number of iterations
+ :param factor: scaling factor applied to the average of ``y(i-w)`` and
+ ``y(i+w)`` before comparing to ``y(i)``
+ :param anchors: Array of anchors, indices of points that will not be
+ modified during the stripping procedure.
+ :return: Data with peaks stripped away
+ """
+ cdef:
+ double[::1] input_c
+ double[::1] output
+ long[::1] anchors_c
+
+ if not isinstance(data, numpy.ndarray):
+ if not hasattr(data, "__len__"):
+ raise TypeError("data must be a sequence (list, tuple) " +
+ "or a numpy array")
+ data_shape = (len(data), )
+ else:
+ data_shape = data.shape
+
+ input_c = numpy.array(data,
+ copy=True,
+ dtype=numpy.float64,
+ order='C').reshape(-1)
+
+ output = numpy.empty(shape=(input_c.size,),
+ dtype=numpy.float64)
+
+ if anchors is not None and len(anchors):
+ # numpy.int_ is the same as C long (http://docs.scipy.org/doc/numpy/user/basics.types.html)
+ anchors_c = numpy.array(anchors,
+ copy=False,
+ dtype=numpy.int_,
+ order='C')
+ len_anchors = anchors_c.size
+ else:
+ # Make a dummy length-1 array, because if I use shape=(0,) I get the error
+ # IndexError: Out of bounds on buffer access (axis 0)
+ anchors_c = numpy.empty(shape=(1,),
+ dtype=numpy.int_)
+ len_anchors = 0
+
+
+ status = filters_wrapper.strip(&input_c[0], input_c.size,
+ factor, niterations, w,
+ &anchors_c[0], len_anchors, &output[0])
+
+ return numpy.asarray(output).reshape(data_shape)
+
+
+def snip1d(data, snip_width):
+ """Estimate the baseline (background) of a 1D data vector by clipping peaks.
+
+ Implementation of the algorithm SNIP in 1D is described in [Morhac97]_.
+ The original idea for 1D and the low-statistics-digital-filter (lsdf) comes
+ from [Ryan88]_.
+
+ :param data: Data array, preferably 1D and of type *numpy.float64*.
+ Else, the data array will be flattened and converted to
+ *dtype=numpy.float64* prior to applying the snip filter.
+ :type data: numpy.ndarray
+ :param snip_width: Width of the snip operator, in number of samples.
+ A sample will be iteratively compared to it's neighbors up to a
+ distance of ``snip_width`` samples. This parameters has a direct
+ influence on the speed of the algorithm.
+ :type width: int
+ :return: Baseline of the input array, as an array of the same shape.
+ :rtype: numpy.ndarray
+ """
+ cdef:
+ double[::1] data_c
+
+ if not isinstance(data, numpy.ndarray):
+ if not hasattr(data, "__len__"):
+ raise TypeError("data must be a sequence (list, tuple) " +
+ "or a numpy array")
+ data_shape = (len(data), )
+ else:
+ data_shape = data.shape
+
+ data_c = numpy.array(data,
+ copy=True,
+ dtype=numpy.float64,
+ order='C').reshape(-1)
+
+ filters_wrapper.snip1d(&data_c[0], data_c.size, snip_width)
+
+ return numpy.asarray(data_c).reshape(data_shape)
+
+
+def snip2d(data, snip_width):
+ """Estimate the baseline (background) of a 2D data signal by clipping peaks.
+
+ Implementation of the algorithm SNIP in 2D described in [Morhac97]_.
+
+ :param data: 2D array
+ :type data: numpy.ndarray
+ :param width: Width of the snip operator, in number of samples. A wider
+ snip operator will result in a smoother result (lower frequency peaks
+ will be clipped), and a longer computation time.
+ :type width: int
+ :return: Baseline of the input array, as an array of the same shape.
+ :rtype: numpy.ndarray
+ """
+ cdef:
+ double[::1] data_c
+
+ if not isinstance(data, numpy.ndarray):
+ if not hasattr(data, "__len__") or not hasattr(data[0], "__len__"):
+ raise TypeError("data must be a 2D sequence (list, tuple) " +
+ "or a 2D numpy array")
+ nrows = len(data)
+ ncolumns = len(data[0])
+ data_shape = (len(data), len(data[0]))
+
+ else:
+ data_shape = data.shape
+ nrows = data_shape[0]
+ if len(data_shape) == 2:
+ ncolumns = data_shape[1]
+ else:
+ raise TypeError("data array must be 2-dimensional")
+
+ data_c = numpy.array(data,
+ copy=True,
+ dtype=numpy.float64,
+ order='C').reshape(-1)
+
+ filters_wrapper.snip2d(&data_c[0], nrows, ncolumns, snip_width)
+
+ return numpy.asarray(data_c).reshape(data_shape)
+
+
+def snip3d(data, snip_width):
+ """Estimate the baseline (background) of a 3D data signal by clipping peaks.
+
+ Implementation of the algorithm SNIP in 3D described in [Morhac97]_.
+
+ :param data: 3D array
+ :type data: numpy.ndarray
+ :param width: Width of the snip operator, in number of samples. A wider
+ snip operator will result in a smoother result (lower frequency peaks
+ will be clipped), and a longer computation time.
+ :type width: int
+
+ :return: Baseline of the input array, as an array of the same shape.
+ :rtype: numpy.ndarray
+ """
+ cdef:
+ double[::1] data_c
+
+ if not isinstance(data, numpy.ndarray):
+ if not hasattr(data, "__len__") or not hasattr(data[0], "__len__") or\
+ not hasattr(data[0][0], "__len__"):
+ raise TypeError("data must be a 3D sequence (list, tuple) " +
+ "or a 3D numpy array")
+ nx = len(data)
+ ny = len(data[0])
+ nz = len(data[0][0])
+ data_shape = (len(data), len(data[0]), len(data[0][0]))
+ else:
+ data_shape = data.shape
+ nrows = data_shape[0]
+ if len(data_shape) == 3:
+ nx = data_shape[0]
+ ny = data_shape[1]
+ nz = data_shape[2]
+ else:
+ raise TypeError("data array must be 3-dimensional")
+
+ data_c = numpy.array(data,
+ copy=True,
+ dtype=numpy.float64,
+ order='C').reshape(-1)
+
+ filters_wrapper.snip3d(&data_c[0], nx, ny, nz, snip_width)
+
+ return numpy.asarray(data_c).reshape(data_shape)
+
+
+def savitsky_golay(data, npoints=5):
+ """Smooth a curve using a Savitsky-Golay filter.
+
+ :param data: Input data
+ :type data: 1D numpy array
+ :param npoints: Size of the smoothing operator in number of samples
+ Must be between 3 and 100.
+ :return: Smoothed data
+ """
+ cdef:
+ double[::1] data_c
+ double[::1] output
+
+ data_c = numpy.array(data,
+ dtype=numpy.float64,
+ order='C').reshape(-1)
+
+ output = numpy.empty(shape=(data_c.size,),
+ dtype=numpy.float64)
+
+ status = filters_wrapper.SavitskyGolay(&data_c[0], data_c.size,
+ npoints, &output[0])
+
+ if status:
+ _logger.error("Smoothing failed. Check that npoints is greater " +
+ "than 3 and smaller than 100.")
+
+ return numpy.asarray(output).reshape(data.shape)
+
+
+def smooth1d(data):
+ """Simple smoothing for 1D data.
+
+ For a data array :math:`y` of length :math:`n`, the smoothed array
+ :math:`ys` is calculated as a weighted average of neighboring samples:
+
+ :math:`ys_0 = 0.75 y_0 + 0.25 y_1`
+
+ :math:`ys_i = 0.25 (y_{i-1} + 2 y_i + y_{i+1})` for :math:`0 < i < n-1`
+
+ :math:`ys_{n-1} = 0.25 y_{n-2} + 0.75 y_{n-1}`
+
+
+ :param data: 1D data array
+ :type data: numpy.ndarray
+ :return: Smoothed data
+ :rtype: numpy.ndarray(dtype=numpy.float64)
+ """
+ cdef:
+ double[::1] data_c
+
+ if not isinstance(data, numpy.ndarray):
+ if not hasattr(data, "__len__"):
+ raise TypeError("data must be a sequence (list, tuple) " +
+ "or a numpy array")
+ data_shape = (len(data), )
+ else:
+ data_shape = data.shape
+
+ data_c = numpy.array(data,
+ copy=True,
+ dtype=numpy.float64,
+ order='C').reshape(-1)
+
+ filters_wrapper.smooth1d(&data_c[0], data_c.size)
+
+ return numpy.asarray(data_c).reshape(data_shape)
+
+
+def smooth2d(data):
+ """Simple smoothing for 2D data:
+ :func:`smooth1d` is applied succesively along both axis
+
+ :param data: 2D data array
+ :type data: numpy.ndarray
+ :return: Smoothed data
+ :rtype: numpy.ndarray(dtype=numpy.float64)
+ """
+ cdef:
+ double[::1] data_c
+
+ if not isinstance(data, numpy.ndarray):
+ if not hasattr(data, "__len__") or not hasattr(data[0], "__len__"):
+ raise TypeError("data must be a 2D sequence (list, tuple) " +
+ "or a 2D numpy array")
+ nrows = len(data)
+ ncolumns = len(data[0])
+ data_shape = (len(data), len(data[0]))
+
+ else:
+ data_shape = data.shape
+ nrows = data_shape[0]
+ if len(data_shape) == 2:
+ ncolumns = data_shape[1]
+ else:
+ raise TypeError("data array must be 2-dimensional")
+
+ data_c = numpy.array(data,
+ copy=True,
+ dtype=numpy.float64,
+ order='C').reshape(-1)
+
+ filters_wrapper.smooth2d(&data_c[0], nrows, ncolumns)
+
+ return numpy.asarray(data_c).reshape(data_shape)
+
+
+def smooth3d(data):
+ """Simple smoothing for 3D data:
+ :func:`smooth2d` is applied on each 2D slice of the data volume along all
+ 3 axis
+
+ :param data: 2D data array
+ :type data: numpy.ndarray
+ :return: Smoothed data
+ :rtype: numpy.ndarray(dtype=numpy.float64)
+ """
+ cdef:
+ double[::1] data_c
+
+ if not isinstance(data, numpy.ndarray):
+ if not hasattr(data, "__len__") or not hasattr(data[0], "__len__") or\
+ not hasattr(data[0][0], "__len__"):
+ raise TypeError("data must be a 3D sequence (list, tuple) " +
+ "or a 3D numpy array")
+ nx = len(data)
+ ny = len(data[0])
+ nz = len(data[0][0])
+ data_shape = (len(data), len(data[0]), len(data[0][0]))
+ else:
+ data_shape = data.shape
+ nrows = data_shape[0]
+ if len(data_shape) == 3:
+ nx = data_shape[0]
+ ny = data_shape[1]
+ nz = data_shape[2]
+ else:
+ raise TypeError("data array must be 3-dimensional")
+
+ data_c = numpy.array(data,
+ copy=True,
+ dtype=numpy.float64,
+ order='C').reshape(-1)
+
+ filters_wrapper.smooth3d(&data_c[0], nx, ny, nz)
+
+ return numpy.asarray(data_c).reshape(data_shape)
diff --git a/src/silx/math/fit/filters/include/filters.h b/src/silx/math/fit/filters/include/filters.h
new file mode 100644
index 0000000..1ee9a95
--- /dev/null
+++ b/src/silx/math/fit/filters/include/filters.h
@@ -0,0 +1,45 @@
+/*##########################################################################
+# Copyright (C) 2016 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
+
+#ifndef FITFILTERS_H
+#define FITFILTERS_H
+
+/* Background functions */
+void snip1d(double *data, int size, int width);
+//void snip1d_multiple(double *data, int n_channels, int snip_width, int n_spectra);
+void snip2d(double *data, int nrows, int ncolumns, int width);
+void snip3d(double *data, int nx, int ny, int nz, int width);
+
+int strip(double* input, long len_input, double c, long niter, int deltai,
+ long* anchors, long len_anchors, double* output);
+
+/* Smoothing functions */
+
+int SavitskyGolay(double* input, long len_input, int npoints, double* output);
+
+void smooth1d(double *data, int size);
+void smooth2d(double *data, int size0, int size1);
+void smooth3d(double *data, int size0, int size1, int size2);
+
+
+#endif /* #define FITFILTERS_H */
diff --git a/src/silx/math/fit/filters/src/smoothnd.c b/src/silx/math/fit/filters/src/smoothnd.c
new file mode 100644
index 0000000..cb96961
--- /dev/null
+++ b/src/silx/math/fit/filters/src/smoothnd.c
@@ -0,0 +1,317 @@
+#/*##########################################################################
+#
+# Copyright (c) 2004-2016 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+#############################################################################*/
+#include <stdlib.h>
+#include <string.h>
+#include <math.h>
+#define MIN(x, y) (((x) < (y)) ? (x) : (y))
+#define MAX(x, y) (((x) > (y)) ? (x) : (y))
+
+#define MAX_SAVITSKY_GOLAY_WIDTH 101
+#define MIN_SAVITSKY_GOLAY_WIDTH 3
+
+/* Wrapped functions */
+void smooth1d(double *data, int size);
+void smooth2d(double *data, int size0, int size1);
+void smooth3d(double *data, int size0, int size1, int size2);
+int SavitskyGolay(double* input, long len_input, int npoints, double* output);
+
+/* Internal functions */
+long index2d(long row_idx, long col_idx, long ncols);
+long index3d(long x_idx, long y_idx, long z_idx, long ny, long nz);
+void smooth1d_rows(double *data, long nrows, long ncols);
+void smooth1d_cols(double *data, long nrows, long ncols);
+void smooth1d_x(double *data, long nx, long ny, long nz);
+void smooth1d_y(double *data, long nx, long ny, long nz);
+void smooth1d_z(double *data, long nx, long ny, long nz);
+void smooth2d_yzslice(double *data, long nx, long ny, long nz);
+void smooth2d_xzslice(double *data, long nx, long ny, long nz);
+void smooth2d_xyslice(double *data, long nx, long ny, long nz);
+
+
+/* Simple smoothing of a 1D array */
+void smooth1d(double *data, int size)
+{
+ long i;
+ double prev_sample;
+ double next_sample;
+
+ if (size < 3)
+ {
+ return;
+ }
+ prev_sample = data[0];
+ for (i=0; i<(size-1); i++)
+ {
+ next_sample = 0.25 * (prev_sample + 2 * data[i] + data[i+1]);
+ prev_sample = data[i];
+ data[i] = next_sample;
+ }
+ data[size-1] = 0.25 * prev_sample + 0.75 * data[size-1];
+ return;
+}
+
+/* Smoothing of a 2D array*/
+void smooth2d(double *data, int nrows, int ncols)
+{
+ /* smooth the first dimension (rows) */
+ smooth1d_rows(data, nrows, ncols);
+
+ /* smooth the 2nd dimension */
+ smooth1d_cols(data, nrows, ncols);
+}
+
+/* Smoothing of a 3D array */
+void smooth3d(double *data, int nx, int ny, int nz)
+{
+ smooth2d_xyslice(data, nx, ny, nz);
+ smooth2d_xzslice(data, nx, ny, nz);
+ smooth2d_yzslice(data, nx, ny, nz);
+}
+
+/* 1D Savitsky-Golay smoothing */
+int SavitskyGolay(double* input, long len_input, int npoints, double* output)
+{
+
+ //double dpoints = 5.;
+ double coeff[MAX_SAVITSKY_GOLAY_WIDTH];
+ int i, j, m;
+ double dhelp, den;
+ double *data;
+
+ memcpy(output, input, len_input * sizeof(double));
+
+ if (!(npoints % 2)) npoints +=1;
+
+ if((npoints < MIN_SAVITSKY_GOLAY_WIDTH) || (len_input < npoints) || \
+ (npoints > MAX_SAVITSKY_GOLAY_WIDTH))
+ {
+ /* do not smooth data */
+ return 1;
+ }
+
+ /* calculate the coefficients */
+ m = (int) (npoints/2);
+ den = (double) ((2*m-1) * (2*m+1) * (2*m + 3));
+ for (i=0; i<= m; i++){
+ coeff[m+i] = (double) (3 * (3*m*m + 3*m - 1 - 5*i*i ));
+ coeff[m-i] = coeff[m+i];
+ }
+
+ /* simple smoothing at the beginning */
+ for (j=0; j<=(int)(npoints/3); j++)
+ {
+ smooth1d(output, m);
+ }
+
+ /* simple smoothing at the end */
+ for (j=0; j<=(int)(npoints/3); j++)
+ {
+ smooth1d((output+len_input-m-1), m);
+ }
+
+ /*one does not need the whole spectrum buffer, but code is clearer */
+ data = (double *) malloc(len_input * sizeof(double));
+ memcpy(data, output, len_input * sizeof(double));
+
+ /* the actual SG smoothing in the middle */
+ for (i=m; i<(len_input-m); i++){
+ dhelp = 0;
+ for (j=-m;j<=m;j++) {
+ dhelp += coeff[m+j] * (*(data+i+j));
+ }
+ if(dhelp > 0.0){
+ *(output+i) = dhelp / den;
+ }
+ }
+ free(data);
+ return (0);
+}
+
+/*********************/
+/* Utility functions */
+/*********************/
+
+long index2d(long row_idx, long col_idx, long ncols)
+{
+ return (row_idx*ncols+col_idx);
+}
+
+/* Apply smooth 1d on all rows in a 2D array*/
+void smooth1d_rows(double *data, long nrows, long ncols)
+{
+ long row_idx;
+
+ for (row_idx=0; row_idx < nrows; row_idx++)
+ {
+ smooth1d(&data[row_idx * ncols], ncols);
+ }
+}
+
+/* Apply smooth 1d on all columns in a 2D array*/
+void smooth1d_cols(double *data, long nrows, long ncols)
+{
+ long row_idx, col_idx;
+ long this_idx2d, next_idx2d;
+ double prev_sample;
+ double next_sample;
+
+ for (col_idx=0; col_idx < ncols; col_idx++)
+ {
+ prev_sample = data[index2d(0, col_idx, ncols)];
+ for (row_idx=0; row_idx<(nrows-1); row_idx++)
+ {
+ this_idx2d = index2d(row_idx, col_idx, ncols);
+ next_idx2d = index2d(row_idx+1, col_idx, ncols);
+
+ next_sample = 0.25 * (prev_sample + \
+ 2 * data[this_idx2d] + \
+ data[next_idx2d]);
+ prev_sample = data[this_idx2d];
+ data[this_idx2d] = next_sample;
+ }
+
+ this_idx2d = index2d(nrows-1, col_idx, ncols);
+ data[this_idx2d] = 0.25 * prev_sample + 0.75 * data[this_idx2d];
+ }
+}
+
+long index3d(long x_idx, long y_idx, long z_idx, long ny, long nz)
+{
+ return ((x_idx*ny + y_idx) * nz + z_idx);
+}
+
+/* Apply smooth 1d along first dimension in a 3D array*/
+void smooth1d_x(double *data, long nx, long ny, long nz)
+{
+ long x_idx, y_idx, z_idx;
+ long this_idx3d, next_idx3d;
+ double prev_sample;
+ double next_sample;
+
+ for (y_idx=0; y_idx < ny; y_idx++)
+ {
+ for (z_idx=0; z_idx < nz; z_idx++)
+ {
+ prev_sample = data[index3d(0, y_idx, z_idx, ny, nz)];
+ for (x_idx=0; x_idx<(nx-1); x_idx++)
+ {
+ this_idx3d = index3d(x_idx, y_idx, z_idx, ny, nz);
+ next_idx3d = index3d(x_idx+1, y_idx, z_idx, ny, nz);
+
+ next_sample = 0.25 * (prev_sample + \
+ 2 * data[this_idx3d] + \
+ data[next_idx3d]);
+ prev_sample = data[this_idx3d];
+ data[this_idx3d] = next_sample;
+ }
+
+ this_idx3d = index3d(nx-1, y_idx, z_idx, ny, nz);
+ data[this_idx3d] = 0.25 * prev_sample + 0.75 * data[this_idx3d];
+ }
+ }
+}
+
+/* Apply smooth 1d along second dimension in a 3D array*/
+void smooth1d_y(double *data, long nx, long ny, long nz)
+{
+ long x_idx, y_idx, z_idx;
+ long this_idx3d, next_idx3d;
+ double prev_sample;
+ double next_sample;
+
+ for (x_idx=0; x_idx < nx; x_idx++)
+ {
+ for (z_idx=0; z_idx < nz; z_idx++)
+ {
+ prev_sample = data[index3d(x_idx, 0, z_idx, ny, nz)];
+ for (y_idx=0; y_idx<(ny-1); y_idx++)
+ {
+ this_idx3d = index3d(x_idx, y_idx, z_idx, ny, nz);
+ next_idx3d = index3d(x_idx, y_idx+1, z_idx, ny, nz);
+
+ next_sample = 0.25 * (prev_sample + \
+ 2 * data[this_idx3d] + \
+ data[next_idx3d]);
+ prev_sample = data[this_idx3d];
+ data[this_idx3d] = next_sample;
+ }
+
+ this_idx3d = index3d(x_idx, ny-1, z_idx, ny, nz);
+ data[this_idx3d] = 0.25 * prev_sample + 0.75 * data[this_idx3d];
+ }
+ }
+}
+
+/* Apply smooth 1d along third dimension in a 3D array*/
+void smooth1d_z(double *data, long nx, long ny, long nz)
+{
+ long x_idx, y_idx;
+ long idx3d_first_sample;
+
+ for (x_idx=0; x_idx < nx; x_idx++)
+ {
+ for (y_idx=0; y_idx < ny; y_idx++)
+ {
+ idx3d_first_sample = index3d(x_idx, y_idx, 0, ny, nz);
+ /*We can use regular 1D smoothing function because z samples
+ are contiguous in memory*/
+ smooth1d(&data[idx3d_first_sample], nz);
+ }
+ }
+}
+
+/* 2D smoothing of a YZ slice in a 3D volume*/
+void smooth2d_yzslice(double *data, long nx, long ny, long nz)
+{
+ long x_idx;
+ long slice_size = ny * nz;
+
+ /* a YZ slice is a "normal" 2D array of memory-contiguous data*/
+ for (x_idx=0; x_idx < nx; x_idx++)
+ {
+ smooth2d(&data[x_idx*slice_size], ny, nz);
+ }
+}
+
+/* 2D smoothing of a XZ slice in a 3D volume*/
+void smooth2d_xzslice(double *data, long nx, long ny, long nz)
+{
+
+ /* smooth along the first dimension */
+ smooth1d_x(data, nx, ny, nz);
+
+ /* smooth along the third dimension */
+ smooth1d_z(data, nx, ny, nz);
+}
+
+/* 2D smoothing of a XY slice in a 3D volume*/
+void smooth2d_xyslice(double *data, long nx, long ny, long nz)
+{
+ /* smooth along the first dimension */
+ smooth1d_x(data, nx, ny, nz);
+
+ /* smooth along the second dimension */
+ smooth1d_y(data, nx, ny, nz);
+}
+
diff --git a/src/silx/math/fit/filters/src/snip1d.c b/src/silx/math/fit/filters/src/snip1d.c
new file mode 100644
index 0000000..994a272
--- /dev/null
+++ b/src/silx/math/fit/filters/src/snip1d.c
@@ -0,0 +1,149 @@
+#/*##########################################################################
+# Copyright (c) 2004-2016 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+#############################################################################*/
+/*
+ Implementation of the algorithm SNIP in 1D described in
+ Miroslav Morhac et al. Nucl. Instruments and Methods in Physics Research A401 (1997) 113-132.
+
+ The original idea for 1D and the low-statistics-digital-filter (lsdf) come from
+ C.G. Ryan et al. Nucl. Instruments and Methods in Physics Research B34 (1988) 396-402.
+*/
+#include <stdlib.h>
+#include <string.h>
+#include <math.h>
+
+#define MIN(x, y) (((x) < (y)) ? (x) : (y))
+#define MAX(x, y) (((x) > (y)) ? (x) : (y))
+
+void lls(double *data, int size);
+void lls_inv(double *data, int size);
+void snip1d(double *data, int n_channels, int snip_width);
+void snip1d_multiple(double *data, int n_channels, int snip_width, int n_spectra);
+void lsdf(double *data, int size, int fwhm, double f, double A, double M, double ratio);
+
+void lls(double *data, int size)
+{
+ int i;
+ for (i=0; i< size; i++)
+ {
+ data[i] = log(log(sqrt(data[i]+1.0)+1.0)+1.0);
+ }
+}
+
+void lls_inv(double *data, int size)
+{
+ int i;
+ double tmp;
+ for (i=0; i< size; i++)
+ {
+ /* slightly different than the published formula because
+ with the original formula:
+
+ tmp = exp(exp(data[i]-1.0)-1.0);
+ data[i] = tmp * tmp - 1.0;
+
+ one does not recover the original data */
+
+ tmp = exp(exp(data[i])-1.0)-1.0;
+ data[i] = tmp * tmp - 1.0;
+ }
+}
+
+void lsdf(double *data, int size, int fwhm, double f, double A, double M, double ratio)
+{
+ int channel, i, j;
+ double L, R, S;
+ int width;
+ double dhelp;
+
+ width = (int) (f * fwhm);
+ for (channel=width; channel<(size-width); channel++)
+ {
+ i = width;
+ while(i>0)
+ {
+ L=0;
+ R=0;
+ for(j=channel-i; j<channel; j++)
+ {
+ L += data[j];
+ }
+ for(j=channel+1; j<channel+i; j++)
+ {
+ R += data[j];
+ }
+ S = data[channel] + L + R;
+ if (S<M)
+ {
+ data[channel] = S /(2*i+1);
+ break;
+ }
+ dhelp = (R+1)/(L+1);
+ if ((dhelp < ratio) && (dhelp > (1/ratio)))
+ {
+ if (S<(A*sqrt(data[channel])))
+ {
+ data[channel] = S /(2*i+1);
+ break;
+ }
+ }
+ i=i-1;
+ }
+ }
+}
+
+
+void snip1d(double *data, int n_channels, int snip_width)
+{
+ snip1d_multiple(data, n_channels, snip_width, 1);
+}
+
+void snip1d_multiple(double *data, int n_channels, int snip_width, int n_spectra)
+{
+ int i;
+ int j;
+ int p;
+ int offset;
+ double *w;
+
+ i = (int) (0.5 * snip_width);
+ /* lsdf(data, size, i, 1.5, 75., 10., 1.3); */
+
+ w = (double *) malloc(n_channels * sizeof(double));
+
+ for (j=0; j < n_spectra; j++)
+ {
+ offset = j * n_channels;
+ for (p = snip_width; p > 0; p--)
+ {
+ for (i=p; i<(n_channels - p); i++)
+ {
+ w[i] = MIN(data[i + offset], 0.5*(data[i + offset - p] + data[ i + offset + p]));
+ }
+ for (i=p; i<(n_channels - p); i++)
+ {
+ data[i+offset] = w[i];
+ }
+ }
+ }
+ free(w);
+}
diff --git a/src/silx/math/fit/filters/src/snip2d.c b/src/silx/math/fit/filters/src/snip2d.c
new file mode 100644
index 0000000..235759c
--- /dev/null
+++ b/src/silx/math/fit/filters/src/snip2d.c
@@ -0,0 +1,96 @@
+#/*##########################################################################
+#
+# The PyMca X-Ray Fluorescence Toolkit
+#
+# Copyright (c) 2004-2014 European Synchrotron Radiation Facility
+#
+# This file is part of the PyMca X-ray Fluorescence Toolkit developed at
+# the ESRF by the Software group.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+#############################################################################*/
+/*
+ Implementation of the algorithm SNIP in 2D described in
+ Miroslav Morhac et al. Nucl. Instruments and Methods in Physics Research A401 (1997) 113-132.
+*/
+#include <stdlib.h>
+#include <string.h>
+#include <math.h>
+#define MIN(x, y) (((x) < (y)) ? (x) : (y))
+#define MAX(x, y) (((x) > (y)) ? (x) : (y))
+
+void lls(double *data, int size);
+void lls_inv(double *data, int size);
+
+void snip2d(double *data, int nrows, int ncolumns, int width)
+{
+ int i, j;
+ int p;
+ int size;
+ double *w;
+ double P1, P2, P3, P4;
+ double S1, S2, S3, S4;
+ double dhelp;
+ int iminuspxncolumns; /* (i-p) * ncolumns */
+ int ixncolumns; /* i * ncolumns */
+ int ipluspxncolumns; /* (i+p) * ncolumns */
+
+ size = nrows * ncolumns;
+ w = (double *) malloc(size * sizeof(double));
+
+ for (p=width; p > 0; p--)
+ {
+ for (i=p; i<(nrows-p); i++)
+ {
+ iminuspxncolumns = (i-p) * ncolumns;
+ ixncolumns = i * ncolumns;
+ ipluspxncolumns = (i+p) * ncolumns;
+ for (j=p; j<(ncolumns-p); j++)
+ {
+ P4 = data[ iminuspxncolumns + (j-p)]; /* P4 = data[i-p][j-p] */
+ S4 = data[ iminuspxncolumns + j]; /* S4 = data[i-p][j] */
+ P2 = data[ iminuspxncolumns + (j+p)]; /* P2 = data[i-p][j+p] */
+ S3 = data[ ixncolumns + (j-p)]; /* S3 = data[i][j-p] */
+ S2 = data[ ixncolumns + (j+p)]; /* S2 = data[i][j+p] */
+ P3 = data[ ipluspxncolumns + (j-p)]; /* P3 = data[i+p][j-p] */
+ S1 = data[ ipluspxncolumns + j]; /* S1 = data[i+p][j] */
+ P1 = data[ ipluspxncolumns + (j+p)]; /* P1 = data[i+p][j+p] */
+ dhelp = 0.5*(P1+P3);
+ S1 = MAX(S1, dhelp) - dhelp;
+ dhelp = 0.5*(P1+P2);
+ S2 = MAX(S2, dhelp) - dhelp;
+ dhelp = 0.5*(P3+P4);
+ S3 = MAX(S3, dhelp) - dhelp;
+ dhelp = 0.5*(P2+P4);
+ S4 = MAX(S4, dhelp) - dhelp;
+ w[ixncolumns + j] = MIN(data[ixncolumns + j], 0.5 * (S1+S2+S3+S4) + 0.25 * (P1+P2+P3+P4));
+ }
+ }
+ for (i=p; i<(nrows-p); i++)
+ {
+ ixncolumns = i * ncolumns;
+ for (j=p; j<(ncolumns-p); j++)
+ {
+ data[ixncolumns + j] = w[ixncolumns + j];
+ }
+ }
+ }
+ free(w);
+}
diff --git a/src/silx/math/fit/filters/src/snip3d.c b/src/silx/math/fit/filters/src/snip3d.c
new file mode 100644
index 0000000..cf48ee4
--- /dev/null
+++ b/src/silx/math/fit/filters/src/snip3d.c
@@ -0,0 +1,186 @@
+#/*##########################################################################
+#
+# The PyMca X-Ray Fluorescence Toolkit
+#
+# Copyright (c) 2004-2014 European Synchrotron Radiation Facility
+#
+# This file is part of the PyMca X-ray Fluorescence Toolkit developed at
+# the ESRF by the Software group.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+#############################################################################*/
+/*
+ Implementation of the algorithm SNIP in 3D described in
+ Miroslav Morhac et al. Nucl. Instruments and Methods in Physics Research A401 (1997) 113-132.
+*/
+
+#include <stdlib.h>
+#include <string.h>
+#include <math.h>
+#define MIN(x, y) (((x) < (y)) ? (x) : (y))
+#define MAX(x, y) (((x) > (y)) ? (x) : (y))
+
+void lls(double *data, int size);
+void lls_inv(double *data, int size);
+
+void snip3d(double *data, int nx, int ny, int nz, int width)
+{
+ int i, j, k;
+ int p;
+ int size;
+ double *w;
+ double P1, P2, P3, P4, P5, P6, P7, P8;
+ double R1, R2, R3, R4, R5, R6;
+ double S1, S2, S3, S4, S5, S6, S7, S8, S9, S10, S11, S12;
+ double dhelp;
+ long ioffset;
+ long iplus;
+ long imin;
+ long joffset;
+ long jplus;
+ long jmin;
+
+ size = nx * ny * nz;
+ w = (double *) malloc(size * sizeof(double));
+
+ for (p=width; p > 0; p--)
+ {
+ for (i=p; i<(nx-p); i++)
+ {
+ ioffset = i * ny * nz;
+ iplus = (i + p) * ny * nz;
+ imin = (i - p) * ny * nz;
+ for (j=p; j<(ny-p); j++)
+ {
+ joffset = j * nz;
+ jplus = (j + p) * nz;
+ jmin = (j - p) * nz;
+ for (k=p; k<(nz-p); k++)
+ {
+ P1 = data[iplus + jplus + k-p]; /* P1 = data[i+p][j+p][k-p] */
+ P2 = data[imin + jplus + k-p]; /* P2 = data[i-p][j+p][k-p] */
+ P3 = data[iplus + jmin + k-p]; /* P3 = data[i+p][j-p][k-p] */
+ P4 = data[imin + jmin + k-p]; /* P4 = data[i-p][j-p][k-p] */
+ P5 = data[iplus + jplus + k+p]; /* P5 = data[i+p][j+p][k+p] */
+ P6 = data[imin + jplus + k+p]; /* P6 = data[i-p][j+p][k+p] */
+ P7 = data[imin + jmin + k+p]; /* P7 = data[i-p][j-p][k+p] */
+ P8 = data[iplus + jmin + k+p]; /* P8 = data[i+p][j-p][k+p] */
+
+ S1 = data[iplus + joffset + k-p]; /* S1 = data[i+p][j][k-p] */
+ S2 = data[ioffset + jmin + k-p]; /* S2 = data[i][j+p][k-p] */
+ S3 = data[imin + joffset + k-p]; /* S3 = data[i-p][j][k-p] */
+ S4 = data[ioffset + jmin + k-p]; /* S4 = data[i][j-p][k-p] */
+ S5 = data[imin + joffset + k+p]; /* S5 = data[i-p][j][k+p] */
+ S6 = data[ioffset + jplus + k+p]; /* S6 = data[i][j+p][k+p] */
+ S7 = data[imin + joffset + k+p]; /* S7 = data[i-p][j][k+p] */
+ S8 = data[ioffset + jmin + k+p]; /* S8 = data[i][j-p][k+p] */
+ S9 = data[imin + jplus + k]; /* S9 = data[i-p][j+p][k] */
+ S10 = data[imin + jmin + k]; /* S10 = data[i-p][j-p][k] */
+ S11 = data[iplus + jmin + k]; /* S11 = data[i+p][j-p][k] */
+ S12 = data[iplus + jplus + k]; /* S12 = data[i+p][j+p][k] */
+
+ R1 = data[ioffset + joffset + k-p]; /* R1 = data[i][j][k-p] */
+ R2 = data[ioffset + joffset + k+p]; /* R2 = data[i][j][k+p] */
+ R3 = data[imin + joffset + k]; /* R3 = data[i-p][j][k] */
+ R4 = data[iplus + joffset + k]; /* R4 = data[i+p][j][k] */
+ R5 = data[ioffset + jplus + k]; /* R5 = data[i][j+p][k] */
+ R6 = data[ioffset + jmin + k]; /* R6 = data[i][j-p][k] */
+
+ dhelp = 0.5*(P1+P3);
+ S1 = MAX(S1, dhelp) - dhelp;
+
+ dhelp = 0.5*(P1+P2);
+ S2 = MAX(S2, dhelp) - dhelp;
+
+ dhelp = 0.5*(P2+P4);
+ S3 = MAX(S3, dhelp) - dhelp;
+
+ dhelp = 0.5*(P3+P4);
+ S4 = MAX(S4, dhelp) - dhelp;
+
+ dhelp = 0.5*(P5+P8); /* Different from paper (P5+P7) but according to drawing */
+ S5 = MAX(S5, dhelp) - dhelp;
+
+ dhelp = 0.5*(P5+P6);
+ S6 = MAX(S6, dhelp) - dhelp;
+
+ dhelp = 0.5*(P6+P7); /* Different from paper (P6+P8) but according to drawing */
+ S7 = MAX(S7, dhelp) - dhelp;
+
+ dhelp = 0.5*(P7+P8);
+ S8 = MAX(S8, dhelp) - dhelp;
+
+ dhelp = 0.5*(P2+P6);
+ S9 = MAX(S9, dhelp) - dhelp;
+
+ dhelp = 0.5*(P4+P7); /* Different from paper (P4+P8) but according to drawing */
+ S10 = MAX(S10, dhelp) - dhelp;
+
+ dhelp = 0.5*(P3+P8); /* Different from paper (P1+P5) but according to drawing */
+ S11 = MAX(S11, dhelp) - dhelp;
+
+ dhelp = 0.5*(P1+P5); /* Different from paper (P3+P7) but according to drawing */
+ S12 = MAX(S12, dhelp) - dhelp;
+
+ /* The published formulae correspond to have:
+ P7 and P8 interchanged, and S11 and S12 interchanged
+ with respect to the published drawing */
+
+ dhelp = 0.5 * (S1+S2+S3+S4) + 0.25 * (P1+P2+P3+P4);
+ R1 = MAX(R1, dhelp) - dhelp;
+
+ dhelp = 0.5 * (S5+S6+S7+S8) + 0.25 * (P5+P6+P7+P8);
+ R2 = MAX(R2, dhelp) - dhelp;
+
+ dhelp = 0.5 * (S3+S7+S9+S10) + 0.25 * (P2+P4+P6+P7); /* Again same P7 and P8 change */
+ R3 = MAX(R3, dhelp) - dhelp;
+
+ dhelp = 0.5 * (S1+S5+S11+S12) + 0.25 * (P1+P3+P5+P8); /* Again same P7 and P8 change */
+ R4 = MAX(R4, dhelp) - dhelp;
+
+ dhelp = 0.5 * (S2+S6+S9+S12) + 0.25 * (P1+P2+P5+P6); /* Again same S11 and S12 change */
+ R5 = MAX(R5, dhelp) - dhelp;
+
+ dhelp = 0.5 * (S4+S8+S10+S11) + 0.25 * (P3+P4+P7+P8); /* Again same S11 and S12 change */
+ R6 = MAX(R6, dhelp) - dhelp;
+
+ dhelp = 0.5 * (R1 + R2 + R3 + R4 + R5 + R6) +\
+ 0.25 * (S1 + S2 + S3 + S4 + S5 + S6) +\
+ 0.25 * (S7 + S8 + S9 + S10 + S11 + S12) +\
+ 0.125 * (P1 + P2 + P3 + P4 + P5 + P6 + P7 + P8);
+ w[ioffset + joffset + k] = MIN(data[ioffset + joffset + k], dhelp);
+ }
+ }
+ }
+ for (i=p; i<(nx-p); i++)
+ {
+ ioffset = i * ny * nz;
+ for (j=p; j<(ny-p); j++)
+ {
+ joffset = j * nz;
+ for (k=p; k<(nz-p); j++)
+ {
+ data[ioffset + joffset + k] = w[ioffset + joffset + k];
+ }
+ }
+ }
+ }
+ free(w);
+}
diff --git a/src/silx/math/fit/filters/src/strip.c b/src/silx/math/fit/filters/src/strip.c
new file mode 100644
index 0000000..dec0742
--- /dev/null
+++ b/src/silx/math/fit/filters/src/strip.c
@@ -0,0 +1,118 @@
+#/*##########################################################################
+# Copyright (c) 2004-2016 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+#############################################################################*/
+/*
+ This file provides a background strip function, to isolate low frequency
+ background signal from a spectrum (and later substact it from the signal
+ to be left only with the peaks to be fitted).
+
+ It is adapted from PyMca source file "SpecFitFuns.c". The main difference
+ with the original code is that this code does not handle the python
+ wrapping, which is done elsewhere using cython.
+
+ Authors: V.A. Sole, P. Knobel
+ License: MIT
+ Last modified: 17/06/2016
+*/
+
+#include <string.h>
+
+#include <stdio.h>
+
+/* strip(double* input, double c, long niter, double* output)
+
+ The strip background is probably PyMca's most popular background model.
+
+ In its simplest implementation it is just as an iterative procedure depending
+ on two parameters. These parameters are the strip background width w, and the
+ strip background number of iterations. At each iteration, if the contents of
+ channel i, y(i), is above the average of the contents of the channels at w
+ channels of distance, y(i-w) and y(i+w), y(i) is replaced by the average.
+ At the end of the process we are left with something that resembles a spectrum
+ in which the peaks have been "stripped".
+
+ Parameters:
+
+ - input: Input data array
+ - c: scaling factor applied to the average of y(i-w) and y(i+w) before
+ comparing to y(i)
+ - niter: number of iterations
+ - deltai: operator width (in number of channels)
+ - anchors: Array of anchors, indices of points that will not be
+ modified during the stripping procedure.
+ - output: output array
+
+*/
+int strip(double* input, long len_input,
+ double c, long niter, int deltai,
+ long* anchors, long len_anchors,
+ double* output)
+{
+ long iter_index, array_index, anchor_index, anchor;
+ int anchor_nearby_flag;
+ double t_mean;
+
+ memcpy(output, input, len_input * sizeof(double));
+
+ if (deltai <=0) deltai = 1;
+
+ if (len_input < (2*deltai+1)) return(-1);
+
+ if (len_anchors > 0) {
+ for (iter_index = 0; iter_index < niter; iter_index++) {
+ for (array_index = deltai; array_index < len_input - deltai; array_index++) {
+ /* if index is within +- deltai of an anchor, don't do anything */
+ anchor_nearby_flag = 0;
+ for (anchor_index=0; anchor_index<len_anchors; anchor_index++)
+ {
+ anchor = anchors[anchor_index];
+ if (array_index > (anchor - deltai) && array_index < (anchor + deltai))
+ {
+ anchor_nearby_flag = 1;
+ break;
+ }
+ }
+ /* skip this array_index index */
+ if (anchor_nearby_flag) {
+ continue;
+ }
+
+ t_mean = 0.5 * (input[array_index-deltai] + input[array_index+deltai]);
+ if (input[array_index] > (t_mean * c))
+ output[array_index] = t_mean;
+ }
+ memcpy(input, output, len_input * sizeof(double));
+ }
+ }
+ else {
+ for (iter_index = 0; iter_index < niter; iter_index++) {
+ for (array_index=deltai; array_index < len_input - deltai; array_index++) {
+ t_mean = 0.5 * (input[array_index-deltai] + input[array_index+deltai]);
+
+ if (input[array_index] > (t_mean * c))
+ output[array_index] = t_mean;
+ }
+ memcpy(input, output, len_input * sizeof(double));
+ }
+ }
+ return(0);
+}
diff --git a/src/silx/math/fit/filters_wrapper.pxd b/src/silx/math/fit/filters_wrapper.pxd
new file mode 100644
index 0000000..e4f7c72
--- /dev/null
+++ b/src/silx/math/fit/filters_wrapper.pxd
@@ -0,0 +1,71 @@
+# coding: utf-8
+#/*##########################################################################
+# Copyright (C) 2016 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+#############################################################################*/
+
+__authors__ = ["P. Knobel"]
+__license__ = "MIT"
+__date__ = "22/06/2016"
+
+cimport cython
+
+cdef extern from "filters.h":
+ void snip1d(double *data,
+ int size,
+ int width)
+
+ void snip2d(double *data,
+ int nrows,
+ int ncolumns,
+ int width)
+
+ void snip3d(double *data,
+ int nx,
+ int ny,
+ int nz,
+ int width)
+
+ int strip(double* input,
+ long len_input,
+ double c,
+ long niter,
+ int deltai,
+ long* anchors,
+ long len_anchors,
+ double* output)
+
+ int SavitskyGolay(double* input,
+ long len_input,
+ int npoints,
+ double* output)
+
+ void smooth1d(double *data,
+ int size)
+
+ void smooth2d(double *data,
+ int size0,
+ int size1)
+
+ void smooth3d(double *data,
+ int size0,
+ int size1,
+ int size2)
diff --git a/src/silx/math/fit/fitmanager.py b/src/silx/math/fit/fitmanager.py
new file mode 100644
index 0000000..226e047
--- /dev/null
+++ b/src/silx/math/fit/fitmanager.py
@@ -0,0 +1,1087 @@
+# coding: utf-8
+# /*#########################################################################
+#
+# Copyright (c) 2004-2021 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ##########################################################################*/
+"""
+This module provides a tool to perform advanced fitting. The actual fit relies
+on :func:`silx.math.fit.leastsq`.
+
+This module deals with:
+
+ - handling of the model functions (using a set of default functions or
+ loading custom user functions)
+ - handling of estimation function, that are used to determine the number
+ of parameters to be fitted for functions with unknown number of
+ parameters (such as the sum of a variable number of gaussian curves),
+ and find reasonable initial parameters for input to the iterative
+ fitting algorithm
+ - handling of custom derivative functions that can be passed as a
+ parameter to :func:`silx.math.fit.leastsq`
+ - providing different background models
+
+"""
+from collections import OrderedDict
+import logging
+import numpy
+from numpy.linalg.linalg import LinAlgError
+import os
+import sys
+
+from .filters import strip, smooth1d
+from .leastsq import leastsq
+from .fittheory import FitTheory
+from . import bgtheories
+
+
+__authors__ = ["V.A. Sole", "P. Knobel"]
+__license__ = "MIT"
+__date__ = "16/01/2017"
+
+_logger = logging.getLogger(__name__)
+
+
+class FitManager(object):
+ """
+ Fit functions manager
+
+ :param x: Abscissa data. If ``None``, :attr:`xdata` is set to
+ ``numpy.array([0.0, 1.0, 2.0, ..., len(y)-1])``
+ :type x: Sequence or numpy array or None
+ :param y: The dependant data ``y = f(x)``. ``y`` must have the same
+ shape as ``x`` if ``x`` is not ``None``.
+ :type y: Sequence or numpy array or None
+ :param sigmay: The uncertainties in the ``ydata`` array. These can be
+ used as weights in the least-squares problem, if ``weight_flag``
+ is ``True``.
+ If ``None``, the uncertainties are assumed to be 1, unless
+ ``weight_flag`` is ``True``, in which case the square-root
+ of ``y`` is used.
+ :type sigmay: Sequence or numpy array or None
+ :param weight_flag: If this parameter is ``True`` and ``sigmay``
+ uncertainties are not specified, the square root of ``y`` is used
+ as weights in the least-squares problem. If ``False``, the
+ uncertainties are set to 1.
+ :type weight_flag: boolean
+ """
+ def __init__(self, x=None, y=None, sigmay=None, weight_flag=False):
+ """
+ """
+ self.fitconfig = {
+ 'WeightFlag': weight_flag,
+ 'fitbkg': 'No Background',
+ 'fittheory': None,
+ # Next few parameters are defined for compatibility with legacy theories
+ # which take the background as argument for their estimation function
+ 'StripWidth': 2,
+ 'StripIterations': 5000,
+ 'StripThresholdFactor': 1.0,
+ 'SmoothingFlag': False
+ }
+ """Dictionary of fit configuration parameters.
+ These parameters can be modified using the :meth:`configure` method.
+
+ Keys are:
+
+ - 'fitbkg': name of the function used for fitting a low frequency
+ background signal
+ - 'FwhmPoints': default full width at half maximum value for the
+ peaks'.
+ - 'Sensitivity': Sensitivity parameter for the peak detection
+ algorithm (:func:`silx.math.fit.peak_search`)
+ """
+
+ self.theories = OrderedDict()
+ """Dictionary of fit theories, defining functions to be fitted
+ to individual peaks.
+
+ Keys are descriptive theory names (e.g "Gaussians" or "Step up").
+ Values are :class:`silx.math.fit.fittheory.FitTheory` objects with
+ the following attributes:
+
+ - *"function"* is the fit function for an individual peak
+ - *"parameters"* is a sequence of parameter names
+ - *"estimate"* is the parameter estimation function
+ - *"configure"* is the function returning the configuration dict
+ for the theory in the format described in the :attr:` fitconfig`
+ documentation
+ - *"derivative"* (optional) is a custom derivative function, whose
+ signature is described in the documentation of
+ :func:`silx.math.fit.leastsq.leastsq`
+ (``model_deriv(xdata, parameters, index)``).
+ - *"description"* is a description string
+ """
+
+ self.selectedtheory = None
+ """Name of currently selected theory. This name matches a key in
+ :attr:`theories`."""
+
+ self.bgtheories = OrderedDict()
+ """Dictionary of background theories.
+
+ See :attr:`theories` for documentation on theories.
+ """
+
+ # Load default theories (constant, linear, strip)
+ self.loadbgtheories(bgtheories)
+
+ self.selectedbg = 'No Background'
+ """Name of currently selected background theory. This name must be
+ an existing key in :attr:`bgtheories`."""
+
+ self.fit_results = []
+ """This list stores detailed information about all fit parameters.
+ It is initialized in :meth:`estimate` and completed with final fit
+ values in :meth:`runfit`.
+
+ Each fit parameter is stored as a dictionary with following fields:
+
+ - 'name': Parameter name.
+ - 'estimation': Estimated value.
+ - 'group': Group number. Group 0 corresponds to the background
+ function parameters. Group ``n`` (for ``n>0``) corresponds to
+ the fit function parameters for the n-th peak.
+ - 'code': Constraint code
+
+ - 0 - FREE
+ - 1 - POSITIVE
+ - 2 - QUOTED
+ - 3 - FIXED
+ - 4 - FACTOR
+ - 5 - DELTA
+ - 6 - SUM
+
+ - 'cons1':
+
+ - Ignored if 'code' is FREE, POSITIVE or FIXED.
+ - Min value of the parameter if code is QUOTED
+ - Index of fitted parameter to which 'cons2' is related
+ if code is FACTOR, DELTA or SUM.
+
+ - 'cons2':
+
+ - Ignored if 'code' is FREE, POSITIVE or FIXED.
+ - Max value of the parameter if QUOTED
+ - Factor to apply to related parameter with index 'cons1' if
+ 'code' is FACTOR
+ - Difference with parameter with index 'cons1' if
+ 'code' is DELTA
+ - Sum obtained when adding parameter with index 'cons1' if
+ 'code' is SUM
+
+ - 'fitresult': Fitted value.
+ - 'sigma': Standard deviation for the parameter estimate
+ - 'xmin': Lower limit of the ``x`` data range on which the fit
+ was performed
+ - 'xmax': Upeer limit of the ``x`` data range on which the fit
+ was performed
+ """
+
+ self.parameter_names = []
+ """This list stores all fit parameter names: background function
+ parameters and fit function parameters for every peak. It is filled
+ in :meth:`estimate`.
+
+ It is the responsibility of the estimate function defined in
+ :attr:`theories` to determine how many parameters are needed,
+ based on how many peaks are detected and how many parameters are needed
+ to fit an individual peak.
+ """
+
+ self.setdata(x, y, sigmay)
+
+ ##################
+ # Public methods #
+ ##################
+ def addbackground(self, bgname, bgtheory):
+ """Add a new background theory to dictionary :attr:`bgtheories`.
+
+ :param bgname: String with the name describing the function
+ :param bgtheory: :class:`FitTheory` object
+ :type bgtheory: :class:`silx.math.fit.fittheory.FitTheory`
+ """
+ self.bgtheories[bgname] = bgtheory
+
+ def addtheory(self, name, theory=None,
+ function=None, parameters=None,
+ estimate=None, configure=None, derivative=None,
+ description=None, pymca_legacy=False):
+ """Add a new theory to dictionary :attr:`theories`.
+
+ You can pass a name and a :class:`FitTheory` object as arguments, or
+ alternatively provide all arguments necessary to instantiate a new
+ :class:`FitTheory` object.
+
+ See :meth:`loadtheories` for more information on estimation functions,
+ configuration functions and custom derivative functions.
+
+ :param name: String with the name describing the function
+ :param theory: :class:`FitTheory` object, defining a fit function and
+ associated information (estimation function, description…).
+ If this parameter is provided, all other parameters, except for
+ ``name``, are ignored.
+ :type theory: :class:`silx.math.fit.fittheory.FitTheory`
+ :param callable function: Mandatory argument if ``theory`` is not provided.
+ See documentation for :attr:`silx.math.fit.fittheory.FitTheory.function`.
+ :param List[str] parameters: Mandatory argument if ``theory`` is not provided.
+ See documentation for :attr:`silx.math.fit.fittheory.FitTheory.parameters`.
+ :param callable estimate: See documentation for
+ :attr:`silx.math.fit.fittheory.FitTheory.estimate`
+ :param callable configure: See documentation for
+ :attr:`silx.math.fit.fittheory.FitTheory.configure`
+ :param callable derivative: See documentation for
+ :attr:`silx.math.fit.fittheory.FitTheory.derivative`
+ :param str description: See documentation for
+ :attr:`silx.math.fit.fittheory.FitTheory.description`
+ :param config_widget: See documentation for
+ :attr:`silx.math.fit.fittheory.FitTheory.config_widget`
+ :param bool pymca_legacy: See documentation for
+ :attr:`silx.math.fit.fittheory.FitTheory.pymca_legacy`
+ """
+ if theory is not None:
+ self.theories[name] = theory
+
+ elif function is not None and parameters is not None:
+ self.theories[name] = FitTheory(
+ description=description,
+ function=function,
+ parameters=parameters,
+ estimate=estimate,
+ configure=configure,
+ derivative=derivative,
+ pymca_legacy=pymca_legacy
+ )
+
+ else:
+ raise TypeError("You must supply a FitTheory object or define " +
+ "a fit function and its parameters.")
+
+ def addbgtheory(self, name, theory=None,
+ function=None, parameters=None,
+ estimate=None, configure=None,
+ derivative=None, description=None):
+ """Add a new theory to dictionary :attr:`bgtheories`.
+
+ You can pass a name and a :class:`FitTheory` object as arguments, or
+ alternatively provide all arguments necessary to instantiate a new
+ :class:`FitTheory` object.
+
+ :param name: String with the name describing the function
+ :param theory: :class:`FitTheory` object, defining a fit function and
+ associated information (estimation function, description…).
+ If this parameter is provided, all other parameters, except for
+ ``name``, are ignored.
+ :type theory: :class:`silx.math.fit.fittheory.FitTheory`
+ :param function function: Mandatory argument if ``theory`` is not provided.
+ See documentation for :attr:`silx.math.fit.fittheory.FitTheory.function`.
+ :param list[str] parameters: Mandatory argument if ``theory`` is not provided.
+ See documentation for :attr:`silx.math.fit.fittheory.FitTheory.parameters`.
+ :param function estimate: See documentation for
+ :attr:`silx.math.fit.fittheory.FitTheory.estimate`
+ :param function configure: See documentation for
+ :attr:`silx.math.fit.fittheory.FitTheory.configure`
+ :param function derivative: See documentation for
+ :attr:`silx.math.fit.fittheory.FitTheory.derivative`
+ :param str description: See documentation for
+ :attr:`silx.math.fit.fittheory.FitTheory.description`
+ """
+ if theory is not None:
+ self.bgtheories[name] = theory
+
+ elif function is not None and parameters is not None:
+ self.bgtheories[name] = FitTheory(
+ description=description,
+ function=function,
+ parameters=parameters,
+ estimate=estimate,
+ configure=configure,
+ derivative=derivative,
+ is_background=True
+ )
+
+ else:
+ raise TypeError("You must supply a FitTheory object or define " +
+ "a background function and its parameters.")
+
+ def configure(self, **kw):
+ """Configure the current theory by filling or updating the
+ :attr:`fitconfig` dictionary.
+ Call the custom configuration function, if any. This allows the user
+ to modify the behavior of the custom fit function or the custom
+ estimate function.
+
+ This methods accepts only named parameters. All ``**kw`` parameters
+ are expected to be fields of :attr:`fitconfig` to be updated, unless
+ they have a special meaning for the custom configuration function
+ of the currently selected theory..
+
+ This method returns the modified config dictionary returned by the
+ custom configuration function.
+ """
+ # inspect **kw to find known keys, update them in self.fitconfig
+ for key in self.fitconfig:
+ if key in kw:
+ self.fitconfig[key] = kw[key]
+
+ # initialize dict with existing config dict
+ result = {}
+ result.update(self.fitconfig)
+
+ if "WeightFlag" in kw:
+ if kw["WeightFlag"]:
+ self.enableweight()
+ else:
+ self.disableweight()
+
+ if self.selectedtheory is None:
+ return result
+
+ # Apply custom configuration function
+ custom_config_fun = self.theories[self.selectedtheory].configure
+ if custom_config_fun is not None:
+ result.update(custom_config_fun(**kw))
+
+ custom_bg_config_fun = self.bgtheories[self.selectedbg].configure
+ if custom_bg_config_fun is not None:
+ result.update(custom_bg_config_fun(**kw))
+
+ # Update self.fitconfig with custom config
+ for key in self.fitconfig:
+ if key in result:
+ self.fitconfig[key] = result[key]
+
+ result.update(self.fitconfig)
+ return result
+
+ def estimate(self, callback=None):
+ """
+ Fill :attr:`fit_results` with an estimation of the fit parameters.
+
+ At first, the background parameters are estimated, if a background
+ model has been specified.
+ Then, a custom estimation function related to the model function is
+ called.
+
+ This process determines the number of needed fit parameters and
+ provides an initial estimation for them, to serve as an input for the
+ actual iterative fitting performed in :meth:`runfit`.
+
+ :param callback: Optional callback function, conforming to the
+ signature ``callback(data)`` with ``data`` being a dictionary.
+ This callback function is called before and after the estimation
+ process, and is given a dictionary containing the values of
+ :attr:`state` (``'Estimate in progress'`` or ``'Ready to Fit'``)
+ and :attr:`chisq`.
+ This is used for instance in :mod:`silx.gui.fit.FitWidget` to
+ update a widget displaying a status message.
+ :return: Estimated parameters
+ """
+ self.state = 'Estimate in progress'
+ self.chisq = None
+
+ if callback is not None:
+ callback(data={'chisq': self.chisq,
+ 'status': self.state})
+
+ CONS = {0: 'FREE',
+ 1: 'POSITIVE',
+ 2: 'QUOTED',
+ 3: 'FIXED',
+ 4: 'FACTOR',
+ 5: 'DELTA',
+ 6: 'SUM',
+ 7: 'IGNORE'}
+
+ # Filter-out not finite data
+ xwork = self.xdata[self._finite_mask]
+ ywork = self.ydata[self._finite_mask]
+
+ # estimate the background
+ bg_params, bg_constraints = self.estimate_bkg(xwork, ywork)
+
+ # estimate the function
+ try:
+ fun_params, fun_constraints = self.estimate_fun(xwork, ywork)
+ except LinAlgError:
+ self.state = 'Estimate failed'
+ if callback is not None:
+ callback(data={'status': self.state})
+ raise
+
+ # build the names
+ self.parameter_names = []
+
+ for bg_param_name in self.bgtheories[self.selectedbg].parameters:
+ self.parameter_names.append(bg_param_name)
+
+ fun_param_names = self.theories[self.selectedtheory].parameters
+ param_index, peak_index = 0, 0
+ while param_index < len(fun_params):
+ peak_index += 1
+ for fun_param_name in fun_param_names:
+ self.parameter_names.append(fun_param_name + "%d" % peak_index)
+ param_index += 1
+
+ self.fit_results = []
+ nb_fun_params_per_group = len(fun_param_names)
+ group_number = 0
+ xmin = min(xwork)
+ xmax = max(xwork)
+ nb_bg_params = len(bg_params)
+ for (pindex, pname) in enumerate(self.parameter_names):
+ # First come background parameters
+ if pindex < nb_bg_params:
+ estimation_value = bg_params[pindex]
+ constraint_code = CONS[int(bg_constraints[pindex][0])]
+ cons1 = bg_constraints[pindex][1]
+ cons2 = bg_constraints[pindex][2]
+ # then come peak function parameters
+ else:
+ fun_param_index = pindex - nb_bg_params
+
+ # increment group_number for each new fitted peak
+ if (fun_param_index % nb_fun_params_per_group) == 0:
+ group_number += 1
+
+ estimation_value = fun_params[fun_param_index]
+ constraint_code = CONS[int(fun_constraints[fun_param_index][0])]
+ # cons1 is the index of another fit parameter. In the global
+ # fit_results, we must adjust the index to account for the bg
+ # params added to the start of the list.
+ cons1 = fun_constraints[fun_param_index][1]
+ if constraint_code in ["FACTOR", "DELTA", "SUM"]:
+ cons1 += nb_bg_params
+ cons2 = fun_constraints[fun_param_index][2]
+
+ self.fit_results.append({'name': pname,
+ 'estimation': estimation_value,
+ 'group': group_number,
+ 'code': constraint_code,
+ 'cons1': cons1,
+ 'cons2': cons2,
+ 'fitresult': 0.0,
+ 'sigma': 0.0,
+ 'xmin': xmin,
+ 'xmax': xmax})
+
+ self.state = 'Ready to Fit'
+ self.chisq = None
+ self.niter = 0
+
+ if callback is not None:
+ callback(data={'chisq': self.chisq,
+ 'status': self.state})
+ return numpy.append(bg_params, fun_params)
+
+ def fit(self):
+ """Convenience method to call :meth:`estimate` followed by :meth:`runfit`.
+
+ :return: Output of :meth:`runfit`"""
+ self.estimate()
+ return self.runfit()
+
+ def gendata(self, x=None, paramlist=None, estimated=False):
+ """Return a data array using the currently selected fit function
+ and the fitted parameters.
+
+ :param x: Independent variable where the function is calculated.
+ If ``None``, use :attr:`xdata`.
+ :param paramlist: List of dictionaries, each dictionary item being a
+ fit parameter. The dictionary's format is documented in
+ :attr:`fit_results`.
+ If ``None`` (default), use parameters from :attr:`fit_results`.
+ :param estimated: If *True*, use estimated parameters.
+ :return: :meth:`fitfunction` calculated for parameters whose code is
+ not set to ``"IGNORE"``.
+
+ This calculates :meth:`fitfunction` on `x` data using fit parameters
+ from a list of parameter dictionaries, if field ``code`` is not set
+ to ``"IGNORE"``.
+ """
+ x = self.xdata if x is None else numpy.array(x, copy=False)
+
+ if paramlist is None:
+ paramlist = self.fit_results
+ active_params = []
+ for param in paramlist:
+ if param['code'] not in ['IGNORE', 7]:
+ if not estimated:
+ active_params.append(param['fitresult'])
+ else:
+ active_params.append(param['estimation'])
+
+ # Mask x with not finite (support nD x)
+ finite_mask = numpy.all(numpy.isfinite(x), axis=tuple(range(1, x.ndim)))
+
+ if numpy.all(finite_mask): # All values are finite: fast path
+ return self.fitfunction(numpy.array(x, copy=True), *active_params)
+
+ else: # Only run fitfunction on finite data and complete result with NaNs
+ # Create result with same number as elements as x, filling holes with NaNs
+ result = numpy.full((x.shape[0],), numpy.nan, dtype=numpy.float64)
+ result[finite_mask] = self.fitfunction(
+ numpy.array(x[finite_mask], copy=True), *active_params)
+ return result
+
+ def get_estimation(self):
+ """Return the list of fit parameter names."""
+ if self.state not in ["Ready to fit", "Fit in progress", "Ready"]:
+ _logger.warning("get_estimation() called before estimate() completed")
+ return [param["estimation"] for param in self.fit_results]
+
+ def get_names(self):
+ """Return the list of fit parameter estimations."""
+ if self.state not in ["Ready to fit", "Fit in progress", "Ready"]:
+ msg = "get_names() called before estimate() completed, "
+ msg += "names are not populated at this stage"
+ _logger.warning(msg)
+ return [param["name"] for param in self.fit_results]
+
+ def get_fitted_parameters(self):
+ """Return the list of fitted parameters."""
+ if self.state not in ["Ready"]:
+ msg = "get_fitted_parameters() called before runfit() completed, "
+ msg += "results are not available a this stage"
+ _logger.warning(msg)
+ return [param["fitresult"] for param in self.fit_results]
+
+ def loadtheories(self, theories):
+ """Import user defined fit functions defined in an external Python
+ source file, and save them in :attr:`theories`.
+
+ An example of such a file can be found in the sources of
+ :mod:`silx.math.fit.fittheories`. It must contain a
+ dictionary named ``THEORY`` with the following structure::
+
+ THEORY = {
+ 'theory_name_1':
+ FitTheory(description='Description of theory 1',
+ function=fitfunction1,
+ parameters=('param name 1', 'param name 2', …),
+ estimate=estimation_function1,
+ configure=configuration_function1,
+ derivative=derivative_function1),
+ 'theory_name_2':
+ FitTheory(…),
+ }
+
+ See documentation of :mod:`silx.math.fit.fittheories` and
+ :mod:`silx.math.fit.fittheory` for more
+ information on designing your fit functions file.
+
+ This method can also load user defined functions in the legacy
+ format used in *PyMca*.
+
+ :param theories: Name of python source file, or module containing the
+ definition of fit functions.
+ :raise: ImportError if theories cannot be imported
+ """
+ from types import ModuleType
+ if isinstance(theories, ModuleType):
+ theories_module = theories
+ else:
+ # if theories is not a module, it must be a string
+ string_types = (basestring,) if sys.version_info[0] == 2 else (str,) # noqa
+ if not isinstance(theories, string_types):
+ raise ImportError("theory must be a python module, a module" +
+ "name or a python filename")
+ # if theories is a filename
+ if os.path.isfile(theories):
+ sys.path.append(os.path.dirname(theories))
+ f = os.path.basename(os.path.splitext(theories)[0])
+ theories_module = __import__(f)
+ # if theories is a module name
+ else:
+ theories_module = __import__(theories)
+
+ if hasattr(theories_module, "INIT"):
+ theories.INIT()
+
+ if not hasattr(theories_module, "THEORY"):
+ msg = "File %s does not contain a THEORY dictionary" % theories
+ raise ImportError(msg)
+
+ elif isinstance(theories_module.THEORY, dict):
+ # silx format for theory definition
+ for theory_name, fittheory in list(theories_module.THEORY.items()):
+ self.addtheory(theory_name, fittheory)
+ else:
+ self._load_legacy_theories(theories_module)
+
+ def loadbgtheories(self, theories):
+ """Import user defined background functions defined in an external Python
+ module (source file), and save them in :attr:`theories`.
+
+ An example of such a file can be found in the sources of
+ :mod:`silx.math.fit.fittheories`. It must contain a
+ dictionary named ``THEORY`` with the following structure::
+
+ THEORY = {
+ 'theory_name_1':
+ FitTheory(description='Description of theory 1',
+ function=fitfunction1,
+ parameters=('param name 1', 'param name 2', …),
+ estimate=estimation_function1,
+ configure=configuration_function1,
+ 'theory_name_2':
+ FitTheory(…),
+ }
+
+ See documentation of :mod:`silx.math.fit.bgtheories` and
+ :mod:`silx.math.fit.fittheory` for more
+ information on designing your background functions file.
+
+ :param theories: Module or name of python source file containing the
+ definition of background functions.
+ :raise: ImportError if theories cannot be imported
+ """
+ from types import ModuleType
+ if isinstance(theories, ModuleType):
+ theories_module = theories
+ else:
+ # if theories is not a module, it must be a string
+ string_types = (basestring,) if sys.version_info[0] == 2 else (str,) # noqa
+ if not isinstance(theories, string_types):
+ raise ImportError("theory must be a python module, a module" +
+ "name or a python filename")
+ # if theories is a filename
+ if os.path.isfile(theories):
+ sys.path.append(os.path.dirname(theories))
+ f = os.path.basename(os.path.splitext(theories)[0])
+ theories_module = __import__(f)
+ # if theories is a module name
+ else:
+ theories_module = __import__(theories)
+
+ if hasattr(theories_module, "INIT"):
+ theories.INIT()
+
+ if not hasattr(theories_module, "THEORY"):
+ msg = "File %s does not contain a THEORY dictionary" % theories
+ raise ImportError(msg)
+
+ elif isinstance(theories_module.THEORY, dict):
+ # silx format for theory definition
+ for theory_name, fittheory in list(theories_module.THEORY.items()):
+ self.addbgtheory(theory_name, fittheory)
+
+ def setbackground(self, theory):
+ """Choose a background type from within :attr:`bgtheories`.
+
+ This updates :attr:`selectedbg`.
+
+ :param theory: The name of the background to be used.
+ :raise: KeyError if ``theory`` is not a key of :attr:`bgtheories``.
+ """
+ if theory in self.bgtheories:
+ self.selectedbg = theory
+ else:
+ msg = "No theory with name %s in bgtheories.\n" % theory
+ msg += "Available theories: %s\n" % self.bgtheories.keys()
+ raise KeyError(msg)
+
+ # run configure to apply our fitconfig to the selected theory
+ # through its custom config function
+ self.configure(**self.fitconfig)
+
+ def setdata(self, x, y, sigmay=None, xmin=None, xmax=None):
+ """Set data attributes:
+
+ - ``xdata0``, ``ydata0`` and ``sigmay0`` store the initial data
+ and uncertainties. These attributes are not modified after
+ initialization.
+ - ``xdata``, ``ydata`` and ``sigmay`` store the data after
+ removing values where ``xdata < xmin`` or ``xdata > xmax``.
+ These attributes may be modified at a latter stage by filters.
+
+ :param x: Abscissa data. If ``None``, :attr:`xdata`` is set to
+ ``numpy.array([0.0, 1.0, 2.0, ..., len(y)-1])``
+ :type x: Sequence or numpy array or None
+ :param y: The dependant data ``y = f(x)``. ``y`` must have the same
+ shape as ``x`` if ``x`` is not ``None``.
+ :type y: Sequence or numpy array or None
+ :param sigmay: The uncertainties in the ``ydata`` array. These are
+ used as weights in the least-squares problem.
+ If ``None``, the uncertainties are assumed to be 1.
+ :type sigmay: Sequence or numpy array or None
+ :param xmin: Lower value of x values to use for fitting
+ :param xmax: Upper value of x values to use for fitting
+ """
+ if y is None:
+ self.xdata0 = numpy.array([], numpy.float64)
+ self.ydata0 = numpy.array([], numpy.float64)
+ # self.sigmay0 = numpy.array([], numpy.float64)
+ self.xdata = numpy.array([], numpy.float64)
+ self.ydata = numpy.array([], numpy.float64)
+ # self.sigmay = numpy.array([], numpy.float64)
+
+ else:
+ self.ydata0 = numpy.array(y)
+ self.ydata = numpy.array(y)
+ if x is None:
+ self.xdata0 = numpy.arange(len(self.ydata0))
+ self.xdata = numpy.arange(len(self.ydata0))
+ else:
+ self.xdata0 = numpy.array(x)
+ self.xdata = numpy.array(x)
+
+ # default weight
+ if sigmay is None:
+ self.sigmay0 = None
+ self.sigmay = numpy.sqrt(self.ydata) if self.fitconfig["WeightFlag"] else None
+ else:
+ self.sigmay0 = numpy.array(sigmay)
+ self.sigmay = numpy.array(sigmay) if self.fitconfig["WeightFlag"] else None
+
+ # take the data between limits, using boolean array indexing
+ if (xmin is not None or xmax is not None) and len(self.xdata):
+ xmin = xmin if xmin is not None else min(self.xdata)
+ xmax = xmax if xmax is not None else max(self.xdata)
+ bool_array = (self.xdata >= xmin) & (self.xdata <= xmax)
+ self.xdata = self.xdata[bool_array]
+ self.ydata = self.ydata[bool_array]
+ self.sigmay = self.sigmay[bool_array] if sigmay is not None else None
+
+ self._finite_mask = numpy.logical_and(
+ numpy.all(numpy.isfinite(self.xdata), axis=tuple(range(1, self.xdata.ndim))),
+ numpy.isfinite(self.ydata))
+
+ def enableweight(self):
+ """This method can be called to set :attr:`sigmay`. If :attr:`sigmay0` was filled with
+ actual uncertainties in :meth:`setdata`, use these values.
+ Else, use ``sqrt(self.ydata)``.
+ """
+ if self.sigmay0 is None:
+ self.sigmay = numpy.sqrt(self.ydata) if self.fitconfig["WeightFlag"] else None
+ else:
+ self.sigmay = self.sigmay0
+
+ def disableweight(self):
+ """This method can be called to set :attr:`sigmay` equal to ``None``.
+ As a result, :func:`leastsq` will consider that the weights in the
+ least square problem are 1 for all samples."""
+ self.sigmay = None
+
+ def settheory(self, theory):
+ """Pick a theory from :attr:`theories`.
+
+ :param theory: Name of the theory to be used.
+ :raise: KeyError if ``theory`` is not a key of :attr:`theories`.
+ """
+ if theory is None:
+ self.selectedtheory = None
+ elif theory in self.theories:
+ self.selectedtheory = theory
+ else:
+ msg = "No theory with name %s in theories.\n" % theory
+ msg += "Available theories: %s\n" % self.theories.keys()
+ raise KeyError(msg)
+
+ # run configure to apply our fitconfig to the selected theory
+ # through its custom config function
+ self.configure(**self.fitconfig)
+
+ def runfit(self, callback=None):
+ """Run the actual fitting and fill :attr:`fit_results` with fit results.
+
+ Before running this method, :attr:`fit_results` must already be
+ populated with a list of all parameters and their estimated values.
+ For this, run :meth:`estimate` beforehand.
+
+ :param callback: Optional callback function, conforming to the
+ signature ``callback(data)`` with ``data`` being a dictionary.
+ This callback function is called before and after the estimation
+ process, and is given a dictionary containing the values of
+ :attr:`state` (``'Fit in progress'`` or ``'Ready'``)
+ and :attr:`chisq`.
+ This is used for instance in :mod:`silx.gui.fit.FitWidget` to
+ update a widget displaying a status message.
+ :return: Tuple ``(fitted parameters, uncertainties, infodict)``.
+ *infodict* is the dictionary returned by
+ :func:`silx.math.fit.leastsq` when called with option
+ ``full_output=True``. Uncertainties is a sequence of uncertainty
+ values associated with each fitted parameter.
+ """
+ # self.dataupdate()
+
+ self.state = 'Fit in progress'
+ self.chisq = None
+
+ if callback is not None:
+ callback(data={'chisq': self.chisq,
+ 'status': self.state})
+
+ param_val = []
+ param_constraints = []
+ # Initial values are set to the ones computed in estimate()
+ for param in self.fit_results:
+ param_val.append(param['estimation'])
+ param_constraints.append([param['code'], param['cons1'], param['cons2']])
+
+ # Filter-out not finite data
+ ywork = self.ydata[self._finite_mask]
+ xwork = self.xdata[self._finite_mask]
+
+ try:
+ params, covariance_matrix, infodict = leastsq(
+ self.fitfunction, # bg + actual model function
+ xwork, ywork, param_val,
+ sigma=self.sigmay,
+ constraints=param_constraints,
+ model_deriv=self.theories[self.selectedtheory].derivative,
+ full_output=True, left_derivative=True)
+ except LinAlgError:
+ self.state = 'Fit failed'
+ callback(data={'status': self.state})
+ raise
+
+ sigmas = infodict['uncertainties']
+
+ for i, param in enumerate(self.fit_results):
+ if param['code'] != 'IGNORE':
+ param['fitresult'] = params[i]
+ param['sigma'] = sigmas[i]
+
+ self.chisq = infodict["reduced_chisq"]
+ self.niter = infodict["niter"]
+ self.state = 'Ready'
+
+ if callback is not None:
+ callback(data={'chisq': self.chisq,
+ 'status': self.state})
+
+ return params, sigmas, infodict
+
+ ###################
+ # Private methods #
+ ###################
+ def fitfunction(self, x, *pars):
+ """Function to be fitted.
+
+ This is the sum of the selected background function plus
+ the selected fit model function.
+
+ :param x: Independent variable where the function is calculated.
+ :param pars: Sequence of all fit parameters. The first few parameters
+ are background parameters, then come the peak function parameters.
+ :return: Output of the fit function with ``x`` as input and ``pars``
+ as fit parameters.
+ """
+ result = numpy.zeros(numpy.shape(x), numpy.float64)
+
+ if self.selectedbg is not None:
+ bg_pars_list = self.bgtheories[self.selectedbg].parameters
+ nb_bg_pars = len(bg_pars_list)
+
+ bgfun = self.bgtheories[self.selectedbg].function
+ result += bgfun(x, self.ydata, *pars[0:nb_bg_pars])
+ else:
+ nb_bg_pars = 0
+
+ selectedfun = self.theories[self.selectedtheory].function
+ result += selectedfun(x, *pars[nb_bg_pars:])
+
+ return result
+
+ def estimate_bkg(self, x, y):
+ """Estimate background parameters using the function defined in
+ the current fit configuration.
+
+ To change the selected background model, attribute :attr:`selectdbg`
+ must be changed using method :meth:`setbackground`.
+
+ The actual background function to be used is
+ referenced in :attr:`bgtheories`
+
+ :param x: Sequence of x data
+ :param y: sequence of y data
+ :return: Tuple of two sequences and one data array
+ ``(estimated_param, constraints, bg_data)``:
+
+ - ``estimated_param`` is a list of estimated values for each
+ background parameter.
+ - ``constraints`` is a 2D sequence of dimension ``(n_parameters, 3)``
+
+ - ``constraints[i][0]``: Constraint code.
+ See explanation about codes in :attr:`fit_results`
+
+ - ``constraints[i][1]``
+ See explanation about 'cons1' in :attr:`fit_results`
+ documentation.
+
+ - ``constraints[i][2]``
+ See explanation about 'cons2' in :attr:`fit_results`
+ documentation.
+ """
+ background_estimate_function = self.bgtheories[self.selectedbg].estimate
+ if background_estimate_function is not None:
+ return background_estimate_function(x, y)
+ else:
+ return [], []
+
+ def estimate_fun(self, x, y):
+ """Estimate fit parameters using the function defined in
+ the current fit configuration.
+
+ :param x: Sequence of x data
+ :param y: sequence of y data
+ :param bg: Background signal, to be subtracted from ``y`` before fitting.
+ :return: Tuple of two sequences ``(estimated_param, constraints)``:
+
+ - ``estimated_param`` is a list of estimated values for each
+ background parameter.
+ - ``constraints`` is a 2D sequence of dimension (n_parameters, 3)
+
+ - ``constraints[i][0]``: Constraint code.
+ See explanation about codes in :attr:`fit_results`
+
+ - ``constraints[i][1]``
+ See explanation about 'cons1' in :attr:`fit_results`
+ documentation.
+
+ - ``constraints[i][2]``
+ See explanation about 'cons2' in :attr:`fit_results`
+ documentation.
+ :raise: ``TypeError`` if estimation function is not callable
+
+ """
+ estimatefunction = self.theories[self.selectedtheory].estimate
+ if hasattr(estimatefunction, '__call__'):
+ if not self.theories[self.selectedtheory].pymca_legacy:
+ return estimatefunction(x, y)
+ else:
+ # legacy pymca estimate functions have a different signature
+ if self.fitconfig["fitbkg"] == "No Background":
+ bg = numpy.zeros_like(y)
+ else:
+ if self.fitconfig["SmoothingFlag"]:
+ y = smooth1d(y)
+ bg = strip(y,
+ w=self.fitconfig["StripWidth"],
+ niterations=self.fitconfig["StripIterations"],
+ factor=self.fitconfig["StripThresholdFactor"])
+ # fitconfig can be filled by user defined config function
+ xscaling = self.fitconfig.get('Xscaling', 1.0)
+ yscaling = self.fitconfig.get('Yscaling', 1.0)
+ return estimatefunction(x, y, bg, xscaling, yscaling)
+ else:
+ raise TypeError("Estimation function in attribute " +
+ "theories[%s]" % self.selectedtheory +
+ " must be callable.")
+
+ def _load_legacy_theories(self, theories_module):
+ """Load theories from a custom module in the old PyMca format.
+
+ See PyMca5.PyMcaMath.fitting.SpecfitFunctions for an example.
+ """
+ mandatory_attributes = ["THEORY", "PARAMETERS",
+ "FUNCTION", "ESTIMATE"]
+ err_msg = "Custom fit function file must define: "
+ err_msg += ", ".join(mandatory_attributes)
+ for attr in mandatory_attributes:
+ if not hasattr(theories_module, attr):
+ raise ImportError(err_msg)
+
+ derivative = theories_module.DERIVATIVE if hasattr(theories_module, "DERIVATIVE") else None
+ configure = theories_module.CONFIGURE if hasattr(theories_module, "CONFIGURE") else None
+ estimate = theories_module.ESTIMATE if hasattr(theories_module, "ESTIMATE") else None
+ if isinstance(theories_module.THEORY, (list, tuple)):
+ # multiple fit functions
+ for i in range(len(theories_module.THEORY)):
+ deriv = derivative[i] if derivative is not None else None
+ config = configure[i] if configure is not None else None
+ estim = estimate[i] if estimate is not None else None
+ self.addtheory(theories_module.THEORY[i],
+ FitTheory(
+ theories_module.FUNCTION[i],
+ theories_module.PARAMETERS[i],
+ estim,
+ config,
+ deriv,
+ pymca_legacy=True))
+ else:
+ # single fit function
+ self.addtheory(theories_module.THEORY,
+ FitTheory(
+ theories_module.FUNCTION,
+ theories_module.PARAMETERS,
+ estimate,
+ configure,
+ derivative,
+ pymca_legacy=True))
+
+
+def test():
+ from .functions import sum_gauss
+ from . import fittheories
+ from . import bgtheories
+
+ # Create synthetic data with a sum of gaussian functions
+ x = numpy.arange(1000).astype(numpy.float64)
+
+ p = [1000, 100., 250,
+ 255, 690., 45,
+ 1500, 800.5, 95]
+ y = 0.5 * x + 13 + sum_gauss(x, *p)
+
+ # Fitting
+ fit = FitManager()
+ # more sensitivity necessary to resolve
+ # overlapping peaks at x=690 and x=800.5
+ fit.setdata(x=x, y=y)
+ fit.loadtheories(fittheories)
+ fit.settheory('Gaussians')
+ fit.loadbgtheories(bgtheories)
+ fit.setbackground('Linear')
+ fit.estimate()
+ fit.runfit()
+
+ print("Searched parameters = ", p)
+ print("Obtained parameters : ")
+ dummy_list = []
+ for param in fit.fit_results:
+ print(param['name'], ' = ', param['fitresult'])
+ dummy_list.append(param['fitresult'])
+ print("chisq = ", fit.chisq)
+
+ # Plot
+ constant, slope = dummy_list[:2]
+ p1 = dummy_list[2:]
+ print(p1)
+ y2 = slope * x + constant + sum_gauss(x, *p1)
+
+ try:
+ from silx.gui import qt
+ from silx.gui.plot.PlotWindow import PlotWindow
+ app = qt.QApplication([])
+ pw = PlotWindow(control=True)
+ pw.addCurve(x, y, "Original")
+ pw.addCurve(x, y2, "Fit result")
+ pw.legendsDockWidget.show()
+ pw.show()
+ app.exec()
+ except ImportError:
+ _logger.warning("Could not import qt to display fit result as curve")
+
+
+if __name__ == "__main__":
+ test()
diff --git a/src/silx/math/fit/fittheories.py b/src/silx/math/fit/fittheories.py
new file mode 100644
index 0000000..5461416
--- /dev/null
+++ b/src/silx/math/fit/fittheories.py
@@ -0,0 +1,1374 @@
+# coding: utf-8
+#/*##########################################################################
+#
+# Copyright (c) 2004-2021 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+########################################################################### */
+"""This modules provides a set of fit functions and associated
+estimation functions in a format that can be imported into a
+:class:`silx.math.fit.FitManager` instance.
+
+These functions are well suited for fitting multiple gaussian shaped peaks
+typically found in spectroscopy data. The estimation functions are designed
+to detect how many peaks are present in the data, and provide an initial
+estimate for their height, their center location and their full-width
+at half maximum (fwhm).
+
+The limitation of these estimation algorithms is that only gaussians having a
+similar fwhm can be detected by the peak search algorithm.
+This *search fwhm* can be defined by the user, if
+he knows the characteristics of his data, or can be automatically estimated
+based on the fwhm of the largest peak in the data.
+
+The source code of this module can serve as template for defining your own
+fit functions.
+
+The functions to be imported by :meth:`FitManager.loadtheories` are defined by
+a dictionary :const:`THEORY`: with the following structure::
+
+ from silx.math.fit.fittheory import FitTheory
+
+ THEORY = {
+ 'theory_name_1': FitTheory(
+ description='Description of theory 1',
+ function=fitfunction1,
+ parameters=('param name 1', 'param name 2', …),
+ estimate=estimation_function1,
+ configure=configuration_function1,
+ derivative=derivative_function1),
+
+ 'theory_name_2': FitTheory(…),
+ }
+
+.. note::
+
+ Consider using an OrderedDict instead of a regular dictionary, when
+ defining your own theory dictionary, if the order matters to you.
+ This will likely be the case if you intend to load a selection of
+ functions in a GUI such as :class:`silx.gui.fit.FitManager`.
+
+Theory names can be customized (e.g. ``gauss, lorentz, splitgauss``…).
+
+The mandatory parameters for :class:`FitTheory` are ``function`` and
+``parameters``.
+
+You can also define an ``INIT`` function that will be executed by
+:meth:`FitManager.loadtheories`.
+
+See the documentation of :class:`silx.math.fit.fittheory.FitTheory`
+for more information.
+
+Module members:
+---------------
+"""
+import numpy
+from collections import OrderedDict
+import logging
+
+from silx.math.fit import functions
+from silx.math.fit.peaks import peak_search, guess_fwhm
+from silx.math.fit.filters import strip, savitsky_golay
+from silx.math.fit.leastsq import leastsq
+from silx.math.fit.fittheory import FitTheory
+
+_logger = logging.getLogger(__name__)
+
+__authors__ = ["V.A. Sole", "P. Knobel"]
+__license__ = "MIT"
+__date__ = "15/05/2017"
+
+
+DEFAULT_CONFIG = {
+ 'NoConstraintsFlag': False,
+ 'PositiveFwhmFlag': True,
+ 'PositiveHeightAreaFlag': True,
+ 'SameFwhmFlag': False,
+ 'QuotedPositionFlag': False, # peak not outside data range
+ 'QuotedEtaFlag': False, # force 0 < eta < 1
+ # Peak detection
+ 'AutoScaling': False,
+ 'Yscaling': 1.0,
+ 'FwhmPoints': 8,
+ 'AutoFwhm': True,
+ 'Sensitivity': 2.5,
+ 'ForcePeakPresence': True,
+ # Hypermet
+ 'HypermetTails': 15,
+ 'QuotedFwhmFlag': 0,
+ 'MaxFwhm2InputRatio': 1.5,
+ 'MinFwhm2InputRatio': 0.4,
+ # short tail parameters
+ 'MinGaussArea4ShortTail': 50000.,
+ 'InitialShortTailAreaRatio': 0.050,
+ 'MaxShortTailAreaRatio': 0.100,
+ 'MinShortTailAreaRatio': 0.0010,
+ 'InitialShortTailSlopeRatio': 0.70,
+ 'MaxShortTailSlopeRatio': 2.00,
+ 'MinShortTailSlopeRatio': 0.50,
+ # long tail parameters
+ 'MinGaussArea4LongTail': 1000.0,
+ 'InitialLongTailAreaRatio': 0.050,
+ 'MaxLongTailAreaRatio': 0.300,
+ 'MinLongTailAreaRatio': 0.010,
+ 'InitialLongTailSlopeRatio': 20.0,
+ 'MaxLongTailSlopeRatio': 50.0,
+ 'MinLongTailSlopeRatio': 5.0,
+ # step tail
+ 'MinGaussHeight4StepTail': 5000.,
+ 'InitialStepTailHeightRatio': 0.002,
+ 'MaxStepTailHeightRatio': 0.0100,
+ 'MinStepTailHeightRatio': 0.0001,
+ # Hypermet constraints
+ # position in range [estimated position +- estimated fwhm/2]
+ 'HypermetQuotedPositionFlag': True,
+ 'DeltaPositionFwhmUnits': 0.5,
+ 'SameSlopeRatioFlag': 1,
+ 'SameAreaRatioFlag': 1,
+ # Strip bg removal
+ 'StripBackgroundFlag': True,
+ 'SmoothingFlag': True,
+ 'SmoothingWidth': 5,
+ 'StripWidth': 2,
+ 'StripIterations': 5000,
+ 'StripThresholdFactor': 1.0}
+"""This dictionary defines default configuration parameters that have effects
+on fit functions and estimation functions, mainly on fit constraints.
+This dictionary is accessible as attribute :attr:`FitTheories.config`,
+which can be modified by configuration functions defined in
+:const:`CONFIGURE`.
+"""
+
+CFREE = 0
+CPOSITIVE = 1
+CQUOTED = 2
+CFIXED = 3
+CFACTOR = 4
+CDELTA = 5
+CSUM = 6
+CIGNORED = 7
+
+
+class FitTheories(object):
+ """Class wrapping functions from :class:`silx.math.fit.functions`
+ and providing estimate functions for all of these fit functions."""
+ def __init__(self, config=None):
+ if config is None:
+ self.config = DEFAULT_CONFIG
+ else:
+ self.config = config
+
+ def ahypermet(self, x, *pars):
+ """
+ Wrapping of :func:`silx.math.fit.functions.sum_ahypermet` without
+ the tail flags in the function signature.
+
+ Depending on the value of `self.config['HypermetTails']`, one can
+ activate or deactivate the various terms of the hypermet function.
+
+ `self.config['HypermetTails']` must be an integer between 0 and 15.
+ It is a set of 4 binary flags, one for activating each one of the
+ hypermet terms: *gaussian function, short tail, long tail, step*.
+
+ For example, 15 can be expressed as ``1111`` in base 2, so a flag of
+ 15 means all terms are active.
+ """
+ g_term = self.config['HypermetTails'] & 1
+ st_term = (self.config['HypermetTails'] >> 1) & 1
+ lt_term = (self.config['HypermetTails'] >> 2) & 1
+ step_term = (self.config['HypermetTails'] >> 3) & 1
+ return functions.sum_ahypermet(x, *pars,
+ gaussian_term=g_term, st_term=st_term,
+ lt_term=lt_term, step_term=step_term)
+
+ def poly(self, x, *pars):
+ """Order n polynomial.
+ The order of the polynomial is defined by the number of
+ coefficients (``*pars``).
+
+ """
+ p = numpy.poly1d(pars)
+ return p(x)
+
+ @staticmethod
+ def estimate_poly(x, y, n=2):
+ """Estimate polynomial coefficients for a degree n polynomial.
+
+ """
+ pcoeffs = numpy.polyfit(x, y, n)
+ constraints = numpy.zeros((n + 1, 3), numpy.float64)
+ return pcoeffs, constraints
+
+ def estimate_quadratic(self, x, y):
+ """Estimate quadratic coefficients
+
+ """
+ return self.estimate_poly(x, y, n=2)
+
+ def estimate_cubic(self, x, y):
+ """Estimate coefficients for a degree 3 polynomial
+
+ """
+ return self.estimate_poly(x, y, n=3)
+
+ def estimate_quartic(self, x, y):
+ """Estimate coefficients for a degree 4 polynomial
+
+ """
+ return self.estimate_poly(x, y, n=4)
+
+ def estimate_quintic(self, x, y):
+ """Estimate coefficients for a degree 5 polynomial
+
+ """
+ return self.estimate_poly(x, y, n=5)
+
+ def strip_bg(self, y):
+ """Return the strip background of y, using parameters from
+ :attr:`config` dictionary (*StripBackgroundFlag, StripWidth,
+ StripIterations, StripThresholdFactor*)"""
+ remove_strip_bg = self.config.get('StripBackgroundFlag', False)
+ if remove_strip_bg:
+ if self.config['SmoothingFlag']:
+ y = savitsky_golay(y, self.config['SmoothingWidth'])
+ strip_width = self.config['StripWidth']
+ strip_niterations = self.config['StripIterations']
+ strip_thr_factor = self.config['StripThresholdFactor']
+ return strip(y, w=strip_width,
+ niterations=strip_niterations,
+ factor=strip_thr_factor)
+ else:
+ return numpy.zeros_like(y)
+
+ def guess_yscaling(self, y):
+ """Estimate scaling for y prior to peak search.
+ A smoothing filter is applied to y to estimate the noise level
+ (chi-squared)
+
+ :param y: Data array
+ :return: Scaling factor
+ """
+ # ensure y is an array
+ yy = numpy.array(y, copy=False)
+
+ # smooth
+ convolution_kernel = numpy.ones(shape=(3,)) / 3.
+ ysmooth = numpy.convolve(y, convolution_kernel, mode="same")
+
+ # remove zeros
+ idx_array = numpy.fabs(y) > 0.0
+ yy = yy[idx_array]
+ ysmooth = ysmooth[idx_array]
+
+ # compute scaling factor
+ chisq = numpy.mean((yy - ysmooth)**2 / numpy.fabs(yy))
+ if chisq > 0:
+ return 1. / chisq
+ else:
+ return 1.0
+
+ def peak_search(self, y, fwhm, sensitivity):
+ """Search for peaks in y array, after padding the array and
+ multiplying its value by a scaling factor.
+
+ :param y: 1-D data array
+ :param int fwhm: Typical full width at half maximum for peaks,
+ in number of points. This parameter is used for to discriminate between
+ true peaks and background fluctuations.
+ :param float sensitivity: Sensitivity parameter. This is a threshold factor
+ for peak detection. Only peaks larger than the standard deviation
+ of the noise multiplied by this sensitivity parameter are detected.
+ :return: List of peak indices
+ """
+ # add padding
+ ysearch = numpy.ones((len(y) + 2 * fwhm,), numpy.float64)
+ ysearch[0:fwhm] = y[0]
+ ysearch[-1:-fwhm - 1:-1] = y[len(y)-1]
+ ysearch[fwhm:fwhm + len(y)] = y[:]
+
+ scaling = self.guess_yscaling(y) if self.config["AutoScaling"] else self.config["Yscaling"]
+
+ if len(ysearch) > 1.5 * fwhm:
+ peaks = peak_search(scaling * ysearch,
+ fwhm=fwhm, sensitivity=sensitivity)
+ return [peak_index - fwhm for peak_index in peaks
+ if 0 <= peak_index - fwhm < len(y)]
+ else:
+ return []
+
+ def estimate_height_position_fwhm(self, x, y):
+ """Estimation of *Height, Position, FWHM* of peaks, for gaussian-like
+ curves.
+
+ This functions finds how many parameters are needed, based on the
+ number of peaks detected. Then it estimates the fit parameters
+ with a few iterations of fitting gaussian functions.
+
+ :param x: Array of abscissa values
+ :param y: Array of ordinate values (``y = f(x)``)
+ :return: Tuple of estimated fit parameters and fit constraints.
+ Parameters to be estimated for each peak are:
+ *Height, Position, FWHM*.
+ Fit constraints depend on :attr:`config`.
+ """
+ fittedpar = []
+
+ bg = self.strip_bg(y)
+
+ if self.config['AutoFwhm']:
+ search_fwhm = guess_fwhm(y)
+ else:
+ search_fwhm = int(float(self.config['FwhmPoints']))
+ search_sens = float(self.config['Sensitivity'])
+
+ if search_fwhm < 3:
+ _logger.warning("Setting peak fwhm to 3 (lower limit)")
+ search_fwhm = 3
+ self.config['FwhmPoints'] = 3
+
+ if search_sens < 1:
+ _logger.warning("Setting peak search sensitivity to 1. " +
+ "(lower limit to filter out noise peaks)")
+ search_sens = 1
+ self.config['Sensitivity'] = 1
+
+ npoints = len(y)
+
+ # Find indices of peaks in data array
+ peaks = self.peak_search(y,
+ fwhm=search_fwhm,
+ sensitivity=search_sens)
+
+ if not len(peaks):
+ forcepeak = int(float(self.config.get('ForcePeakPresence', 0)))
+ if forcepeak:
+ delta = y - bg
+ # get index of global maximum
+ # (first one if several samples are equal to this value)
+ peaks = [numpy.nonzero(delta == delta.max())[0][0]]
+
+ # Find index of largest peak in peaks array
+ index_largest_peak = 0
+ if len(peaks) > 0:
+ # estimate fwhm as 5 * sampling interval
+ sig = 5 * abs(x[npoints - 1] - x[0]) / npoints
+ peakpos = x[int(peaks[0])]
+ if abs(peakpos) < 1.0e-16:
+ peakpos = 0.0
+ param = numpy.array(
+ [y[int(peaks[0])] - bg[int(peaks[0])], peakpos, sig])
+ height_largest_peak = param[0]
+ peak_index = 1
+ for i in peaks[1:]:
+ param2 = numpy.array(
+ [y[int(i)] - bg[int(i)], x[int(i)], sig])
+ param = numpy.concatenate((param, param2))
+ if param2[0] > height_largest_peak:
+ height_largest_peak = param2[0]
+ index_largest_peak = peak_index
+ peak_index += 1
+
+ # Subtract background
+ xw = x
+ yw = y - bg
+
+ cons = numpy.zeros((len(param), 3), numpy.float64)
+
+ # peak height must be positive
+ cons[0:len(param):3, 0] = CPOSITIVE
+ # force peaks to stay around their position
+ cons[1:len(param):3, 0] = CQUOTED
+
+ # set possible peak range to estimated peak +- guessed fwhm
+ if len(xw) > search_fwhm:
+ fwhmx = numpy.fabs(xw[int(search_fwhm)] - xw[0])
+ cons[1:len(param):3, 1] = param[1:len(param):3] - 0.5 * fwhmx
+ cons[1:len(param):3, 2] = param[1:len(param):3] + 0.5 * fwhmx
+ else:
+ shape = [max(1, int(x)) for x in (param[1:len(param):3])]
+ cons[1:len(param):3, 1] = min(xw) * numpy.ones(
+ shape,
+ numpy.float64)
+ cons[1:len(param):3, 2] = max(xw) * numpy.ones(
+ shape,
+ numpy.float64)
+
+ # ensure fwhm is positive
+ cons[2:len(param):3, 0] = CPOSITIVE
+
+ # run a quick iterative fit (4 iterations) to improve
+ # estimations
+ fittedpar, _, _ = leastsq(functions.sum_gauss, xw, yw, param,
+ max_iter=4, constraints=cons.tolist(),
+ full_output=True)
+
+ # set final constraints based on config parameters
+ cons = numpy.zeros((len(fittedpar), 3), numpy.float64)
+ peak_index = 0
+ for i in range(len(peaks)):
+ # Setup height area constrains
+ if not self.config['NoConstraintsFlag']:
+ if self.config['PositiveHeightAreaFlag']:
+ cons[peak_index, 0] = CPOSITIVE
+ cons[peak_index, 1] = 0
+ cons[peak_index, 2] = 0
+ peak_index += 1
+
+ # Setup position constrains
+ if not self.config['NoConstraintsFlag']:
+ if self.config['QuotedPositionFlag']:
+ cons[peak_index, 0] = CQUOTED
+ cons[peak_index, 1] = min(x)
+ cons[peak_index, 2] = max(x)
+ peak_index += 1
+
+ # Setup positive FWHM constrains
+ if not self.config['NoConstraintsFlag']:
+ if self.config['PositiveFwhmFlag']:
+ cons[peak_index, 0] = CPOSITIVE
+ cons[peak_index, 1] = 0
+ cons[peak_index, 2] = 0
+ if self.config['SameFwhmFlag']:
+ if i != index_largest_peak:
+ cons[peak_index, 0] = CFACTOR
+ cons[peak_index, 1] = 3 * index_largest_peak + 2
+ cons[peak_index, 2] = 1.0
+ peak_index += 1
+
+ return fittedpar, cons
+
+ def estimate_agauss(self, x, y):
+ """Estimation of *Area, Position, FWHM* of peaks, for gaussian-like
+ curves.
+
+ This functions uses :meth:`estimate_height_position_fwhm`, then
+ converts the height parameters to area under the curve with the
+ formula ``area = sqrt(2*pi) * height * fwhm / (2 * sqrt(2 * log(2))``
+
+ :param x: Array of abscissa values
+ :param y: Array of ordinate values (``y = f(x)``)
+ :return: Tuple of estimated fit parameters and fit constraints.
+ Parameters to be estimated for each peak are:
+ *Area, Position, FWHM*.
+ Fit constraints depend on :attr:`config`.
+ """
+ fittedpar, cons = self.estimate_height_position_fwhm(x, y)
+ # get the number of found peaks
+ npeaks = len(fittedpar) // 3
+ for i in range(npeaks):
+ height = fittedpar[3 * i]
+ fwhm = fittedpar[3 * i + 2]
+ # Replace height with area in fittedpar
+ fittedpar[3 * i] = numpy.sqrt(2 * numpy.pi) * height * fwhm / (
+ 2.0 * numpy.sqrt(2 * numpy.log(2)))
+ return fittedpar, cons
+
+ def estimate_alorentz(self, x, y):
+ """Estimation of *Area, Position, FWHM* of peaks, for Lorentzian
+ curves.
+
+ This functions uses :meth:`estimate_height_position_fwhm`, then
+ converts the height parameters to area under the curve with the
+ formula ``area = height * fwhm * 0.5 * pi``
+
+ :param x: Array of abscissa values
+ :param y: Array of ordinate values (``y = f(x)``)
+ :return: Tuple of estimated fit parameters and fit constraints.
+ Parameters to be estimated for each peak are:
+ *Area, Position, FWHM*.
+ Fit constraints depend on :attr:`config`.
+ """
+ fittedpar, cons = self.estimate_height_position_fwhm(x, y)
+ # get the number of found peaks
+ npeaks = len(fittedpar) // 3
+ for i in range(npeaks):
+ height = fittedpar[3 * i]
+ fwhm = fittedpar[3 * i + 2]
+ # Replace height with area in fittedpar
+ fittedpar[3 * i] = (height * fwhm * 0.5 * numpy.pi)
+ return fittedpar, cons
+
+ def estimate_splitgauss(self, x, y):
+ """Estimation of *Height, Position, FWHM1, FWHM2* of peaks, for
+ asymmetric gaussian-like curves.
+
+ This functions uses :meth:`estimate_height_position_fwhm`, then
+ adds a second (identical) estimation of FWHM to the fit parameters
+ for each peak, and the corresponding constraint.
+
+ :param x: Array of abscissa values
+ :param y: Array of ordinate values (``y = f(x)``)
+ :return: Tuple of estimated fit parameters and fit constraints.
+ Parameters to be estimated for each peak are:
+ *Height, Position, FWHM1, FWHM2*.
+ Fit constraints depend on :attr:`config`.
+ """
+ fittedpar, cons = self.estimate_height_position_fwhm(x, y)
+ # get the number of found peaks
+ npeaks = len(fittedpar) // 3
+ estimated_parameters = []
+ estimated_constraints = numpy.zeros((4 * npeaks, 3), numpy.float64)
+ for i in range(npeaks):
+ for j in range(3):
+ estimated_parameters.append(fittedpar[3 * i + j])
+ # fwhm2 estimate = fwhm1
+ estimated_parameters.append(fittedpar[3 * i + 2])
+ # height
+ estimated_constraints[4 * i, 0] = cons[3 * i, 0]
+ estimated_constraints[4 * i, 1] = cons[3 * i, 1]
+ estimated_constraints[4 * i, 2] = cons[3 * i, 2]
+ # position
+ estimated_constraints[4 * i + 1, 0] = cons[3 * i + 1, 0]
+ estimated_constraints[4 * i + 1, 1] = cons[3 * i + 1, 1]
+ estimated_constraints[4 * i + 1, 2] = cons[3 * i + 1, 2]
+ # fwhm1
+ estimated_constraints[4 * i + 2, 0] = cons[3 * i + 2, 0]
+ estimated_constraints[4 * i + 2, 1] = cons[3 * i + 2, 1]
+ estimated_constraints[4 * i + 2, 2] = cons[3 * i + 2, 2]
+ # fwhm2
+ estimated_constraints[4 * i + 3, 0] = cons[3 * i + 2, 0]
+ estimated_constraints[4 * i + 3, 1] = cons[3 * i + 2, 1]
+ estimated_constraints[4 * i + 3, 2] = cons[3 * i + 2, 2]
+ if cons[3 * i + 2, 0] == CFACTOR:
+ # convert indices of related parameters
+ # (this happens if SameFwhmFlag == True)
+ estimated_constraints[4 * i + 2, 1] = \
+ int(cons[3 * i + 2, 1] / 3) * 4 + 2
+ estimated_constraints[4 * i + 3, 1] = \
+ int(cons[3 * i + 2, 1] / 3) * 4 + 3
+ return estimated_parameters, estimated_constraints
+
+ def estimate_pvoigt(self, x, y):
+ """Estimation of *Height, Position, FWHM, eta* of peaks, for
+ pseudo-Voigt curves.
+
+ Pseudo-Voigt are a sum of a gaussian curve *G(x)* and a lorentzian
+ curve *L(x)* with the same height, center, fwhm parameters:
+ ``y(x) = eta * G(x) + (1-eta) * L(x)``
+
+ This functions uses :meth:`estimate_height_position_fwhm`, then
+ adds a constant estimation of *eta* (0.5) to the fit parameters
+ for each peak, and the corresponding constraint.
+
+ :param x: Array of abscissa values
+ :param y: Array of ordinate values (``y = f(x)``)
+ :return: Tuple of estimated fit parameters and fit constraints.
+ Parameters to be estimated for each peak are:
+ *Height, Position, FWHM, eta*.
+ Constraint for the eta parameter can be set to QUOTED (0.--1.)
+ by setting :attr:`config`['QuotedEtaFlag'] to ``True``.
+ If this is not the case, the constraint code is set to FREE.
+ """
+ fittedpar, cons = self.estimate_height_position_fwhm(x, y)
+ npeaks = len(fittedpar) // 3
+ newpar = []
+ newcons = numpy.zeros((4 * npeaks, 3), numpy.float64)
+ # find out related parameters proper index
+ if not self.config['NoConstraintsFlag']:
+ if self.config['SameFwhmFlag']:
+ j = 0
+ # get the index of the free FWHM
+ for i in range(npeaks):
+ if cons[3 * i + 2, 0] != 4:
+ j = i
+ for i in range(npeaks):
+ if i != j:
+ cons[3 * i + 2, 1] = 4 * j + 2
+ for i in range(npeaks):
+ newpar.append(fittedpar[3 * i])
+ newpar.append(fittedpar[3 * i + 1])
+ newpar.append(fittedpar[3 * i + 2])
+ newpar.append(0.5)
+ # height
+ newcons[4 * i, 0] = cons[3 * i, 0]
+ newcons[4 * i, 1] = cons[3 * i, 1]
+ newcons[4 * i, 2] = cons[3 * i, 2]
+ # position
+ newcons[4 * i + 1, 0] = cons[3 * i + 1, 0]
+ newcons[4 * i + 1, 1] = cons[3 * i + 1, 1]
+ newcons[4 * i + 1, 2] = cons[3 * i + 1, 2]
+ # fwhm
+ newcons[4 * i + 2, 0] = cons[3 * i + 2, 0]
+ newcons[4 * i + 2, 1] = cons[3 * i + 2, 1]
+ newcons[4 * i + 2, 2] = cons[3 * i + 2, 2]
+ # Eta constrains
+ newcons[4 * i + 3, 0] = CFREE
+ newcons[4 * i + 3, 1] = 0
+ newcons[4 * i + 3, 2] = 0
+ if self.config['QuotedEtaFlag']:
+ newcons[4 * i + 3, 0] = CQUOTED
+ newcons[4 * i + 3, 1] = 0.0
+ newcons[4 * i + 3, 2] = 1.0
+ return newpar, newcons
+
+ def estimate_splitpvoigt(self, x, y):
+ """Estimation of *Height, Position, FWHM1, FWHM2, eta* of peaks, for
+ asymmetric pseudo-Voigt curves.
+
+ This functions uses :meth:`estimate_height_position_fwhm`, then
+ adds an identical FWHM2 parameter and a constant estimation of
+ *eta* (0.5) to the fit parameters for each peak, and the corresponding
+ constraints.
+
+ Constraint for the eta parameter can be set to QUOTED (0.--1.)
+ by setting :attr:`config`['QuotedEtaFlag'] to ``True``.
+ If this is not the case, the constraint code is set to FREE.
+
+ :param x: Array of abscissa values
+ :param y: Array of ordinate values (``y = f(x)``)
+ :return: Tuple of estimated fit parameters and fit constraints.
+ Parameters to be estimated for each peak are:
+ *Height, Position, FWHM1, FWHM2, eta*.
+ """
+ fittedpar, cons = self.estimate_height_position_fwhm(x, y)
+ npeaks = len(fittedpar) // 3
+ newpar = []
+ newcons = numpy.zeros((5 * npeaks, 3), numpy.float64)
+ # find out related parameters proper index
+ if not self.config['NoConstraintsFlag']:
+ if self.config['SameFwhmFlag']:
+ j = 0
+ # get the index of the free FWHM
+ for i in range(npeaks):
+ if cons[3 * i + 2, 0] != 4:
+ j = i
+ for i in range(npeaks):
+ if i != j:
+ cons[3 * i + 2, 1] = 4 * j + 2
+ for i in range(npeaks):
+ # height
+ newpar.append(fittedpar[3 * i])
+ # position
+ newpar.append(fittedpar[3 * i + 1])
+ # fwhm1
+ newpar.append(fittedpar[3 * i + 2])
+ # fwhm2 estimate equal to fwhm1
+ newpar.append(fittedpar[3 * i + 2])
+ # eta
+ newpar.append(0.5)
+ # constraint codes
+ # ----------------
+ # height
+ newcons[5 * i, 0] = cons[3 * i, 0]
+ # position
+ newcons[5 * i + 1, 0] = cons[3 * i + 1, 0]
+ # fwhm1
+ newcons[5 * i + 2, 0] = cons[3 * i + 2, 0]
+ # fwhm2
+ newcons[5 * i + 3, 0] = cons[3 * i + 2, 0]
+ # cons 1
+ # ------
+ newcons[5 * i, 1] = cons[3 * i, 1]
+ newcons[5 * i + 1, 1] = cons[3 * i + 1, 1]
+ newcons[5 * i + 2, 1] = cons[3 * i + 2, 1]
+ newcons[5 * i + 3, 1] = cons[3 * i + 2, 1]
+ # cons 2
+ # ------
+ newcons[5 * i, 2] = cons[3 * i, 2]
+ newcons[5 * i + 1, 2] = cons[3 * i + 1, 2]
+ newcons[5 * i + 2, 2] = cons[3 * i + 2, 2]
+ newcons[5 * i + 3, 2] = cons[3 * i + 2, 2]
+
+ if cons[3 * i + 2, 0] == CFACTOR:
+ # fwhm2 connstraint depends on fwhm1
+ newcons[5 * i + 3, 1] = newcons[5 * i + 2, 1] + 1
+ # eta constraints
+ newcons[5 * i + 4, 0] = CFREE
+ newcons[5 * i + 4, 1] = 0
+ newcons[5 * i + 4, 2] = 0
+ if self.config['QuotedEtaFlag']:
+ newcons[5 * i + 4, 0] = CQUOTED
+ newcons[5 * i + 4, 1] = 0.0
+ newcons[5 * i + 4, 2] = 1.0
+ return newpar, newcons
+
+ def estimate_apvoigt(self, x, y):
+ """Estimation of *Area, Position, FWHM1, eta* of peaks, for
+ pseudo-Voigt curves.
+
+ This functions uses :meth:`estimate_pvoigt`, then converts the height
+ parameter to area.
+
+ :param x: Array of abscissa values
+ :param y: Array of ordinate values (``y = f(x)``)
+ :return: Tuple of estimated fit parameters and fit constraints.
+ Parameters to be estimated for each peak are:
+ *Area, Position, FWHM, eta*.
+ """
+ fittedpar, cons = self.estimate_pvoigt(x, y)
+ npeaks = len(fittedpar) // 4
+ # Assume 50% of the area is determined by the gaussian and 50% by
+ # the Lorentzian.
+ for i in range(npeaks):
+ height = fittedpar[4 * i]
+ fwhm = fittedpar[4 * i + 2]
+ fittedpar[4 * i] = 0.5 * (height * fwhm * 0.5 * numpy.pi) +\
+ 0.5 * (height * fwhm / (2.0 * numpy.sqrt(2 * numpy.log(2)))
+ ) * numpy.sqrt(2 * numpy.pi)
+ return fittedpar, cons
+
+ def estimate_ahypermet(self, x, y):
+ """Estimation of *area, position, fwhm, st_area_r, st_slope_r,
+ lt_area_r, lt_slope_r, step_height_r* of peaks, for hypermet curves.
+
+ :param x: Array of abscissa values
+ :param y: Array of ordinate values (``y = f(x)``)
+ :return: Tuple of estimated fit parameters and fit constraints.
+ Parameters to be estimated for each peak are:
+ *area, position, fwhm, st_area_r, st_slope_r,
+ lt_area_r, lt_slope_r, step_height_r* .
+ """
+ yscaling = self.config.get('Yscaling', 1.0)
+ if yscaling == 0:
+ yscaling = 1.0
+ fittedpar, cons = self.estimate_height_position_fwhm(x, y)
+ npeaks = len(fittedpar) // 3
+ newpar = []
+ newcons = numpy.zeros((8 * npeaks, 3), numpy.float64)
+ main_peak = 0
+ # find out related parameters proper index
+ if not self.config['NoConstraintsFlag']:
+ if self.config['SameFwhmFlag']:
+ j = 0
+ # get the index of the free FWHM
+ for i in range(npeaks):
+ if cons[3 * i + 2, 0] != 4:
+ j = i
+ for i in range(npeaks):
+ if i != j:
+ cons[3 * i + 2, 1] = 8 * j + 2
+ main_peak = j
+ for i in range(npeaks):
+ if fittedpar[3 * i] > fittedpar[3 * main_peak]:
+ main_peak = i
+
+ for i in range(npeaks):
+ height = fittedpar[3 * i]
+ position = fittedpar[3 * i + 1]
+ fwhm = fittedpar[3 * i + 2]
+ area = (height * fwhm / (2.0 * numpy.sqrt(2 * numpy.log(2)))
+ ) * numpy.sqrt(2 * numpy.pi)
+ # the gaussian parameters
+ newpar.append(area)
+ newpar.append(position)
+ newpar.append(fwhm)
+ # print "area, pos , fwhm = ",area,position,fwhm
+ # Avoid zero derivatives because of not calculating contribution
+ g_term = 1
+ st_term = 1
+ lt_term = 1
+ step_term = 1
+ if self.config['HypermetTails'] != 0:
+ g_term = self.config['HypermetTails'] & 1
+ st_term = (self.config['HypermetTails'] >> 1) & 1
+ lt_term = (self.config['HypermetTails'] >> 2) & 1
+ step_term = (self.config['HypermetTails'] >> 3) & 1
+ if g_term == 0:
+ # fix the gaussian parameters
+ newcons[8 * i, 0] = CFIXED
+ newcons[8 * i + 1, 0] = CFIXED
+ newcons[8 * i + 2, 0] = CFIXED
+ # the short tail parameters
+ if ((area * yscaling) <
+ self.config['MinGaussArea4ShortTail']) | \
+ (st_term == 0):
+ newpar.append(0.0)
+ newpar.append(0.0)
+ newcons[8 * i + 3, 0] = CFIXED
+ newcons[8 * i + 3, 1] = 0.0
+ newcons[8 * i + 3, 2] = 0.0
+ newcons[8 * i + 4, 0] = CFIXED
+ newcons[8 * i + 4, 1] = 0.0
+ newcons[8 * i + 4, 2] = 0.0
+ else:
+ newpar.append(self.config['InitialShortTailAreaRatio'])
+ newpar.append(self.config['InitialShortTailSlopeRatio'])
+ newcons[8 * i + 3, 0] = CQUOTED
+ newcons[8 * i + 3, 1] = self.config['MinShortTailAreaRatio']
+ newcons[8 * i + 3, 2] = self.config['MaxShortTailAreaRatio']
+ newcons[8 * i + 4, 0] = CQUOTED
+ newcons[8 * i + 4, 1] = self.config['MinShortTailSlopeRatio']
+ newcons[8 * i + 4, 2] = self.config['MaxShortTailSlopeRatio']
+ # the long tail parameters
+ if ((area * yscaling) <
+ self.config['MinGaussArea4LongTail']) | \
+ (lt_term == 0):
+ newpar.append(0.0)
+ newpar.append(0.0)
+ newcons[8 * i + 5, 0] = CFIXED
+ newcons[8 * i + 5, 1] = 0.0
+ newcons[8 * i + 5, 2] = 0.0
+ newcons[8 * i + 6, 0] = CFIXED
+ newcons[8 * i + 6, 1] = 0.0
+ newcons[8 * i + 6, 2] = 0.0
+ else:
+ newpar.append(self.config['InitialLongTailAreaRatio'])
+ newpar.append(self.config['InitialLongTailSlopeRatio'])
+ newcons[8 * i + 5, 0] = CQUOTED
+ newcons[8 * i + 5, 1] = self.config['MinLongTailAreaRatio']
+ newcons[8 * i + 5, 2] = self.config['MaxLongTailAreaRatio']
+ newcons[8 * i + 6, 0] = CQUOTED
+ newcons[8 * i + 6, 1] = self.config['MinLongTailSlopeRatio']
+ newcons[8 * i + 6, 2] = self.config['MaxLongTailSlopeRatio']
+ # the step parameters
+ if ((height * yscaling) <
+ self.config['MinGaussHeight4StepTail']) | \
+ (step_term == 0):
+ newpar.append(0.0)
+ newcons[8 * i + 7, 0] = CFIXED
+ newcons[8 * i + 7, 1] = 0.0
+ newcons[8 * i + 7, 2] = 0.0
+ else:
+ newpar.append(self.config['InitialStepTailHeightRatio'])
+ newcons[8 * i + 7, 0] = CQUOTED
+ newcons[8 * i + 7, 1] = self.config['MinStepTailHeightRatio']
+ newcons[8 * i + 7, 2] = self.config['MaxStepTailHeightRatio']
+ # if self.config['NoConstraintsFlag'] == 1:
+ # newcons=numpy.zeros((8*npeaks, 3),numpy.float64)
+ if npeaks > 0:
+ if g_term:
+ if self.config['PositiveHeightAreaFlag']:
+ for i in range(npeaks):
+ newcons[8 * i, 0] = CPOSITIVE
+ if self.config['PositiveFwhmFlag']:
+ for i in range(npeaks):
+ newcons[8 * i + 2, 0] = CPOSITIVE
+ if self.config['SameFwhmFlag']:
+ for i in range(npeaks):
+ if i != main_peak:
+ newcons[8 * i + 2, 0] = CFACTOR
+ newcons[8 * i + 2, 1] = 8 * main_peak + 2
+ newcons[8 * i + 2, 2] = 1.0
+ if self.config['HypermetQuotedPositionFlag']:
+ for i in range(npeaks):
+ delta = self.config['DeltaPositionFwhmUnits'] * fwhm
+ newcons[8 * i + 1, 0] = CQUOTED
+ newcons[8 * i + 1, 1] = newpar[8 * i + 1] - delta
+ newcons[8 * i + 1, 2] = newpar[8 * i + 1] + delta
+ if self.config['SameSlopeRatioFlag']:
+ for i in range(npeaks):
+ if i != main_peak:
+ newcons[8 * i + 4, 0] = CFACTOR
+ newcons[8 * i + 4, 1] = 8 * main_peak + 4
+ newcons[8 * i + 4, 2] = 1.0
+ newcons[8 * i + 6, 0] = CFACTOR
+ newcons[8 * i + 6, 1] = 8 * main_peak + 6
+ newcons[8 * i + 6, 2] = 1.0
+ if self.config['SameAreaRatioFlag']:
+ for i in range(npeaks):
+ if i != main_peak:
+ newcons[8 * i + 3, 0] = CFACTOR
+ newcons[8 * i + 3, 1] = 8 * main_peak + 3
+ newcons[8 * i + 3, 2] = 1.0
+ newcons[8 * i + 5, 0] = CFACTOR
+ newcons[8 * i + 5, 1] = 8 * main_peak + 5
+ newcons[8 * i + 5, 2] = 1.0
+ return newpar, newcons
+
+ def estimate_stepdown(self, x, y):
+ """Estimation of parameters for stepdown curves.
+
+ The functions estimates gaussian parameters for the derivative of
+ the data, takes the largest gaussian peak and uses its estimated
+ parameters to define the center of the step and its fwhm. The
+ estimated amplitude returned is simply ``max(y) - min(y)``.
+
+ :param x: Array of abscissa values
+ :param y: Array of ordinate values (``y = f(x)``)
+ :return: Tuple of estimated fit parameters and fit newconstraints.
+ Parameters to be estimated for each stepdown are:
+ *height, centroid, fwhm* .
+ """
+ crappyfilter = [-0.25, -0.75, 0.0, 0.75, 0.25]
+ cutoff = len(crappyfilter) // 2
+ y_deriv = numpy.convolve(y,
+ crappyfilter,
+ mode="valid")
+
+ # make the derivative's peak have the same amplitude as the step
+ if max(y_deriv) > 0:
+ y_deriv = y_deriv * max(y) / max(y_deriv)
+
+ fittedpar, newcons = self.estimate_height_position_fwhm(
+ x[cutoff:-cutoff], y_deriv)
+
+ data_amplitude = max(y) - min(y)
+
+ # use parameters from largest gaussian found
+ if len(fittedpar):
+ npeaks = len(fittedpar) // 3
+ largest_index = 0
+ largest = [data_amplitude,
+ fittedpar[3 * largest_index + 1],
+ fittedpar[3 * largest_index + 2]]
+ for i in range(npeaks):
+ if fittedpar[3 * i] > largest[0]:
+ largest_index = i
+ largest = [data_amplitude,
+ fittedpar[3 * largest_index + 1],
+ fittedpar[3 * largest_index + 2]]
+ else:
+ # no peak was found
+ largest = [data_amplitude, # height
+ x[len(x)//2], # center: middle of x range
+ self.config["FwhmPoints"] * (x[1] - x[0])] # fwhm: default value
+
+ # Setup constrains
+ newcons = numpy.zeros((3, 3), numpy.float64)
+ if not self.config['NoConstraintsFlag']:
+ # Setup height constrains
+ if self.config['PositiveHeightAreaFlag']:
+ newcons[0, 0] = CPOSITIVE
+ newcons[0, 1] = 0
+ newcons[0, 2] = 0
+
+ # Setup position constrains
+ if self.config['QuotedPositionFlag']:
+ newcons[1, 0] = CQUOTED
+ newcons[1, 1] = min(x)
+ newcons[1, 2] = max(x)
+
+ # Setup positive FWHM constrains
+ if self.config['PositiveFwhmFlag']:
+ newcons[2, 0] = CPOSITIVE
+ newcons[2, 1] = 0
+ newcons[2, 2] = 0
+
+ return largest, newcons
+
+ def estimate_slit(self, x, y):
+ """Estimation of parameters for slit curves.
+
+ The functions estimates stepup and stepdown parameters for the largest
+ steps, and uses them for calculating the center (middle between stepup
+ and stepdown), the height (maximum amplitude in data), the fwhm
+ (distance between the up- and down-step centers) and the beamfwhm
+ (average of FWHM for up- and down-step).
+
+ :param x: Array of abscissa values
+ :param y: Array of ordinate values (``y = f(x)``)
+ :return: Tuple of estimated fit parameters and fit constraints.
+ Parameters to be estimated for each slit are:
+ *height, position, fwhm, beamfwhm* .
+ """
+ largestup, cons = self.estimate_stepup(x, y)
+ largestdown, cons = self.estimate_stepdown(x, y)
+ fwhm = numpy.fabs(largestdown[1] - largestup[1])
+ beamfwhm = 0.5 * (largestup[2] + largestdown[1])
+ beamfwhm = min(beamfwhm, fwhm / 10.0)
+ beamfwhm = max(beamfwhm, (max(x) - min(x)) * 3.0 / len(x))
+
+ y_minus_bg = y - self.strip_bg(y)
+ height = max(y_minus_bg)
+
+ i1 = numpy.nonzero(y_minus_bg >= 0.5 * height)[0]
+ xx = numpy.take(x, i1)
+ position = (xx[0] + xx[-1]) / 2.0
+ fwhm = xx[-1] - xx[0]
+ largest = [height, position, fwhm, beamfwhm]
+ cons = numpy.zeros((4, 3), numpy.float64)
+ # Setup constrains
+ if not self.config['NoConstraintsFlag']:
+ # Setup height constrains
+ if self.config['PositiveHeightAreaFlag']:
+ cons[0, 0] = CPOSITIVE
+ cons[0, 1] = 0
+ cons[0, 2] = 0
+
+ # Setup position constrains
+ if self.config['QuotedPositionFlag']:
+ cons[1, 0] = CQUOTED
+ cons[1, 1] = min(x)
+ cons[1, 2] = max(x)
+
+ # Setup positive FWHM constrains
+ if self.config['PositiveFwhmFlag']:
+ cons[2, 0] = CPOSITIVE
+ cons[2, 1] = 0
+ cons[2, 2] = 0
+
+ # Setup positive FWHM constrains
+ if self.config['PositiveFwhmFlag']:
+ cons[3, 0] = CPOSITIVE
+ cons[3, 1] = 0
+ cons[3, 2] = 0
+ return largest, cons
+
+ def estimate_stepup(self, x, y):
+ """Estimation of parameters for a single step up curve.
+
+ The functions estimates gaussian parameters for the derivative of
+ the data, takes the largest gaussian peak and uses its estimated
+ parameters to define the center of the step and its fwhm. The
+ estimated amplitude returned is simply ``max(y) - min(y)``.
+
+ :param x: Array of abscissa values
+ :param y: Array of ordinate values (``y = f(x)``)
+ :return: Tuple of estimated fit parameters and fit constraints.
+ Parameters to be estimated for each stepup are:
+ *height, centroid, fwhm* .
+ """
+ crappyfilter = [0.25, 0.75, 0.0, -0.75, -0.25]
+ cutoff = len(crappyfilter) // 2
+ y_deriv = numpy.convolve(y, crappyfilter, mode="valid")
+ if max(y_deriv) > 0:
+ y_deriv = y_deriv * max(y) / max(y_deriv)
+
+ fittedpar, cons = self.estimate_height_position_fwhm(
+ x[cutoff:-cutoff], y_deriv)
+
+ # for height, use the data amplitude after removing the background
+ data_amplitude = max(y) - min(y)
+
+ # find params of the largest gaussian found
+ if len(fittedpar):
+ npeaks = len(fittedpar) // 3
+ largest_index = 0
+ largest = [data_amplitude,
+ fittedpar[3 * largest_index + 1],
+ fittedpar[3 * largest_index + 2]]
+ for i in range(npeaks):
+ if fittedpar[3 * i] > largest[0]:
+ largest_index = i
+ largest = [fittedpar[3 * largest_index],
+ fittedpar[3 * largest_index + 1],
+ fittedpar[3 * largest_index + 2]]
+ else:
+ # no peak was found
+ largest = [data_amplitude, # height
+ x[len(x)//2], # center: middle of x range
+ self.config["FwhmPoints"] * (x[1] - x[0])] # fwhm: default value
+
+ newcons = numpy.zeros((3, 3), numpy.float64)
+ # Setup constrains
+ if not self.config['NoConstraintsFlag']:
+ # Setup height constraints
+ if self.config['PositiveHeightAreaFlag']:
+ newcons[0, 0] = CPOSITIVE
+ newcons[0, 1] = 0
+ newcons[0, 2] = 0
+
+ # Setup position constraints
+ if self.config['QuotedPositionFlag']:
+ newcons[1, 0] = CQUOTED
+ newcons[1, 1] = min(x)
+ newcons[1, 2] = max(x)
+
+ # Setup positive FWHM constraints
+ if self.config['PositiveFwhmFlag']:
+ newcons[2, 0] = CPOSITIVE
+ newcons[2, 1] = 0
+ newcons[2, 2] = 0
+
+ return largest, newcons
+
+ def estimate_periodic_gauss(self, x, y):
+ """Estimation of parameters for periodic gaussian curves:
+ *number of peaks, distance between peaks, height, position of the
+ first peak, fwhm*
+
+ The functions detects all peaks, then computes the parameters the
+ following way:
+
+ - *distance*: average of distances between detected peaks
+ - *height*: average height of detected peaks
+ - *fwhm*: fwhm of the highest peak (in number of samples) if
+ field ``'AutoFwhm'`` in :attr:`config` is ``True``, else take
+ the default value (field ``'FwhmPoints'`` in :attr:`config`)
+
+ :param x: Array of abscissa values
+ :param y: Array of ordinate values (``y = f(x)``)
+ :return: Tuple of estimated fit parameters and fit constraints.
+ """
+ yscaling = self.config.get('Yscaling', 1.0)
+ if yscaling == 0:
+ yscaling = 1.0
+
+ bg = self.strip_bg(y)
+
+ if self.config['AutoFwhm']:
+ search_fwhm = guess_fwhm(y)
+ else:
+ search_fwhm = int(float(self.config['FwhmPoints']))
+ search_sens = float(self.config['Sensitivity'])
+
+ if search_fwhm < 3:
+ search_fwhm = 3
+
+ if search_sens < 1:
+ search_sens = 1
+
+ if len(y) > 1.5 * search_fwhm:
+ peaks = peak_search(yscaling * y, fwhm=search_fwhm,
+ sensitivity=search_sens)
+ else:
+ peaks = []
+ npeaks = len(peaks)
+ if not npeaks:
+ fittedpar = []
+ cons = numpy.zeros((len(fittedpar), 3), numpy.float64)
+ return fittedpar, cons
+
+ fittedpar = [0.0, 0.0, 0.0, 0.0, 0.0]
+
+ # The number of peaks
+ fittedpar[0] = npeaks
+
+ # The separation between peaks in x units
+ delta = 0.0
+ height = 0.0
+ for i in range(npeaks):
+ height += y[int(peaks[i])] - bg[int(peaks[i])]
+ if i != npeaks - 1:
+ delta += (x[int(peaks[i + 1])] - x[int(peaks[i])])
+
+ # delta between peaks
+ if npeaks > 1:
+ fittedpar[1] = delta / (npeaks - 1)
+
+ # starting height
+ fittedpar[2] = height / npeaks
+
+ # position of the first peak
+ fittedpar[3] = x[int(peaks[0])]
+
+ # Estimate the fwhm
+ fittedpar[4] = search_fwhm
+
+ # setup constraints
+ cons = numpy.zeros((5, 3), numpy.float64)
+ cons[0, 0] = CFIXED # the number of gaussians
+ if npeaks == 1:
+ cons[1, 0] = CFIXED # the delta between peaks
+ else:
+ cons[1, 0] = CFREE
+ j = 2
+ # Setup height area constrains
+ if not self.config['NoConstraintsFlag']:
+ if self.config['PositiveHeightAreaFlag']:
+ # POSITIVE = 1
+ cons[j, 0] = CPOSITIVE
+ cons[j, 1] = 0
+ cons[j, 2] = 0
+ j += 1
+
+ # Setup position constrains
+ if not self.config['NoConstraintsFlag']:
+ if self.config['QuotedPositionFlag']:
+ # QUOTED = 2
+ cons[j, 0] = CQUOTED
+ cons[j, 1] = min(x)
+ cons[j, 2] = max(x)
+ j += 1
+
+ # Setup positive FWHM constrains
+ if not self.config['NoConstraintsFlag']:
+ if self.config['PositiveFwhmFlag']:
+ # POSITIVE=1
+ cons[j, 0] = CPOSITIVE
+ cons[j, 1] = 0
+ cons[j, 2] = 0
+ j += 1
+ return fittedpar, cons
+
+ def configure(self, **kw):
+ """Add new / unknown keyword arguments to :attr:`config`,
+ update entries in :attr:`config` if the parameter name is a existing
+ key.
+
+ :param kw: Dictionary of keyword arguments.
+ :return: Configuration dictionary :attr:`config`
+ """
+ if not kw.keys():
+ return self.config
+ for key in kw.keys():
+ notdone = 1
+ # take care of lower / upper case problems ...
+ for config_key in self.config.keys():
+ if config_key.lower() == key.lower():
+ self.config[config_key] = kw[key]
+ notdone = 0
+ if notdone:
+ self.config[key] = kw[key]
+ return self.config
+
+fitfuns = FitTheories()
+
+THEORY = OrderedDict((
+ ('Gaussians',
+ FitTheory(description='Gaussian functions',
+ function=functions.sum_gauss,
+ parameters=('Height', 'Position', 'FWHM'),
+ estimate=fitfuns.estimate_height_position_fwhm,
+ configure=fitfuns.configure)),
+ ('Lorentz',
+ FitTheory(description='Lorentzian functions',
+ function=functions.sum_lorentz,
+ parameters=('Height', 'Position', 'FWHM'),
+ estimate=fitfuns.estimate_height_position_fwhm,
+ configure=fitfuns.configure)),
+ ('Area Gaussians',
+ FitTheory(description='Gaussian functions (area)',
+ function=functions.sum_agauss,
+ parameters=('Area', 'Position', 'FWHM'),
+ estimate=fitfuns.estimate_agauss,
+ configure=fitfuns.configure)),
+ ('Area Lorentz',
+ FitTheory(description='Lorentzian functions (area)',
+ function=functions.sum_alorentz,
+ parameters=('Area', 'Position', 'FWHM'),
+ estimate=fitfuns.estimate_alorentz,
+ configure=fitfuns.configure)),
+ ('Pseudo-Voigt Line',
+ FitTheory(description='Pseudo-Voigt functions',
+ function=functions.sum_pvoigt,
+ parameters=('Height', 'Position', 'FWHM', 'Eta'),
+ estimate=fitfuns.estimate_pvoigt,
+ configure=fitfuns.configure)),
+ ('Area Pseudo-Voigt',
+ FitTheory(description='Pseudo-Voigt functions (area)',
+ function=functions.sum_apvoigt,
+ parameters=('Area', 'Position', 'FWHM', 'Eta'),
+ estimate=fitfuns.estimate_apvoigt,
+ configure=fitfuns.configure)),
+ ('Split Gaussian',
+ FitTheory(description='Asymmetric gaussian functions',
+ function=functions.sum_splitgauss,
+ parameters=('Height', 'Position', 'LowFWHM',
+ 'HighFWHM'),
+ estimate=fitfuns.estimate_splitgauss,
+ configure=fitfuns.configure)),
+ ('Split Lorentz',
+ FitTheory(description='Asymmetric lorentzian functions',
+ function=functions.sum_splitlorentz,
+ parameters=('Height', 'Position', 'LowFWHM', 'HighFWHM'),
+ estimate=fitfuns.estimate_splitgauss,
+ configure=fitfuns.configure)),
+ ('Split Pseudo-Voigt',
+ FitTheory(description='Asymmetric pseudo-Voigt functions',
+ function=functions.sum_splitpvoigt,
+ parameters=('Height', 'Position', 'LowFWHM',
+ 'HighFWHM', 'Eta'),
+ estimate=fitfuns.estimate_splitpvoigt,
+ configure=fitfuns.configure)),
+ ('Step Down',
+ FitTheory(description='Step down function',
+ function=functions.sum_stepdown,
+ parameters=('Height', 'Position', 'FWHM'),
+ estimate=fitfuns.estimate_stepdown,
+ configure=fitfuns.configure)),
+ ('Step Up',
+ FitTheory(description='Step up function',
+ function=functions.sum_stepup,
+ parameters=('Height', 'Position', 'FWHM'),
+ estimate=fitfuns.estimate_stepup,
+ configure=fitfuns.configure)),
+ ('Slit',
+ FitTheory(description='Slit function',
+ function=functions.sum_slit,
+ parameters=('Height', 'Position', 'FWHM', 'BeamFWHM'),
+ estimate=fitfuns.estimate_slit,
+ configure=fitfuns.configure)),
+ ('Atan',
+ FitTheory(description='Arctan step up function',
+ function=functions.atan_stepup,
+ parameters=('Height', 'Position', 'Width'),
+ estimate=fitfuns.estimate_stepup,
+ configure=fitfuns.configure)),
+ ('Hypermet',
+ FitTheory(description='Hypermet functions',
+ function=fitfuns.ahypermet, # customized version of functions.sum_ahypermet
+ parameters=('G_Area', 'Position', 'FWHM', 'ST_Area',
+ 'ST_Slope', 'LT_Area', 'LT_Slope', 'Step_H'),
+ estimate=fitfuns.estimate_ahypermet,
+ configure=fitfuns.configure)),
+ # ('Periodic Gaussians',
+ # FitTheory(description='Periodic gaussian functions',
+ # function=functions.periodic_gauss,
+ # parameters=('N', 'Delta', 'Height', 'Position', 'FWHM'),
+ # estimate=fitfuns.estimate_periodic_gauss,
+ # configure=fitfuns.configure))
+ ('Degree 2 Polynomial',
+ FitTheory(description='Degree 2 polynomial'
+ '\ny = a*x^2 + b*x +c',
+ function=fitfuns.poly,
+ parameters=['a', 'b', 'c'],
+ estimate=fitfuns.estimate_quadratic)),
+ ('Degree 3 Polynomial',
+ FitTheory(description='Degree 3 polynomial'
+ '\ny = a*x^3 + b*x^2 + c*x + d',
+ function=fitfuns.poly,
+ parameters=['a', 'b', 'c', 'd'],
+ estimate=fitfuns.estimate_cubic)),
+ ('Degree 4 Polynomial',
+ FitTheory(description='Degree 4 polynomial'
+ '\ny = a*x^4 + b*x^3 + c*x^2 + d*x + e',
+ function=fitfuns.poly,
+ parameters=['a', 'b', 'c', 'd', 'e'],
+ estimate=fitfuns.estimate_quartic)),
+ ('Degree 5 Polynomial',
+ FitTheory(description='Degree 5 polynomial'
+ '\ny = a*x^5 + b*x^4 + c*x^3 + d*x^2 + e*x + f',
+ function=fitfuns.poly,
+ parameters=['a', 'b', 'c', 'd', 'e', 'f'],
+ estimate=fitfuns.estimate_quintic)),
+))
+"""Dictionary of fit theories: fit functions and their associated estimation
+function, parameters list, configuration function and description.
+"""
+
+
+def test(a):
+ from silx.math.fit import fitmanager
+ x = numpy.arange(1000).astype(numpy.float64)
+ p = [1500, 100., 50.0,
+ 1500, 700., 50.0]
+ y_synthetic = functions.sum_gauss(x, *p) + 1
+
+ fit = fitmanager.FitManager(x, y_synthetic)
+ fit.addtheory('Gaussians', functions.sum_gauss, ['Height', 'Position', 'FWHM'],
+ a.estimate_height_position_fwhm)
+ fit.settheory('Gaussians')
+ fit.setbackground('Linear')
+
+ fit.estimate()
+ fit.runfit()
+
+ y_fit = fit.gendata()
+
+ print("Fit parameter names: %s" % str(fit.get_names()))
+ print("Theoretical parameters: %s" % str(numpy.append([1, 0], p)))
+ print("Fitted parameters: %s" % str(fit.get_fitted_parameters()))
+
+ try:
+ from silx.gui import qt
+ from silx.gui.plot import plot1D
+ app = qt.QApplication([])
+
+ # Offset of 1 to see the difference in log scale
+ plot1D(x, (y_synthetic + 1, y_fit), "Input data + 1, Fit")
+
+ app.exec()
+ except ImportError:
+ _logger.warning("Unable to load qt binding, can't plot results.")
+
+
+if __name__ == "__main__":
+ test(fitfuns)
diff --git a/src/silx/math/fit/fittheory.py b/src/silx/math/fit/fittheory.py
new file mode 100644
index 0000000..fa42e6b
--- /dev/null
+++ b/src/silx/math/fit/fittheory.py
@@ -0,0 +1,161 @@
+# coding: utf-8
+#/*##########################################################################
+#
+# Copyright (c) 2004-2018 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+########################################################################### */
+"""
+This module defines the :class:`FitTheory` object that is used by
+:class:`silx.math.fit.FitManager` to define fit functions and background
+models.
+"""
+
+__authors__ = ["P. Knobel"]
+__license__ = "MIT"
+__date__ = "09/08/2016"
+
+
+class FitTheory(object):
+ """This class defines a fit theory, which consists of:
+
+ - a model function, the actual function to be fitted
+ - parameters names
+ - an estimation function, that return the estimated initial parameters
+ that serve as input for :func:`silx.math.fit.leastsq`
+ - an optional configuration function, that can be used to modify
+ configuration parameters to alter the behavior of the fit function
+ and the estimation function
+ - an optional derivative function, that replaces the default model
+ derivative used in :func:`silx.math.fit.leastsq`
+ """
+ def __init__(self, function, parameters,
+ estimate=None, configure=None, derivative=None,
+ description=None, pymca_legacy=False, is_background=False):
+ """
+ :param function function: Actual function. See documentation for
+ :attr:`function`.
+ :param list[str] parameters: List of parameter names for the function.
+ See documentation for :attr:`parameters`.
+ :param function estimate: Optional estimation function.
+ See documentation for :attr:`estimate`
+ :param function configure: Optional configuration function.
+ See documentation for :attr:`configure`
+ :param function derivative: Optional custom derivative function.
+ See documentation for :attr:`derivative`
+ :param str description: Optional description string.
+ See documentation for :attr:`description`
+ :param bool pymca_legacy: Flag to indicate that the theory is a PyMca
+ legacy theory. See documentation for :attr:`pymca_legacy`
+ :param bool is_background: Flag to indicate that the theory is a
+ background theory. This has implications regarding the function's
+ signature, as explained in the documentation for :attr:`function`.
+ """
+ self.function = function
+ """Regular fit functions must have the signature ``f(x, *params) -> y``,
+ where *x* is a 1D array of values for the independent variable,
+ *params* are the parameters to be fitted and *y* is the output array
+ that we want to have the best fit to a series of data points.
+
+ Background functions used by :class:`FitManager` must have a slightly
+ different signature: ``f(x, y0, *params) -> bg``, where *y0* is the
+ array of original data points and *bg* is the background signal that
+ we want to subtract from the data array prior to fitting the regular
+ fit function.
+
+ The number of parameters must be the same as in :attr:`parameters`, or
+ a multiple of this number if the function is defined as a sum of a
+ variable number of base functions and if :attr:`estimate` is designed
+ to be able to estimate the number of needed base functions.
+ """
+
+ self.parameters = parameters
+ """List of parameters names.
+
+ This list can contain the minimum number of parameters, if the
+ function takes a variable number of parameters,
+ and if the estimation function is responsible for finding the number
+ of required parameters """
+
+ self.estimate = estimate
+ """The estimation function should have the following signature::
+
+ f(x, y) -> (estimated_param, constraints)
+
+ Parameters:
+
+ - ``x`` is a sequence of values for the independent variable
+ - ``y`` is a sequence of the same length as ``x`` containing the
+ data to be fitted
+
+ Return values:
+
+ - ``estimated_param`` is a sequence of estimated fit parameters to
+ be used as initial values for an iterative fit.
+ - ``constraints`` is a sequence of shape *(n, 3)*, where *n* is the
+ number of estimated parameters, containing the constraints for each
+ parameter to be fitted. See :func:`silx.math.fit.leastsq` for more
+ explanations about constraints."""
+ if estimate is None:
+ self.estimate = self.default_estimate
+
+ self.configure = configure
+ """The optional configuration function must conform to the signature
+ ``f(**kw) -> dict`` (i.e it must accept any named argument and
+ return a dictionary).
+ It can be used to modify configuration parameters to alter the
+ behavior of the fit function and the estimation function."""
+
+ self.derivative = derivative
+ """The optional derivative function must conform to the signature
+ ``model_deriv(xdata, parameters, index)``, where parameters is a
+ sequence with the current values of the fitting parameters, index is
+ the fitting parameter index for which the the derivative has to be
+ provided in the supplied array of xdata points."""
+
+ self.description = description
+ """Optional description string for this particular fit theory."""
+
+ self.pymca_legacy = pymca_legacy
+ """This attribute can be set to *True* to indicate that the theory
+ is a PyMca legacy theory.
+
+ This tells :mod:`silx.math.fit.fitmanager` that the signature of
+ the estimate function is::
+
+ f(x, y, bg, xscaling, yscaling) -> (estimated_param, constraints)
+ """
+
+ self.is_background = is_background
+ """Flag to indicate that the theory is background theory.
+
+ A background function is an secondary function that needs to be added
+ to the main fit function to better fit the original data.
+ If this flag is set to *True*, modules using this theory are informed
+ that :attr:`function` has the signature ``f(x, y0, *params) -> bg``,
+ instead of the usual fit function signature."""
+
+ def default_estimate(self, x=None, y=None, bg=None):
+ """Default estimate function. Return an array of *ones* as the
+ initial estimated parameters, and set all constraints to zero
+ (FREE)"""
+ estimated_parameters = [1. for _ in self.parameters]
+ estimated_constraints = [[0, 0, 0] for _ in self.parameters]
+ return estimated_parameters, estimated_constraints
diff --git a/src/silx/math/fit/functions.pyx b/src/silx/math/fit/functions.pyx
new file mode 100644
index 0000000..1f78563
--- /dev/null
+++ b/src/silx/math/fit/functions.pyx
@@ -0,0 +1,985 @@
+# coding: utf-8
+#/*##########################################################################
+# Copyright (C) 2016-2020 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+#############################################################################*/
+"""This module provides fit functions.
+
+List of fit functions:
+-----------------------
+
+ - :func:`sum_gauss`
+ - :func:`sum_agauss`
+ - :func:`sum_splitgauss`
+ - :func:`sum_fastagauss`
+
+ - :func:`sum_apvoigt`
+ - :func:`sum_pvoigt`
+ - :func:`sum_splitpvoigt`
+
+ - :func:`sum_lorentz`
+ - :func:`sum_alorentz`
+ - :func:`sum_splitlorentz`
+
+ - :func:`sum_stepdown`
+ - :func:`sum_stepup`
+ - :func:`sum_slit`
+
+ - :func:`sum_ahypermet`
+ - :func:`sum_fastahypermet`
+
+Full documentation:
+-------------------
+
+"""
+
+__authors__ = ["P. Knobel"]
+__license__ = "MIT"
+__date__ = "16/08/2017"
+
+import logging
+import numpy
+
+_logger = logging.getLogger(__name__)
+
+cimport cython
+cimport silx.math.fit.functions_wrapper as functions_wrapper
+
+
+def erf(x):
+ """Return the gaussian error function
+
+ :param x: Independent variable where the gaussian error function is
+ calculated
+ :type x: numpy.ndarray or scalar
+ :return: Gaussian error function ``y=erf(x)``
+ :raise: IndexError if ``x`` is an empty array
+ """
+ cdef:
+ double[::1] x_c
+ double[::1] y_c
+
+
+ # force list into numpy array
+ if not hasattr(x, "shape"):
+ x = numpy.asarray(x)
+
+ for len_dim in x.shape:
+ if len_dim == 0:
+ raise IndexError("Cannot compute erf for an empty array")
+
+ x_c = numpy.array(x, copy=False, dtype=numpy.float64, order='C').reshape(-1)
+ y_c = numpy.empty(shape=(x_c.size,), dtype=numpy.float64)
+
+ status = functions_wrapper.erf_array(&x_c[0], x_c.size, &y_c[0])
+
+ return numpy.asarray(y_c).reshape(x.shape)
+
+
+def erfc(x):
+ """Return the gaussian complementary error function
+
+ :param x: Independent variable where the gaussian complementary error
+ function is calculated
+ :type x: numpy.ndarray or scalar
+ :return: Gaussian complementary error function ``y=erfc(x)``
+ :type rtype: numpy.ndarray
+ :raise: IndexError if ``x`` is an empty array
+ """
+ cdef:
+ double[::1] x_c
+ double[::1] y_c
+
+ # force list into numpy array
+ if not hasattr(x, "shape"):
+ x = numpy.asarray(x)
+
+ for len_dim in x.shape:
+ if len_dim == 0:
+ raise IndexError("Cannot compute erfc for an empty array")
+
+ x_c = numpy.array(x, copy=False, dtype=numpy.float64, order='C').reshape(-1)
+ y_c = numpy.empty(shape=(x_c.size,), dtype=numpy.float64)
+
+ status = functions_wrapper.erfc_array(&x_c[0], x_c.size, &y_c[0])
+
+ return numpy.asarray(y_c).reshape(x.shape)
+
+
+def sum_gauss(x, *params):
+ """Return a sum of gaussian functions defined by *(height, centroid, fwhm)*,
+ where:
+
+ - *height* is the peak amplitude
+ - *centroid* is the peak x-coordinate
+ - *fwhm* is the full-width at half maximum
+
+ :param x: Independent variable where the gaussians are calculated
+ :type x: numpy.ndarray
+ :param params: Array of gaussian parameters (length must be a multiple
+ of 3):
+ *(height1, centroid1, fwhm1, height2, centroid2, fwhm2,...)*
+ :return: Array of sum of gaussian functions at each ``x`` coordinate.
+ """
+ cdef:
+ double[::1] x_c
+ double[::1] params_c
+ double[::1] y_c
+
+ if not len(params):
+ raise IndexError("No gaussian parameters specified. " +
+ "At least 3 parameters are required.")
+
+ # ensure float64 (double) type and 1D contiguous data layout in memory
+ x_c = numpy.array(x,
+ copy=False,
+ dtype=numpy.float64,
+ order='C').reshape(-1)
+ params_c = numpy.array(params,
+ copy=False,
+ dtype=numpy.float64,
+ order='C').reshape(-1)
+ y_c = numpy.empty(shape=(x.size,),
+ dtype=numpy.float64)
+
+ status = functions_wrapper.sum_gauss(
+ &x_c[0], x.size,
+ &params_c[0], params_c.size,
+ &y_c[0])
+
+ if status:
+ raise IndexError("Wrong number of parameters for function")
+
+ # reshape y_c to match original, possibly unusual, data shape
+ return numpy.asarray(y_c).reshape(x.shape)
+
+
+def sum_agauss(x, *params):
+ """Return a sum of gaussian functions defined by *(area, centroid, fwhm)*,
+ where:
+
+ - *area* is the area underneath the peak
+ - *centroid* is the peak x-coordinate
+ - *fwhm* is the full-width at half maximum
+
+ :param x: Independent variable where the gaussians are calculated
+ :type x: numpy.ndarray
+ :param params: Array of gaussian parameters (length must be a multiple
+ of 3):
+ *(area1, centroid1, fwhm1, area2, centroid2, fwhm2,...)*
+ :return: Array of sum of gaussian functions at each ``x`` coordinate.
+ """
+ cdef:
+ double[::1] x_c
+ double[::1] params_c
+ double[::1] y_c
+
+ if not len(params):
+ raise IndexError("No gaussian parameters specified. " +
+ "At least 3 parameters are required.")
+
+ x_c = numpy.array(x,
+ copy=False,
+ dtype=numpy.float64,
+ order='C').reshape(-1)
+ params_c = numpy.array(params,
+ copy=False,
+ dtype=numpy.float64,
+ order='C').reshape(-1)
+ y_c = numpy.empty(shape=(x.size,),
+ dtype=numpy.float64)
+
+ status = functions_wrapper.sum_agauss(
+ &x_c[0], x.size,
+ &params_c[0], params_c.size,
+ &y_c[0])
+
+ if status:
+ raise IndexError("Wrong number of parameters for function")
+
+ return numpy.asarray(y_c).reshape(x.shape)
+
+
+def sum_fastagauss(x, *params):
+ """Return a sum of gaussian functions defined by *(area, centroid, fwhm)*,
+ where:
+
+ - *area* is the area underneath the peak
+ - *centroid* is the peak x-coordinate
+ - *fwhm* is the full-width at half maximum
+
+ This implementation differs from :func:`sum_agauss` by the usage of a
+ lookup table with precalculated exponential values. This might speed up
+ the computation for large numbers of individual gaussian functions.
+
+ :param x: Independent variable where the gaussians are calculated
+ :type x: numpy.ndarray
+ :param params: Array of gaussian parameters (length must be a multiple
+ of 3):
+ *(area1, centroid1, fwhm1, area2, centroid2, fwhm2,...)*
+ :return: Array of sum of gaussian functions at each ``x`` coordinate.
+ """
+ cdef:
+ double[::1] x_c
+ double[::1] params_c
+ double[::1] y_c
+
+ if not len(params):
+ raise IndexError("No gaussian parameters specified. " +
+ "At least 3 parameters are required.")
+
+ x_c = numpy.array(x,
+ copy=False,
+ dtype=numpy.float64,
+ order='C').reshape(-1)
+ params_c = numpy.array(params,
+ copy=False,
+ dtype=numpy.float64,
+ order='C').reshape(-1)
+ y_c = numpy.empty(shape=(x.size,),
+ dtype=numpy.float64)
+
+ status = functions_wrapper.sum_fastagauss(
+ &x_c[0], x.size,
+ &params_c[0], params_c.size,
+ &y_c[0])
+
+ if status:
+ raise IndexError("Wrong number of parameters for function")
+
+ return numpy.asarray(y_c).reshape(x.shape)
+
+
+def sum_splitgauss(x, *params):
+ """Return a sum of gaussian functions defined by *(area, centroid, fwhm1, fwhm2)*,
+ where:
+
+ - *height* is the peak amplitude
+ - *centroid* is the peak x-coordinate
+ - *fwhm1* is the full-width at half maximum for the distribution
+ when ``x < centroid``
+ - *fwhm2* is the full-width at half maximum for the distribution
+ when ``x > centroid``
+
+ :param x: Independent variable where the gaussians are calculated
+ :type x: numpy.ndarray
+ :param params: Array of gaussian parameters (length must be a multiple
+ of 4):
+ *(height1, centroid1, fwhm11, fwhm21, height2, centroid2, fwhm12, fwhm22,...)*
+ :return: Array of sum of split gaussian functions at each ``x`` coordinate
+ """
+ cdef:
+ double[::1] x_c
+ double[::1] params_c
+ double[::1] y_c
+
+ if not len(params):
+ raise IndexError("No gaussian parameters specified. " +
+ "At least 4 parameters are required.")
+
+ x_c = numpy.array(x,
+ copy=False,
+ dtype=numpy.float64,
+ order='C').reshape(-1)
+ params_c = numpy.array(params,
+ copy=False,
+ dtype=numpy.float64,
+ order='C').reshape(-1)
+ y_c = numpy.empty(shape=(x.size,),
+ dtype=numpy.float64)
+
+ status = functions_wrapper.sum_splitgauss(
+ &x_c[0], x.size,
+ &params_c[0], params_c.size,
+ &y_c[0])
+
+ if status:
+ raise IndexError("Wrong number of parameters for function")
+
+ return numpy.asarray(y_c).reshape(x.shape)
+
+
+def sum_apvoigt(x, *params):
+ """Return a sum of pseudo-Voigt functions, defined by *(area, centroid, fwhm,
+ eta)*.
+
+ The pseudo-Voigt profile ``PV(x)`` is an approximation of the Voigt
+ profile using a linear combination of a Gaussian curve ``G(x)`` and a
+ Lorentzian curve ``L(x)`` instead of their convolution.
+
+ - *area* is the area underneath both G(x) and L(x)
+ - *centroid* is the peak x-coordinate for both functions
+ - *fwhm* is the full-width at half maximum of both functions
+ - *eta* is the Lorentz factor: PV(x) = eta * L(x) + (1 - eta) * G(x)
+
+ :param x: Independent variable where the gaussians are calculated
+ :type x: numpy.ndarray
+ :param params: Array of pseudo-Voigt parameters (length must be a multiple
+ of 4):
+ *(area1, centroid1, fwhm1, eta1, area2, centroid2, fwhm2, eta2,...)*
+ :return: Array of sum of pseudo-Voigt functions at each ``x`` coordinate
+ """
+ cdef:
+ double[::1] x_c
+ double[::1] params_c
+ double[::1] y_c
+
+ if not len(params):
+ raise IndexError("No parameters specified. " +
+ "At least 4 parameters are required.")
+ x_c = numpy.array(x,
+ copy=False,
+ dtype=numpy.float64,
+ order='C').reshape(-1)
+ params_c = numpy.array(params,
+ copy=False,
+ dtype=numpy.float64,
+ order='C').reshape(-1)
+ y_c = numpy.empty(shape=(x.size,),
+ dtype=numpy.float64)
+
+ status = functions_wrapper.sum_apvoigt(
+ &x_c[0], x.size,
+ &params_c[0], params_c.size,
+ &y_c[0])
+
+ if status:
+ raise IndexError("Wrong number of parameters for function")
+
+ return numpy.asarray(y_c).reshape(x.shape)
+
+
+def sum_pvoigt(x, *params):
+ """Return a sum of pseudo-Voigt functions, defined by *(height, centroid,
+ fwhm, eta)*.
+
+ The pseudo-Voigt profile ``PV(x)`` is an approximation of the Voigt
+ profile using a linear combination of a Gaussian curve ``G(x)`` and a
+ Lorentzian curve ``L(x)`` instead of their convolution.
+
+ - *height* is the peak amplitude of G(x) and L(x)
+ - *centroid* is the peak x-coordinate for both functions
+ - *fwhm* is the full-width at half maximum of both functions
+ - *eta* is the Lorentz factor: PV(x) = eta * L(x) + (1 - eta) * G(x)
+
+ :param x: Independent variable where the gaussians are calculated
+ :type x: numpy.ndarray
+ :param params: Array of pseudo-Voigt parameters (length must be a multiple
+ of 4):
+ *(height1, centroid1, fwhm1, eta1, height2, centroid2, fwhm2, eta2,...)*
+ :return: Array of sum of pseudo-Voigt functions at each ``x`` coordinate
+ """
+ cdef:
+ double[::1] x_c
+ double[::1] params_c
+ double[::1] y_c
+
+ if not len(params):
+ raise IndexError("No parameters specified. " +
+ "At least 4 parameters are required.")
+
+ x_c = numpy.array(x,
+ copy=False,
+ dtype=numpy.float64,
+ order='C').reshape(-1)
+ params_c = numpy.array(params,
+ copy=False,
+ dtype=numpy.float64,
+ order='C').reshape(-1)
+ y_c = numpy.empty(shape=(x.size,),
+ dtype=numpy.float64)
+
+ status = functions_wrapper.sum_pvoigt(
+ &x_c[0], x.size,
+ &params_c[0], params_c.size,
+ &y_c[0])
+
+ if status:
+ raise IndexError("Wrong number of parameters for function")
+
+ return numpy.asarray(y_c).reshape(x.shape)
+
+
+def sum_splitpvoigt(x, *params):
+ """Return a sum of split pseudo-Voigt functions, defined by *(height,
+ centroid, fwhm1, fwhm2, eta)*.
+
+ The pseudo-Voigt profile ``PV(x)`` is an approximation of the Voigt
+ profile using a linear combination of a Gaussian curve ``G(x)`` and a
+ Lorentzian curve ``L(x)`` instead of their convolution.
+
+ - *height* is the peak amplitudefor G(x) and L(x)
+ - *centroid* is the peak x-coordinate for both functions
+ - *fwhm1* is the full-width at half maximum of both functions
+ when ``x < centroid``
+ - *fwhm2* is the full-width at half maximum of both functions
+ when ``x > centroid``
+ - *eta* is the Lorentz factor: PV(x) = eta * L(x) + (1 - eta) * G(x)
+
+ :param x: Independent variable where the gaussians are calculated
+ :type x: numpy.ndarray
+ :param params: Array of pseudo-Voigt parameters (length must be a multiple
+ of 5):
+ *(height1, centroid1, fwhm11, fwhm21, eta1,...)*
+ :return: Array of sum of split pseudo-Voigt functions at each ``x``
+ coordinate
+ """
+ cdef:
+ double[::1] x_c
+ double[::1] params_c
+ double[::1] y_c
+
+ if not len(params):
+ raise IndexError("No parameters specified. " +
+ "At least 5 parameters are required.")
+
+ x_c = numpy.array(x,
+ copy=False,
+ dtype=numpy.float64,
+ order='C').reshape(-1)
+ params_c = numpy.array(params,
+ copy=False,
+ dtype=numpy.float64,
+ order='C').reshape(-1)
+ y_c = numpy.empty(shape=(x.size,),
+ dtype=numpy.float64)
+
+ status = functions_wrapper.sum_splitpvoigt(
+ &x_c[0], x.size,
+ &params_c[0], params_c.size,
+ &y_c[0])
+
+ if status:
+ raise IndexError("Wrong number of parameters for function")
+
+ return numpy.asarray(y_c).reshape(x.shape)
+
+
+def sum_lorentz(x, *params):
+ """Return a sum of Lorentz distributions, also known as Cauchy distribution,
+ defined by *(height, centroid, fwhm)*.
+
+ - *height* is the peak amplitude
+ - *centroid* is the peak x-coordinate
+ - *fwhm* is the full-width at half maximum
+
+ :param x: Independent variable where the gaussians are calculated
+ :type x: numpy.ndarray
+ :param params: Array of Lorentz parameters (length must be a multiple
+ of 3):
+ *(height1, centroid1, fwhm1,...)*
+ :return: Array of sum Lorentz functions at each ``x``
+ coordinate
+ """
+ cdef:
+ double[::1] x_c
+ double[::1] params_c
+ double[::1] y_c
+
+ if not len(params):
+ raise IndexError("No parameters specified. " +
+ "At least 3 parameters are required.")
+
+ x_c = numpy.array(x,
+ copy=False,
+ dtype=numpy.float64,
+ order='C').reshape(-1)
+ params_c = numpy.array(params,
+ copy=False,
+ dtype=numpy.float64,
+ order='C').reshape(-1)
+ y_c = numpy.empty(shape=(x.size,),
+ dtype=numpy.float64)
+
+ status = functions_wrapper.sum_lorentz(
+ &x_c[0], x.size,
+ &params_c[0], params_c.size,
+ &y_c[0])
+
+ if status:
+ raise IndexError("Wrong number of parameters for function")
+
+ return numpy.asarray(y_c).reshape(x.shape)
+
+
+def sum_alorentz(x, *params):
+ """Return a sum of Lorentz distributions, also known as Cauchy distribution,
+ defined by *(area, centroid, fwhm)*.
+
+ - *area* is the area underneath the peak
+ - *centroid* is the peak x-coordinate for both functions
+ - *fwhm* is the full-width at half maximum
+
+ :param x: Independent variable where the gaussians are calculated
+ :type x: numpy.ndarray
+ :param params: Array of Lorentz parameters (length must be a multiple
+ of 3):
+ *(area1, centroid1, fwhm1,...)*
+ :return: Array of sum of Lorentz functions at each ``x``
+ coordinate
+ """
+ cdef:
+ double[::1] x_c
+ double[::1] params_c
+ double[::1] y_c
+
+ if not len(params):
+ raise IndexError("No parameters specified. " +
+ "At least 3 parameters are required.")
+
+ x_c = numpy.array(x,
+ copy=False,
+ dtype=numpy.float64,
+ order='C').reshape(-1)
+ params_c = numpy.array(params,
+ copy=False,
+ dtype=numpy.float64,
+ order='C').reshape(-1)
+ y_c = numpy.empty(shape=(x.size,),
+ dtype=numpy.float64)
+
+ status = functions_wrapper.sum_alorentz(
+ &x_c[0], x.size,
+ &params_c[0], params_c.size,
+ &y_c[0])
+
+ if status:
+ raise IndexError("Wrong number of parameters for function")
+
+ return numpy.asarray(y_c).reshape(x.shape)
+
+
+def sum_splitlorentz(x, *params):
+ """Return a sum of split Lorentz distributions,
+ defined by *(height, centroid, fwhm1, fwhm2)*.
+
+ - *height* is the peak amplitude
+ - *centroid* is the peak x-coordinate for both functions
+ - *fwhm1* is the full-width at half maximum for ``x < centroid``
+ - *fwhm2* is the full-width at half maximum for ``x > centroid``
+
+ :param x: Independent variable where the gaussians are calculated
+ :type x: numpy.ndarray
+ :param params: Array of Lorentz parameters (length must be a multiple
+ of 4):
+ *(height1, centroid1, fwhm11, fwhm21...)*
+ :return: Array of sum of Lorentz functions at each ``x``
+ coordinate
+ """
+ cdef:
+ double[::1] x_c
+ double[::1] params_c
+ double[::1] y_c
+
+ if not len(params):
+ raise IndexError("No parameters specified. " +
+ "At least 4 parameters are required.")
+
+ x_c = numpy.array(x,
+ copy=False,
+ dtype=numpy.float64,
+ order='C').reshape(-1)
+ params_c = numpy.array(params,
+ copy=False,
+ dtype=numpy.float64,
+ order='C').reshape(-1)
+ y_c = numpy.empty(shape=(x.size,),
+ dtype=numpy.float64)
+
+ status = functions_wrapper.sum_splitlorentz(
+ &x_c[0], x.size,
+ &params_c[0], params_c.size,
+ &y_c[0])
+
+ if status:
+ raise IndexError("Wrong number of parameters for function")
+
+ return numpy.asarray(y_c).reshape(x.shape)
+
+
+def sum_stepdown(x, *params):
+ """Return a sum of stepdown functions.
+ defined by *(height, centroid, fwhm)*.
+
+ - *height* is the step's amplitude
+ - *centroid* is the step's x-coordinate
+ - *fwhm* is the full-width at half maximum for the derivative,
+ which is a measure of the *sharpness* of the step-down's edge
+
+ :param x: Independent variable where the gaussians are calculated
+ :type x: numpy.ndarray
+ :param params: Array of stepdown parameters (length must be a multiple
+ of 3):
+ *(height1, centroid1, fwhm1,...)*
+ :return: Array of sum of stepdown functions at each ``x``
+ coordinate
+ """
+ cdef:
+ double[::1] x_c
+ double[::1] params_c
+ double[::1] y_c
+
+ if not len(params):
+ raise IndexError("No parameters specified. " +
+ "At least 3 parameters are required.")
+ x_c = numpy.array(x,
+ copy=False,
+ dtype=numpy.float64,
+ order='C').reshape(-1)
+ params_c = numpy.array(params,
+ copy=False,
+ dtype=numpy.float64,
+ order='C').reshape(-1)
+ y_c = numpy.empty(shape=(x.size,),
+ dtype=numpy.float64)
+
+ status = functions_wrapper.sum_stepdown(&x_c[0],
+ x.size,
+ &params_c[0],
+ params_c.size,
+ &y_c[0])
+
+ if status:
+ raise IndexError("Wrong number of parameters for function")
+
+ return numpy.asarray(y_c).reshape(x.shape)
+
+
+def sum_stepup(x, *params):
+ """Return a sum of stepup functions.
+ defined by *(height, centroid, fwhm)*.
+
+ - *height* is the step's amplitude
+ - *centroid* is the step's x-coordinate
+ - *fwhm* is the full-width at half maximum for the derivative,
+ which is a measure of the *sharpness* of the step-up's edge
+
+ :param x: Independent variable where the gaussians are calculated
+ :type x: numpy.ndarray
+ :param params: Array of stepup parameters (length must be a multiple
+ of 3):
+ *(height1, centroid1, fwhm1,...)*
+ :return: Array of sum of stepup functions at each ``x``
+ coordinate
+ """
+ cdef:
+ double[::1] x_c
+ double[::1] params_c
+ double[::1] y_c
+
+ if not len(params):
+ raise IndexError("No parameters specified. " +
+ "At least 3 parameters are required.")
+
+ x_c = numpy.array(x,
+ copy=False,
+ dtype=numpy.float64,
+ order='C').reshape(-1)
+ params_c = numpy.array(params,
+ copy=False,
+ dtype=numpy.float64,
+ order='C').reshape(-1)
+ y_c = numpy.empty(shape=(x.size,),
+ dtype=numpy.float64)
+
+ status = functions_wrapper.sum_stepup(&x_c[0],
+ x.size,
+ &params_c[0],
+ params_c.size,
+ &y_c[0])
+
+ if status:
+ raise IndexError("Wrong number of parameters for function")
+
+ return numpy.asarray(y_c).reshape(x.shape)
+
+
+def sum_slit(x, *params):
+ """Return a sum of slit functions.
+ defined by *(height, position, fwhm, beamfwhm)*.
+
+ - *height* is the slit's amplitude
+ - *position* is the center of the slit's x-coordinate
+ - *fwhm* is the full-width at half maximum of the slit
+ - *beamfwhm* is the full-width at half maximum of the
+ derivative, which is a measure of the *sharpness*
+ of the edges of the slit
+
+ :param x: Independent variable where the slits are calculated
+ :type x: numpy.ndarray
+ :param params: Array of slit parameters (length must be a multiple
+ of 4):
+ *(height1, centroid1, fwhm1, beamfwhm1,...)*
+ :return: Array of sum of slit functions at each ``x``
+ coordinate
+ """
+ cdef:
+ double[::1] x_c
+ double[::1] params_c
+ double[::1] y_c
+
+ if not len(params):
+ raise IndexError("No parameters specified. " +
+ "At least 4 parameters are required.")
+
+ x_c = numpy.array(x,
+ copy=False,
+ dtype=numpy.float64,
+ order='C').reshape(-1)
+ params_c = numpy.array(params,
+ copy=False,
+ dtype=numpy.float64,
+ order='C').reshape(-1)
+ y_c = numpy.empty(shape=(x.size,),
+ dtype=numpy.float64)
+
+ status = functions_wrapper.sum_slit(&x_c[0],
+ x.size,
+ &params_c[0],
+ params_c.size,
+ &y_c[0])
+
+ if status:
+ raise IndexError("Wrong number of parameters for function")
+
+ return numpy.asarray(y_c).reshape(x.shape)
+
+
+def sum_ahypermet(x, *params,
+ gaussian_term=True, st_term=True, lt_term=True, step_term=True):
+ """Return a sum of ahypermet functions.
+ defined by *(area, position, fwhm, st_area_r, st_slope_r, lt_area_r,
+ lt_slope_r, step_height_r)*.
+
+ - *area* is the area underneath the gaussian peak
+ - *position* is the center of the various peaks and the position of
+ the step down
+ - *fwhm* is the full-width at half maximum of the terms
+ - *st_area_r* is factor between the gaussian area and the area of the
+ short tail term
+ - *st_slope_r* is a ratio related to the slope of the short tail
+ in the low ``x`` values (the lower, the steeper)
+ - *lt_area_r* is ratio between the gaussian area and the area of the
+ long tail term
+ - *lt_slope_r* is a ratio related to the slope of the long tail
+ in the low ``x`` values (the lower, the steeper)
+ - *step_height_r* is the ratio between the height of the step down
+ and the gaussian height
+
+ A hypermet function is a sum of four functions (terms):
+
+ - a gaussian term
+ - a long tail term
+ - a short tail term
+ - a step down term
+
+ :param x: Independent variable where the hypermets are calculated
+ :type x: numpy.ndarray
+ :param params: Array of hypermet parameters (length must be a multiple
+ of 8):
+ *(area1, position1, fwhm1, st_area_r1, st_slope_r1, lt_area_r1,
+ lt_slope_r1, step_height_r1...)*
+ :param gaussian_term: If ``True``, enable gaussian term. Default ``True``
+ :param st_term: If ``True``, enable gaussian term. Default ``True``
+ :param lt_term: If ``True``, enable gaussian term. Default ``True``
+ :param step_term: If ``True``, enable gaussian term. Default ``True``
+ :return: Array of sum of hypermet functions at each ``x`` coordinate
+ """
+ cdef:
+ double[::1] x_c
+ double[::1] params_c
+ double[::1] y_c
+
+ if not len(params):
+ raise IndexError("No parameters specified. " +
+ "At least 8 parameters are required.")
+
+ # Sum binary flags to activate various terms of the equation
+ tail_flags = 1 if gaussian_term else 0
+ if st_term:
+ tail_flags += 2
+ if lt_term:
+ tail_flags += 4
+ if step_term:
+ tail_flags += 8
+
+ x_c = numpy.array(x,
+ copy=False,
+ dtype=numpy.float64,
+ order='C').reshape(-1)
+ params_c = numpy.array(params,
+ copy=False,
+ dtype=numpy.float64,
+ order='C').reshape(-1)
+ y_c = numpy.empty(shape=(x.size,),
+ dtype=numpy.float64)
+
+ status = functions_wrapper.sum_ahypermet(&x_c[0],
+ x.size,
+ &params_c[0],
+ params_c.size,
+ &y_c[0],
+ tail_flags)
+
+ if status:
+ raise IndexError("Wrong number of parameters for function")
+
+ return numpy.asarray(y_c).reshape(x.shape)
+
+
+def sum_fastahypermet(x, *params,
+ gaussian_term=True, st_term=True,
+ lt_term=True, step_term=True):
+ """Return a sum of hypermet functions defined by *(area, position, fwhm,
+ st_area_r, st_slope_r, lt_area_r, lt_slope_r, step_height_r)*.
+
+ - *area* is the area underneath the gaussian peak
+ - *position* is the center of the various peaks and the position of
+ the step down
+ - *fwhm* is the full-width at half maximum of the terms
+ - *st_area_r* is factor between the gaussian area and the area of the
+ short tail term
+ - *st_slope_r* is a parameter related to the slope of the short tail
+ in the low ``x`` values (the lower, the steeper)
+ - *lt_area_r* is factor between the gaussian area and the area of the
+ long tail term
+ - *lt_slope_r* is a parameter related to the slope of the long tail
+ in the low ``x`` values (the lower, the steeper)
+ - *step_height_r* is the factor between the height of the step down
+ and the gaussian height
+
+ A hypermet function is a sum of four functions (terms):
+
+ - a gaussian term
+ - a long tail term
+ - a short tail term
+ - a step down term
+
+ This function differs from :func:`sum_ahypermet` by the use of a lookup
+ table for calculating exponentials. This offers better performance when
+ calculating many functions for large ``x`` arrays.
+
+ :param x: Independent variable where the hypermets are calculated
+ :type x: numpy.ndarray
+ :param params: Array of hypermet parameters (length must be a multiple
+ of 8):
+ *(area1, position1, fwhm1, st_area_r1, st_slope_r1, lt_area_r1,
+ lt_slope_r1, step_height_r1...)*
+ :param gaussian_term: If ``True``, enable gaussian term. Default ``True``
+ :param st_term: If ``True``, enable gaussian term. Default ``True``
+ :param lt_term: If ``True``, enable gaussian term. Default ``True``
+ :param step_term: If ``True``, enable gaussian term. Default ``True``
+ :return: Array of sum of hypermet functions at each ``x`` coordinate
+ """
+ cdef:
+ double[::1] x_c
+ double[::1] params_c
+ double[::1] y_c
+
+ if not len(params):
+ raise IndexError("No parameters specified. " +
+ "At least 8 parameters are required.")
+
+ # Sum binary flags to activate various terms of the equation
+ tail_flags = 1 if gaussian_term else 0
+ if st_term:
+ tail_flags += 2
+ if lt_term:
+ tail_flags += 4
+ if step_term:
+ tail_flags += 8
+
+ # TODO (maybe):
+ # Set flags according to params, to move conditional
+ # branches out of the C code.
+ # E.g., set st_term = False if any of the st_slope_r params
+ # (params[8*i + 4]) is 0, to prevent division by 0. Same thing for
+ # lt_slope_r (params[8*i + 6]) and lt_term.
+
+ x_c = numpy.array(x,
+ copy=False,
+ dtype=numpy.float64,
+ order='C').reshape(-1)
+ params_c = numpy.array(params,
+ copy=False,
+ dtype=numpy.float64,
+ order='C').reshape(-1)
+ y_c = numpy.empty(shape=(x.size,),
+ dtype=numpy.float64)
+
+ status = functions_wrapper.sum_fastahypermet(&x_c[0],
+ x.size,
+ &params_c[0],
+ params_c.size,
+ &y_c[0],
+ tail_flags)
+
+ if status:
+ raise IndexError("Wrong number of parameters for function")
+
+ return numpy.asarray(y_c).reshape(x.shape)
+
+
+def atan_stepup(x, a, b, c):
+ """
+ Step up function using an inverse tangent.
+
+ :param x: Independent variable where the function is calculated
+ :type x: numpy array
+ :param a: Height of the step up
+ :param b: Center of the step up
+ :param c: Parameter related to the slope of the step. A lower ``c``
+ value yields a sharper step.
+ :return: ``a * (0.5 + (arctan((x - b) / c) / pi))``
+ :rtype: numpy array
+ """
+ if not hasattr(x, "shape"):
+ x = numpy.array(x)
+ return a * (0.5 + (numpy.arctan((1.0 * x - b) / c) / numpy.pi))
+
+
+def periodic_gauss(x, *pars):
+ """
+ Return a sum of gaussian functions defined by
+ *(npeaks, delta, height, centroid, fwhm)*,
+ where:
+
+ - *npeaks* is the number of gaussians peaks
+ - *delta* is the constant distance between 2 peaks
+ - *height* is the peak amplitude of all the gaussians
+ - *centroid* is the peak x-coordinate of the first gaussian
+ - *fwhm* is the full-width at half maximum for all the gaussians
+
+ :param x: Independent variable where the function is calculated
+ :param pars: *(npeaks, delta, height, centroid, fwhm)*
+ :return: Sum of ``npeaks`` gaussians
+ """
+
+ if not len(pars):
+ raise IndexError("No parameters specified. " +
+ "At least 5 parameters are required.")
+
+ newpars = numpy.zeros((pars[0], 3), numpy.float64)
+ for i in range(int(pars[0])):
+ newpars[i, 0] = pars[2]
+ newpars[i, 1] = pars[3] + i * pars[1]
+ newpars[:, 2] = pars[4]
+ return sum_gauss(x, newpars)
diff --git a/src/silx/math/fit/functions/include/functions.h b/src/silx/math/fit/functions/include/functions.h
new file mode 100644
index 0000000..de4209b
--- /dev/null
+++ b/src/silx/math/fit/functions/include/functions.h
@@ -0,0 +1,68 @@
+/*##########################################################################
+# Copyright (C) 2016 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
+
+#ifndef FITFUNCTIONS_H
+#define FITFUNCTIONS_H
+
+/* Helper functions */
+int test_params(int len_params, int len_params_one_function, char* fun_name, char* param_names);
+double myerfc(double x);
+double myerf(double x);
+int erfc_array(double* x, int len_x, double* y);
+int erf_array(double* x, int len_x, double* y);
+
+/* Background functions */
+void snip1d(double *data, int size, int width);
+//void snip1d_multiple(double *data, int n_channels, int snip_width, int n_spectra);
+void snip2d(double *data, int nrows, int ncolumns, int width);
+void snip3d(double *data, int nx, int ny, int nz, int width);
+
+int strip(double* input, long len_input, double c, long niter, int deltai,
+ long* anchors, long len_anchors, double* output);
+
+/* Smoothing functions */
+
+int SavitskyGolay(double* input, long len_input, int npoints, double* output);
+
+/* Fit functions */
+int sum_gauss(double* x, int len_x, double* pgauss, int len_pgauss, double* y);
+int sum_agauss(double* x, int len_x, double* pgauss, int len_pgauss, double* y);
+int sum_fastagauss(double* x, int len_x, double* pgauss, int len_pgauss, double* y);
+int sum_splitgauss(double* x, int len_x, double* pgauss, int len_pgauss, double* y);
+
+int sum_apvoigt(double* x, int len_x, double* pvoigt, int len_pvoigt, double* y);
+int sum_pvoigt(double* x, int len_x, double* pvoigt, int len_pvoigt, double* y);
+int sum_splitpvoigt(double* x, int len_x, double* pvoigt, int len_pvoigt, double* y);
+
+int sum_lorentz(double* x, int len_x, double* plorentz, int len_plorentz, double* y);
+int sum_alorentz(double* x, int len_x, double* plorentz, int len_plorentz, double* y);
+int sum_splitlorentz(double* x, int len_x, double* plorentz, int len_plorentz, double* y);
+
+int sum_stepdown(double* x, int len_x, double* pdstep, int len_pdstep, double* y);
+int sum_stepup(double* x, int len_x, double* pustep, int len_pustep, double* y);
+int sum_slit(double* x, int len_x, double* pslit, int len_pslit, double* y);
+
+int sum_ahypermet(double* x, int len_x, double* phypermet, int len_phypermet, double* y, int tail_flags);
+int sum_fastahypermet(double* x, int len_x, double* phypermet, int len_phypermet, double* y, int tail_flags);
+
+#endif /* #define FITFUNCTIONS_H */
diff --git a/src/silx/math/fit/functions/src/funs.c b/src/silx/math/fit/functions/src/funs.c
new file mode 100644
index 0000000..aae173f
--- /dev/null
+++ b/src/silx/math/fit/functions/src/funs.c
@@ -0,0 +1,1265 @@
+#/*##########################################################################
+# Copyright (c) 2004-2016 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+#############################################################################*/
+/*
+ This file provides fit functions.
+
+ It is adapted from PyMca source file "SpecFitFuns.c". The main difference
+ with the original code is that this code does not handle the python
+ wrapping, which is done elsewhere using cython.
+
+ Authors: V.A. Sole, P. Knobel
+ License: MIT
+ Last modified: 17/06/2016
+*/
+#include <math.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include "functions.h"
+
+#ifndef M_PI
+#define M_PI 3.1415926535
+#endif
+
+#define MIN(x, y) (((x) < (y)) ? (x) : (y))
+#define MAX(x, y) (((x) > (y)) ? (x) : (y))
+
+#if defined(_WIN32)
+#define erf myerf
+#define erfc myerfc
+#endif
+
+#define LOG2 0.69314718055994529
+
+
+int test_params(int len_params,
+ int len_params_one_function,
+ char* fun_name,
+ char* param_names)
+{
+ if (len_params % len_params_one_function) {
+ printf("[%s]Error: Number of parameters must be a multiple of %d.",
+ fun_name, len_params_one_function);
+ printf("\nParameters expected for %s: %s\n",
+ fun_name, param_names);
+ return(1);
+ }
+ if (len_params == 0) {
+ printf("[%s]Error: No parameters specified.", fun_name);
+ printf("\nParameters expected for %s: %s\n",
+ fun_name, param_names);
+ return(1);
+ }
+ return(0);
+}
+
+/* Complementary error function for a single value*/
+double myerfc(double x)
+{
+ double z;
+ double t;
+ double r;
+
+ z=fabs(x);
+ t=1.0/(1.0+0.5*z);
+ r=t * exp(-z * z - 1.26551223 + t * (1.00002368 + t * (0.3740916 +
+ t * (0.09678418 + t * (-0.18628806 + t * (0.27886807 + t * (-1.13520398 +
+ t * (1.48851587 + t * (-0.82215223+t*0.17087277)))))))));
+ if (x<0)
+ r=2.0-r;
+ return (r);
+}
+
+/* Gauss error function for a single value*/
+double myerf(double x)
+{
+ return (1.0 - myerfc(x));
+}
+
+/* Gauss error function for an array
+ y[i]=erf(x[i])
+ returns status code 0
+*/
+int erf_array(double* x, int len_x, double* y)
+{
+ int j;
+ for (j=0; j<len_x; j++) {
+ y[j] = erf(x[j]);
+ }
+ return(0);
+}
+
+/* Complementary error function for an array
+ y[i]=erfc(x[i])
+ returns status code 0*/
+int erfc_array(double* x, int len_x, double* y)
+{
+ int j;
+ for (j=0; j<len_x; j++) {
+ y[j] = erfc(x[j]);
+ }
+ return(0);
+}
+
+/* Use lookup table for fast exp computation */
+double fastexp(double x)
+{
+ int expindex;
+ static double EXP[5000] = {0.0};
+ int i;
+
+/*initialize */
+ if (EXP[0] < 1){
+ for (i=0;i<5000;i++){
+ EXP[i] = exp(-0.01 * i);
+ }
+ }
+/*calculate*/
+ if (x < 0){
+ x = -x;
+ if (x < 50){
+ expindex = (int) (x * 100);
+ return EXP[expindex]*(1.0 - (x - 0.01 * expindex)) ;
+ }else if (x < 100) {
+ expindex = (int) (x * 10);
+ return pow(EXP[expindex]*(1.0 - (x - 0.1 * expindex)),10) ;
+ }else if (x < 1000){
+ expindex = (int) x;
+ return pow(EXP[expindex]*(1.0 - (x - expindex)),20) ;
+ }else if (x < 10000){
+ expindex = (int) (x * 0.1);
+ return pow(EXP[expindex]*(1.0 - (x - 10.0 * expindex)),30) ;
+ }else{
+ return 0;
+ }
+ }else{
+ if (x < 50){
+ expindex = (int) (x * 100);
+ return 1.0/EXP[expindex]*(1.0 - (x - 0.01 * expindex)) ;
+ }else if (x < 100) {
+ expindex = (int) (x * 10);
+ return pow(EXP[expindex]*(1.0 - (x - 0.1 * expindex)),-10) ;
+ }else{
+ return exp(x);
+ }
+ }
+}
+
+
+/* sum_gauss
+ Sum of gaussian functions, defined by (height, centroid, fwhm)
+
+ *height* is the peak amplitude
+ *centroid* is the peak x-coordinate
+ *fwhm* is the full-width at half maximum
+
+ Parameters:
+ -----------
+
+ - x: Independant variable where the gaussians are calculated.
+ - len_x: Number of elements in the x array.
+ - pvoigt: Array of gaussian parameters:
+ (height1, centroid1, fwhm1, height2, centroid2, fwhm2,...)
+ - len_pgauss: Number of elements in the pgauss array. Must be
+ a multiple of 3.
+ - y: Output array. Must have memory allocated for the same number
+ of elements as x (len_x).
+
+ Adapted from PyMca module SpecFitFuns
+*/
+int sum_gauss(double* x, int len_x, double* pgauss, int len_pgauss, double* y)
+{
+ int i, j;
+ double dhelp, inv_two_sqrt_two_log2, sigma;
+ double fwhm, centroid, height;
+
+ if (test_params(len_pgauss, 3, "sum_gauss", "height, centroid, fwhm")) {
+ return(1);
+ }
+
+ /* Initialize output array */
+ for (j=0; j<len_x; j++) {
+ y[j] = 0.;
+ }
+
+ inv_two_sqrt_two_log2 = 1.0 / (2.0 * sqrt(2.0 * LOG2));
+
+ for (i=0; i<len_pgauss/3; i++) {
+ height = pgauss[3*i];
+ centroid = pgauss[3*i+1];
+ fwhm = pgauss[3*i+2];
+
+ sigma = fwhm * inv_two_sqrt_two_log2;
+
+ for (j=0; j<len_x; j++) {
+ dhelp = (x[j] - centroid) / sigma;
+ if (dhelp <= 20) {
+ y[j] += height * exp (-0.5 * dhelp * dhelp);
+ }
+ }
+ }
+ return(0);
+}
+
+/* sum_agauss
+ Sum of gaussian functions defined by (area, centroid, fwhm)
+
+ *area* is the area underneath the peak
+ *centroid* is the peak x-coordinate
+ *fwhm* is the full-width at half maximum
+
+ Parameters:
+ -----------
+
+ - x: Independant variable where the gaussians are calculated.
+ - len_x: Number of elements in the x array.
+ - pgauss: Array of gaussian parameters:
+ (area1, centroid1, fwhm1, area2, centroid2, fwhm2,...)
+ - len_pgauss: Number of elements in the pgauss array. Must be
+ a multiple of 3.
+ - y: Output array. Must have memory allocated for the same number
+ of elements as x (len_x).
+
+ Adapted from PyMca module SpecFitFuns
+*/
+int sum_agauss(double* x, int len_x, double* pgauss, int len_pgauss, double* y)
+{
+ int i, j;
+ double dhelp, height, sqrt2PI, sigma, inv_two_sqrt_two_log2;
+ double fwhm, centroid, area;
+
+ if (test_params(len_pgauss, 3, "sum_agauss", "area, centroid, fwhm")) {
+ return(1);
+ }
+
+ /* Initialize output array */
+ for (j=0; j<len_x; j++) {
+ y[j] = 0.;
+ }
+
+ inv_two_sqrt_two_log2 = 1.0 / (2.0 * sqrt(2.0 * LOG2));
+ sqrt2PI = sqrt(2.0*M_PI);
+
+ for (i=0; i<len_pgauss/3; i++) {
+ area = pgauss[3*i];
+ centroid = pgauss[3*i+1];
+ fwhm = pgauss[3*i+2];
+
+ sigma = fwhm * inv_two_sqrt_two_log2;
+ height = area / (sigma * sqrt2PI);
+
+ for (j=0; j<len_x; j++) {
+ dhelp = (x[j] - centroid)/sigma;
+ if (dhelp <= 35) {
+ y[j] += height * exp (-0.5 * dhelp * dhelp);
+ }
+ }
+ }
+ return(0);
+}
+
+
+/* sum_fastagauss
+ Sum of gaussian functions defined by (area, centroid, fwhm).
+ This implementation uses a lookup table of precalculated exp values
+ and a limited development (exp(-x) = 1 - x for small values of x)
+
+ *area* is the area underneath the peak
+ *centroid* is the peak x-coordinate
+ *fwhm* is the full-width at half maximum
+
+ Parameters:
+ -----------
+
+ - x: Independant variable where the gaussians are calculated.
+ - len_x: Number of elements in the x array.
+ - pgauss: Array of gaussian parameters:
+ (area1, centroid1, fwhm1, area2, centroid2, fwhm2,...)
+ - len_pgauss: Number of elements in the pgauss array. Must be
+ a multiple of 3.
+ - y: Output array. Must have memory allocated for the same number
+ of elements as x (len_x).
+
+ Adapted from PyMca module SpecFitFuns
+*/
+
+int sum_fastagauss(double* x, int len_x, double* pgauss, int len_pgauss, double* y)
+{
+ int i, j, expindex;
+ double dhelp, height, sqrt2PI, sigma, inv_two_sqrt_two_log2;
+ double fwhm, centroid, area;
+ static double EXP[5000];
+
+ if (test_params(len_pgauss, 3, "sum_fastagauss", "area, centroid, fwhm")) {
+ return(1);
+ }
+
+ if (EXP[0] < 1){
+ for (i=0; i<5000; i++){
+ EXP[i] = exp(-0.01 * i);
+ }
+ }
+
+ /* Initialize output array */
+ for (j=0; j<len_x; j++) {
+ y[j] = 0.;
+ }
+
+ inv_two_sqrt_two_log2 = 1.0 / (2.0 * sqrt(2.0 * LOG2));
+ sqrt2PI = sqrt(2.0*M_PI);
+
+ for (i=0; i<len_pgauss/3; i++) {
+ area = pgauss[3*i];
+ centroid = pgauss[3*i+1];
+ fwhm = pgauss[3*i+2];
+
+ sigma = fwhm * inv_two_sqrt_two_log2;
+ height = area / (sigma * sqrt2PI);
+
+ for (j=0; j<len_x; j++) {
+ dhelp = (x[j] - centroid)/sigma;
+ if (dhelp <= 15){
+ dhelp = 0.5 * dhelp * dhelp;
+ if (dhelp < 50){
+ expindex = (int) (dhelp * 100);
+ y[j] += height * EXP[expindex] * (1.0 - (dhelp - 0.01 * expindex));
+ }
+ else if (dhelp < 100) {
+ expindex = (int) (dhelp * 10);
+ y[j] += height * pow(EXP[expindex] * (1.0 - (dhelp - 0.1 * expindex)), 10);
+ }
+ else if (dhelp < 1000){
+ expindex = (int) (dhelp);
+ y[j] += height * pow(EXP[expindex] * (1.0 - (dhelp - expindex)), 20);
+ }
+ }
+ }
+ }
+ return(0);
+}
+
+/* sum_splitgauss
+ Sum of split gaussian functions, defined by (height, centroid, fwhm1, fwhm2)
+
+ *height* is the peak amplitude
+ *centroid* is the peak x-coordinate
+ *fwhm1* is the full-width at half maximum of the left half of the curve (x < centroid)
+ *fwhm1* is the full-width at half maximum of the right half of the curve (x > centroid)
+
+ Parameters:
+ -----------
+
+ - x: Independant variable where the gaussians are calculated.
+ - len_x: Number of elements in the x array.
+ - pgauss: Array of gaussian parameters:
+ (height1, centroid1, fwhm11, fwhm21, height2, centroid2, fwhm12, fwhm22,...)
+ - len_pgauss: Number of elements in the pgauss array. Must be
+ a multiple of 4.
+ - y: Output array. Must have memory allocated for the same number
+ of elements as x (len_x).
+
+ Adapted from PyMca module SpecFitFuns
+*/
+int sum_splitgauss(double* x, int len_x, double* pgauss, int len_pgauss, double* y)
+{
+ int i, j;
+ double dhelp, inv_two_sqrt_two_log2, sigma1, sigma2;
+ double fwhm1, fwhm2, centroid, height;
+
+ if (test_params(len_pgauss, 4, "sum_splitgauss", "height, centroid, fwhm1, fwhm2")) {
+ return(1);
+ }
+
+ /* Initialize output array */
+ for (j=0; j<len_x; j++) {
+ y[j] = 0.;
+ }
+
+ inv_two_sqrt_two_log2 = 1.0 / (2.0 * sqrt(2.0 * LOG2));
+
+ for (i=0; i<len_pgauss/4; i++) {
+ height = pgauss[4*i];
+ centroid = pgauss[4*i+1];
+ fwhm1 = pgauss[4*i+2];
+ fwhm2 = pgauss[4*i+3];
+
+ sigma1 = fwhm1 * inv_two_sqrt_two_log2;
+ sigma2 = fwhm2 * inv_two_sqrt_two_log2;
+
+ for (j=0; j<len_x; j++) {
+ dhelp = (x[j] - centroid);
+ if (dhelp > 0) {
+ /* Use fwhm2 when x > centroid */
+ dhelp = dhelp / sigma2;
+ }
+ else {
+ /* Use fwhm1 when x < centroid */
+ dhelp = dhelp / sigma1;
+ }
+
+ if (dhelp <= 20) {
+ y[j] += height * exp (-0.5 * dhelp * dhelp);
+ }
+ }
+ }
+ return(0);
+}
+
+/* sum_apvoigt
+ Sum of pseudo-Voigt functions, defined by (area, centroid, fwhm, eta).
+
+ The pseudo-Voigt profile PV(x) is an approximation of the Voigt profile
+ using a linear combination of a Gaussian curve G(x) and a Lorentzian curve
+ L(x) instead of their convolution.
+
+ *area* is the area underneath both G(x) and L(x)
+ *centroid* is the peak x-coordinate for both functions
+ *fwhm* is the full-width at half maximum of both functions
+ *eta* is the Lorentz factor: PV(x) = eta * L(x) + (1 - eta) * G(x)
+
+ Parameters:
+ -----------
+
+ - x: Independant variable where the gaussians are calculated.
+ - len_x: Number of elements in the x array.
+ - pvoigt: Array of Voigt function parameters:
+ (area1, centroid1, fwhm1, eta1, area2, centroid2, fwhm2, eta2,...)
+ - len_voigt: Number of elements in the pvoigt array. Must be
+ a multiple of 4.
+ - y: Output array. Must have memory allocated for the same number
+ of elements as x (len_x).
+
+ Adapted from PyMca module SpecFitFuns
+*/
+int sum_apvoigt(double* x, int len_x, double* pvoigt, int len_pvoigt, double* y)
+{
+ int i, j;
+ double dhelp, inv_two_sqrt_two_log2, sqrt2PI, sigma, height;
+ double area, centroid, fwhm, eta;
+
+ if (test_params(len_pvoigt, 4, "sum_apvoigt", "area, centroid, fwhm, eta")) {
+ return(1);
+ }
+
+ /* Initialize output array */
+ for (j=0; j<len_x; j++) {
+ y[j] = 0.;
+ }
+
+ inv_two_sqrt_two_log2 = 1.0 / (2.0 * sqrt(2.0 * LOG2));
+ sqrt2PI = sqrt(2.0*M_PI);
+
+
+ for (i=0; i<len_pvoigt/4; i++) {
+ area = pvoigt[4*i];
+ centroid = pvoigt[4*i+1];
+ fwhm = pvoigt[4*i+2];
+ eta = pvoigt[4*i+3];
+
+ sigma = fwhm * inv_two_sqrt_two_log2;
+ height = area / (sigma * sqrt2PI);
+
+ for (j=0; j<len_x; j++) {
+ /* Lorentzian term */
+ dhelp = (x[j] - centroid) / (0.5 * fwhm);
+ dhelp = 1.0 + (dhelp * dhelp);
+ y[j] += eta * (area / (0.5 * M_PI * fwhm * dhelp));
+
+ /* Gaussian term */
+ dhelp = (x[j] - centroid) / sigma;
+ if (dhelp <= 35) {
+ y[j] += (1.0 - eta) * height * exp (-0.5 * dhelp * dhelp);
+ }
+ }
+ }
+ return(0);
+}
+
+/* sum_pvoigt
+ Sum of pseudo-Voigt functions, defined by (height, centroid, fwhm, eta).
+
+ The pseudo-Voigt profile PV(x) is an approximation of the Voigt profile
+ using a linear combination of a Gaussian curve G(x) and a Lorentzian curve
+ L(x) instead of their convolution.
+
+ *height* is the peak amplitude of G(x) and L(x)
+ *centroid* is the peak x-coordinate for both functions
+ *fwhm* is the full-width at half maximum of both functions
+ *eta* is the Lorentz factor: PV(x) = eta * L(x) + (1 - eta) * G(x)
+
+ Parameters:
+ -----------
+
+ - x: Independant variable where the gaussians are calculated.
+ - len_x: Number of elements in the x array.
+ - pvoigt: Array of Voigt function parameters:
+ (height1, centroid1, fwhm1, eta1, height2, centroid2, fwhm2, eta2,...)
+ - len_voigt: Number of elements in the pvoigt array. Must be
+ a multiple of 4.
+ - y: Output array. Must have memory allocated for the same number
+ of elements as x (len_x).
+
+ Adapted from PyMca module SpecFitFuns
+*/
+int sum_pvoigt(double* x, int len_x, double* pvoigt, int len_pvoigt, double* y)
+{
+ int i, j;
+ double dhelp, inv_two_sqrt_two_log2, sigma;
+ double height, centroid, fwhm, eta;
+
+ if (test_params(len_pvoigt, 4, "sum_pvoigt", "height, centroid, fwhm, eta")) {
+ return(1);
+ }
+
+ /* Initialize output array */
+ for (j=0; j<len_x; j++) {
+ y[j] = 0.;
+ }
+
+ inv_two_sqrt_two_log2 = 1.0 / (2.0 * sqrt(2.0 * LOG2));
+
+ for (i=0; i<len_pvoigt/4; i++) {
+ height = pvoigt[4*i];
+ centroid = pvoigt[4*i+1];
+ fwhm = pvoigt[4*i+2];
+ eta = pvoigt[4*i+3];
+
+ sigma = fwhm * inv_two_sqrt_two_log2;
+
+ for (j=0; j<len_x; j++) {
+ /* Lorentzian term */
+ dhelp = (x[j] - centroid) / (0.5 * fwhm);
+ dhelp = 1.0 + (dhelp * dhelp);
+ y[j] += eta * height / dhelp;
+
+ /* Gaussian term */
+ dhelp = (x[j] - centroid) / sigma;
+ if (dhelp <= 35) {
+ y[j] += (1.0 - eta) * height * exp (-0.5 * dhelp * dhelp);
+ }
+ }
+ }
+ return(0);
+}
+
+/* sum_splitpvoigt
+ Sum of split pseudo-Voigt functions, defined by
+ (height, centroid, fwhm1, fwhm2, eta).
+
+ The pseudo-Voigt profile PV(x) is an approximation of the Voigt profile
+ using a linear combination of a Gaussian curve G(x) and a Lorentzian curve
+ L(x) instead of their convolution.
+
+ *height* is the peak amplitude of G(x) and L(x)
+ *centroid* is the peak x-coordinate for both functions
+ *fwhm1* is the full-width at half maximum of both functions for x < centroid
+ *fwhm2* is the full-width at half maximum of both functions for x > centroid
+ *eta* is the Lorentz factor: PV(x) = eta * L(x) + (1 - eta) * G(x)
+
+ Parameters:
+ -----------
+
+ - x: Independant variable where the gaussians are calculated.
+ - len_x: Number of elements in the x array.
+ - pvoigt: Array of Voigt function parameters:
+ (height1, centroid1, fwhm11, fwhm21, eta1, ...)
+ - len_voigt: Number of elements in the pvoigt array. Must be
+ a multiple of 5.
+ - y: Output array. Must have memory allocated for the same number
+ of elements as x (len_x).
+
+ Adapted from PyMca module SpecFitFuns
+*/
+int sum_splitpvoigt(double* x, int len_x, double* pvoigt, int len_pvoigt, double* y)
+{
+ int i, j;
+ double dhelp, inv_two_sqrt_two_log2, x_minus_centroid, sigma1, sigma2;
+ double height, centroid, fwhm1, fwhm2, eta;
+
+ if (test_params(len_pvoigt, 5, "sum_splitpvoigt", "height, centroid, fwhm1, fwhm2, eta")) {
+ return(1);
+ }
+
+ /* Initialize output array */
+ for (j=0; j<len_x; j++) {
+ y[j] = 0.;
+ }
+
+ inv_two_sqrt_two_log2 = 1.0 / (2.0 * sqrt(2.0 * LOG2));
+
+ for (i=0; i<len_pvoigt/5; i++) {
+ height = pvoigt[5*i];
+ centroid = pvoigt[5*i+1];
+ fwhm1 = pvoigt[5*i+2];
+ fwhm2 = pvoigt[5*i+3];
+ eta = pvoigt[5*i+4];
+
+ sigma1 = fwhm1 * inv_two_sqrt_two_log2;
+ sigma2 = fwhm2 * inv_two_sqrt_two_log2;
+
+ for (j=0; j<len_x; j++) {
+ x_minus_centroid = (x[j] - centroid);
+
+ /* Use fwhm2 when x > centroid */
+ if (x_minus_centroid > 0) {
+ /* Lorentzian term */
+ dhelp = x_minus_centroid / (0.5 * fwhm2);
+ dhelp = 1.0 + (dhelp * dhelp);
+ y[j] += eta * height / dhelp;
+
+ /* Gaussian term */
+ dhelp = x_minus_centroid / sigma2;
+ if (dhelp <= 35) {
+ y[j] += (1.0 - eta) * height * exp (-0.5 * dhelp * dhelp);
+ }
+ }
+ /* Use fwhm1 when x < centroid */
+ else {
+ /* Lorentzian term */
+ dhelp = x_minus_centroid / (0.5 * fwhm1);
+ dhelp = 1.0 + (dhelp * dhelp);
+ y[j] += eta * height / dhelp;
+
+ /* Gaussian term */
+ dhelp = x_minus_centroid / sigma1;
+ if (dhelp <= 35) {
+ y[j] += (1.0 - eta) * height * exp (-0.5 * dhelp * dhelp);
+ }
+ }
+ }
+ }
+ return(0);
+}
+
+/* sum_lorentz
+ Sum of Lorentz functions, defined by (height, centroid, fwhm).
+
+ *height* is the peak amplitude
+ *centroid* is the peak's x-coordinate
+ *fwhm* is the full-width at half maximum
+
+ Parameters:
+ -----------
+
+ - x: Independant variable where the Lorentzians are calculated.
+ - len_x: Number of elements in the x array.
+ - plorentz: Array of lorentz function parameters:
+ (height1, centroid1, fwhm1, ...)
+ - len_lorentz: Number of elements in the plorentz array. Must be
+ a multiple of 3.
+ - y: Output array. Must have memory allocated for the same number
+ of elements as x (len_x).
+
+ Adapted from PyMca module SpecFitFuns
+*/
+int sum_lorentz(double* x, int len_x, double* plorentz, int len_plorentz, double* y)
+{
+ int i, j;
+ double dhelp;
+ double height, centroid, fwhm;
+
+ if (test_params(len_plorentz, 3, "sum_lorentz", "height, centroid, fwhm")) {
+ return(1);
+ }
+
+ /* Initialize output array */
+ for (j=0; j<len_x; j++) {
+ y[j] = 0.;
+ }
+
+ for (i=0; i<len_plorentz/3; i++) {
+ height = plorentz[3*i];
+ centroid = plorentz[3*i+1];
+ fwhm = plorentz[3*i+2];
+
+ for (j=0; j<len_x; j++) {
+ dhelp = (x[j] - centroid) / (0.5 * fwhm);
+ dhelp = 1.0 + (dhelp * dhelp);
+ y[j] += height / dhelp;
+ }
+ }
+ return(0);
+}
+
+
+/* sum_alorentz
+ Sum of Lorentz functions, defined by (area, centroid, fwhm).
+
+ *area* is the area underneath the peak
+ *centroid* is the peak's x-coordinate
+ *fwhm* is the full-width at half maximum
+
+ Parameters:
+ -----------
+
+ - x: Independant variable where the Lorentzians are calculated.
+ - len_x: Number of elements in the x array.
+ - plorentz: Array of lorentz function parameters:
+ (area1, centroid1, fwhm1, ...)
+ - len_lorentz: Number of elements in the plorentz array. Must be
+ a multiple of 3.
+ - y: Output array. Must have memory allocated for the same number
+ of elements as x (len_x).
+
+ Adapted from PyMca module SpecFitFuns
+*/
+int sum_alorentz(double* x, int len_x, double* plorentz, int len_plorentz, double* y)
+{
+ int i, j;
+ double dhelp;
+ double area, centroid, fwhm;
+
+ if (test_params(len_plorentz, 3, "sum_alorentz", "area, centroid, fwhm")) {
+ return(1);
+ }
+
+ /* Initialize output array */
+ for (j=0; j<len_x; j++) {
+ y[j] = 0.;
+ }
+
+ for (i=0; i<len_plorentz/3; i++) {
+ area = plorentz[3*i];
+ centroid = plorentz[3*i+1];
+ fwhm = plorentz[3*i+2];
+
+ for (j=0; j<len_x; j++) {
+ dhelp = (x[j] - centroid) / (0.5 * fwhm);
+ dhelp = 1.0 + (dhelp * dhelp);
+ y[j] += area / (0.5 * M_PI * fwhm * dhelp);
+ }
+ }
+ return(0);
+}
+
+
+/* sum_splitlorentz
+ Sum of Lorentz functions, defined by (height, centroid, fwhm1, fwhm2).
+
+ *height* is the peak amplitude
+ *centroid* is the peak's x-coordinate
+ *fwhm1* is the full-width at half maximum for x < centroid
+ *fwhm2* is the full-width at half maximum for x > centroid
+
+ Parameters:
+ -----------
+
+ - x: Independant variable where the Lorentzians are calculated.
+ - len_x: Number of elements in the x array.
+ - plorentz: Array of lorentz function parameters:
+ (height1, centroid1, fwhm11, fwhm21 ...)
+ - len_lorentz: Number of elements in the plorentz array. Must be
+ a multiple of 4.
+ - y: Output array. Must have memory allocated for the same number
+ of elements as x (len_x).
+
+ Adapted from PyMca module SpecFitFuns
+*/
+int sum_splitlorentz(double* x, int len_x, double* plorentz, int len_plorentz, double* y)
+{
+ int i, j;
+ double dhelp;
+ double height, centroid, fwhm1, fwhm2;
+
+ if (test_params(len_plorentz, 4, "sum_splitlorentz", "height, centroid, fwhm1, fwhm2")) {
+ return(1);
+ }
+
+ /* Initialize output array */
+ for (j=0; j<len_x; j++) {
+ y[j] = 0.;
+ }
+
+ for (i=0; i<len_plorentz/4; i++) {
+ height = plorentz[4*i];
+ centroid = plorentz[4*i+1];
+ fwhm1 = plorentz[4*i+2];
+ fwhm2 = plorentz[4*i+3];
+
+ for (j=0; j<len_x; j++) {
+ dhelp = (x[j] - centroid);
+ if (dhelp>0) {
+ dhelp = dhelp / (0.5 * fwhm2);
+ }
+ else {
+ dhelp = dhelp / (0.5 * fwhm1);
+ }
+ dhelp = 1.0 + (dhelp * dhelp);
+ y[j] += height / dhelp;
+ }
+ }
+ return(0);
+}
+
+/* sum_stepdown
+ Sum of stepdown functions, defined by (height, centroid, fwhm).
+
+ *height* is the step amplitude
+ *centroid* is the step's x-coordinate
+ *fwhm* is the full-width at half maximum of the derivative
+
+ Parameters:
+ -----------
+
+ - x: Independant variable where the stepdown functions are calculated.
+ - len_x: Number of elements in the x array.
+ - pdstep: Array of downstpe function parameters:
+ (height1, centroid1, fwhm1, ...)
+ - len_pdstep: Number of elements in the pdstep array. Must be
+ a multiple of 3.
+ - y: Output array. Must have memory allocated for the same number
+ of elements as x (len_x).
+
+ Adapted from PyMca module SpecFitFuns
+*/
+int sum_stepdown(double* x, int len_x, double* pdstep, int len_pdstep, double* y)
+{
+ int i, j;
+ double dhelp, sqrt2_inv_2_sqrt_two_log2 ;
+ double height, centroid, fwhm;
+
+ if (test_params(len_pdstep, 3, "sum_stepdown", "height, centroid, fwhm")) {
+ return(1);
+ }
+
+ /* Initialize output array */
+ for (j=0; j<len_x; j++) {
+ y[j] = 0.;
+ }
+
+ sqrt2_inv_2_sqrt_two_log2 = sqrt(2.0) / (2.0 * sqrt(2.0 * LOG2));
+
+ for (i=0; i<len_pdstep/3; i++) {
+ height = pdstep[3*i];
+ centroid = pdstep[3*i+1];
+ fwhm = pdstep[3*i+2];
+
+ for (j=0; j<len_x; j++) {
+ dhelp = fwhm * sqrt2_inv_2_sqrt_two_log2;
+ dhelp = (x[j] - centroid) / dhelp;
+ y[j] += height * 0.5 * erfc(dhelp);
+ }
+ }
+ return(0);
+}
+
+/* sum_stepup
+ Sum of stepup functions, defined by (height, centroid, fwhm).
+
+ *height* is the step amplitude
+ *centroid* is the step's x-coordinate
+ *fwhm* is the full-width at half maximum of the derivative
+
+ Parameters:
+ -----------
+
+ - x: Independant variable where the stepup functions are calculated.
+ - len_x: Number of elements in the x array.
+ - pustep: Array of stepdown function parameters:
+ (height1, centroid1, fwhm1, ...)
+ - len_pustep: Number of elements in the pustep array. Must be
+ a multiple of 3.
+ - y: Output array. Must have memory allocated for the same number
+ of elements as x (len_x).
+
+ Adapted from PyMca module SpecFitFuns
+*/
+int sum_stepup(double* x, int len_x, double* pustep, int len_pustep, double* y)
+{
+ int i, j;
+ double dhelp, sqrt2_inv_2_sqrt_two_log2 ;
+ double height, centroid, fwhm;
+
+ if (test_params(len_pustep, 3, "sum_stepup", "height, centroid, fwhm")) {
+ return(1);
+ }
+
+ /* Initialize output array */
+ for (j=0; j<len_x; j++) {
+ y[j] = 0.;
+ }
+
+ sqrt2_inv_2_sqrt_two_log2 = sqrt(2.0) / (2.0 * sqrt(2.0 * LOG2));
+
+ for (i=0; i<len_pustep/3; i++) {
+ height = pustep[3*i];
+ centroid = pustep[3*i+1];
+ fwhm = pustep[3*i+2];
+
+ for (j=0; j<len_x; j++) {
+ dhelp = fwhm * sqrt2_inv_2_sqrt_two_log2;
+ dhelp = (x[j] - centroid) / dhelp;
+ y[j] += height * 0.5 * (1.0 + erf(dhelp));
+ }
+ }
+ return(0);
+}
+
+
+/* sum_slit
+ Sum of slit functions, defined by (height, position, fwhm, beamfwhm).
+
+ *height* is the slit height
+ *position* is the slit's center x-coordinate
+ *fwhm* is the full-width at half maximum of the slit
+ *beamfwhm* is the full-width at half maximum of derivative's peaks
+
+ Parameters:
+ -----------
+
+ - x: Independant variable where the slit functions are calculated.
+ - len_x: Number of elements in the x array.
+ - pslit: Array of slit function parameters:
+ (height1, centroid1, fwhm1, beamfwhm1 ...)
+ - len_pslit: Number of elements in the pslit array. Must be
+ a multiple of 3.
+ - y: Output array. Must have memory allocated for the same number
+ of elements as x (len_x).
+
+ Adapted from PyMca module SpecFitFuns
+*/
+int sum_slit(double* x, int len_x, double* pslit, int len_pslit, double* y)
+{
+ int i, j;
+ double dhelp, dhelp1, dhelp2, sqrt2_inv_2_sqrt_two_log2, centroid1, centroid2;
+ double height, position, fwhm, beamfwhm;
+
+ if (test_params(len_pslit, 4, "sum_slit", "height, centroid, fwhm, beamfwhm")) {
+ return(1);
+ }
+
+ /* Initialize output array */
+ for (j=0; j<len_x; j++) {
+ y[j] = 0.;
+ }
+
+ sqrt2_inv_2_sqrt_two_log2 = sqrt(2.0) / (2.0 * sqrt(2.0 * LOG2));
+
+ for (i=0; i<len_pslit/4; i++) {
+ height = pslit[4*i];
+ position = pslit[4*i+1];
+ fwhm = pslit[4*i+2];
+ beamfwhm = pslit[4*i+3];
+
+ centroid1 = position - 0.5 * fwhm;
+ centroid2 = position + 0.5 * fwhm;
+
+ for (j=0; j<len_x; j++) {
+ dhelp = beamfwhm * sqrt2_inv_2_sqrt_two_log2;
+ dhelp1 = (x[j] - centroid1) / dhelp;
+ dhelp2 = (x[j] - centroid2) / dhelp;
+ y[j] += height * 0.25 * (1.0 + erf(dhelp1)) * erfc(dhelp2);
+ }
+ }
+ return(0);
+}
+
+
+/* sum_ahypermet
+ Sum of hypermet functions, defined by
+ (area, position, fwhm, st_area_r, st_slope_r, lt_area_r, lt_slope_r, step_height_r).
+
+ - *area* is the area underneath the gaussian peak
+ - *position* is the center of the various peaks and the position of
+ the step down
+ - *fwhm* is the full-width at half maximum of the terms
+ - *st_area_r* is factor between the gaussian area and the area of the
+ short tail term
+ - *st_slope_r* is a parameter related to the slope of the short tail
+ in the low ``x`` values (the lower, the steeper)
+ - *lt_area_r* is factor between the gaussian area and the area of the
+ long tail term
+ - *lt_slope_r* is a parameter related to the slope of the long tail
+ in the low ``x`` values (the lower, the steeper)
+ - *step_height_r* is the factor between the height of the step down
+ and the gaussian height
+
+ Parameters:
+ -----------
+
+ - x: Independant variable where the functions are calculated.
+ - len_x: Number of elements in the x array.
+ - phypermet: Array of hypermet function parameters:
+ *(area1, position1, fwhm1, st_area_r1, st_slope_r1, lt_area_r1,
+ lt_slope_r1, step_height_r1, ...)*
+ - len_phypermet: Number of elements in the phypermet array. Must be
+ a multiple of 8.
+ - y: Output array. Must have memory allocated for the same number
+ of elements as x (len_x).
+ - tail_flags: sum of binary flags to activate the various terms of the
+ function:
+
+ - 1 (b0001): Gaussian term
+ - 2 (b0010): st term
+ - 4 (b0100): lt term
+ - 8 (b1000): step term
+
+ E.g., to activate all termsof the hypermet, use ``tail_flags = 1 + 2 + 4 + 8 = 15``
+
+ Adapted from PyMca module SpecFitFuns
+*/
+int sum_ahypermet(double* x, int len_x, double* phypermet, int len_phypermet, double* y, int tail_flags)
+{
+ int i, j;
+ int g_term_flag, st_term_flag, lt_term_flag, step_term_flag;
+ double c1, c2, sigma, height, sigma_sqrt2, sqrt2PI, inv_2_sqrt_2_log2, x_minus_position, epsilon;
+ double area, position, fwhm, st_area_r, st_slope_r, lt_area_r, lt_slope_r, step_height_r;
+
+ if (test_params(len_phypermet, 8, "sum_hypermet",
+ "height, centroid, fwhm, st_area_r, st_slope_r, lt_area_r, lt_slope_r, step_height_r")) {
+ return(1);
+ }
+
+ g_term_flag = tail_flags & 1;
+ st_term_flag = (tail_flags>>1) & 1;
+ lt_term_flag = (tail_flags>>2) & 1;
+ step_term_flag = (tail_flags>>3) & 1;
+
+ /* Initialize output array */
+ for (j=0; j<len_x; j++) {
+ y[j] = 0.;
+ }
+
+ /* define epsilon to compare floating point values with 0. */
+ epsilon = 0.00000000001;
+
+ sqrt2PI= sqrt(2.0 * M_PI);
+ inv_2_sqrt_2_log2 = 1.0 / (2.0 * sqrt(2.0 * LOG2));
+
+ for (i=0; i<len_phypermet/8; i++) {
+ area = phypermet[8*i];
+ position = phypermet[8*i+1];
+ fwhm = phypermet[8*i+2];
+ st_area_r = phypermet[8*i+3];
+ st_slope_r = phypermet[8*i+4];
+ lt_area_r = phypermet[8*i+5];
+ lt_slope_r = phypermet[8*i+6];
+ step_height_r = phypermet[8*i+7];
+
+ sigma = fwhm * inv_2_sqrt_2_log2;
+ height = area / (sigma * sqrt2PI);
+
+ /* Prevent division by 0 */
+ if (sigma == 0) {
+ printf("fwhm must not be equal to 0");
+ return(1);
+ }
+ sigma_sqrt2 = sigma * 1.4142135623730950488;
+
+ for (j=0; j<len_x; j++) {
+ x_minus_position = x[j] - position;
+ c2 = (0.5 * x_minus_position * x_minus_position) / (sigma * sigma);
+ /* gaussian term */
+ if (g_term_flag) {
+ y[j] += exp(-c2) * height;
+ }
+
+ /* st term */
+ if (st_term_flag) {
+ if (fabs(st_slope_r) > epsilon) {
+ c1 = st_area_r * 0.5 * \
+ erfc((x_minus_position/sigma_sqrt2) + 0.5 * sigma_sqrt2 / st_slope_r);
+ y[j] += ((area * c1) / st_slope_r) * \
+ exp(0.5 * (sigma / st_slope_r) * (sigma / st_slope_r) + \
+ (x_minus_position / st_slope_r));
+ }
+ }
+
+ /* lt term */
+ if (lt_term_flag) {
+ if (fabs(lt_slope_r) > epsilon) {
+ c1 = lt_area_r * \
+ 0.5 * erfc((x_minus_position/sigma_sqrt2) + 0.5 * sigma_sqrt2 / lt_slope_r);
+ y[j] += ((area * c1) / lt_slope_r) * \
+ exp(0.5 * (sigma / lt_slope_r) * (sigma / lt_slope_r) + \
+ (x_minus_position / lt_slope_r));
+ }
+ }
+
+ /* step term flag */
+ if (step_term_flag) {
+ y[j] += step_height_r * (area / (sigma * sqrt2PI)) * \
+ 0.5 * erfc(x_minus_position / sigma_sqrt2);
+ }
+ }
+ }
+ return(0);
+}
+
+/* sum_fastahypermet
+
+ Sum of hypermet functions, defined by
+ (area, position, fwhm, st_area_r, st_slope_r, lt_area_r, lt_slope_r, step_height_r).
+
+ - *area* is the area underneath the gaussian peak
+ - *position* is the center of the various peaks and the position of
+ the step down
+ - *fwhm* is the full-width at half maximum of the terms
+ - *st_area_r* is factor between the gaussian area and the area of the
+ short tail term
+ - *st_slope_r* is a parameter related to the slope of the short tail
+ in the low ``x`` values (the lower, the steeper)
+ - *lt_area_r* is factor between the gaussian area and the area of the
+ long tail term
+ - *lt_slope_r* is a parameter related to the slope of the long tail
+ in the low ``x`` values (the lower, the steeper)
+ - *step_height_r* is the factor between the height of the step down
+ and the gaussian height
+
+ Parameters:
+ -----------
+
+ - x: Independant variable where the functions are calculated.
+ - len_x: Number of elements in the x array.
+ - phypermet: Array of hypermet function parameters:
+ *(area1, position1, fwhm1, st_area_r1, st_slope_r1, lt_area_r1,
+ lt_slope_r1, step_height_r1, ...)*
+ - len_phypermet: Number of elements in the phypermet array. Must be
+ a multiple of 8.
+ - y: Output array. Must have memory allocated for the same number
+ of elements as x (len_x).
+ - tail_flags: sum of binary flags to activate the various terms of the
+ function:
+
+ - 1 (b0001): Gaussian term
+ - 2 (b0010): st term
+ - 4 (b0100): lt term
+ - 8 (b1000): step term
+
+ E.g., to activate all termsof the hypermet, use ``tail_flags = 1 + 2 + 4 + 8 = 15``
+
+ Adapted from PyMca module SpecFitFuns
+*/
+int sum_fastahypermet(double* x, int len_x, double* phypermet, int len_phypermet, double* y, int tail_flags)
+{
+ int i, j;
+ int g_term_flag, st_term_flag, lt_term_flag, step_term_flag;
+ double c1, c2, sigma, height, sigma_sqrt2, sqrt2PI, inv_2_sqrt_2_log2, x_minus_position, epsilon;
+ double area, position, fwhm, st_area_r, st_slope_r, lt_area_r, lt_slope_r, step_height_r;
+
+ if (test_params(len_phypermet, 8, "sum_hypermet",
+ "height, centroid, fwhm, st_area_r, st_slope_r, lt_area_r, lt_slope_r, step_height_r")) {
+ return(1);
+ }
+
+ g_term_flag = tail_flags & 1;
+ st_term_flag = (tail_flags>>1) & 1;
+ lt_term_flag = (tail_flags>>2) & 1;
+ step_term_flag = (tail_flags>>3) & 1;
+
+ /* Initialize output array */
+ for (j=0; j<len_x; j++) {
+ y[j] = 0.;
+ }
+
+ /* define epsilon to compare floating point values with 0. */
+ epsilon = 0.00000000001;
+
+ sqrt2PI= sqrt(2.0 * M_PI);
+ inv_2_sqrt_2_log2 = 1.0 / (2.0 * sqrt(2.0 * LOG2));
+
+ for (i=0; i<len_phypermet/8; i++) {
+ area = phypermet[8*i];
+ position = phypermet[8*i+1];
+ fwhm = phypermet[8*i+2];
+ st_area_r = phypermet[8*i+3];
+ st_slope_r = phypermet[8*i+4];
+ lt_area_r = phypermet[8*i+5];
+ lt_slope_r = phypermet[8*i+6];
+ step_height_r = phypermet[8*i+7];
+
+ sigma = fwhm * inv_2_sqrt_2_log2;
+ height = area / (sigma * sqrt2PI);
+
+ /* Prevent division by 0 */
+ if (sigma == 0) {
+ printf("fwhm must not be equal to 0");
+ return(1);
+ }
+ sigma_sqrt2 = sigma * 1.4142135623730950488;
+
+ for (j=0; j<len_x; j++) {
+ x_minus_position = x[j] - position;
+ c2 = (0.5 * x_minus_position * x_minus_position) / (sigma * sigma);
+ /* gaussian term */
+ if (g_term_flag && c2 < 100) {
+ y[j] += fastexp(-c2) * height;
+ }
+
+ /* st term */
+ if (st_term_flag && (fabs(st_slope_r) > epsilon) && (x_minus_position / st_slope_r) <= 612) {
+ c1 = st_area_r * 0.5 * \
+ erfc((x_minus_position/sigma_sqrt2) + 0.5 * sigma_sqrt2 / st_slope_r);
+ y[j] += ((area * c1) / st_slope_r) * \
+ fastexp(0.5 * (sigma / st_slope_r) * (sigma / st_slope_r) +\
+ (x_minus_position / st_slope_r));
+ }
+
+ /* lt term */
+ if (lt_term_flag && (fabs(lt_slope_r) > epsilon) && (x_minus_position / lt_slope_r) <= 612) {
+ c1 = lt_area_r * \
+ 0.5 * erfc((x_minus_position/sigma_sqrt2) + 0.5 * sigma_sqrt2 / lt_slope_r);
+ y[j] += ((area * c1) / lt_slope_r) * \
+ fastexp(0.5 * (sigma / lt_slope_r) * (sigma / lt_slope_r) +\
+ (x_minus_position / lt_slope_r));
+
+ }
+
+ /* step term flag */
+ if (step_term_flag) {
+ y[j] += step_height_r * (area / (sigma * sqrt2PI)) *\
+ 0.5 * erfc(x_minus_position / sigma_sqrt2);
+ }
+ }
+ }
+ return(0);
+}
+
+void pileup(double* x, long len_x, double* ret, int input2, double zero, double gain)
+{
+ //int input2=0;
+ //double zero=0.0;
+ //double gain=1.0;
+
+ int i, j, k;
+ double *px, *pret, *pall;
+
+ /* the pointer to the starting position of par data */
+ px = x;
+ pret = ret;
+
+ *pret = 0;
+ k = (int )(zero/gain);
+ for (i=input2; i<len_x; i++){
+ pall = x;
+ if ((i+k) >= 0)
+ {
+ pret = (double *) ret+(i+k);
+ for (j=0; j<len_x-i-k ;j++){
+ *pret += *px * (*pall);
+ pall++;
+ pret++;
+ }
+ }
+ px++;
+ }
+}
diff --git a/src/silx/math/fit/functions_wrapper.pxd b/src/silx/math/fit/functions_wrapper.pxd
new file mode 100644
index 0000000..780116c
--- /dev/null
+++ b/src/silx/math/fit/functions_wrapper.pxd
@@ -0,0 +1,170 @@
+# coding: utf-8
+#/*##########################################################################
+# Copyright (C) 2016 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+#############################################################################*/
+
+__authors__ = ["P. Knobel"]
+__license__ = "MIT"
+__date__ = "14/06/2016"
+
+cimport cython
+
+cdef extern from "functions.h":
+ int erfc_array(double* x,
+ int len_x,
+ double* y)
+
+ int erf_array(double* x,
+ int len_x,
+ double* y);
+
+ void snip1d(double *data,
+ int size,
+ int width)
+
+ void snip2d(double *data,
+ int nrows,
+ int ncolumns,
+ int width)
+
+ void snip3d(double *data,
+ int nx,
+ int ny,
+ int nz,
+ int width)
+
+ int strip(double* input,
+ long len_input,
+ double c,
+ long niter,
+ int deltai,
+ long* anchors,
+ long len_anchors,
+ double* output)
+
+ int sum_gauss(double* x,
+ int len_x,
+ double* pgauss,
+ int len_pgauss,
+ double* y)
+
+ int sum_agauss(double* x,
+ int len_x,
+ double* pgauss,
+ int len_pgauss,
+ double* y)
+
+ int sum_fastagauss(double* x,
+ int len_x,
+ double* pgauss,
+ int len_pgauss,
+ double* y)
+
+ int sum_splitgauss(double* x,
+ int len_x,
+ double* pgauss,
+ int len_pgauss,
+ double* y)
+
+ int sum_apvoigt(double* x,
+ int len_x,
+ double* pvoigt,
+ int len_pvoigt,
+ double* y)
+
+ int sum_pvoigt(double* x,
+ int len_x,
+ double* pvoigt,
+ int len_pvoigt,
+ double* y)
+
+ int sum_splitpvoigt(double* x,
+ int len_x,
+ double* pvoigt,
+ int len_pvoigt,
+ double* y)
+
+ int sum_lorentz(double* x,
+ int len_x,
+ double* plorentz,
+ int len_plorentz,
+ double* y)
+
+ int sum_alorentz(double* x,
+ int len_x,
+ double* plorentz,
+ int len_plorentz,
+ double* y)
+
+ int sum_splitlorentz(double* x,
+ int len_x,
+ double* plorentz,
+ int len_plorentz,
+ double* y)
+
+ int sum_stepdown(double* x,
+ int len_x,
+ double* pdstep,
+ int len_pdstep,
+ double* y)
+
+ int sum_stepup(double* x,
+ int len_x,
+ double* pustep,
+ int len_pustep,
+ double* y)
+
+ int sum_slit(double* x,
+ int len_x,
+ double* pslit,
+ int len_pslit,
+ double* y)
+
+ int sum_ahypermet(double* x,
+ int len_x,
+ double* phypermet,
+ int len_phypermet,
+ double* y,
+ int tail_flags)
+
+ int sum_fastahypermet(double* x,
+ int len_x,
+ double* phypermet,
+ int len_phypermet,
+ double* y,
+ int tail_flags)
+
+ long seek(long begin_index,
+ long end_index,
+ long nsamples,
+ double fwhm,
+ double sensitivity,
+ double debug_info,
+ long max_npeaks,
+ double * data,
+ double * peaks,
+ double * relevances)
+
+ int SavitskyGolay(double* input,
+ long len_input,
+ int npoints,
+ double* output)
diff --git a/src/silx/math/fit/leastsq.py b/src/silx/math/fit/leastsq.py
new file mode 100644
index 0000000..3df1a35
--- /dev/null
+++ b/src/silx/math/fit/leastsq.py
@@ -0,0 +1,901 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2004-2020 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
+"""
+This module implements a Levenberg-Marquardt algorithm with constraints on the
+fitted parameters without introducing any other dependendency than numpy.
+
+If scipy dependency is not an issue, and no constraints are applied to the fitting
+parameters, there is no real gain compared to the use of scipy.optimize.curve_fit
+other than a more conservative calculation of uncertainties on fitted parameters.
+
+This module is a refactored version of PyMca Gefit.py module.
+"""
+__authors__ = ["V.A. Sole"]
+__license__ = "MIT"
+__date__ = "15/05/2017"
+__copyright__ = "European Synchrotron Radiation Facility, Grenoble, France"
+
+import numpy
+from numpy.linalg import inv
+from numpy.linalg.linalg import LinAlgError
+import time
+import logging
+import copy
+
+_logger = logging.getLogger(__name__)
+
+# codes understood by the routine
+CFREE = 0
+CPOSITIVE = 1
+CQUOTED = 2
+CFIXED = 3
+CFACTOR = 4
+CDELTA = 5
+CSUM = 6
+CIGNORED = 7
+
+def leastsq(model, xdata, ydata, p0, sigma=None,
+ constraints=None, model_deriv=None, epsfcn=None,
+ deltachi=None, full_output=None,
+ check_finite=True,
+ left_derivative=False,
+ max_iter=100):
+ """
+ Use non-linear least squares Levenberg-Marquardt algorithm to fit a function, f, to
+ data with optional constraints on the fitted parameters.
+
+ Assumes ``ydata = f(xdata, *params) + eps``
+
+ :param model: callable
+ The model function, f(x, ...). It must take the independent
+ variable as the first argument and the parameters to fit as
+ separate remaining arguments.
+ The returned value is a one dimensional array of floats.
+
+ :param xdata: An M-length sequence.
+ The independent variable where the data is measured.
+
+ :param ydata: An M-length sequence
+ The dependent data --- nominally f(xdata, ...)
+
+ :param p0: N-length sequence
+ Initial guess for the parameters.
+
+ :param sigma: None or M-length sequence, optional
+ If not None, the uncertainties in the ydata array. These are used as
+ weights in the least-squares problem
+ i.e. minimising ``np.sum( ((f(xdata, *popt) - ydata) / sigma)**2 )``
+ If None, the uncertainties are assumed to be 1
+
+ :param constraints:
+ If provided, it is a 2D sequence of dimension (n_parameters, 3) where,
+ for each parameter denoted by the index i, the meaning is
+
+ - constraints[i][0]
+
+ - 0 - Free (CFREE)
+ - 1 - Positive (CPOSITIVE)
+ - 2 - Quoted (CQUOTED)
+ - 3 - Fixed (CFIXED)
+ - 4 - Factor (CFACTOR)
+ - 5 - Delta (CDELTA)
+ - 6 - Sum (CSUM)
+
+
+ - constraints[i][1]
+
+ - Ignored if constraints[i][0] is 0, 1, 3
+ - Min value of the parameter if constraints[i][0] is CQUOTED
+ - Index of fitted parameter to which it is related
+
+ - constraints[i][2]
+
+ - Ignored if constraints[i][0] is 0, 1, 3
+ - Max value of the parameter if constraints[i][0] is CQUOTED
+ - Factor to apply to related parameter with index constraints[i][1]
+ - Difference with parameter with index constraints[i][1]
+ - Sum obtained when adding parameter with index constraints[i][1]
+ :type constraints: *optional*, None or 2D sequence
+
+ :param model_deriv:
+ None (default) or function providing the derivatives of the fitting function respect to the fitted parameters.
+ It will be called as model_deriv(xdata, parameters, index) where parameters is a sequence with the current
+ values of the fitting parameters, index is the fitting parameter index for which the the derivative has
+ to be provided in the supplied array of xdata points.
+ :type model_deriv: *optional*, None or callable
+
+
+ :param epsfcn: float
+ A variable used in determining a suitable parameter variation when
+ calculating the numerical derivatives (for model_deriv=None).
+ Normally the actual step length will be sqrt(epsfcn)*x
+ Original Gefit module was using epsfcn 1.0e-5 while default value
+ is now numpy.finfo(numpy.float64).eps as in scipy
+ :type epsfcn: *optional*, float
+
+ :param deltachi: float
+ A variable used to control the minimum change in chisq to consider the
+ fitting process not worth to be continued. Default is 0.1 %.
+ :type deltachi: *optional*, float
+
+ :param full_output: bool, optional
+ non-zero to return all optional outputs. The default is None what will give a warning in case
+ of a constrained fit without having set this kweyword.
+
+ :param check_finite: bool, optional
+ If True, check that the input arrays do not contain nans of infs,
+ and raise a ValueError if they do. Setting this parameter to
+ False will ignore input arrays values containing nans.
+ Default is True.
+
+ :param left_derivative:
+ This parameter only has an influence if no derivative function
+ is provided. When True the left and right derivatives of the
+ model will be calculated for each fitted parameters thus leading to
+ the double number of function evaluations. Default is False.
+ Original Gefit module was always using left_derivative as True.
+ :type left_derivative: *optional*, bool
+
+ :param max_iter: Maximum number of iterations (default is 100)
+
+ :return: Returns a tuple of length 2 (or 3 if full_ouput is True) with the content:
+
+ ``popt``: array
+ Optimal values for the parameters so that the sum of the squared error
+ of ``f(xdata, *popt) - ydata`` is minimized
+ ``pcov``: 2d array
+ If no constraints are applied, this array contains the estimated covariance
+ of popt. The diagonal provides the variance of the parameter estimate.
+ To compute one standard deviation errors use ``perr = np.sqrt(np.diag(pcov))``.
+ If constraints are applied, this array does not contain the estimated covariance of
+ the parameters actually used during the fitting process but the uncertainties after
+ recalculating the covariance if all the parameters were free.
+ To get the actual uncertainties following error propagation of the actually fitted
+ parameters one should set full_output to True and access the uncertainties key.
+ ``infodict``: dict
+ a dictionary of optional outputs with the keys:
+
+ ``uncertainties``
+ The actual uncertainty on the optimized parameters.
+ ``nfev``
+ The number of function calls
+ ``fvec``
+ The function evaluated at the output
+ ``niter``
+ The number of iterations performed
+ ``chisq``
+ The chi square ``np.sum( ((f(xdata, *popt) - ydata) / sigma)**2 )``
+ ``reduced_chisq``
+ The chi square ``np.sum( ((f(xdata, *popt) - ydata) / sigma)**2 )`` divided
+ by the number of degrees of freedom ``(M - number_of_free_parameters)``
+ """
+ function_call_counter = 0
+ if numpy.isscalar(p0):
+ p0 = [p0]
+ parameters = numpy.array(p0, dtype=numpy.float64, copy=False)
+ if deltachi is None:
+ deltachi = 0.001
+
+ # NaNs can not be handled
+ if check_finite:
+ xdata = numpy.asarray_chkfinite(xdata)
+ ydata = numpy.asarray_chkfinite(ydata)
+ if sigma is not None:
+ sigma = numpy.asarray_chkfinite(sigma)
+ else:
+ sigma = numpy.ones((ydata.shape), dtype=numpy.float64)
+ ydata.shape = -1
+ sigma.shape = -1
+ else:
+ ydata = numpy.asarray(ydata)
+ xdata = numpy.asarray(xdata)
+ ydata.shape = -1
+ if sigma is not None:
+ sigma = numpy.asarray(sigma)
+ else:
+ sigma = numpy.ones((ydata.shape), dtype=numpy.float64)
+ sigma.shape = -1
+ # get rid of NaN in input data
+ idx = numpy.isfinite(ydata)
+ if False in idx:
+ # xdata must have a shape able to be understood by the user function
+ # in principle, one should not need to change it, however, if there are
+ # points to be excluded, one has to be able to exclude them.
+ # We can only hope that the sequence is properly arranged
+ if xdata.size == ydata.size:
+ if len(xdata.shape) != 1:
+ msg = "Need to reshape input xdata."
+ _logger.warning(msg)
+ xdata.shape = -1
+ else:
+ raise ValueError("Cannot reshape xdata to deal with NaN in ydata")
+ ydata = ydata[idx]
+ xdata = xdata[idx]
+ sigma = sigma[idx]
+ idx = numpy.isfinite(sigma)
+ if False in idx:
+ # xdata must have a shape able to be understood by the user function
+ # in principle, one should not need to change it, however, if there are
+ # points to be excluded, one has to be able to exclude them.
+ # We can only hope that the sequence is properly arranged
+ ydata = ydata[idx]
+ xdata = xdata[idx]
+ sigma = sigma[idx]
+ idx = numpy.isfinite(xdata)
+ filter_xdata = False
+ if False in idx:
+ # What to do?
+ try:
+ # Let's see if the function is able to deal with non-finite data
+ msg = "Checking if function can deal with non-finite data"
+ _logger.debug(msg)
+ evaluation = model(xdata, *parameters)
+ function_call_counter += 1
+ if evaluation.shape != ydata.shape:
+ if evaluation.size == ydata.size:
+ msg = "Supplied function does not return a proper array of floats."
+ msg += "\nFunction should be rewritten to return a 1D array of floats."
+ msg += "\nTrying to reshape output."
+ _logger.warning(msg)
+ evaluation.shape = ydata.shape
+ if False in numpy.isfinite(evaluation):
+ msg = "Supplied function unable to handle non-finite x data"
+ msg += "\nAttempting to filter out those x data values."
+ _logger.warning(msg)
+ filter_xdata = True
+ else:
+ filter_xdata = False
+ evaluation = None
+ except:
+ # function cannot handle input data
+ filter_xdata = True
+ if filter_xdata:
+ if xdata.size != ydata.size:
+ raise ValueError("xdata contains non-finite data that cannot be filtered")
+ else:
+ # we leave the xdata as they where
+ old_shape = xdata.shape
+ xdata.shape = ydata.shape
+ idx0 = numpy.isfinite(xdata)
+ xdata.shape = old_shape
+ ydata = ydata[idx0]
+ xdata = xdata[idx]
+ sigma = sigma[idx0]
+ weight = 1.0 / (sigma + numpy.equal(sigma, 0))
+ weight0 = weight * weight
+
+ nparameters = len(parameters)
+
+ if epsfcn is None:
+ epsfcn = numpy.finfo(numpy.float64).eps
+ else:
+ epsfcn = max(epsfcn, numpy.finfo(numpy.float64).eps)
+
+ # check if constraints have been passed as text
+ constrained_fit = False
+ if constraints is not None:
+ # make sure we work with a list of lists
+ input_constraints = constraints
+ tmp_constraints = [None] * len(input_constraints)
+ for i in range(nparameters):
+ tmp_constraints[i] = list(input_constraints[i])
+ constraints = tmp_constraints
+ for i in range(nparameters):
+ if hasattr(constraints[i][0], "upper"):
+ txt = constraints[i][0].upper()
+ if txt == "FREE":
+ constraints[i][0] = CFREE
+ elif txt == "POSITIVE":
+ constraints[i][0] = CPOSITIVE
+ elif txt == "QUOTED":
+ constraints[i][0] = CQUOTED
+ elif txt == "FIXED":
+ constraints[i][0] = CFIXED
+ elif txt == "FACTOR":
+ constraints[i][0] = CFACTOR
+ constraints[i][1] = int(constraints[i][1])
+ elif txt == "DELTA":
+ constraints[i][0] = CDELTA
+ constraints[i][1] = int(constraints[i][1])
+ elif txt == "SUM":
+ constraints[i][0] = CSUM
+ constraints[i][1] = int(constraints[i][1])
+ elif txt in ["IGNORED", "IGNORE"]:
+ constraints[i][0] = CIGNORED
+ else:
+ #I should raise an exception
+ raise ValueError("Unknown constraint %s" % constraints[i][0])
+ if constraints[i][0] > 0:
+ constrained_fit = True
+ if constrained_fit:
+ if full_output is None:
+ _logger.info("Recommended to set full_output to True when using constraints")
+
+ # Levenberg-Marquardt algorithm
+ fittedpar = parameters.__copy__()
+ flambda = 0.001
+ iiter = max_iter
+ #niter = 0
+ last_evaluation=None
+ x = xdata
+ y = ydata
+ chisq0 = -1
+ iteration_counter = 0
+ while (iiter > 0):
+ weight = weight0
+ """
+ I cannot evaluate the initial chisq here because I do not know
+ if some parameters are to be ignored, otherways I could do it as follows:
+ if last_evaluation is None:
+ yfit = model(x, *fittedpar)
+ last_evaluation = yfit
+ chisq0 = (weight * pow(y-yfit, 2)).sum()
+ and chisq would not need to be recalculated.
+ Passing the last_evaluation assumes that there are no parameters being
+ ignored or not between calls.
+ """
+ iteration_counter += 1
+ chisq0, alpha0, beta, internal_output = chisq_alpha_beta(
+ model, fittedpar,
+ x, y, weight, constraints=constraints,
+ model_deriv=model_deriv,
+ epsfcn=epsfcn,
+ left_derivative=left_derivative,
+ last_evaluation=last_evaluation,
+ full_output=True)
+ n_free = internal_output["n_free"]
+ free_index = internal_output["free_index"]
+ noigno = internal_output["noigno"]
+ fitparam = internal_output["fitparam"]
+ function_calls = internal_output["function_calls"]
+ function_call_counter += function_calls
+ #print("chisq0 = ", chisq0, n_free, fittedpar)
+ #raise
+ nr, nc = alpha0.shape
+ flag = 0
+ #lastdeltachi = chisq0
+ while flag == 0:
+ alpha = alpha0 * (1.0 + flambda * numpy.identity(nr))
+ deltapar = numpy.dot(beta, inv(alpha))
+ if constraints is None:
+ newpar = fitparam + deltapar [0]
+ else:
+ newpar = parameters.__copy__()
+ pwork = numpy.zeros(deltapar.shape, numpy.float64)
+ for i in range(n_free):
+ if constraints is None:
+ pwork [0] [i] = fitparam [i] + deltapar [0] [i]
+ elif constraints [free_index[i]][0] == CFREE:
+ pwork [0] [i] = fitparam [i] + deltapar [0] [i]
+ elif constraints [free_index[i]][0] == CPOSITIVE:
+ #abs method
+ pwork [0] [i] = fitparam [i] + deltapar [0] [i]
+ #square method
+ #pwork [0] [i] = (numpy.sqrt(fitparam [i]) + deltapar [0] [i]) * \
+ # (numpy.sqrt(fitparam [i]) + deltapar [0] [i])
+ elif constraints[free_index[i]][0] == CQUOTED:
+ pmax = max(constraints[free_index[i]][1],
+ constraints[free_index[i]][2])
+ pmin = min(constraints[free_index[i]][1],
+ constraints[free_index[i]][2])
+ A = 0.5 * (pmax + pmin)
+ B = 0.5 * (pmax - pmin)
+ if B != 0:
+ pwork [0] [i] = A + \
+ B * numpy.sin(numpy.arcsin((fitparam[i] - A)/B)+ \
+ deltapar [0] [i])
+ else:
+ txt = "Error processing constrained fit\n"
+ txt += "Parameter limits are %g and %g\n" % (pmin, pmax)
+ txt += "A = %g B = %g" % (A, B)
+ raise ValueError("Invalid parameter limits")
+ newpar[free_index[i]] = pwork [0] [i]
+ newpar = numpy.array(_get_parameters(newpar, constraints))
+ workpar = numpy.take(newpar, noigno)
+ yfit = model(x, *workpar)
+ if last_evaluation is None:
+ if len(yfit.shape) > 1:
+ msg = "Supplied function does not return a 1D array of floats."
+ msg += "\nFunction should be rewritten."
+ msg += "\nTrying to reshape output."
+ _logger.warning(msg)
+ yfit.shape = -1
+ function_call_counter += 1
+ chisq = (weight * pow(y-yfit, 2)).sum()
+ absdeltachi = chisq0 - chisq
+ if absdeltachi < 0:
+ flambda *= 10.0
+ if flambda > 1000:
+ flag = 1
+ iiter = 0
+ else:
+ flag = 1
+ fittedpar = newpar.__copy__()
+ lastdeltachi = 100 * (absdeltachi / (chisq + (chisq == 0)))
+ if iteration_counter < 2:
+ # ignore any limit, the fit *has* to be improved
+ pass
+ elif (lastdeltachi) < deltachi:
+ iiter = 0
+ elif absdeltachi < numpy.sqrt(epsfcn):
+ iiter = 0
+ _logger.info("Iteration finished due to too small absolute chi decrement")
+ chisq0 = chisq
+ flambda = flambda / 10.0
+ last_evaluation = yfit
+ iiter = iiter - 1
+ # this is the covariance matrix of the actually fitted parameters
+ cov0 = inv(alpha0)
+ if constraints is None:
+ cov = cov0
+ else:
+ # yet another call needed with all the parameters being free except those
+ # that are FIXED and that will be assigned a 100 % uncertainty.
+ new_constraints = copy.deepcopy(constraints)
+ flag_special = [0] * len(fittedpar)
+ for idx, constraint in enumerate(constraints):
+ if constraints[idx][0] in [CFIXED, CIGNORED]:
+ flag_special[idx] = constraints[idx][0]
+ else:
+ new_constraints[idx][0] = CFREE
+ new_constraints[idx][1] = 0
+ new_constraints[idx][2] = 0
+ chisq, alpha, beta, internal_output = chisq_alpha_beta(
+ model, fittedpar,
+ x, y, weight, constraints=new_constraints,
+ model_deriv=model_deriv,
+ epsfcn=epsfcn,
+ left_derivative=left_derivative,
+ last_evaluation=last_evaluation,
+ full_output=True)
+ # obtained chisq should be identical to chisq0
+ try:
+ cov = inv(alpha)
+ except LinAlgError:
+ _logger.critical("Error calculating covariance matrix after successful fit")
+ cov = None
+ if cov is not None:
+ for idx, value in enumerate(flag_special):
+ if value in [CFIXED, CIGNORED]:
+ cov = numpy.insert(numpy.insert(cov, idx, 0, axis=1), idx, 0, axis=0)
+ cov[idx, idx] = fittedpar[idx] * fittedpar[idx]
+
+ if not full_output:
+ return fittedpar, cov
+ else:
+ sigma0 = numpy.sqrt(abs(numpy.diag(cov0)))
+ sigmapar = _get_sigma_parameters(fittedpar, sigma0, constraints)
+ ddict = {}
+ ddict["chisq"] = chisq0
+ ddict["reduced_chisq"] = chisq0 / (len(yfit)-n_free)
+ ddict["covariance"] = cov0
+ ddict["uncertainties"] = sigmapar
+ ddict["fvec"] = last_evaluation
+ ddict["nfev"] = function_call_counter
+ ddict["niter"] = iteration_counter
+ return fittedpar, cov, ddict #, chisq/(len(yfit)-len(sigma0)), sigmapar,niter,lastdeltachi
+
+def chisq_alpha_beta(model, parameters, x, y, weight, constraints=None,
+ model_deriv=None, epsfcn=None, left_derivative=False,
+ last_evaluation=None, full_output=False):
+
+ """
+ Get chi square, the curvature matrix alpha and the matrix beta according to the input parameters.
+ If all the parameters are unconstrained, the covariance matrix is the inverse of the alpha matrix.
+
+ :param model: callable
+ The model function, f(x, ...). It must take the independent
+ variable as the first argument and the parameters to fit as
+ separate remaining arguments.
+ The returned value is a one dimensional array of floats.
+
+ :param parameters: N-length sequence
+ Values of parameters at which function and derivatives are to be calculated.
+
+ :param x: An M-length sequence.
+ The independent variable where the data is measured.
+
+ :param y: An M-length sequence
+ The dependent data --- nominally f(xdata, ...)
+
+ :param weight: M-length sequence
+ Weights to be applied in the calculation of chi square
+ As a reminder ``chisq = np.sum(weigth * (model(x, *parameters) - y)**2)``
+
+ :param constraints:
+ If provided, it is a 2D sequence of dimension (n_parameters, 3) where,
+ for each parameter denoted by the index i, the meaning is
+
+ - constraints[i][0]
+
+ - 0 - Free (CFREE)
+ - 1 - Positive (CPOSITIVE)
+ - 2 - Quoted (CQUOTED)
+ - 3 - Fixed (CFIXED)
+ - 4 - Factor (CFACTOR)
+ - 5 - Delta (CDELTA)
+ - 6 - Sum (CSUM)
+
+
+ - constraints[i][1]
+
+ - Ignored if constraints[i][0] is 0, 1, 3
+ - Min value of the parameter if constraints[i][0] is CQUOTED
+ - Index of fitted parameter to which it is related
+
+ - constraints[i][2]
+
+ - Ignored if constraints[i][0] is 0, 1, 3
+ - Max value of the parameter if constraints[i][0] is CQUOTED
+ - Factor to apply to related parameter with index constraints[i][1]
+ - Difference with parameter with index constraints[i][1]
+ - Sum obtained when adding parameter with index constraints[i][1]
+ :type constraints: *optional*, None or 2D sequence
+
+ :param model_deriv:
+ None (default) or function providing the derivatives of the fitting function respect to the fitted parameters.
+ It will be called as model_deriv(xdata, parameters, index) where parameters is a sequence with the current
+ values of the fitting parameters, index is the fitting parameter index for which the the derivative has
+ to be provided in the supplied array of xdata points.
+ :type model_deriv: *optional*, None or callable
+
+
+ :param epsfcn: float
+ A variable used in determining a suitable parameter variation when
+ calculating the numerical derivatives (for model_deriv=None).
+ Normally the actual step length will be sqrt(epsfcn)*x
+ Original Gefit module was using epsfcn 1.0e-10 while default value
+ is now numpy.finfo(numpy.float64).eps as in scipy
+ :type epsfcn: *optional*, float
+
+ :param left_derivative:
+ This parameter only has an influence if no derivative function
+ is provided. When True the left and right derivatives of the
+ model will be calculated for each fitted parameters thus leading to
+ the double number of function evaluations. Default is False.
+ Original Gefit module was always using left_derivative as True.
+ :type left_derivative: *optional*, bool
+
+ :param last_evaluation: An M-length array
+ Used for optimization purposes. If supplied, this array will be taken as the result of
+ evaluating the function, that is as the result of ``model(x, *parameters)`` thus avoiding
+ the evaluation call.
+
+ :param full_output: bool, optional
+ Additional output used for internal purposes with the keys:
+ ``function_calls``
+ The number of model function calls performed.
+ ``fitparam``
+ A sequence with the actual free parameters
+ ``free_index``
+ Sequence with the indices of the free parameters in input parameters sequence.
+ ``noigno``
+ Sequence with the indices of the original parameters considered in the calculations.
+ """
+ if epsfcn is None:
+ epsfcn = numpy.finfo(numpy.float64).eps
+ else:
+ epsfcn = max(epsfcn, numpy.finfo(numpy.float64).eps)
+ #nr0, nc = data.shape
+ n_param = len(parameters)
+ if constraints is None:
+ derivfactor = numpy.ones((n_param, ))
+ n_free = n_param
+ noigno = numpy.arange(n_param)
+ free_index = noigno * 1
+ fitparam = parameters * 1
+ else:
+ n_free = 0
+ fitparam = []
+ free_index = []
+ noigno = []
+ derivfactor = []
+ for i in range(n_param):
+ if constraints[i][0] != CIGNORED:
+ noigno.append(i)
+ if constraints[i][0] == CFREE:
+ fitparam.append(parameters [i])
+ derivfactor.append(1.0)
+ free_index.append(i)
+ n_free += 1
+ elif constraints[i][0] == CPOSITIVE:
+ fitparam.append(abs(parameters[i]))
+ derivfactor.append(1.0)
+ #fitparam.append(numpy.sqrt(abs(parameters[i])))
+ #derivfactor.append(2.0*numpy.sqrt(abs(parameters[i])))
+ free_index.append(i)
+ n_free += 1
+ elif constraints[i][0] == CQUOTED:
+ pmax = max(constraints[i][1], constraints[i][2])
+ pmin =min(constraints[i][1], constraints[i][2])
+ if ((pmax-pmin) > 0) & \
+ (parameters[i] <= pmax) & \
+ (parameters[i] >= pmin):
+ A = 0.5 * (pmax + pmin)
+ B = 0.5 * (pmax - pmin)
+ fitparam.append(parameters[i])
+ derivfactor.append(B*numpy.cos(numpy.arcsin((parameters[i] - A)/B)))
+ free_index.append(i)
+ n_free += 1
+ elif (pmax-pmin) > 0:
+ print("WARNING: Quoted parameter outside boundaries")
+ print("Initial value = %f" % parameters[i])
+ print("Limits are %f and %f" % (pmin, pmax))
+ print("Parameter will be kept at its starting value")
+ fitparam = numpy.array(fitparam, numpy.float64)
+ alpha = numpy.zeros((n_free, n_free), numpy.float64)
+ beta = numpy.zeros((1, n_free), numpy.float64)
+ #delta = (fitparam + numpy.equal(fitparam, 0.0)) * 0.00001
+ delta = (fitparam + numpy.equal(fitparam, 0.0)) * numpy.sqrt(epsfcn)
+ nr = y.size
+ ##############
+ # Prior to each call to the function one has to re-calculate the
+ # parameters
+ pwork = parameters.__copy__()
+ for i in range(n_free):
+ pwork [free_index[i]] = fitparam [i]
+ if n_free == 0:
+ raise ValueError("No free parameters to fit")
+ function_calls = 0
+ if not left_derivative:
+ if last_evaluation is not None:
+ f2 = last_evaluation
+ else:
+ f2 = model(x, *parameters)
+ f2.shape = -1
+ function_calls += 1
+ for i in range(n_free):
+ if model_deriv is None:
+ #pwork = parameters.__copy__()
+ pwork[free_index[i]] = fitparam [i] + delta [i]
+ newpar = _get_parameters(pwork.tolist(), constraints)
+ newpar = numpy.take(newpar, noigno)
+ f1 = model(x, *newpar)
+ f1.shape = -1
+ function_calls += 1
+ if left_derivative:
+ pwork[free_index[i]] = fitparam [i] - delta [i]
+ newpar = _get_parameters(pwork.tolist(), constraints)
+ newpar=numpy.take(newpar, noigno)
+ f2 = model(x, *newpar)
+ function_calls += 1
+ help0 = (f1 - f2) / (2.0 * delta[i])
+ else:
+ help0 = (f1 - f2) / (delta[i])
+ help0 = help0 * derivfactor[i]
+ pwork[free_index[i]] = fitparam [i]
+ #removed I resize outside the loop:
+ #help0 = numpy.resize(help0, (1, nr))
+ else:
+ help0 = model_deriv(x, pwork, free_index[i])
+ help0 = help0 * derivfactor[i]
+
+ if i == 0:
+ deriv = help0
+ else:
+ deriv = numpy.concatenate((deriv, help0), 0)
+
+ #line added to resize outside the loop
+ deriv = numpy.resize(deriv, (n_free, nr))
+ if last_evaluation is None:
+ if constraints is None:
+ yfit = model(x, *fitparam)
+ yfit.shape = -1
+ else:
+ newpar = _get_parameters(pwork.tolist(), constraints)
+ newpar = numpy.take(newpar, noigno)
+ yfit = model(x, *newpar)
+ yfit.shape = -1
+ function_calls += 1
+ else:
+ yfit = last_evaluation
+ deltay = y - yfit
+ help0 = weight * deltay
+ for i in range(n_free):
+ derivi = numpy.resize(deriv[i, :], (1, nr))
+ help1 = numpy.resize(numpy.sum((help0 * derivi), 1), (1, 1))
+ if i == 0:
+ beta = help1
+ else:
+ beta = numpy.concatenate((beta, help1), 1)
+ help1 = numpy.inner(deriv, weight*derivi)
+ if i == 0:
+ alpha = help1
+ else:
+ alpha = numpy.concatenate((alpha, help1), 1)
+ chisq = (help0 * deltay).sum()
+ if full_output:
+ ddict = {}
+ ddict["n_free"] = n_free
+ ddict["free_index"] = free_index
+ ddict["noigno"] = noigno
+ ddict["fitparam"] = fitparam
+ ddict["derivfactor"] = derivfactor
+ ddict["function_calls"] = function_calls
+ return chisq, alpha, beta, ddict
+ else:
+ return chisq, alpha, beta
+
+
+def _get_parameters(parameters, constraints):
+ """
+ Apply constraints to input parameters.
+
+ Parameters not depending on other parameters, they are returned as the input.
+
+ Parameters depending on other parameters, return the value after applying the
+ relation to the parameter wo which they are related.
+ """
+ # 0 = Free 1 = Positive 2 = Quoted
+ # 3 = Fixed 4 = Factor 5 = Delta
+ if constraints is None:
+ return parameters * 1
+ newparam = []
+ #first I make the free parameters
+ #because the quoted ones put troubles
+ for i in range(len(constraints)):
+ if constraints[i][0] == CFREE:
+ newparam.append(parameters[i])
+ elif constraints[i][0] == CPOSITIVE:
+ #newparam.append(parameters[i] * parameters[i])
+ newparam.append(abs(parameters[i]))
+ elif constraints[i][0] == CQUOTED:
+ newparam.append(parameters[i])
+ elif abs(constraints[i][0]) == CFIXED:
+ newparam.append(parameters[i])
+ else:
+ newparam.append(parameters[i])
+ for i in range(len(constraints)):
+ if constraints[i][0] == CFACTOR:
+ newparam[i] = constraints[i][2] * newparam[int(constraints[i][1])]
+ elif constraints[i][0] == CDELTA:
+ newparam[i] = constraints[i][2] + newparam[int(constraints[i][1])]
+ elif constraints[i][0] == CIGNORED:
+ # The whole ignored stuff should not be documented because setting
+ # a parameter to 0 is not the same as being ignored.
+ # Being ignored should imply the parameter is simply not accounted for
+ # and should be stripped out of the list of parameters by the program
+ # using this module
+ newparam[i] = 0
+ elif constraints[i][0] == CSUM:
+ newparam[i] = constraints[i][2]-newparam[int(constraints[i][1])]
+ return newparam
+
+
+def _get_sigma_parameters(parameters, sigma0, constraints):
+ """
+ Internal function propagating the uncertainty on the actually fitted parameters and related parameters to the
+ final parameters considering the applied constraints.
+
+ Parameters
+ ----------
+ parameters : 1D sequence of length equal to the number of free parameters N
+ The parameters actually used in the fitting process.
+ sigma0 : 1D sequence of length N
+ Uncertainties calculated as the square-root of the diagonal of
+ the covariance matrix
+ constraints : The set of constraints applied in the fitting process
+ """
+ # 0 = Free 1 = Positive 2 = Quoted
+ # 3 = Fixed 4 = Factor 5 = Delta
+ if constraints is None:
+ return sigma0
+ n_free = 0
+ sigma_par = numpy.zeros(parameters.shape, numpy.float64)
+ for i in range(len(constraints)):
+ if constraints[i][0] == CFREE:
+ sigma_par [i] = sigma0[n_free]
+ n_free += 1
+ elif constraints[i][0] == CPOSITIVE:
+ #sigma_par [i] = 2.0 * sigma0[n_free]
+ sigma_par [i] = sigma0[n_free]
+ n_free += 1
+ elif constraints[i][0] == CQUOTED:
+ pmax = max(constraints [i][1], constraints [i][2])
+ pmin = min(constraints [i][1], constraints [i][2])
+ # A = 0.5 * (pmax + pmin)
+ B = 0.5 * (pmax - pmin)
+ if (B > 0) & (parameters [i] < pmax) & (parameters [i] > pmin):
+ sigma_par [i] = abs(B * numpy.cos(parameters[i]) * sigma0[n_free])
+ n_free += 1
+ else:
+ sigma_par [i] = parameters[i]
+ elif abs(constraints[i][0]) == CFIXED:
+ sigma_par[i] = parameters[i]
+ for i in range(len(constraints)):
+ if constraints[i][0] == CFACTOR:
+ sigma_par [i] = constraints[i][2]*sigma_par[int(constraints[i][1])]
+ elif constraints[i][0] == CDELTA:
+ sigma_par [i] = sigma_par[int(constraints[i][1])]
+ elif constraints[i][0] == CSUM:
+ sigma_par [i] = sigma_par[int(constraints[i][1])]
+ return sigma_par
+
+
+def main(argv=None):
+ if argv is None:
+ npoints = 10000
+ elif hasattr(argv, "__len__"):
+ if len(argv) > 1:
+ npoints = int(argv[1])
+ else:
+ print("Usage:")
+ print("fit [npoints]")
+ else:
+ # expected a number
+ npoints = argv
+
+ def gauss(t0, *param0):
+ param = numpy.array(param0)
+ t = numpy.array(t0)
+ dummy = 2.3548200450309493 * (t - param[3]) / param[4]
+ return param[0] + param[1] * t + param[2] * myexp(-0.5 * dummy * dummy)
+
+
+ def myexp(x):
+ # put a (bad) filter to avoid over/underflows
+ # with no python looping
+ return numpy.exp(x * numpy.less(abs(x), 250)) -\
+ 1.0 * numpy.greater_equal(abs(x), 250)
+
+ xx = numpy.arange(npoints, dtype=numpy.float64)
+ yy = gauss(xx, *[10.5, 2, 1000.0, 20., 15])
+ sy = numpy.sqrt(abs(yy))
+ parameters = [0.0, 1.0, 900.0, 25., 10]
+ stime = time.time()
+
+ fittedpar, cov, ddict = leastsq(gauss, xx, yy, parameters,
+ sigma=sy,
+ left_derivative=False,
+ full_output=True,
+ check_finite=True)
+ etime = time.time()
+ sigmapars = numpy.sqrt(numpy.diag(cov))
+ print("Took ", etime - stime, "seconds")
+ print("Function calls = ", ddict["nfev"])
+ print("chi square = ", ddict["chisq"])
+ print("Fitted pars = ", fittedpar)
+ print("Sigma pars = ", sigmapars)
+ try:
+ from scipy.optimize import curve_fit as cfit
+ SCIPY = True
+ except ImportError:
+ SCIPY = False
+ if SCIPY:
+ counter = 0
+ stime = time.time()
+ scipy_fittedpar, scipy_cov = cfit(gauss,
+ xx,
+ yy,
+ parameters,
+ sigma=sy)
+ etime = time.time()
+ print("Scipy Took ", etime - stime, "seconds")
+ print("Counter = ", counter)
+ print("scipy = ", scipy_fittedpar)
+ print("Sigma = ", numpy.sqrt(numpy.diag(scipy_cov)))
+
+if __name__ == "__main__":
+ main()
diff --git a/src/silx/math/fit/peaks.pyx b/src/silx/math/fit/peaks.pyx
new file mode 100644
index 0000000..a4fce89
--- /dev/null
+++ b/src/silx/math/fit/peaks.pyx
@@ -0,0 +1,175 @@
+# coding: utf-8
+#/*##########################################################################
+# Copyright (C) 2016-2018 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+#############################################################################*/
+"""This module provides a peak search function and tools related to peak
+analysis.
+"""
+
+__authors__ = ["P. Knobel"]
+__license__ = "MIT"
+__date__ = "15/05/2017"
+
+import logging
+import numpy
+
+from silx.math.fit import filters
+
+_logger = logging.getLogger(__name__)
+
+cimport cython
+from libc.stdlib cimport free
+
+cimport silx.math.fit.peaks_wrapper as peaks_wrapper
+
+
+def peak_search(y, fwhm, sensitivity=3.5,
+ begin_index=None, end_index=None,
+ debug=False, relevance_info=False):
+ """Find peaks in a curve.
+
+ :param y: Data array
+ :type y: numpy.ndarray
+ :param fwhm: Estimated full width at half maximum of the typical peaks we
+ are interested in (expressed in number of samples)
+ :param sensitivity: Threshold factor used for peak detection. Only peaks
+ with amplitudes higher than ``σ * sensitivity`` - where ``σ`` is the
+ standard deviation of the noise - qualify as peaks.
+ :param begin_index: Index of the first sample of the region of interest
+ in the ``y`` array. If ``None``, start from the first sample.
+ :param end_index: Index of the last sample of the region of interest in
+ the ``y`` array. If ``None``, process until the last sample.
+ :param debug: If ``True``, print debug messages. Default: ``False``
+ :param relevance_info: If ``True``, add a second dimension with relevance
+ information to the output array. Default: ``False``
+ :return: 1D sequence with indices of peaks in the data
+ if ``relevance_info`` is ``False``.
+ Else, sequence of ``(peak_index, peak_relevance)`` tuples (one tuple
+ per peak).
+ :raise: ``IndexError`` if the number of peaks is too large to fit in the
+ output array.
+ """
+ cdef:
+ int i
+ double[::1] y_c
+ double* peaks_c
+ double* relevances_c
+
+ y_c = numpy.array(y,
+ copy=True,
+ dtype=numpy.float64,
+ order='C').reshape(-1)
+ if debug:
+ debug = 1
+ else:
+ debug = 0
+
+ if begin_index is None:
+ begin_index = 0
+ if end_index is None:
+ end_index = y_c.size - 1
+
+ n_peaks = peaks_wrapper.seek(begin_index, end_index, y_c.size,
+ fwhm, sensitivity, debug,
+ &y_c[0], &peaks_c, &relevances_c)
+
+
+ # A negative return value means that peaks were found but not enough
+ # memory could be allocated for all
+ if n_peaks < 0 and n_peaks != -123456:
+ msg = "Before memory allocation error happened, "
+ msg += "we found %d peaks.\n" % abs(n_peaks)
+ _logger.debug(msg)
+ msg = ""
+ for i in range(abs(n_peaks)):
+ msg += "peak index %f, " % peaks_c[i]
+ msg += "relevance %f\n" % relevances_c[i]
+ _logger.debug(msg)
+ free(peaks_c)
+ free(relevances_c)
+ raise MemoryError("Failed to reallocate memory for output arrays")
+ # Special value -123456 is returned if the initial memory allocation
+ # fails, before any search could be performed
+ elif n_peaks == -123456:
+ raise MemoryError("Failed to allocate initial memory for " +
+ "output arrays")
+
+ peaks = numpy.empty(shape=(n_peaks,),
+ dtype=numpy.float64)
+ relevances = numpy.empty(shape=(n_peaks,),
+ dtype=numpy.float64)
+
+ for i in range(n_peaks):
+ peaks[i] = peaks_c[i]
+ relevances[i] = relevances_c[i]
+
+ free(peaks_c)
+ free(relevances_c)
+
+ if not relevance_info:
+ return peaks
+ else:
+ return list(zip(peaks, relevances))
+
+
+def guess_fwhm(y):
+ """Return the full-width at half maximum for the largest peak in
+ the data array.
+
+ The algorithm removes the background, then finds a global maximum
+ and its corresponding FWHM.
+
+ This value can be used as an initial fit parameter, used as input for
+ an iterative fit function.
+
+ :param y: Data to be used for guessing the fwhm.
+ :return: Estimation of full-width at half maximum, based on fwhm of
+ the global maximum.
+ """
+ # set at a minimum value for the fwhm
+ fwhm_min = 4
+
+ # remove data background (computed with a strip filter)
+ background = filters.strip(y, w=1, niterations=1000)
+ yfit = y - background
+
+ # basic peak search: find the global maximum
+ maximum = max(yfit)
+ # find indices of all values == maximum
+ idx = numpy.nonzero(yfit == maximum)[0]
+ # take the last one (if any)
+ if not len(idx):
+ return 0
+ posindex = idx[-1]
+ height = yfit[posindex]
+
+ # now find the width of the peak at half maximum
+ imin = posindex
+ while yfit[imin] > 0.5 * height and imin > 0:
+ imin -= 1
+ imax = posindex
+ while yfit[imax] > 0.5 * height and imax < len(yfit) - 1:
+ imax += 1
+
+ fwhm = max(imax - imin - 1, fwhm_min)
+
+ return fwhm
diff --git a/src/silx/math/fit/peaks/include/peaks.h b/src/silx/math/fit/peaks/include/peaks.h
new file mode 100644
index 0000000..bd25d96
--- /dev/null
+++ b/src/silx/math/fit/peaks/include/peaks.h
@@ -0,0 +1,32 @@
+/*##########################################################################
+# Copyright (C) 2016 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
+
+#ifndef PEAKS_H
+#define PEAKS_H
+
+/* Smoothing functions */
+
+long seek(long begin_index, long end_index, long nsamples, double fwhm, double sensitivity,
+ double debug_info, double *data, double **peaks, double **relevances);
+
+#endif /* #define PEAKS_H */
diff --git a/src/silx/math/fit/peaks/src/peaks.c b/src/silx/math/fit/peaks/src/peaks.c
new file mode 100644
index 0000000..65cb4f6
--- /dev/null
+++ b/src/silx/math/fit/peaks/src/peaks.c
@@ -0,0 +1,255 @@
+#/*##########################################################################
+# Copyright (c) 2004-2016 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+#############################################################################*/
+#include <math.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include "peaks.h"
+
+
+#define MIN(x, y) (((x) < (y)) ? (x) : (y))
+#define MAX(x, y) (((x) > (y)) ? (x) : (y))
+
+/* Peak search function, adapted from PyMca SpecFitFuns
+
+ This uses a convolution with the second-derivative of a gaussian curve, to
+ smooth the data.
+
+ Arguments:
+
+ - begin_index: First index of the region of interest in the input data
+ array
+ - end_index: Last index of the region of interest in the input data
+ array
+ - nsamples: Number of samples in the input array
+ - fwhm: Full width at half maximum for the gaussian used for smoothing.
+ - sensitivity:
+ - debug_info: If different from 0, print debugging messages
+ - data: input array of 1D data
+ - peaks: pointer to output array of peak indices
+ - relevances: pointer to output array of peak relevances
+*/
+long seek(long begin_index,
+ long end_index,
+ long nsamples,
+ double fwhm,
+ double sensitivity,
+ double debug_info,
+ double *data,
+ double **peaks,
+ double **relevances)
+{
+ /* local variables */
+ double *peaks0, *relevances0;
+ double *realloc_peaks, *realloc_relevances;
+ double sigma, sigma2, sigma4;
+ long max_gfactor = 100;
+ double gfactor[100];
+ long nr_factor;
+ double lowthreshold;
+ double data2[2];
+ double nom;
+ double den2;
+ long channel1;
+ long lld;
+ long cch;
+ long cfac, cfac2, max_cfac;
+ long ihelp1, ihelp2;
+ long i;
+ long max_npeaks = 100;
+ long n_peaks = 0;
+ double peakstarted = 0;
+
+ peaks0 = malloc(100 * sizeof(double));
+ relevances0 = malloc(100 * sizeof(double));
+ if (peaks0 == NULL || relevances0 == NULL) {
+ printf("Error: failed to allocate memory for peaks array.");
+ return(-123456);
+ }
+ /* Make sure the peaks matrix is filled with zeros */
+ for (i=0;i<100;i++){
+ peaks0[i] = 0.0;
+ relevances0[i] = 0.0;
+ }
+ /* Output pointers */
+ *peaks = peaks0;
+ *relevances = relevances0;
+
+ /* prepare the calculation of the Gaussian scaling factors */
+
+ sigma = fwhm / 2.35482;
+ sigma2 = sigma * sigma;
+ sigma4 = sigma2 * sigma2;
+ lowthreshold = 0.01 / sigma2;
+
+ /* calculate the factors until lower threshold reached */
+ nr_factor = 0;
+ max_cfac = MIN(max_gfactor, ((end_index - begin_index - 2) / 2) - 1);
+ for (cfac=0; cfac < max_cfac; cfac++) {
+ nr_factor++;
+ cfac2 = (cfac+1) * (cfac+1);
+ gfactor[cfac] = (sigma2 - cfac2) * exp(-cfac2/(sigma2*2.0)) / sigma4;
+
+ if ((gfactor[cfac] < lowthreshold)
+ && (gfactor[cfac] > (-lowthreshold))){
+ break;
+ }
+ }
+
+ /* What comes now is specific to MCA spectra ... */
+ lld = 0;
+ while (data[lld] == 0) {
+ lld++;
+ }
+ lld = lld + (int) (0.5 * fwhm);
+
+ channel1 = begin_index - nr_factor - 1;
+ channel1 = MAX (channel1, lld);
+ if(debug_info){
+ printf("nrfactor = %ld\n", nr_factor);
+ }
+ /* calculates smoothed value and variance at begincalc */
+ cch = MAX(begin_index, 0);
+ nom = data[cch] / sigma2;
+ den2 = data[cch] / sigma4;
+ for (cfac = 0; cfac < nr_factor; cfac++){
+ ihelp1 = cch-cfac;
+ if (ihelp1 < 0){
+ ihelp1 = 0;
+ }
+ ihelp2 = cch+cfac;
+ if (ihelp2 >= nsamples){
+ ihelp2 = nsamples-1;
+ }
+ nom += gfactor[cfac] * (data[ihelp2] + data[ihelp1]);
+ den2 += gfactor[cfac] * gfactor[cfac] *
+ (data[ihelp2] + data[ihelp1]);
+ }
+
+ /* now normalize the smoothed value to the standard deviation */
+ if (den2 <= 0.0) {
+ data2[1] = 0.0;
+ }else{
+ data2[1] = nom / sqrt(den2);
+ }
+ data[0] = data[1];
+
+ while (cch <= MIN(end_index,nsamples-2)){
+ /* calculate gaussian smoothed values */
+ data2[0] = data2[1];
+ cch++;
+ nom = data[cch]/sigma2;
+ den2 = data[cch] / sigma4;
+ for (cfac = 1; cfac < nr_factor; cfac++){
+ ihelp1 = cch-cfac;
+ if (ihelp1 < 0){
+ ihelp1 = 0;
+ }
+ ihelp2 = cch+cfac;
+ if (ihelp2 >= nsamples){
+ ihelp2 = nsamples-1;
+ }
+ nom += gfactor[cfac-1] * (data[ihelp2] + data[ihelp1]);
+ den2 += gfactor[cfac-1] * gfactor[cfac-1] *
+ (data[ihelp2] + data[ihelp1]);
+ }
+ /* now normalize the smoothed value to the standard deviation */
+ if (den2 <= 0) {
+ data2[1] = 0;
+ }else{
+ data2[1] = nom / sqrt(den2);
+ }
+ /* look if the current point falls in a peak */
+ if (data2[1] > sensitivity) {
+ if(peakstarted == 0){
+ if (data2[1] > data2[0]){
+ /* this second test is to prevent a peak from outside
+ the region from being detected at the beginning of the search */
+ peakstarted=1;
+ }
+ }
+ /* there is a peak */
+ if (debug_info){
+ printf("At cch = %ld y[cch] = %g\n", cch, data[cch]);
+ printf("data2[0] = %g\n", data2[0]);
+ printf("data2[1] = %g\n", data2[1]);
+ printf("sensitivity = %g\n", sensitivity);
+ }
+ if(peakstarted == 1){
+ /* look for the top of the peak */
+ if (data2[1] < data2[0]) {
+ /* we are close to the top of the peak */
+ if (debug_info){
+ printf("we are close to the top of the peak\n");
+ }
+ if (n_peaks == max_npeaks) {
+ max_npeaks = max_npeaks + 100;
+ realloc_peaks = realloc(peaks0, max_npeaks * sizeof(double));
+ realloc_relevances = realloc(relevances0, max_npeaks * sizeof(double));
+ if (realloc_peaks == NULL || realloc_relevances == NULL) {
+ printf("Error: failed to extend memory for peaks array.");
+ *peaks = peaks0;
+ *relevances = relevances0;
+ return(-n_peaks);
+ }
+ else {
+ peaks0 = realloc_peaks;
+ relevances0 = realloc_relevances;
+ }
+ }
+ peaks0[n_peaks] = cch-1;
+ relevances0[n_peaks] = data2[0];
+ n_peaks++;
+ peakstarted=2;
+ }
+ }
+ /* Doublet case */
+ if(peakstarted == 2){
+ if ((cch-peaks0[n_peaks-1]) > 0.6 * fwhm) {
+ if (data2[1] > data2[0]){
+ if(debug_info){
+ printf("We may have a doublet\n");
+ }
+ peakstarted=1;
+ }
+ }
+ }
+ }else{
+ if (peakstarted==1){
+ /* We were on a peak but we did not find the top */
+ if(debug_info){
+ printf("We were on a peak but we did not find the top\n");
+ }
+ }
+ peakstarted=0;
+ }
+ }
+ if(debug_info){
+ for (i=0;i< n_peaks;i++){
+ printf("Peak %ld found at ",i+1);
+ printf("index %g with y = %g\n", peaks0[i],data[(long ) peaks0[i]]);
+ }
+ }
+ *peaks = peaks0;
+ *relevances = relevances0;
+ return (n_peaks);
+}
diff --git a/src/silx/math/fit/peaks_wrapper.pxd b/src/silx/math/fit/peaks_wrapper.pxd
new file mode 100644
index 0000000..4c77dc6
--- /dev/null
+++ b/src/silx/math/fit/peaks_wrapper.pxd
@@ -0,0 +1,41 @@
+# coding: utf-8
+#/*##########################################################################
+# Copyright (C) 2016 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+#############################################################################*/
+
+__authors__ = ["P. Knobel"]
+__license__ = "MIT"
+__date__ = "22/06/2016"
+
+cimport cython
+
+cdef extern from "peaks.h":
+ long seek(long begin_index,
+ long end_index,
+ long nsamples,
+ double fwhm,
+ double sensitivity,
+ double debug_info,
+ double * data,
+ double ** peaks,
+ double ** relevances)
+
diff --git a/src/silx/math/fit/setup.py b/src/silx/math/fit/setup.py
new file mode 100644
index 0000000..649387f
--- /dev/null
+++ b/src/silx/math/fit/setup.py
@@ -0,0 +1,85 @@
+# coding: utf-8
+# /*##########################################################################
+# Copyright (C) 2016-2018 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
+
+
+__authors__ = ["P. Knobel"]
+__license__ = "MIT"
+__date__ = "22/06/2016"
+
+
+import os.path
+
+from numpy.distutils.misc_util import Configuration
+
+
+def configuration(parent_package='', top_path=None):
+ config = Configuration('fit', parent_package, top_path)
+ config.add_subpackage('test')
+
+ # =====================================
+ # fit functions
+ # =====================================
+ fun_src = [os.path.join('functions', "src", "funs.c"),
+ "functions.pyx"]
+ fun_inc = [os.path.join('functions', 'include')]
+
+ config.add_extension('functions',
+ sources=fun_src,
+ include_dirs=fun_inc,
+ language='c')
+
+ # =====================================
+ # fit filters
+ # =====================================
+ filt_src = [os.path.join('filters', "src", srcf)
+ for srcf in ["smoothnd.c", "snip1d.c",
+ "snip2d.c", "snip3d.c", "strip.c"]]
+ filt_src.append("filters.pyx")
+ filt_inc = [os.path.join('filters', 'include')]
+
+ config.add_extension('filters',
+ sources=filt_src,
+ include_dirs=filt_inc,
+ language='c')
+
+ # =====================================
+ # peaks
+ # =====================================
+ peaks_src = [os.path.join('peaks', "src", "peaks.c"),
+ "peaks.pyx"]
+ peaks_inc = [os.path.join('peaks', 'include')]
+
+ config.add_extension('peaks',
+ sources=peaks_src,
+ include_dirs=peaks_inc,
+ language='c')
+ # =====================================
+ # =====================================
+ return config
+
+
+if __name__ == "__main__":
+ from numpy.distutils.core import setup
+
+ setup(configuration=configuration)
diff --git a/src/silx/math/fit/test/__init__.py b/src/silx/math/fit/test/__init__.py
new file mode 100644
index 0000000..745efe3
--- /dev/null
+++ b/src/silx/math/fit/test/__init__.py
@@ -0,0 +1,23 @@
+# coding: utf-8
+# /*##########################################################################
+# Copyright (C) 2016 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
diff --git a/src/silx/math/fit/test/test_bgtheories.py b/src/silx/math/fit/test/test_bgtheories.py
new file mode 100644
index 0000000..6620d38
--- /dev/null
+++ b/src/silx/math/fit/test/test_bgtheories.py
@@ -0,0 +1,154 @@
+# coding: utf-8
+# /*##########################################################################
+# Copyright (C) 2016 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
+import copy
+import unittest
+import numpy
+import random
+
+from silx.math.fit import bgtheories
+from silx.math.fit.functions import sum_gauss
+
+
+class TestBgTheories(unittest.TestCase):
+ """
+ """
+ def setUp(self):
+ self.x = numpy.arange(100)
+ self.y = 10 + 0.05 * self.x + sum_gauss(self.x, 10., 45., 15.)
+ # add a very narrow high amplitude peak to test strip and snip
+ self.y += sum_gauss(self.x, 100., 75., 2.)
+ self.narrow_peak_index = list(self.x).index(75)
+ random.seed()
+
+ def tearDown(self):
+ pass
+
+ def testTheoriesAttrs(self):
+ for theory_name in bgtheories.THEORY:
+ self.assertIsInstance(theory_name, str)
+ self.assertTrue(hasattr(bgtheories.THEORY[theory_name],
+ "function"))
+ self.assertTrue(hasattr(bgtheories.THEORY[theory_name].function,
+ "__call__"))
+ # Ensure legacy functions are not renamed accidentally
+ self.assertTrue(
+ {"No Background", "Constant", "Linear", "Strip", "Snip"}.issubset(
+ set(bgtheories.THEORY)))
+
+ def testNoBg(self):
+ nobgfun = bgtheories.THEORY["No Background"].function
+ self.assertTrue(numpy.array_equal(nobgfun(self.x, self.y),
+ numpy.zeros_like(self.x)))
+ # default estimate
+ self.assertEqual(bgtheories.THEORY["No Background"].estimate(self.x, self.y),
+ ([], []))
+
+ def testConstant(self):
+ consfun = bgtheories.THEORY["Constant"].function
+ c = random.random() * 100
+ self.assertTrue(numpy.array_equal(consfun(self.x, self.y, c),
+ c * numpy.ones_like(self.x)))
+ # default estimate
+ esti_par, cons = bgtheories.THEORY["Constant"].estimate(self.x, self.y)
+ self.assertEqual(cons,
+ [[0, 0, 0]])
+ self.assertAlmostEqual(esti_par,
+ min(self.y))
+
+ def testLinear(self):
+ linfun = bgtheories.THEORY["Linear"].function
+ a = random.random() * 100
+ b = random.random() * 100
+ self.assertTrue(numpy.array_equal(linfun(self.x, self.y, a, b),
+ a + b * self.x))
+ # default estimate
+ esti_par, cons = bgtheories.THEORY["Linear"].estimate(self.x, self.y)
+
+ self.assertEqual(cons,
+ [[0, 0, 0], [0, 0, 0]])
+ self.assertAlmostEqual(esti_par[0], 10, places=3)
+ self.assertAlmostEqual(esti_par[1], 0.05, places=3)
+
+ def testStrip(self):
+ stripfun = bgtheories.THEORY["Strip"].function
+ anchors = sorted(random.sample(list(self.x), 4))
+ anchors_indices = [list(self.x).index(a) for a in anchors]
+
+ # we really want to strip away the narrow peak
+ anchors_indices_copy = copy.deepcopy(anchors_indices)
+ for idx in anchors_indices_copy:
+ if abs(idx - self.narrow_peak_index) < 5:
+ anchors_indices.remove(idx)
+ anchors.remove(self.x[idx])
+
+ width = 2
+ niter = 1000
+ bgtheories.THEORY["Strip"].configure(AnchorsList=anchors, AnchorsFlag=True)
+
+ bg = stripfun(self.x, self.y, width, niter)
+
+ # assert peak amplitude has been decreased
+ self.assertLess(bg[self.narrow_peak_index],
+ self.y[self.narrow_peak_index])
+
+ # default estimate
+ for i in anchors_indices:
+ self.assertEqual(bg[i], self.y[i])
+
+ # estimated parameters are equal to the default ones in the config dict
+ bgtheories.THEORY["Strip"].configure(StripWidth=7, StripIterations=8)
+ esti_par, cons = bgtheories.THEORY["Strip"].estimate(self.x, self.y)
+ self.assertTrue(numpy.array_equal(cons, [[3, 0, 0], [3, 0, 0]]))
+ self.assertEqual(esti_par, [7, 8])
+
+ def testSnip(self):
+ snipfun = bgtheories.THEORY["Snip"].function
+ anchors = sorted(random.sample(list(self.x), 4))
+ anchors_indices = [list(self.x).index(a) for a in anchors]
+
+ # we want to strip away the narrow peak, so remove nearby anchors
+ anchors_indices_copy = copy.deepcopy(anchors_indices)
+ for idx in anchors_indices_copy:
+ if abs(idx - self.narrow_peak_index) < 5:
+ anchors_indices.remove(idx)
+ anchors.remove(self.x[idx])
+
+ width = 16
+ bgtheories.THEORY["Snip"].configure(AnchorsList=anchors, AnchorsFlag=True)
+ bg = snipfun(self.x, self.y, width)
+
+ # assert peak amplitude has been decreased
+ self.assertLess(bg[self.narrow_peak_index],
+ self.y[self.narrow_peak_index],
+ "Snip didn't decrease the peak amplitude.")
+
+ # anchored data must remain fixed
+ for i in anchors_indices:
+ self.assertEqual(bg[i], self.y[i])
+
+ # estimated parameters are equal to the default ones in the config dict
+ bgtheories.THEORY["Snip"].configure(SnipWidth=7)
+ esti_par, cons = bgtheories.THEORY["Snip"].estimate(self.x, self.y)
+ self.assertTrue(numpy.array_equal(cons, [[3, 0, 0]]))
+ self.assertEqual(esti_par, [7])
diff --git a/src/silx/math/fit/test/test_filters.py b/src/silx/math/fit/test/test_filters.py
new file mode 100644
index 0000000..8314bdc
--- /dev/null
+++ b/src/silx/math/fit/test/test_filters.py
@@ -0,0 +1,122 @@
+# coding: utf-8
+# /*##########################################################################
+# Copyright (C) 2016 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
+import numpy
+import unittest
+from silx.math.fit import filters
+from silx.math.fit import functions
+from silx.test.utils import add_relative_noise
+
+
+class TestSmooth(unittest.TestCase):
+ """
+ Unit tests of smoothing functions.
+
+ Test that the difference between a synthetic curve with 5% added random
+ noise and the result of smoothing that signal is less than 5%. We compare
+ the sum of all samples in each curve.
+ """
+ def setUp(self):
+ x = numpy.arange(5000)
+ # (height1, center1, fwhm1, beamfwhm...)
+ slit_params = (50, 500, 200, 100,
+ 50, 600, 80, 30,
+ 20, 2000, 150, 150,
+ 50, 2250, 110, 100,
+ 40, 3000, 50, 10,
+ 23, 4980, 250, 20)
+
+ self.y1 = functions.sum_slit(x, *slit_params)
+ # 5% noise
+ self.y1 = add_relative_noise(self.y1, 5.)
+
+ # (height1, center1, fwhm1...)
+ step_params = (50, 500, 200,
+ 50, 600, 80,
+ 20, 2000, 150,
+ 50, 2250, 110,
+ 40, 3000, 50,
+ 23, 4980, 250,)
+
+ self.y2 = functions.sum_stepup(x, *step_params)
+ # 5% noise
+ self.y2 = add_relative_noise(self.y2, 5.)
+
+ self.y3 = functions.sum_stepdown(x, *step_params)
+ # 5% noise
+ self.y3 = add_relative_noise(self.y3, 5.)
+
+ def tearDown(self):
+ pass
+
+ def testSavitskyGolay(self):
+ npts = 25
+ for y in [self.y1, self.y2, self.y3]:
+ smoothed_y = filters.savitsky_golay(y, npoints=npts)
+
+ # we added +-5% of random noise. The difference must be much lower
+ # than 5%.
+ diff = abs(sum(smoothed_y) - sum(y)) / sum(y)
+ self.assertLess(diff, 0.05,
+ "Difference between data with 5%% noise and " +
+ "smoothed data is > 5%% (%f %%)" % (diff * 100))
+
+ # Try various smoothing levels
+ npts += 25
+
+ def testSmooth1d(self):
+ """Test the 1D smoothing against the formula
+ ys[i] = (y[i-1] + 2 * y[i] + y[i+1]) / 4 (for 1 < i < n-1)"""
+ smoothed_y = filters.smooth1d(self.y1)
+
+ for i in range(1, len(self.y1) - 1):
+ self.assertAlmostEqual(4 * smoothed_y[i],
+ self.y1[i-1] + 2 * self.y1[i] + self.y1[i+1])
+
+ def testSmooth2d(self):
+ """Test that a 2D smoothing is the same as two successive and
+ orthogonal 1D smoothings"""
+ x = numpy.arange(10000)
+
+ noise = 2 * numpy.random.random(10000) - 1
+ noise *= 0.05
+ y = x * (1 + noise)
+
+ y.shape = (100, 100)
+
+ smoothed_y = filters.smooth2d(y)
+
+ intermediate_smooth = numpy.zeros_like(y)
+ expected_smooth = numpy.zeros_like(y)
+ # smooth along first dimension
+ for i in range(0, y.shape[0]):
+ intermediate_smooth[i, :] = filters.smooth1d(y[i, :])
+
+ # smooth along second dimension
+ for j in range(0, y.shape[1]):
+ expected_smooth[:, j] = filters.smooth1d(intermediate_smooth[:, j])
+
+ for i in range(0, y.shape[0]):
+ for j in range(0, y.shape[1]):
+ self.assertAlmostEqual(smoothed_y[i, j],
+ expected_smooth[i, j])
diff --git a/src/silx/math/fit/test/test_fit.py b/src/silx/math/fit/test/test_fit.py
new file mode 100644
index 0000000..00f04e2
--- /dev/null
+++ b/src/silx/math/fit/test/test_fit.py
@@ -0,0 +1,373 @@
+# coding: utf-8
+# /*##########################################################################
+# Copyright (C) 2016-2021 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
+"""
+Nominal tests of the leastsq function.
+"""
+
+import unittest
+
+import numpy
+import sys
+
+from silx.utils import testutils
+from silx.math.fit.leastsq import _logger as fitlogger
+
+
+class Test_leastsq(unittest.TestCase):
+ """
+ Unit tests of the leastsq function.
+ """
+
+ ndims = None
+
+ def setUp(self):
+ try:
+ from silx.math.fit import leastsq
+ self.instance = leastsq
+ except ImportError:
+ self.instance = None
+
+ def myexp(x):
+ # put a (bad) filter to avoid over/underflows
+ # with no python looping
+ with numpy.errstate(invalid='ignore'):
+ return numpy.exp(x*numpy.less(abs(x), 250)) - \
+ 1.0 * numpy.greater_equal(abs(x), 250)
+
+ self.my_exp = myexp
+
+ def gauss(x, *params):
+ params = numpy.array(params, copy=False, dtype=numpy.float64)
+ result = params[0] + params[1] * x
+ for i in range(2, len(params), 3):
+ p = params[i:(i+3)]
+ dummy = 2.3548200450309493*(x - p[1])/p[2]
+ result += p[0] * self.my_exp(-0.5 * dummy * dummy)
+ return result
+
+ self.gauss = gauss
+
+ def gauss_derivative(x, params, idx):
+ if idx == 0:
+ return numpy.ones(len(x), numpy.float64)
+ if idx == 1:
+ return x
+ gaussian_peak = (idx - 2) // 3
+ gaussian_parameter = (idx - 2) % 3
+ actual_idx = 2 + 3 * gaussian_peak
+ p = params[actual_idx:(actual_idx+3)]
+ if gaussian_parameter == 0:
+ return self.gauss(x, *[0, 0, 1.0, p[1], p[2]])
+ if gaussian_parameter == 1:
+ tmp = self.gauss(x, *[0, 0, p[0], p[1], p[2]])
+ tmp *= 2.3548200450309493*(x - p[1])/p[2]
+ return tmp * 2.3548200450309493/p[2]
+ if gaussian_parameter == 2:
+ tmp = self.gauss(x, *[0, 0, p[0], p[1], p[2]])
+ tmp *= 2.3548200450309493*(x - p[1])/p[2]
+ return tmp * 2.3548200450309493*(x - p[1])/(p[2]*p[2])
+
+ self.gauss_derivative = gauss_derivative
+
+ def tearDown(self):
+ self.instance = None
+ self.gauss = None
+ self.gauss_derivative = None
+ self.my_exp = None
+ self.model_function = None
+ self.model_derivative = None
+
+ def testImport(self):
+ self.assertTrue(self.instance is not None,
+ "Cannot import leastsq from silx.math.fit")
+
+ def testUnconstrainedFitNoWeight(self):
+ parameters_actual = [10.5, 2, 1000.0, 20., 15]
+ x = numpy.arange(10000.)
+ y = self.gauss(x, *parameters_actual)
+ parameters_estimate = [0.0, 1.0, 900.0, 25., 10]
+ model_function = self.gauss
+
+ fittedpar, cov = self.instance(model_function, x, y, parameters_estimate)
+ test_condition = numpy.allclose(parameters_actual, fittedpar)
+ if not test_condition:
+ msg = "Unsuccessfull fit\n"
+ for i in range(len(fittedpar)):
+ msg += "Expected %g obtained %g\n" % (parameters_actual[i],
+ fittedpar[i])
+ self.assertTrue(test_condition, msg)
+
+ def testUnconstrainedFitWeight(self):
+ parameters_actual = [10.5,2,1000.0,20.,15]
+ x = numpy.arange(10000.)
+ y = self.gauss(x, *parameters_actual)
+ sigma = numpy.sqrt(y)
+ parameters_estimate = [0.0, 1.0, 900.0, 25., 10]
+ model_function = self.gauss
+
+ fittedpar, cov = self.instance(model_function, x, y,
+ parameters_estimate,
+ sigma=sigma)
+ test_condition = numpy.allclose(parameters_actual, fittedpar)
+ if not test_condition:
+ msg = "Unsuccessfull fit\n"
+ for i in range(len(fittedpar)):
+ msg += "Expected %g obtained %g\n" % (parameters_actual[i],
+ fittedpar[i])
+ self.assertTrue(test_condition, msg)
+
+ def testDerivativeFunction(self):
+ parameters_actual = [10.5, 2, 10000.0, 20., 150, 5000, 900., 300]
+ x = numpy.arange(10000.)
+ y = self.gauss(x, *parameters_actual)
+ delta = numpy.sqrt(numpy.finfo(numpy.float64).eps)
+ for i in range(len(parameters_actual)):
+ p = parameters_actual * 1
+ if p[i] == 0:
+ delta_par = delta
+ else:
+ delta_par = p[i] * delta
+ if i > 2:
+ p[0] = 0.0
+ p[1] = 0.0
+ p[i] += delta_par
+ yPlus = self.gauss(x, *p)
+ p[i] = parameters_actual[i] - delta_par
+ yMinus = self.gauss(x, *p)
+ numerical_derivative = (yPlus - yMinus) / (2 * delta_par)
+ #numerical_derivative = (self.gauss(x, *p) - y) / delta_par
+ p[i] = parameters_actual[i]
+ derivative = self.gauss_derivative(x, p, i)
+ diff = numerical_derivative - derivative
+ test_condition = numpy.allclose(numerical_derivative,
+ derivative, atol=5.0e-6)
+ if not test_condition:
+ msg = "Error calculating derivative of parameter %d." % i
+ msg += "\n diff min = %g diff max = %g" % (diff.min(), diff.max())
+ self.assertTrue(test_condition, msg)
+
+ def testConstrainedFit(self):
+ CFREE = 0
+ CPOSITIVE = 1
+ CQUOTED = 2
+ CFIXED = 3
+ CFACTOR = 4
+ CDELTA = 5
+ CSUM = 6
+ parameters_actual = [10.5, 2, 10000.0, 20., 150, 5000, 900., 300]
+ x = numpy.arange(10000.)
+ y = self.gauss(x, *parameters_actual)
+ parameters_estimate = [0.0, 1.0, 900.0, 25., 10, 400, 850, 200]
+ model_function = self.gauss
+ model_deriv = self.gauss_derivative
+ constraints_all_free = [[0, 0, 0]] * len(parameters_actual)
+ constraints_all_positive = [[1, 0, 0]] * len(parameters_actual)
+ constraints_delta_position = [[0, 0, 0]] * len(parameters_actual)
+ constraints_delta_position[6] = [CDELTA, 3, 880]
+ constraints_sum_position = constraints_all_positive * 1
+ constraints_sum_position[6] = [CSUM, 3, 920]
+ constraints_factor = constraints_delta_position * 1
+ constraints_factor[2] = [CFACTOR, 5, 2]
+ constraints_list = [None,
+ constraints_all_free,
+ constraints_all_positive,
+ constraints_delta_position,
+ constraints_sum_position]
+
+ # for better code coverage, the warning recommending to set full_output
+ # to True when using constraints should be shown at least once
+ full_output = True
+ for index, constraints in enumerate(constraints_list):
+ if index == 2:
+ full_output = None
+ elif index == 3:
+ full_output = 0
+ for model_deriv in [None, self.gauss_derivative]:
+ for sigma in [None, numpy.sqrt(y)]:
+ fittedpar, cov = self.instance(model_function, x, y,
+ parameters_estimate,
+ sigma=sigma,
+ constraints=constraints,
+ model_deriv=model_deriv,
+ full_output=full_output)[:2]
+ full_output = True
+
+ test_condition = numpy.allclose(parameters_actual, fittedpar)
+ if not test_condition:
+ msg = "Unsuccessfull fit\n"
+ for i in range(len(fittedpar)):
+ msg += "Expected %g obtained %g\n" % (parameters_actual[i],
+ fittedpar[i])
+ self.assertTrue(test_condition, msg)
+
+ def testUnconstrainedFitAnalyticalDerivative(self):
+ parameters_actual = [10.5, 2, 1000.0, 20., 15]
+ x = numpy.arange(10000.)
+ y = self.gauss(x, *parameters_actual)
+ sigma = numpy.sqrt(y)
+ parameters_estimate = [0.0, 1.0, 900.0, 25., 10]
+ model_function = self.gauss
+ model_deriv = self.gauss_derivative
+
+ fittedpar, cov = self.instance(model_function, x, y,
+ parameters_estimate,
+ sigma=sigma,
+ model_deriv=model_deriv)
+ test_condition = numpy.allclose(parameters_actual, fittedpar)
+ if not test_condition:
+ msg = "Unsuccessfull fit\n"
+ for i in range(len(fittedpar)):
+ msg += "Expected %g obtained %g\n" % (parameters_actual[i],
+ fittedpar[i])
+ self.assertTrue(test_condition, msg)
+
+ @testutils.validate_logging(fitlogger.name, warning=2)
+ def testBadlyShapedData(self):
+ parameters_actual = [10.5, 2, 1000.0, 20., 15]
+ x = numpy.arange(10000.).reshape(1000, 10)
+ y = self.gauss(x, *parameters_actual)
+ sigma = numpy.sqrt(y)
+ parameters_estimate = [0.0, 1.0, 900.0, 25., 10]
+ model_function = self.gauss
+
+ for check_finite in [True, False]:
+ fittedpar, cov = self.instance(model_function, x, y,
+ parameters_estimate,
+ sigma=sigma,
+ check_finite=check_finite)
+ test_condition = numpy.allclose(parameters_actual, fittedpar)
+ if not test_condition:
+ msg = "Unsuccessfull fit\n"
+ for i in range(len(fittedpar)):
+ msg += "Expected %g obtained %g\n" % (parameters_actual[i],
+ fittedpar[i])
+ self.assertTrue(test_condition, msg)
+
+ @testutils.validate_logging(fitlogger.name, warning=3)
+ def testDataWithNaN(self):
+ parameters_actual = [10.5, 2, 1000.0, 20., 15]
+ x = numpy.arange(10000.).reshape(1000, 10)
+ y = self.gauss(x, *parameters_actual)
+ sigma = numpy.sqrt(y)
+ parameters_estimate = [0.0, 1.0, 900.0, 25., 10]
+ model_function = self.gauss
+ x[500] = numpy.inf
+ # check default behavior
+ try:
+ self.instance(model_function, x, y,
+ parameters_estimate,
+ sigma=sigma)
+ except ValueError:
+ info = "%s" % sys.exc_info()[1]
+ self.assertTrue("array must not contain inf" in info)
+
+ # check requested behavior
+ try:
+ self.instance(model_function, x, y,
+ parameters_estimate,
+ sigma=sigma,
+ check_finite=True)
+ except ValueError:
+ info = "%s" % sys.exc_info()[1]
+ self.assertTrue("array must not contain inf" in info)
+
+ fittedpar, cov = self.instance(model_function, x, y,
+ parameters_estimate,
+ sigma=sigma,
+ check_finite=False)
+ test_condition = numpy.allclose(parameters_actual, fittedpar)
+ if not test_condition:
+ msg = "Unsuccessfull fit\n"
+ for i in range(len(fittedpar)):
+ msg += "Expected %g obtained %g\n" % (parameters_actual[i],
+ fittedpar[i])
+ self.assertTrue(test_condition, msg)
+
+ # testing now with ydata containing NaN
+ x = numpy.arange(10000.).reshape(1000, 10)
+ y[500] = numpy.nan
+ fittedpar, cov = self.instance(model_function, x, y,
+ parameters_estimate,
+ sigma=sigma,
+ check_finite=False)
+
+ test_condition = numpy.allclose(parameters_actual, fittedpar)
+ if not test_condition:
+ msg = "Unsuccessfull fit\n"
+ for i in range(len(fittedpar)):
+ msg += "Expected %g obtained %g\n" % (parameters_actual[i],
+ fittedpar[i])
+ self.assertTrue(test_condition, msg)
+
+ # testing now with sigma containing NaN
+ sigma[300] = numpy.nan
+ fittedpar, cov = self.instance(model_function, x, y,
+ parameters_estimate,
+ sigma=sigma,
+ check_finite=False)
+ test_condition = numpy.allclose(parameters_actual, fittedpar)
+ if not test_condition:
+ msg = "Unsuccessfull fit\n"
+ for i in range(len(fittedpar)):
+ msg += "Expected %g obtained %g\n" % (parameters_actual[i],
+ fittedpar[i])
+ self.assertTrue(test_condition, msg)
+
+ def testUncertainties(self):
+ """Test for validity of uncertainties in returned full-output
+ dictionary. This is a non-regression test for pull request #197"""
+ parameters_actual = [10.5, 2, 1000.0, 20., 15, 2001.0, 30.1, 16]
+ x = numpy.arange(10000.)
+ y = self.gauss(x, *parameters_actual)
+ parameters_estimate = [0.0, 1.0, 900.0, 25., 10., 1500., 20., 2.0]
+
+ # test that uncertainties are not 0.
+ fittedpar, cov, infodict = self.instance(self.gauss, x, y, parameters_estimate,
+ full_output=True)
+ uncertainties = infodict["uncertainties"]
+ self.assertEqual(len(uncertainties), len(parameters_actual))
+ self.assertEqual(len(uncertainties), len(fittedpar))
+ for uncertainty in uncertainties:
+ self.assertNotAlmostEqual(uncertainty, 0.)
+
+ # set constraint FIXED for half the parameters.
+ # This should cause leastsq to return 100% uncertainty.
+ parameters_estimate = [10.6, 2.1, 1000.1, 20.1, 15.1, 2001.1, 30.2, 16.1]
+ CFIXED = 3
+ CFREE = 0
+ constraints = []
+ for i in range(len(parameters_estimate)):
+ if i % 2:
+ constraints.append([CFIXED, 0, 0])
+ else:
+ constraints.append([CFREE, 0, 0])
+ fittedpar, cov, infodict = self.instance(self.gauss, x, y, parameters_estimate,
+ constraints=constraints,
+ full_output=True)
+ uncertainties = infodict["uncertainties"]
+ for i in range(len(parameters_estimate)):
+ if i % 2:
+ # test that all FIXED parameters have 100% uncertainty
+ self.assertAlmostEqual(uncertainties[i],
+ parameters_estimate[i])
diff --git a/src/silx/math/fit/test/test_fitmanager.py b/src/silx/math/fit/test/test_fitmanager.py
new file mode 100644
index 0000000..4ab56a5
--- /dev/null
+++ b/src/silx/math/fit/test/test_fitmanager.py
@@ -0,0 +1,498 @@
+# coding: utf-8
+# /*##########################################################################
+# Copyright (C) 2016-2020 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
+"""
+Tests for fitmanager module
+"""
+
+import unittest
+import numpy
+import os.path
+
+from silx.math.fit import fitmanager
+from silx.math.fit import fittheories
+from silx.math.fit import bgtheories
+from silx.math.fit.fittheory import FitTheory
+from silx.math.fit.functions import sum_gauss, sum_stepdown, sum_stepup
+
+from silx.utils.testutils import ParametricTestCase
+from silx.test.utils import temp_dir
+
+custom_function_definition = """
+import copy
+from silx.math.fit.fittheory import FitTheory
+
+CONFIG = {'d': 1.}
+
+def myfun(x, a, b, c):
+ "Model function"
+ return (a * x**2 + b * x + c) / CONFIG['d']
+
+def myesti(x, y):
+ "Initial parameters for iterative fit (a, b, c) = (1, 1, 1)"
+ return (1., 1., 1.), ((0, 0, 0), (0, 0, 0), (0, 0, 0))
+
+def myconfig(d=1., **kw):
+ "This function can modify CONFIG"
+ CONFIG["d"] = d
+ return CONFIG
+
+def myderiv(x, parameters, index):
+ "Custom derivative (does not work, causes singular matrix)"
+ pars_plus = copy.copy(parameters)
+ pars_plus[index] *= 1.0001
+
+ pars_minus = parameters
+ pars_minus[index] *= copy.copy(0.9999)
+
+ delta_fun = myfun(x, *pars_plus) - myfun(x, *pars_minus)
+ delta_par = parameters[index] * 0.0001 * 2
+
+ return delta_fun / delta_par
+
+THEORY = {
+ 'my fit theory':
+ FitTheory(function=myfun,
+ parameters=('A', 'B', 'C'),
+ estimate=myesti,
+ configure=myconfig,
+ derivative=myderiv)
+}
+
+"""
+
+old_custom_function_definition = """
+CONFIG = {'d': 1.0}
+
+def myfun(x, a, b, c):
+ "Model function"
+ return (a * x**2 + b * x + c) / CONFIG['d']
+
+def myesti(x, y, bg, xscalinq, yscaling):
+ "Initial parameters for iterative fit (a, b, c) = (1, 1, 1)"
+ return (1., 1., 1.), ((0, 0, 0), (0, 0, 0), (0, 0, 0))
+
+def myconfig(**kw):
+ "Update or complete CONFIG dictionary"
+ for key in kw:
+ CONFIG[key] = kw[key]
+ return CONFIG
+
+THEORY = ['my fit theory']
+PARAMETERS = [('A', 'B', 'C')]
+FUNCTION = [myfun]
+ESTIMATE = [myesti]
+CONFIGURE = [myconfig]
+
+"""
+
+
+def _order_of_magnitude(x):
+ return numpy.log10(x).round()
+
+
+class TestFitmanager(ParametricTestCase):
+ """
+ Unit tests of multi-peak functions.
+ """
+ def setUp(self):
+ pass
+
+ def tearDown(self):
+ pass
+
+ def testFitManager(self):
+ """Test fit manager on synthetic data using a gaussian function
+ and a linear background"""
+ # Create synthetic data with a sum of gaussian functions
+ x = numpy.arange(1000).astype(numpy.float64)
+
+ p = [1000, 100., 250,
+ 255, 650., 45,
+ 1500, 800.5, 95]
+ linear_bg = 2.65 * x + 13
+ y = linear_bg + sum_gauss(x, *p)
+
+ y_with_nans = numpy.array(y)
+ y_with_nans[::10] = numpy.nan
+
+ x_with_nans = numpy.array(x)
+ x_with_nans[5::15] = numpy.nan
+
+ tests = {
+ 'all finite': (x, y),
+ 'y with NaNs': (x, y_with_nans),
+ 'x with NaNs': (x_with_nans, y),
+ }
+
+ for name, (xdata, ydata) in tests.items():
+ with self.subTest(name=name):
+ # Fitting
+ fit = fitmanager.FitManager()
+ fit.setdata(x=xdata, y=ydata)
+ fit.loadtheories(fittheories)
+ # Use one of the default fit functions
+ fit.settheory('Gaussians')
+ fit.setbackground('Linear')
+ fit.estimate()
+ fit.runfit()
+
+ # fit.fit_results[]
+
+ # first 2 parameters are related to the linear background
+ self.assertEqual(fit.fit_results[0]["name"], "Constant")
+ self.assertAlmostEqual(fit.fit_results[0]["fitresult"], 13)
+ self.assertEqual(fit.fit_results[1]["name"], "Slope")
+ self.assertAlmostEqual(fit.fit_results[1]["fitresult"], 2.65)
+
+ for i, param in enumerate(fit.fit_results[2:]):
+ param_number = i // 3 + 1
+ if i % 3 == 0:
+ self.assertEqual(param["name"],
+ "Height%d" % param_number)
+ elif i % 3 == 1:
+ self.assertEqual(param["name"],
+ "Position%d" % param_number)
+ elif i % 3 == 2:
+ self.assertEqual(param["name"],
+ "FWHM%d" % param_number)
+
+ self.assertAlmostEqual(param["fitresult"],
+ p[i])
+ self.assertAlmostEqual(_order_of_magnitude(param["estimation"]),
+ _order_of_magnitude(p[i]))
+
+ def testLoadCustomFitFunction(self):
+ """Test FitManager using a custom fit function defined in an external
+ file and imported with FitManager.loadtheories"""
+ # Create synthetic data with a sum of gaussian functions
+ x = numpy.arange(100).astype(numpy.float64)
+
+ # a, b, c are the fit parameters
+ # d is a known scaling parameter that is set using configure()
+ a, b, c, d = 1.5, 2.5, 3.5, 4.5
+ y = (a * x**2 + b * x + c) / d
+
+ # Fitting
+ fit = fitmanager.FitManager()
+ fit.setdata(x=x, y=y)
+
+ # Create a temporary function definition file, and import it
+ with temp_dir() as tmpDir:
+ tmpfile = os.path.join(tmpDir, 'customfun.py')
+ # custom_function_definition
+ fd = open(tmpfile, "w")
+ fd.write(custom_function_definition)
+ fd.close()
+ fit.loadtheories(tmpfile)
+ tmpfile_pyc = os.path.join(tmpDir, 'customfun.pyc')
+ if os.path.exists(tmpfile_pyc):
+ os.unlink(tmpfile_pyc)
+ os.unlink(tmpfile)
+
+ fit.settheory('my fit theory')
+ # Test configure
+ fit.configure(d=4.5)
+ fit.estimate()
+ fit.runfit()
+
+ self.assertEqual(fit.fit_results[0]["name"],
+ "A1")
+ self.assertAlmostEqual(fit.fit_results[0]["fitresult"],
+ 1.5)
+ self.assertEqual(fit.fit_results[1]["name"],
+ "B1")
+ self.assertAlmostEqual(fit.fit_results[1]["fitresult"],
+ 2.5)
+ self.assertEqual(fit.fit_results[2]["name"],
+ "C1")
+ self.assertAlmostEqual(fit.fit_results[2]["fitresult"],
+ 3.5)
+
+ def testLoadOldCustomFitFunction(self):
+ """Test FitManager using a custom fit function defined in an external
+ file and imported with FitManager.loadtheories (legacy PyMca format)"""
+ # Create synthetic data with a sum of gaussian functions
+ x = numpy.arange(100).astype(numpy.float64)
+
+ # a, b, c are the fit parameters
+ # d is a known scaling parameter that is set using configure()
+ a, b, c, d = 1.5, 2.5, 3.5, 4.5
+ y = (a * x**2 + b * x + c) / d
+
+ # Fitting
+ fit = fitmanager.FitManager()
+ fit.setdata(x=x, y=y)
+
+ # Create a temporary function definition file, and import it
+ with temp_dir() as tmpDir:
+ tmpfile = os.path.join(tmpDir, 'oldcustomfun.py')
+ # custom_function_definition
+ fd = open(tmpfile, "w")
+ fd.write(old_custom_function_definition)
+ fd.close()
+ fit.loadtheories(tmpfile)
+ tmpfile_pyc = os.path.join(tmpDir, 'oldcustomfun.pyc')
+ if os.path.exists(tmpfile_pyc):
+ os.unlink(tmpfile_pyc)
+ os.unlink(tmpfile)
+
+ fit.settheory('my fit theory')
+ fit.configure(d=4.5)
+ fit.estimate()
+ fit.runfit()
+
+ self.assertEqual(fit.fit_results[0]["name"],
+ "A1")
+ self.assertAlmostEqual(fit.fit_results[0]["fitresult"],
+ 1.5)
+ self.assertEqual(fit.fit_results[1]["name"],
+ "B1")
+ self.assertAlmostEqual(fit.fit_results[1]["fitresult"],
+ 2.5)
+ self.assertEqual(fit.fit_results[2]["name"],
+ "C1")
+ self.assertAlmostEqual(fit.fit_results[2]["fitresult"],
+ 3.5)
+
+ def testAddTheory(self, estimate=True):
+ """Test FitManager using a custom fit function imported with
+ FitManager.addtheory"""
+ # Create synthetic data with a sum of gaussian functions
+ x = numpy.arange(100).astype(numpy.float64)
+
+ # a, b, c are the fit parameters
+ # d is a known scaling parameter that is set using configure()
+ a, b, c, d = -3.14, 1234.5, 10000, 4.5
+ y = (a * x**2 + b * x + c) / d
+
+ # Fitting
+ fit = fitmanager.FitManager()
+ fit.setdata(x=x, y=y)
+
+ # Define and add the fit theory
+ CONFIG = {'d': 1.}
+
+ def myfun(x_, a_, b_, c_):
+ """"Model function"""
+ return (a_ * x_**2 + b_ * x_ + c_) / CONFIG['d']
+
+ def myesti(x_, y_):
+ """"Initial parameters for iterative fit:
+ (a, b, c) = (1, 1, 1)
+ Constraints all set to 0 (FREE)"""
+ return (1., 1., 1.), ((0, 0, 0), (0, 0, 0), (0, 0, 0))
+
+ def myconfig(d_=1., **kw):
+ """This function can modify CONFIG"""
+ CONFIG["d"] = d_
+ return CONFIG
+
+ def myderiv(x_, parameters, index):
+ """Custom derivative"""
+ pars_plus = numpy.array(parameters, copy=True)
+ pars_plus[index] *= 1.001
+
+ pars_minus = numpy.array(parameters, copy=True)
+ pars_minus[index] *= 0.999
+
+ delta_fun = myfun(x_, *pars_plus) - myfun(x_, *pars_minus)
+ delta_par = parameters[index] * 0.001 * 2
+
+ return delta_fun / delta_par
+
+ fit.addtheory("polynomial",
+ FitTheory(function=myfun,
+ parameters=["A", "B", "C"],
+ estimate=myesti if estimate else None,
+ configure=myconfig,
+ derivative=myderiv))
+
+ fit.settheory('polynomial')
+ fit.configure(d_=4.5)
+ fit.estimate()
+ params1, sigmas, infodict = fit.runfit()
+
+ self.assertEqual(fit.fit_results[0]["name"],
+ "A1")
+ self.assertAlmostEqual(fit.fit_results[0]["fitresult"],
+ -3.14)
+ self.assertEqual(fit.fit_results[1]["name"],
+ "B1")
+ # params1[1] is the same as fit.fit_results[1]["fitresult"]
+ self.assertAlmostEqual(params1[1],
+ 1234.5)
+ self.assertEqual(fit.fit_results[2]["name"],
+ "C1")
+ self.assertAlmostEqual(params1[2],
+ 10000)
+
+ # change configuration scaling factor and check that the fit returns
+ # different values
+ fit.configure(d_=5.)
+ fit.estimate()
+ params2, sigmas, infodict = fit.runfit()
+ for p1, p2 in zip(params1, params2):
+ self.assertFalse(numpy.array_equal(p1, p2),
+ "Fit parameters are equal even though the " +
+ "configuration has been changed")
+
+ def testNoEstimate(self):
+ """Ensure that the in the absence of the estimation function,
+ the default estimation function :meth:`FitTheory.default_estimate`
+ is used."""
+ self.testAddTheory(estimate=False)
+
+ def testStep(self):
+ """Test fit manager on a step function with a more complex estimate
+ function than the gaussian (convolution filter)"""
+ for theory_name, theory_fun in (('Step Down', sum_stepdown),
+ ('Step Up', sum_stepup)):
+ # Create synthetic data with a sum of gaussian functions
+ x = numpy.arange(1000).astype(numpy.float64)
+
+ # ('Height', 'Position', 'FWHM')
+ p = [1000, 439, 250]
+
+ constantbg = 13
+ y = theory_fun(x, *p) + constantbg
+
+ # Fitting
+ fit = fitmanager.FitManager()
+ fit.setdata(x=x, y=y)
+ fit.loadtheories(fittheories)
+ fit.settheory(theory_name)
+ fit.setbackground('Constant')
+
+ fit.estimate()
+
+ params, sigmas, infodict = fit.runfit()
+
+ # first parameter is the constant background
+ self.assertAlmostEqual(params[0], 13, places=5)
+ for i, param in enumerate(params[1:]):
+ self.assertAlmostEqual(param, p[i], places=5)
+ self.assertAlmostEqual(_order_of_magnitude(fit.fit_results[i+1]["estimation"]),
+ _order_of_magnitude(p[i]))
+
+
+def quadratic(x, a, b, c):
+ return a * x**2 + b * x + c
+
+
+def cubic(x, a, b, c, d):
+ return a * x**3 + b * x**2 + c * x + d
+
+
+class TestPolynomials(unittest.TestCase):
+ """Test polynomial fit theories and fit background"""
+ def setUp(self):
+ self.x = numpy.arange(100).astype(numpy.float64)
+
+ def testQuadraticBg(self):
+ gaussian_params = [100, 45, 8]
+ poly_params = [0.05, -2, 3]
+ p = numpy.poly1d(poly_params)
+
+ y = p(self.x) + sum_gauss(self.x, *gaussian_params)
+
+ fm = fitmanager.FitManager(self.x, y)
+ fm.loadbgtheories(bgtheories)
+ fm.loadtheories(fittheories)
+ fm.settheory("Gaussians")
+ fm.setbackground("Degree 2 Polynomial")
+ esti_params = fm.estimate()
+ fit_params = fm.runfit()[0]
+
+ for p, pfit in zip(poly_params + gaussian_params, fit_params):
+ self.assertAlmostEqual(p,
+ pfit)
+
+ def testCubicBg(self):
+ gaussian_params = [1000, 45, 8]
+ poly_params = [0.0005, -0.05, 3, -4]
+ p = numpy.poly1d(poly_params)
+
+ y = p(self.x) + sum_gauss(self.x, *gaussian_params)
+
+ fm = fitmanager.FitManager(self.x, y)
+ fm.loadtheories(fittheories)
+ fm.settheory("Gaussians")
+ fm.setbackground("Degree 3 Polynomial")
+ esti_params = fm.estimate()
+ fit_params = fm.runfit()[0]
+
+ for p, pfit in zip(poly_params + gaussian_params, fit_params):
+ self.assertAlmostEqual(p,
+ pfit)
+
+ def testQuarticcBg(self):
+ gaussian_params = [10000, 69, 25]
+ poly_params = [5e-10, 0.0005, 0.005, 2, 4]
+ p = numpy.poly1d(poly_params)
+
+ y = p(self.x) + sum_gauss(self.x, *gaussian_params)
+
+ fm = fitmanager.FitManager(self.x, y)
+ fm.loadtheories(fittheories)
+ fm.settheory("Gaussians")
+ fm.setbackground("Degree 4 Polynomial")
+ esti_params = fm.estimate()
+ fit_params = fm.runfit()[0]
+
+ for p, pfit in zip(poly_params + gaussian_params, fit_params):
+ self.assertAlmostEqual(p,
+ pfit,
+ places=5)
+
+ def _testPoly(self, poly_params, theory, places=5):
+ p = numpy.poly1d(poly_params)
+
+ y = p(self.x)
+
+ fm = fitmanager.FitManager(self.x, y)
+ fm.loadbgtheories(bgtheories)
+ fm.loadtheories(fittheories)
+ fm.settheory(theory)
+ esti_params = fm.estimate()
+ fit_params = fm.runfit()[0]
+
+ for p, pfit in zip(poly_params, fit_params):
+ self.assertAlmostEqual(p, pfit, places=places)
+
+ def testQuadratic(self):
+ self._testPoly([0.05, -2, 3],
+ "Degree 2 Polynomial")
+
+ def testCubic(self):
+ self._testPoly([0.0005, -0.05, 3, -4],
+ "Degree 3 Polynomial")
+
+ def testQuartic(self):
+ self._testPoly([1, -2, 3, -4, -5],
+ "Degree 4 Polynomial")
+
+ def testQuintic(self):
+ self._testPoly([1, -2, 3, -4, -5, 6],
+ "Degree 5 Polynomial",
+ places=4)
diff --git a/src/silx/math/fit/test/test_functions.py b/src/silx/math/fit/test/test_functions.py
new file mode 100644
index 0000000..7e3ff63
--- /dev/null
+++ b/src/silx/math/fit/test/test_functions.py
@@ -0,0 +1,259 @@
+# coding: utf-8
+# /*##########################################################################
+# Copyright (C) 2016 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
+"""
+Tests for functions module
+"""
+
+import unittest
+import numpy
+import math
+
+from silx.math.fit import functions
+
+__authors__ = ["P. Knobel"]
+__license__ = "MIT"
+__date__ = "21/07/2016"
+
+class Test_functions(unittest.TestCase):
+ """
+ Unit tests of multi-peak functions.
+ """
+ def setUp(self):
+ self.x = numpy.arange(11)
+
+ # height, center, sigma1, sigma2
+ (h, c, s1, s2) = (7., 5., 3., 2.1)
+ self.g_params = {
+ "height": h,
+ "center": c,
+ #"sigma": s,
+ "fwhm1": 2 * math.sqrt(2 * math.log(2)) * s1,
+ "fwhm2": 2 * math.sqrt(2 * math.log(2)) * s2,
+ "area1": h * s1 * math.sqrt(2 * math.pi)
+ }
+ # result of `7 * scipy.signal.gaussian(11, 3)`
+ self.scipy_gaussian = numpy.array(
+ [1.74546546, 2.87778603, 4.24571462, 5.60516182, 6.62171628,
+ 7., 6.62171628, 5.60516182, 4.24571462, 2.87778603,
+ 1.74546546]
+ )
+
+ # result of:
+ # numpy.concatenate((7 * scipy.signal.gaussian(11, 3)[0:5],
+ # 7 * scipy.signal.gaussian(11, 2.1)[5:11]))
+ self.scipy_asym_gaussian = numpy.array(
+ [1.74546546, 2.87778603, 4.24571462, 5.60516182, 6.62171628,
+ 7., 6.24968751, 4.44773692, 2.52313452, 1.14093853, 0.41124877]
+ )
+
+ def tearDown(self):
+ pass
+
+ def testGauss(self):
+ """Compare sum_gauss with scipy.signals.gaussian"""
+ y = functions.sum_gauss(self.x,
+ self.g_params["height"],
+ self.g_params["center"],
+ self.g_params["fwhm1"])
+
+ for i in range(11):
+ self.assertAlmostEqual(y[i], self.scipy_gaussian[i])
+
+ def testAGauss(self):
+ """Compare sum_agauss with scipy.signals.gaussian"""
+ y = functions.sum_agauss(self.x,
+ self.g_params["area1"],
+ self.g_params["center"],
+ self.g_params["fwhm1"])
+ for i in range(11):
+ self.assertAlmostEqual(y[i], self.scipy_gaussian[i])
+
+ def testFastAGauss(self):
+ """Compare sum_fastagauss with scipy.signals.gaussian
+ Limit precision to 3 decimal places."""
+ y = functions.sum_fastagauss(self.x,
+ self.g_params["area1"],
+ self.g_params["center"],
+ self.g_params["fwhm1"])
+ for i in range(11):
+ self.assertAlmostEqual(y[i], self.scipy_gaussian[i], 3)
+
+
+ def testSplitGauss(self):
+ """Compare sum_splitgauss with scipy.signals.gaussian"""
+ y = functions.sum_splitgauss(self.x,
+ self.g_params["height"],
+ self.g_params["center"],
+ self.g_params["fwhm1"],
+ self.g_params["fwhm2"])
+ for i in range(11):
+ self.assertAlmostEqual(y[i], self.scipy_asym_gaussian[i])
+
+ def testErf(self):
+ """Compare erf with math.erf"""
+ # scalars
+ self.assertAlmostEqual(functions.erf(0.14), math.erf(0.14), places=5)
+ self.assertAlmostEqual(functions.erf(0), math.erf(0), places=5)
+ self.assertAlmostEqual(functions.erf(-0.74), math.erf(-0.74), places=5)
+
+ # lists
+ x = [-5, -2, -1.5, -0.6, 0, 0.1, 2, 3]
+ erfx = functions.erf(x)
+ for i in range(len(x)):
+ self.assertAlmostEqual(erfx[i],
+ math.erf(x[i]),
+ places=5)
+
+ # ndarray
+ x = numpy.array([[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]])
+ erfx = functions.erf(x)
+ for i in range(x.shape[0]):
+ for j in range(x.shape[1]):
+ self.assertAlmostEqual(erfx[i, j],
+ math.erf(x[i, j]),
+ places=5)
+
+ def testErfc(self):
+ """Compare erf with math.erf"""
+ # scalars
+ self.assertAlmostEqual(functions.erfc(0.14), math.erfc(0.14), places=5)
+ self.assertAlmostEqual(functions.erfc(0), math.erfc(0), places=5)
+ self.assertAlmostEqual(functions.erfc(-0.74), math.erfc(-0.74), places=5)
+
+ # lists
+ x = [-5, -2, -1.5, -0.6, 0, 0.1, 2, 3]
+ erfcx = functions.erfc(x)
+ for i in range(len(x)):
+ self.assertAlmostEqual(erfcx[i], math.erfc(x[i]), places=5)
+
+ # ndarray
+ x = numpy.array([[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]])
+ erfcx = functions.erfc(x)
+ for i in range(x.shape[0]):
+ for j in range(x.shape[1]):
+ self.assertAlmostEqual(erfcx[i, j], math.erfc(x[i, j]), places=5)
+
+ def testAtanStepUp(self):
+ """Compare atan_stepup with math.atan
+
+ atan_stepup(x, a, b, c) = a * (0.5 + (arctan((x - b) / c) / pi))"""
+ x0 = numpy.arange(100) / 6.33
+ y0 = functions.atan_stepup(x0, 11.1, 22.2, 3.33)
+
+ for x, y in zip(x0, y0):
+ self.assertAlmostEqual(
+ 11.1 * (0.5 + math.atan((x - 22.2) / 3.33) / math.pi),
+ y
+ )
+
+ def testStepUp(self):
+ """sanity check for step up:
+
+ - derivative must be largest around the step center
+ - max value must be close to height parameter
+
+ """
+ x0 = numpy.arange(1000)
+ center = 444
+ height = 1234
+ fwhm = 210
+ y0 = functions.sum_stepup(x0, height, center, fwhm)
+
+ self.assertLess(max(y0), height)
+ self.assertAlmostEqual(max(y0), height, places=1)
+ self.assertAlmostEqual(min(y0), 0, places=1)
+
+ deriv0 = _numerical_derivative(functions.sum_stepup, x0, [height, center, fwhm])
+
+ # Test center position within +- 1 sample of max derivative
+ index_max_deriv = numpy.argmax(deriv0)
+ self.assertLess(abs(index_max_deriv - center),
+ 1)
+
+ def testStepDown(self):
+ """sanity check for step down:
+
+ - absolute value of derivative must be largest around the step center
+ - max value must be close to height parameter
+
+ """
+ x0 = numpy.arange(1000)
+ center = 444
+ height = 1234
+ fwhm = 210
+ y0 = functions.sum_stepdown(x0, height, center, fwhm)
+
+ self.assertLess(max(y0), height)
+ self.assertAlmostEqual(max(y0), height, places=1)
+ self.assertAlmostEqual(min(y0), 0, places=1)
+
+ deriv0 = _numerical_derivative(functions.sum_stepdown, x0, [height, center, fwhm])
+
+ # Test center position within +- 1 sample of max derivative
+ index_min_deriv = numpy.argmax(-deriv0)
+ self.assertLess(abs(index_min_deriv - center),
+ 1)
+
+ def testSlit(self):
+ """sanity check for slit:
+
+ - absolute value of derivative must be largest around the step center
+ - max value must be close to height parameter
+
+ """
+ x0 = numpy.arange(1000)
+ center = 444
+ height = 1234
+ fwhm = 210
+ beamfwhm = 30
+ y0 = functions.sum_slit(x0, height, center, fwhm, beamfwhm)
+
+ self.assertAlmostEqual(max(y0), height, places=1)
+ self.assertAlmostEqual(min(y0), 0, places=1)
+
+ deriv0 = _numerical_derivative(functions.sum_slit, x0, [height, center, fwhm, beamfwhm])
+
+ # Test step up center position (center - fwhm/2) within +- 1 sample of max derivative
+ index_max_deriv = numpy.argmax(deriv0)
+ self.assertLess(abs(index_max_deriv - (center - fwhm/2)),
+ 1)
+ # Test step down center position (center + fwhm/2) within +- 1 sample of min derivative
+ index_min_deriv = numpy.argmin(deriv0)
+ self.assertLess(abs(index_min_deriv - (center + fwhm/2)),
+ 1)
+
+
+def _numerical_derivative(f, x, params=[], delta_factor=0.0001):
+ """Compute the numerical derivative of ``f`` for all values of ``x``.
+
+ :param f: function
+ :param x: Array of evenly spaced abscissa values
+ :param params: list of additional parameters
+ :return: Array of derivative values
+ """
+ deltax = (x[1] - x[0]) * delta_factor
+ y_plus = f(x + deltax, *params)
+ y_minus = f(x - deltax, *params)
+
+ return (y_plus - y_minus) / (2 * deltax)
diff --git a/src/silx/math/fit/test/test_peaks.py b/src/silx/math/fit/test/test_peaks.py
new file mode 100644
index 0000000..495c70d
--- /dev/null
+++ b/src/silx/math/fit/test/test_peaks.py
@@ -0,0 +1,132 @@
+# coding: utf-8
+# /*##########################################################################
+# Copyright (C) 2016 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
+"""
+Tests for peaks module
+"""
+
+import unittest
+import numpy
+import math
+
+from silx.math.fit import functions
+from silx.math.fit import peaks
+
+class Test_peak_search(unittest.TestCase):
+ """
+ Unit tests of peak_search on various types of multi-peak functions.
+ """
+ def setUp(self):
+ self.x = numpy.arange(5000)
+ # (height1, center1, fwhm1, ...)
+ self.h_c_fwhm = (50, 500, 100,
+ 50, 600, 80,
+ 20, 2000, 100,
+ 50, 2250, 110,
+ 40, 3000, 99,
+ 23, 4980, 80)
+ # (height1, center1, fwhm1, eta1 ...)
+ self.h_c_fwhm_eta = (50, 500, 100, 0.4,
+ 50, 600, 80, 0.5,
+ 20, 2000, 100, 0.6,
+ 50, 2250, 110, 0.7,
+ 40, 3000, 99, 0.8,
+ 23, 4980, 80, 0.3,)
+ # (height1, center1, fwhm11, fwhm21, ...)
+ self.h_c_fwhm_fwhm = (50, 500, 100, 85,
+ 50, 600, 80, 110,
+ 20, 2000, 100, 100,
+ 50, 2250, 110, 99,
+ 40, 3000, 99, 110,
+ 23, 4980, 80, 80,)
+ # (height1, center1, fwhm11, fwhm21, eta1 ...)
+ self.h_c_fwhm_fwhm_eta = (50, 500, 100, 85, 0.4,
+ 50, 600, 80, 110, 0.5,
+ 20, 2000, 100, 100, 0.6,
+ 50, 2250, 110, 99, 0.7,
+ 40, 3000, 99, 110, 0.8,
+ 23, 4980, 80, 80, 0.3,)
+ # (area1, center1, fwhm1, ...)
+ self.a_c_fwhm = (2550, 500, 100,
+ 2000, 600, 80,
+ 500, 2000, 100,
+ 4000, 2250, 110,
+ 2300, 3000, 99,
+ 3333, 4980, 80)
+ # (area1, center1, fwhm1, eta1 ...)
+ self.a_c_fwhm_eta = (500, 500, 100, 0.4,
+ 500, 600, 80, 0.5,
+ 200, 2000, 100, 0.6,
+ 500, 2250, 110, 0.7,
+ 400, 3000, 99, 0.8,
+ 230, 4980, 80, 0.3,)
+ # (area, position, fwhm, st_area_r, st_slope_r, lt_area_r, lt_slope_r, step_height_r)
+ self.hypermet_params = (1000, 500, 200, 0.2, 100, 0.3, 100, 0.05,
+ 1000, 1000, 200, 0.2, 100, 0.3, 100, 0.05,
+ 1000, 2000, 200, 0.2, 100, 0.3, 100, 0.05,
+ 1000, 2350, 200, 0.2, 100, 0.3, 100, 0.05,
+ 1000, 3000, 200, 0.2, 100, 0.3, 100, 0.05,
+ 1000, 4900, 200, 0.2, 100, 0.3, 100, 0.05,)
+
+
+ def tearDown(self):
+ pass
+
+ def get_peaks(self, function, params):
+ """
+
+ :param function: Multi-peak function
+ :param params: Parameter for this function
+ :return: list of (peak, relevance) tuples
+ """
+ y = function(self.x, *params)
+ return peaks.peak_search(y=y, fwhm=100, relevance_info=True)
+
+ def testPeakSearch_various_functions(self):
+ """Run peak search on a variety of synthetic functions, and
+ check that result falls within +-25 samples of the actual peak
+ (reasonable delta considering a fwhm of ~100 samples) and effects
+ of overlapping peaks)."""
+ f_p = ((functions.sum_gauss, self.h_c_fwhm ),
+ (functions.sum_lorentz, self.h_c_fwhm),
+ (functions.sum_pvoigt, self.h_c_fwhm_eta),
+ (functions.sum_splitgauss, self.h_c_fwhm_fwhm),
+ (functions.sum_splitlorentz, self.h_c_fwhm_fwhm),
+ (functions.sum_splitpvoigt, self.h_c_fwhm_fwhm_eta),
+ (functions.sum_agauss, self.a_c_fwhm),
+ (functions.sum_fastagauss, self.a_c_fwhm),
+ (functions.sum_alorentz, self.a_c_fwhm),
+ (functions.sum_apvoigt, self.a_c_fwhm_eta),
+ (functions.sum_ahypermet, self.hypermet_params),
+ (functions.sum_fastahypermet, self.hypermet_params),)
+
+ for function, params in f_p:
+ peaks = self.get_peaks(function, params)
+
+ self.assertEqual(len(peaks), 6,
+ "Wrong number of peaks detected")
+
+ for i in range(6):
+ theoretical_peak_index = params[i*(len(params)//6) + 1]
+ found_peak_index = peaks[i][0]
+ self.assertLess(abs(found_peak_index - theoretical_peak_index), 25)
diff --git a/src/silx/math/histogram.py b/src/silx/math/histogram.py
new file mode 100644
index 0000000..af9ee68
--- /dev/null
+++ b/src/silx/math/histogram.py
@@ -0,0 +1,593 @@
+# coding: utf-8
+# /*##########################################################################
+# Copyright (C) 2016 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
+
+
+"""
+This module provides a function and a class to compute multidimensional
+histograms.
+
+
+Classes
+=======
+
+- :class:`Histogramnd` : multi dimensional histogram.
+- :class:`HistogramndLut` : optimized to compute several histograms from data sharing the same coordinates.
+
+Examples
+========
+
+Single histogram
+----------------
+
+Given some 3D data:
+
+>>> import numpy as np
+>>> shape = (10**7, 3)
+>>> sample = np.random.random(shape) * 500
+>>> weights = np.random.random((shape[0],))
+
+Computing the histogram with Histogramnd :
+
+>>> from silx.math import Histogramnd
+>>> n_bins = 35
+>>> ranges = [[40., 150.], [-130., 250.], [0., 505]]
+>>> histo, w_histo, edges = Histogramnd(sample, n_bins=n_bins, histo_range=ranges, weights=weights)
+
+Histogramnd can accumulate sets of data that don't have the same
+coordinates :
+
+>>> from silx.math import Histogramnd
+>>> histo_obj = Histogramnd(sample, n_bins=n_bins, histo_range=ranges, weights=weights)
+>>> sample_2 = np.random.random(shape) * 200
+>>> weights_2 = np.random.random((shape[0],))
+>>> histo_obj.accumulate(sample_2, weights=weights_2)
+
+And then access the results:
+
+>>> histo = histo_obj.histo
+>>> weighted_histo = histo_obj.weighted_histo
+
+or even:
+
+>>> histo, w_histo, edges = histo_obj
+
+Accumulating histograms (LUT)
+-----------------------------
+In some situations we need to compute the weighted histogram of several
+sets of data (weights) that have the same coordinates (sample).
+
+Again, some data (2 sets of weights) :
+
+>>> import numpy as np
+>>> shape = (10**7, 3)
+>>> sample = np.random.random(shape) * 500
+>>> weights_1 = np.random.random((shape[0],))
+>>> weights_2 = np.random.random((shape[0],))
+
+And getting the result with HistogramLut :
+
+>>> from silx.math import HistogramndLut
+
+>>> n_bins = 35
+>>> ranges = [[40., 150.], [-130., 250.], [0., 505]]
+
+>>> histo_lut = HistogramndLut(sample, ranges, n_bins)
+
+First call, with weight_1 :
+
+>>> histo_lut.accumulate(weights_1)
+
+Second call, with weight_2 :
+
+>>> histo_lut.accumulate(weights_2)
+
+Retrieving the results (this is a copy of what's actually stored in
+this instance) :
+
+>>> histo = histo_lut.histo
+>>> w_histo = histo_lut.weighted_histo
+
+Note that the following code gives the same result, but the
+HistogramndLut instance does not store the accumulated weighted histogram.
+
+First call with weights_1
+
+>>> histo, w_histo = histo_lut.apply_lut(weights_1)
+
+Second call with weights_2
+
+>>> histo, w_histo = histo_lut.apply_lut(weights_2, histo=histo, weighted_histo=w_histo)
+
+Bin edges
+---------
+When computing an histogram the caller is asked to provide the histogram
+range along each coordinates (parameter *histo_range*). This parameter must
+be given a [N, 2] array where N is the number of dimensions of the histogram.
+
+In other words, the caller must provide, for each dimension,
+the left edge of the first (*leftmost*) bin, and the right edge of the
+last (*rightmost*) bin.
+
+E.g. : for a 1D sample, for a histo_range equal to [0, 10] and n_bins=4, the
+bins ranges will be :
+
+* [0, 2.5[, [2.5, 5[, [5, 7.5[, [7.5, 10 **[** if last_bin_closed = **False**
+* [0, 2.5[, [2.5, 5[, [5, 7.5[, [7.5, 10 **]** if last_bin_closed = **True**
+
+....
+"""
+
+__authors__ = ["D. Naudet"]
+__license__ = "MIT"
+__date__ = "02/10/2017"
+
+import numpy as np
+from .chistogramnd import chistogramnd as _chistogramnd # noqa
+from .chistogramnd_lut import histogramnd_get_lut as _histo_get_lut
+from .chistogramnd_lut import histogramnd_from_lut as _histo_from_lut
+
+
+class Histogramnd(object):
+ """
+ Computes the multidimensional histogram of some data.
+ """
+
+ def __init__(self,
+ sample,
+ histo_range,
+ n_bins,
+ weights=None,
+ weight_min=None,
+ weight_max=None,
+ last_bin_closed=False,
+ wh_dtype=None):
+ """
+ :param sample:
+ The data to be histogrammed.
+ Its shape must be either
+ (N,) if it contains one dimensional coordinates,
+ or an (N,D) array where the rows are the
+ coordinates of points in a D dimensional space.
+ The following dtypes are supported : :class:`numpy.float64`,
+ :class:`numpy.float32`, :class:`numpy.int32`.
+
+ .. warning:: if sample is not a C_CONTIGUOUS ndarray (e.g : a non
+ contiguous slice) then histogramnd will have to do make an internal
+ copy.
+ :type sample: :class:`numpy.array`
+
+ :param histo_range:
+ A (N, 2) array containing the histogram range along each dimension,
+ where N is the sample's number of dimensions.
+ :type histo_range: array_like
+
+ :param n_bins:
+ The number of bins :
+ * a scalar (same number of bins for all dimensions)
+ * a D elements array (number of bins for each dimensions)
+ :type n_bins: scalar or array_like
+
+ :param weights:
+ A N elements numpy array of values associated with
+ each sample.
+ The values of the *weighted_histo* array
+ returned by the function are equal to the sum of
+ the weights associated with the samples falling
+ into each bin.
+ The following dtypes are supported : :class:`numpy.float64`,
+ :class:`numpy.float32`, :class:`numpy.int32`.
+
+ .. note:: If None, the weighted histogram returned will be None.
+ :type weights: *optional*, :class:`numpy.array`
+
+ :param weight_min:
+ Use this parameter to filter out all samples whose
+ weights are lower than this value.
+
+ .. note:: This value will be cast to the same type
+ as *weights*.
+ :type weight_min: *optional*, scalar
+
+ :param weight_max:
+ Use this parameter to filter out all samples whose
+ weights are higher than this value.
+
+ .. note:: This value will be cast to the same type
+ as *weights*.
+
+ :type weight_max: *optional*, scalar
+
+ :param last_bin_closed:
+ By default the last bin is half
+ open (i.e.: [x,y) ; x included, y
+ excluded), like all the other bins.
+ Set this parameter to true if you want
+ the LAST bin to be closed.
+ :type last_bin_closed: *optional*, :class:`python.boolean`
+
+ :param wh_dtype: type of the weighted histogram array.
+ If not provided, the weighted histogram array will contain values
+ of type numpy.double. Allowed values are : `numpy.double` and
+ `numpy.float32`
+ :type wh_dtype: *optional*, numpy data type
+ """
+
+ self.__histo_range = histo_range
+ self.__n_bins = n_bins
+ self.__last_bin_closed = last_bin_closed
+ self.__wh_dtype = wh_dtype
+
+ if sample is None:
+ self.__data = [None, None, None]
+ else:
+ self.__data = _chistogramnd(sample,
+ self.__histo_range,
+ self.__n_bins,
+ weights=weights,
+ weight_min=weight_min,
+ weight_max=weight_max,
+ last_bin_closed=self.__last_bin_closed,
+ wh_dtype=self.__wh_dtype)
+
+ def __getitem__(self, key):
+ """
+ If necessary, results can be unpacked from an instance of Histogramnd :
+ *histogram*, *weighted histogram*, *bins edge*.
+
+ Example :
+
+ .. code-block:: python
+
+ histo, w_histo, edges = Histogramnd(sample, histo_range, n_bins, weights)
+
+ """
+ return self.__data[key]
+
+ def accumulate(self,
+ sample,
+ weights=None,
+ weight_min=None,
+ weight_max=None):
+ """
+ Computes the multidimensional histogram of some data and accumulates it
+ into the histogram held by this instance of Histogramnd.
+
+ :param sample:
+ The data to be histogrammed.
+ Its shape must be either
+ (N,) if it contains one dimensional coordinates,
+ or an (N,D) array where the rows are the
+ coordinates of points in a D dimensional space.
+ The following dtypes are supported : :class:`numpy.float64`,
+ :class:`numpy.float32`, :class:`numpy.int32`.
+
+ .. warning:: if sample is not a C_CONTIGUOUS ndarray (e.g : a non
+ contiguous slice) then histogramnd will have to do make an internal
+ copy.
+ :type sample: :class:`numpy.array`
+
+ :param weights:
+ A N elements numpy array of values associated with
+ each sample.
+ The values of the *weighted_histo* array
+ returned by the function are equal to the sum of
+ the weights associated with the samples falling
+ into each bin.
+ The following dtypes are supported : :class:`numpy.float64`,
+ :class:`numpy.float32`, :class:`numpy.int32`.
+
+ .. note:: If None, the weighted histogram returned will be None.
+ :type weights: *optional*, :class:`numpy.array`
+
+ :param weight_min:
+ Use this parameter to filter out all samples whose
+ weights are lower than this value.
+
+ .. note:: This value will be cast to the same type
+ as *weights*.
+ :type weight_min: *optional*, scalar
+
+ :param weight_max:
+ Use this parameter to filter out all samples whose
+ weights are higher than this value.
+
+ .. note:: This value will be cast to the same type
+ as *weights*.
+ :type weight_max: *optional*, scalar
+ """
+ result = _chistogramnd(sample,
+ self.__histo_range,
+ self.__n_bins,
+ weights=weights,
+ weight_min=weight_min,
+ weight_max=weight_max,
+ last_bin_closed=self.__last_bin_closed,
+ histo=self.__data[0],
+ weighted_histo=self.__data[1],
+ wh_dtype=self.__wh_dtype)
+ if self.__data[0] is None:
+ self.__data = result
+ elif self.__data[1] is None and result[1] is not None:
+ self.__data = result
+
+ histo = property(lambda self: self[0])
+ """ Histogram array, or None if this instance was initialized without
+ <sample> and accumulate has not been called yet.
+
+ .. note:: this is a **reference** to the array store in this
+ Histogramnd instance, use with caution.
+ """
+ weighted_histo = property(lambda self: self[1])
+ """ Weighted Histogram, or None if this instance was initialized without
+ <sample>, or no weights have been passed to __init__ nor accumulate.
+
+ .. note:: this is a **reference** to the array store in this
+ Histogramnd instance, use with caution.
+ """
+ edges = property(lambda self: self[2])
+ """ Bins edges, or None if this instance was initialized without
+ <sample> and accumulate has not been called yet.
+ """
+
+
+class HistogramndLut(object):
+ """
+ The HistogramndLut class allows you to bin data onto a regular grid.
+ The use of HistogramndLut is interesting when several sets of data that
+ share the same coordinates (*sample*) have to be mapped onto the same grid.
+ """
+
+ def __init__(self,
+ sample,
+ histo_range,
+ n_bins,
+ last_bin_closed=False,
+ dtype=None):
+ """
+ :param sample:
+ The coordinates of the data to be histogrammed.
+ Its shape must be either (N,) if it contains one dimensional
+ coordinates, or an (N, D) array where the rows are the
+ coordinates of points in a D dimensional space.
+ The following dtypes are supported : :class:`numpy.float64`,
+ :class:`numpy.float32`, :class:`numpy.int32`.
+ :type sample: :class:`numpy.array`
+
+ :param histo_range:
+ A (N, 2) array containing the histogram range along each dimension,
+ where N is the sample's number of dimensions.
+ :type histo_range: array_like
+
+ :param n_bins:
+ The number of bins :
+ * a scalar (same number of bins for all dimensions)
+ * a D elements array (number of bins for each dimensions)
+ :type n_bins: scalar or array_like
+
+ :param dtype: data type of the weighted histogram. If None, the data type
+ will be the same as the first weights array provided (on first call of
+ the instance).
+ :type dtype: `numpy.dtype`
+
+ :param last_bin_closed:
+ By default the last bin is half
+ open (i.e.: [x,y) ; x included, y
+ excluded), like all the other bins.
+ Set this parameter to true if you want
+ the LAST bin to be closed.
+ :type last_bin_closed: *optional*, :class:`python.boolean`
+ """
+ lut, histo, edges = _histo_get_lut(sample,
+ histo_range,
+ n_bins,
+ last_bin_closed=last_bin_closed)
+
+ self.__n_bins = np.array(histo.shape)
+ self.__histo_range = histo_range
+ self.__lut = lut
+ self.__histo = None
+ self.__weighted_histo = None
+ self.__edges = edges
+ self.__dtype = dtype
+ self.__shape = histo.shape
+ self.__last_bin_closed = last_bin_closed
+ self.clear()
+
+ def clear(self):
+ """
+ Resets the instance (zeroes the histograms).
+ """
+ self.__weighted_histo = None
+ self.__histo = None
+
+ @property
+ def lut(self):
+ """
+ Copy of the Lut
+ """
+ return self.__lut.copy()
+
+ def histo(self, copy=True):
+ """
+ Histogram (a copy of it), or None if `~accumulate` has not been called yet
+ (or clear was just called).
+ If *copy* is set to False then the actual reference to the array is
+ returned *(use with caution)*.
+ """
+ if copy and self.__histo is not None:
+ return self.__histo.copy()
+ return self.__histo
+
+ def weighted_histo(self, copy=True):
+ """
+ Weighted histogram (a copy of it), or None if `~accumulate` has not been called yet
+ (or clear was just called). If *copy* is set to False then the actual
+ reference to the array is returned *(use with caution)*.
+ """
+ if copy and self.__weighted_histo is not None:
+ return self.__weighted_histo.copy()
+ return self.__weighted_histo
+
+ @property
+ def histo_range(self):
+ """
+ Bins ranges.
+ """
+ return self.__histo_range.copy()
+
+ @property
+ def n_bins(self):
+ """
+ Number of bins in each direction.
+ """
+ return self.__n_bins.copy()
+
+ @property
+ def bins_edges(self):
+ """
+ Bins edges of the histograms, one array for each dimensions.
+ """
+ return tuple([edges[:] for edges in self.__edges])
+
+ @property
+ def last_bin_closed(self):
+ """
+ Returns True if the rightmost bin in each dimension is close (i.e :
+ values equal to the rightmost bin edge is included in the bin).
+ """
+ return self.__last_bin_closed
+
+ def accumulate(self,
+ weights,
+ weight_min=None,
+ weight_max=None):
+ """
+ Computes the multidimensional histogram of some data and adds it to
+ the current histogram stored by this instance. The results can be
+ retrieved with the :attr:`~.histo` and :attr:`~.weighted_histo`
+ properties.
+
+ :param weights:
+ A numpy array of values associated with each sample. The number of
+ elements in the array must be the same as the number of samples
+ provided at instantiation time.
+ :type histo_range: array_like
+
+ :param weight_min:
+ Use this parameter to filter out all samples whose
+ weights are lower than this value.
+
+ .. note:: This value will be cast to the same type
+ as *weights*.
+ :type weight_min: *optional*, scalar
+
+ :param weight_max:
+ Use this parameter to filter out all samples whose
+ weights are higher than this value.
+
+ .. note:: This value will be cast to the same type
+ as *weights*.
+
+ :type weight_max: *optional*, scalar
+ """
+ if self.__dtype is None:
+ self.__dtype = weights.dtype
+
+ histo, w_histo = _histo_from_lut(weights,
+ self.__lut,
+ histo=self.__histo,
+ weighted_histo=self.__weighted_histo,
+ shape=self.__shape,
+ dtype=self.__dtype,
+ weight_min=weight_min,
+ weight_max=weight_max)
+
+ if self.__histo is None:
+ self.__histo = histo
+
+ if self.__weighted_histo is None:
+ self.__weighted_histo = w_histo
+
+ def apply_lut(self,
+ weights,
+ histo=None,
+ weighted_histo=None,
+ weight_min=None,
+ weight_max=None):
+ """
+ Computes the multidimensional histogram of some data and returns the
+ result (it is NOT added to the current histogram stored by this
+ instance).
+
+ :param weights:
+ A numpy array of values associated with each sample. The number of
+ elements in the array must be the same as the number of samples
+ provided at instantiation time.
+ :type histo_range: array_like
+
+ :param histo:
+ Use this parameter if you want to pass your
+ own histogram array instead of the one
+ created by this function. New values
+ will be added to this array. The returned array
+ will then be this one.
+ :type histo: *optional*, :class:`numpy.array`
+
+ :param weighted_histo:
+ Use this parameter if you want to pass your
+ own weighted histogram array instead of
+ the created by this function. New
+ values will be added to this array. The returned array
+ will then be this one (same reference).
+ :type weighted_histo: *optional*, :class:`numpy.array`
+
+ :param weight_min:
+ Use this parameter to filter out all samples whose
+ weights are lower than this value.
+
+ .. note:: This value will be cast to the same type
+ as *weights*.
+ :type weight_min: *optional*, scalar
+
+ :param weight_max:
+ Use this parameter to filter out all samples whose
+ weights are higher than this value.
+
+ .. note:: This value will be cast to the same type
+ as *weights*.
+ :type weight_max: *optional*, scalar
+ """
+ histo, w_histo = _histo_from_lut(weights,
+ self.__lut,
+ histo=histo,
+ weighted_histo=weighted_histo,
+ shape=self.__shape,
+ dtype=self.__dtype,
+ weight_min=weight_min,
+ weight_max=weight_max)
+ self.__dtype = w_histo.dtype
+ return histo, w_histo
+
+if __name__ == '__main__':
+ pass
diff --git a/src/silx/math/histogramnd/include/histogramnd_c.h b/src/silx/math/histogramnd/include/histogramnd_c.h
new file mode 100644
index 0000000..abe464f
--- /dev/null
+++ b/src/silx/math/histogramnd/include/histogramnd_c.h
@@ -0,0 +1,313 @@
+/*##########################################################################
+# Copyright (C) 2016 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
+
+#ifndef HISTOGRAMND_C_H
+#define HISTOGRAMND_C_H
+
+/* checking for MSVC version because VS 2008 doesnt fully support C99
+ so inttypes.h and stdint.h are not provided with the compiler. */
+#if defined(_MSC_VER) && _MSC_VER < 1600
+ #include "msvc/stdint.h"
+#else
+ #include <inttypes.h>
+#endif
+
+#include "templates.h"
+
+/** Allowed flag values for the i_opt_flags arguments.
+ */
+typedef enum {
+ HISTO_NONE = 0, /**< No options. */
+ HISTO_WEIGHT_MIN = 1, /**< Filter weights with i_weight_min. */
+ HISTO_WEIGHT_MAX = 1<<1, /**< Filter weights with i_weight_max. */
+ HISTO_LAST_BIN_CLOSED = 1<<2 /**< Last bin is closed. */
+} histo_opt_type;
+
+/** Return codees for the histogramnd function.
+ */
+typedef enum {
+ HISTO_OK = 0, /**< No error. */
+ HISTO_ERR_ALLOC /**< Failed to allocate memory. */
+} histo_rc_t;
+
+/*=====================
+ * double sample, double cumul
+ * ====================
+*/
+
+int histogramnd_double_double_double(double *i_sample,
+ double *i_weigths,
+ int i_n_dim,
+ int i_n_elem,
+ double *i_bin_ranges,
+ int *i_n_bin,
+ uint32_t *o_histo,
+ double *o_cumul,
+ double *o_bin_edges,
+ int i_opt_flags,
+ double i_weight_min,
+ double i_weight_max);
+
+int histogramnd_double_float_double(double *i_sample,
+ float *i_weigths,
+ int i_n_dim,
+ int i_n_elem,
+ double *i_bin_ranges,
+ int *i_n_bin,
+ uint32_t *o_histo,
+ double *o_cumul,
+ double *o_bin_edges,
+ int i_opt_flags,
+ float i_weight_min,
+ float i_weight_max);
+
+int histogramnd_double_int32_t_double(double *i_sample,
+ int32_t *i_weigths,
+ int i_n_dim,
+ int i_n_elem,
+ double *i_bin_ranges,
+ int *i_n_bin,
+ uint32_t *o_histo,
+ double *o_cumul,
+ double *o_bin_edges,
+ int i_opt_flags,
+ int32_t i_weight_min,
+ int32_t i_weight_max);
+
+/*=====================
+ * float sample, double cumul
+ * ====================
+*/
+int histogramnd_float_double_double(float *i_sample,
+ double *i_weigths,
+ int i_n_dim,
+ int i_n_elem,
+ double *i_bin_ranges,
+ int *i_n_bin,
+ uint32_t *o_histo,
+ double *o_cumul,
+ double *o_bin_edges,
+ int i_opt_flags,
+ double i_weight_min,
+ double i_weight_max);
+
+int histogramnd_float_float_double(float *i_sample,
+ float *i_weigths,
+ int i_n_dim,
+ int i_n_elem,
+ double *i_bin_ranges,
+ int *i_n_bin,
+ uint32_t *o_histo,
+ double *o_cumul,
+ double *o_bin_edges,
+ int i_opt_flags,
+ float i_weight_min,
+ float i_weight_max);
+
+int histogramnd_float_int32_t_double(float *i_sample,
+ int32_t *i_weigths,
+ int i_n_dim,
+ int i_n_elem,
+ double *i_bin_ranges,
+ int *i_n_bin,
+ uint32_t *o_histo,
+ double *o_cumul,
+ double *o_bin_edges,
+ int i_opt_flags,
+ int32_t i_weight_min,
+ int32_t i_weight_max);
+
+/*=====================
+ * int32_t sample, double cumul
+ * ====================
+*/
+int histogramnd_int32_t_double_double(int32_t *i_sample,
+ double *i_weigths,
+ int i_n_dim,
+ int i_n_elem,
+ double *i_bin_ranges,
+ int *i_n_bin,
+ uint32_t *o_histo,
+ double *o_cumul,
+ double *o_bin_edges,
+ int i_opt_flags,
+ double i_weight_min,
+ double i_weight_max);
+
+int histogramnd_int32_t_float_double(int32_t *i_sample,
+ float *i_weigths,
+ int i_n_dim,
+ int i_n_elem,
+ double *i_bin_ranges,
+ int *i_n_bin,
+ uint32_t *o_histo,
+ double *o_cumul,
+ double *o_bin_edges,
+ int i_opt_flags,
+ float i_weight_min,
+ float i_weight_max);
+
+int histogramnd_int32_t_int32_t_double(int32_t *i_sample,
+ int32_t *i_weigths,
+ int i_n_dim,
+ int i_n_elem,
+ double *i_bin_ranges,
+ int *i_n_bin,
+ uint32_t *o_histo,
+ double *o_cumul,
+ double *o_bin_edges,
+ int i_opt_flags,
+ int32_t i_weight_min,
+ int32_t i_weight_max);
+
+/*=====================
+ * double sample, float cumul
+ * ====================
+*/
+
+int histogramnd_double_double_float(double *i_sample,
+ double *i_weigths,
+ int i_n_dim,
+ int i_n_elem,
+ double *i_bin_ranges,
+ int *i_n_bin,
+ uint32_t *o_histo,
+ float *o_cumul,
+ double *o_bin_edges,
+ int i_opt_flags,
+ double i_weight_min,
+ double i_weight_max);
+
+int histogramnd_double_float_float(double *i_sample,
+ float *i_weigths,
+ int i_n_dim,
+ int i_n_elem,
+ double *i_bin_ranges,
+ int *i_n_bin,
+ uint32_t *o_histo,
+ float *o_cumul,
+ double *o_bin_edges,
+ int i_opt_flags,
+ float i_weight_min,
+ float i_weight_max);
+
+int histogramnd_double_int32_t_float(double *i_sample,
+ int32_t *i_weigths,
+ int i_n_dim,
+ int i_n_elem,
+ double *i_bin_ranges,
+ int *i_n_bin,
+ uint32_t *o_histo,
+ float *o_cumul,
+ double *o_bin_edges,
+ int i_opt_flags,
+ int32_t i_weight_min,
+ int32_t i_weight_max);
+
+/*=====================
+ * float sample, float cumul
+ * ====================
+*/
+int histogramnd_float_double_float(float *i_sample,
+ double *i_weigths,
+ int i_n_dim,
+ int i_n_elem,
+ double *i_bin_ranges,
+ int *i_n_bin,
+ uint32_t *o_histo,
+ float *o_cumul,
+ double *o_bin_edges,
+ int i_opt_flags,
+ double i_weight_min,
+ double i_weight_max);
+
+int histogramnd_float_float_float(float *i_sample,
+ float *i_weigths,
+ int i_n_dim,
+ int i_n_elem,
+ double *i_bin_ranges,
+ int *i_n_bin,
+ uint32_t *o_histo,
+ float *o_cumul,
+ double *o_bin_edges,
+ int i_opt_flags,
+ float i_weight_min,
+ float i_weight_max);
+
+int histogramnd_float_int32_t_float(float *i_sample,
+ int32_t *i_weigths,
+ int i_n_dim,
+ int i_n_elem,
+ double *i_bin_ranges,
+ int *i_n_bin,
+ uint32_t *o_histo,
+ float *o_cumul,
+ double *o_bin_edges,
+ int i_opt_flags,
+ int32_t i_weight_min,
+ int32_t i_weight_max);
+
+/*=====================
+ * int32_t sample, double cumul
+ * ====================
+*/
+int histogramnd_int32_t_double_float(int32_t *i_sample,
+ double *i_weigths,
+ int i_n_dim,
+ int i_n_elem,
+ double *i_bin_ranges,
+ int *i_n_bin,
+ uint32_t *o_histo,
+ float *o_cumul,
+ double *o_bin_edges,
+ int i_opt_flags,
+ double i_weight_min,
+ double i_weight_max);
+
+int histogramnd_int32_t_float_float(int32_t *i_sample,
+ float *i_weigths,
+ int i_n_dim,
+ int i_n_elem,
+ double *i_bin_ranges,
+ int *i_n_bin,
+ uint32_t *o_histo,
+ float *o_cumul,
+ double *o_bin_edges,
+ int i_opt_flags,
+ float i_weight_min,
+ float i_weight_max);
+
+int histogramnd_int32_t_int32_t_float(int32_t *i_sample,
+ int32_t *i_weigths,
+ int i_n_dim,
+ int i_n_elem,
+ double *i_bin_ranges,
+ int *i_n_bin,
+ uint32_t *o_histo,
+ float *o_cumul,
+ double *o_bin_edges,
+ int i_opt_flags,
+ int32_t i_weight_min,
+ int32_t i_weight_max);
+
+#endif /* #define HISTOGRAMND_C_H */
diff --git a/src/silx/math/histogramnd/include/msvc/stdint.h b/src/silx/math/histogramnd/include/msvc/stdint.h
new file mode 100644
index 0000000..e236bb0
--- /dev/null
+++ b/src/silx/math/histogramnd/include/msvc/stdint.h
@@ -0,0 +1,247 @@
+// ISO C9x compliant stdint.h for Microsoft Visual Studio
+// Based on ISO/IEC 9899:TC2 Committee draft (May 6, 2005) WG14/N1124
+//
+// Copyright (c) 2006-2008 Alexander Chemeris
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice,
+// this list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright
+// notice, this list of conditions and the following disclaimer in the
+// documentation and/or other materials provided with the distribution.
+//
+// 3. The name of the author may be used to endorse or promote products
+// derived from this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+//
+///////////////////////////////////////////////////////////////////////////////
+
+#ifndef _MSC_VER // [
+#error "Use this header only with Microsoft Visual C++ compilers!"
+#endif // _MSC_VER ]
+
+#ifndef _MSC_STDINT_H_ // [
+#define _MSC_STDINT_H_
+
+#if _MSC_VER > 1000
+#pragma once
+#endif
+
+#include <limits.h>
+
+// For Visual Studio 6 in C++ mode and for many Visual Studio versions when
+// compiling for ARM we should wrap <wchar.h> include with 'extern "C++" {}'
+// or compiler give many errors like this:
+// error C2733: second C linkage of overloaded function 'wmemchr' not allowed
+#ifdef __cplusplus
+extern "C" {
+#endif
+# include <wchar.h>
+#ifdef __cplusplus
+}
+#endif
+
+// Define _W64 macros to mark types changing their size, like intptr_t.
+#ifndef _W64
+# if !defined(__midl) && (defined(_X86_) || defined(_M_IX86)) && _MSC_VER >= 1300
+# define _W64 __w64
+# else
+# define _W64
+# endif
+#endif
+
+
+// 7.18.1 Integer types
+
+// 7.18.1.1 Exact-width integer types
+
+// Visual Studio 6 and Embedded Visual C++ 4 doesn't
+// realize that, e.g. char has the same size as __int8
+// so we give up on __intX for them.
+#if (_MSC_VER < 1300)
+ typedef char int8_t;
+ typedef short int16_t;
+ typedef int int32_t;
+ typedef unsigned char uint8_t;
+ typedef unsigned short uint16_t;
+ typedef unsigned int uint32_t;
+#else
+ typedef __int8 int8_t;
+ typedef __int16 int16_t;
+ typedef __int32 int32_t;
+ typedef unsigned __int8 uint8_t;
+ typedef unsigned __int16 uint16_t;
+ typedef unsigned __int32 uint32_t;
+#endif
+typedef __int64 int64_t;
+typedef unsigned __int64 uint64_t;
+
+
+// 7.18.1.2 Minimum-width integer types
+typedef int8_t int_least8_t;
+typedef int16_t int_least16_t;
+typedef int32_t int_least32_t;
+typedef int64_t int_least64_t;
+typedef uint8_t uint_least8_t;
+typedef uint16_t uint_least16_t;
+typedef uint32_t uint_least32_t;
+typedef uint64_t uint_least64_t;
+
+// 7.18.1.3 Fastest minimum-width integer types
+typedef int8_t int_fast8_t;
+typedef int16_t int_fast16_t;
+typedef int32_t int_fast32_t;
+typedef int64_t int_fast64_t;
+typedef uint8_t uint_fast8_t;
+typedef uint16_t uint_fast16_t;
+typedef uint32_t uint_fast32_t;
+typedef uint64_t uint_fast64_t;
+
+// 7.18.1.4 Integer types capable of holding object pointers
+#ifdef _WIN64 // [
+ typedef __int64 intptr_t;
+ typedef unsigned __int64 uintptr_t;
+#else // _WIN64 ][
+ typedef _W64 int intptr_t;
+ typedef _W64 unsigned int uintptr_t;
+#endif // _WIN64 ]
+
+// 7.18.1.5 Greatest-width integer types
+typedef int64_t intmax_t;
+typedef uint64_t uintmax_t;
+
+
+// 7.18.2 Limits of specified-width integer types
+
+#if !defined(__cplusplus) || defined(__STDC_LIMIT_MACROS) // [ See footnote 220 at page 257 and footnote 221 at page 259
+
+// 7.18.2.1 Limits of exact-width integer types
+#define INT8_MIN ((int8_t)_I8_MIN)
+#define INT8_MAX _I8_MAX
+#define INT16_MIN ((int16_t)_I16_MIN)
+#define INT16_MAX _I16_MAX
+#define INT32_MIN ((int32_t)_I32_MIN)
+#define INT32_MAX _I32_MAX
+#define INT64_MIN ((int64_t)_I64_MIN)
+#define INT64_MAX _I64_MAX
+#define UINT8_MAX _UI8_MAX
+#define UINT16_MAX _UI16_MAX
+#define UINT32_MAX _UI32_MAX
+#define UINT64_MAX _UI64_MAX
+
+// 7.18.2.2 Limits of minimum-width integer types
+#define INT_LEAST8_MIN INT8_MIN
+#define INT_LEAST8_MAX INT8_MAX
+#define INT_LEAST16_MIN INT16_MIN
+#define INT_LEAST16_MAX INT16_MAX
+#define INT_LEAST32_MIN INT32_MIN
+#define INT_LEAST32_MAX INT32_MAX
+#define INT_LEAST64_MIN INT64_MIN
+#define INT_LEAST64_MAX INT64_MAX
+#define UINT_LEAST8_MAX UINT8_MAX
+#define UINT_LEAST16_MAX UINT16_MAX
+#define UINT_LEAST32_MAX UINT32_MAX
+#define UINT_LEAST64_MAX UINT64_MAX
+
+// 7.18.2.3 Limits of fastest minimum-width integer types
+#define INT_FAST8_MIN INT8_MIN
+#define INT_FAST8_MAX INT8_MAX
+#define INT_FAST16_MIN INT16_MIN
+#define INT_FAST16_MAX INT16_MAX
+#define INT_FAST32_MIN INT32_MIN
+#define INT_FAST32_MAX INT32_MAX
+#define INT_FAST64_MIN INT64_MIN
+#define INT_FAST64_MAX INT64_MAX
+#define UINT_FAST8_MAX UINT8_MAX
+#define UINT_FAST16_MAX UINT16_MAX
+#define UINT_FAST32_MAX UINT32_MAX
+#define UINT_FAST64_MAX UINT64_MAX
+
+// 7.18.2.4 Limits of integer types capable of holding object pointers
+#ifdef _WIN64 // [
+# define INTPTR_MIN INT64_MIN
+# define INTPTR_MAX INT64_MAX
+# define UINTPTR_MAX UINT64_MAX
+#else // _WIN64 ][
+# define INTPTR_MIN INT32_MIN
+# define INTPTR_MAX INT32_MAX
+# define UINTPTR_MAX UINT32_MAX
+#endif // _WIN64 ]
+
+// 7.18.2.5 Limits of greatest-width integer types
+#define INTMAX_MIN INT64_MIN
+#define INTMAX_MAX INT64_MAX
+#define UINTMAX_MAX UINT64_MAX
+
+// 7.18.3 Limits of other integer types
+
+#ifdef _WIN64 // [
+# define PTRDIFF_MIN _I64_MIN
+# define PTRDIFF_MAX _I64_MAX
+#else // _WIN64 ][
+# define PTRDIFF_MIN _I32_MIN
+# define PTRDIFF_MAX _I32_MAX
+#endif // _WIN64 ]
+
+#define SIG_ATOMIC_MIN INT_MIN
+#define SIG_ATOMIC_MAX INT_MAX
+
+#ifndef SIZE_MAX // [
+# ifdef _WIN64 // [
+# define SIZE_MAX _UI64_MAX
+# else // _WIN64 ][
+# define SIZE_MAX _UI32_MAX
+# endif // _WIN64 ]
+#endif // SIZE_MAX ]
+
+// WCHAR_MIN and WCHAR_MAX are also defined in <wchar.h>
+#ifndef WCHAR_MIN // [
+# define WCHAR_MIN 0
+#endif // WCHAR_MIN ]
+#ifndef WCHAR_MAX // [
+# define WCHAR_MAX _UI16_MAX
+#endif // WCHAR_MAX ]
+
+#define WINT_MIN 0
+#define WINT_MAX _UI16_MAX
+
+#endif // __STDC_LIMIT_MACROS ]
+
+
+// 7.18.4 Limits of other integer types
+
+#if !defined(__cplusplus) || defined(__STDC_CONSTANT_MACROS) // [ See footnote 224 at page 260
+
+// 7.18.4.1 Macros for minimum-width integer constants
+
+#define INT8_C(val) val##i8
+#define INT16_C(val) val##i16
+#define INT32_C(val) val##i32
+#define INT64_C(val) val##i64
+
+#define UINT8_C(val) val##ui8
+#define UINT16_C(val) val##ui16
+#define UINT32_C(val) val##ui32
+#define UINT64_C(val) val##ui64
+
+// 7.18.4.2 Macros for greatest-width integer constants
+#define INTMAX_C INT64_C
+#define UINTMAX_C UINT64_C
+
+#endif // __STDC_CONSTANT_MACROS ]
+
+
+#endif // _MSC_STDINT_H_ ]
diff --git a/src/silx/math/histogramnd/include/templates.h b/src/silx/math/histogramnd/include/templates.h
new file mode 100644
index 0000000..490eed3
--- /dev/null
+++ b/src/silx/math/histogramnd/include/templates.h
@@ -0,0 +1,30 @@
+/*##########################################################################
+# Copyright (C) 2016 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
+
+#ifndef TEMPLATES_H_
+#define TEMPLATES_H_
+
+#define CONCAT(X,Y,Z,T) X##_##Y##_##Z##_##T
+#define TEMPLATE(X,Y,Z,T) CONCAT(X,Y,Z,T)
+
+#endif
diff --git a/src/silx/math/histogramnd/src/histogramnd_c.c b/src/silx/math/histogramnd/src/histogramnd_c.c
new file mode 100644
index 0000000..fc9d77e
--- /dev/null
+++ b/src/silx/math/histogramnd/src/histogramnd_c.c
@@ -0,0 +1,301 @@
+/*##########################################################################
+# Copyright (C) 2016 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
+
+#include "histogramnd_c.h"
+
+/*=====================
+ * double sample, double cumul
+ * =====================
+*/
+#ifdef HISTO_SAMPLE_T
+#undef HISTO_SAMPLE_T
+#endif
+#define HISTO_SAMPLE_T double
+#ifdef HISTO_WEIGHT_T
+#undef HISTO_WEIGHT_T
+#endif
+#define HISTO_WEIGHT_T double
+#ifdef HISTO_CUMUL_T
+#undef HISTO_CUMUL_T
+#endif
+#define HISTO_CUMUL_T double
+#include "histogramnd_template.c"
+
+#ifdef HISTO_SAMPLE_T
+#undef HISTO_SAMPLE_T
+#endif
+#define HISTO_SAMPLE_T double
+#ifdef HISTO_WEIGHT_T
+#undef HISTO_WEIGHT_T
+#endif
+#define HISTO_WEIGHT_T float
+#ifdef HISTO_CUMUL_T
+#undef HISTO_CUMUL_T
+#endif
+#define HISTO_CUMUL_T double
+#include "histogramnd_template.c"
+
+#ifdef HISTO_SAMPLE_T
+#undef HISTO_SAMPLE_T
+#endif
+#define HISTO_SAMPLE_T double
+#ifdef HISTO_WEIGHT_T
+#undef HISTO_WEIGHT_T
+#endif
+#define HISTO_WEIGHT_T int32_t
+#ifdef HISTO_CUMUL_T
+#undef HISTO_CUMUL_T
+#endif
+#define HISTO_CUMUL_T double
+#include "histogramnd_template.c"
+
+/*=====================
+ * float sample, double cumul
+ * =====================
+*/
+#ifdef HISTO_SAMPLE_T
+#undef HISTO_SAMPLE_T
+#endif
+#define HISTO_SAMPLE_T float
+#ifdef HISTO_WEIGHT_T
+#undef HISTO_WEIGHT_T
+#endif
+#define HISTO_WEIGHT_T double
+#ifdef HISTO_CUMUL_T
+#undef HISTO_CUMUL_T
+#endif
+#define HISTO_CUMUL_T double
+#include "histogramnd_template.c"
+
+#ifdef HISTO_SAMPLE_T
+#undef HISTO_SAMPLE_T
+#endif
+#define HISTO_SAMPLE_T float
+#ifdef HISTO_WEIGHT_T
+#undef HISTO_WEIGHT_T
+#endif
+#define HISTO_WEIGHT_T float
+#ifdef HISTO_CUMUL_T
+#undef HISTO_CUMUL_T
+#endif
+#define HISTO_CUMUL_T double
+#include "histogramnd_template.c"
+
+#ifdef HISTO_SAMPLE_T
+#undef HISTO_SAMPLE_T
+#endif
+#define HISTO_SAMPLE_T float
+#ifdef HISTO_WEIGHT_T
+#undef HISTO_WEIGHT_T
+#endif
+#define HISTO_WEIGHT_T int32_t
+#ifdef HISTO_CUMUL_T
+#undef HISTO_CUMUL_T
+#endif
+#define HISTO_CUMUL_T double
+#include "histogramnd_template.c"
+
+/*=====================
+ * int32_t sample, double cumul
+ * =====================
+*/
+#ifdef HISTO_SAMPLE_T
+#undef HISTO_SAMPLE_T
+#endif
+#define HISTO_SAMPLE_T int32_t
+#ifdef HISTO_WEIGHT_T
+#undef HISTO_WEIGHT_T
+#endif
+#define HISTO_WEIGHT_T double
+#ifdef HISTO_CUMUL_T
+#undef HISTO_CUMUL_T
+#endif
+#define HISTO_CUMUL_T double
+#include "histogramnd_template.c"
+
+#ifdef HISTO_SAMPLE_T
+#undef HISTO_SAMPLE_T
+#endif
+#define HISTO_SAMPLE_T int32_t
+#ifdef HISTO_WEIGHT_T
+#undef HISTO_WEIGHT_T
+#endif
+#define HISTO_WEIGHT_T float
+#ifdef HISTO_CUMUL_T
+#undef HISTO_CUMUL_T
+#endif
+#define HISTO_CUMUL_T double
+#include "histogramnd_template.c"
+
+#ifdef HISTO_SAMPLE_T
+#undef HISTO_SAMPLE_T
+#endif
+#define HISTO_SAMPLE_T int32_t
+#ifdef HISTO_WEIGHT_T
+#undef HISTO_WEIGHT_T
+#endif
+#define HISTO_WEIGHT_T int32_t
+#ifdef HISTO_CUMUL_T
+#undef HISTO_CUMUL_T
+#endif
+#define HISTO_CUMUL_T double
+#include "histogramnd_template.c"
+
+
+/*=====================
+ * double sample, float cumul
+ * =====================
+*/
+#ifdef HISTO_SAMPLE_T
+#undef HISTO_SAMPLE_T
+#endif
+#define HISTO_SAMPLE_T double
+#ifdef HISTO_WEIGHT_T
+#undef HISTO_WEIGHT_T
+#endif
+#define HISTO_WEIGHT_T double
+#ifdef HISTO_CUMUL_T
+#undef HISTO_CUMUL_T
+#endif
+#define HISTO_CUMUL_T float
+#include "histogramnd_template.c"
+
+#ifdef HISTO_SAMPLE_T
+#undef HISTO_SAMPLE_T
+#endif
+#define HISTO_SAMPLE_T double
+#ifdef HISTO_WEIGHT_T
+#undef HISTO_WEIGHT_T
+#endif
+#define HISTO_WEIGHT_T float
+#ifdef HISTO_CUMUL_T
+#undef HISTO_CUMUL_T
+#endif
+#define HISTO_CUMUL_T float
+#include "histogramnd_template.c"
+
+#ifdef HISTO_SAMPLE_T
+#undef HISTO_SAMPLE_T
+#endif
+#define HISTO_SAMPLE_T double
+#ifdef HISTO_WEIGHT_T
+#undef HISTO_WEIGHT_T
+#endif
+#define HISTO_WEIGHT_T int32_t
+#ifdef HISTO_CUMUL_T
+#undef HISTO_CUMUL_T
+#endif
+#define HISTO_CUMUL_T float
+#include "histogramnd_template.c"
+
+/*=====================
+ * float sample, float cumul
+ * =====================
+*/
+#ifdef HISTO_SAMPLE_T
+#undef HISTO_SAMPLE_T
+#endif
+#define HISTO_SAMPLE_T float
+#ifdef HISTO_WEIGHT_T
+#undef HISTO_WEIGHT_T
+#endif
+#define HISTO_WEIGHT_T double
+#ifdef HISTO_CUMUL_T
+#undef HISTO_CUMUL_T
+#endif
+#define HISTO_CUMUL_T float
+#include "histogramnd_template.c"
+
+#ifdef HISTO_SAMPLE_T
+#undef HISTO_SAMPLE_T
+#endif
+#define HISTO_SAMPLE_T float
+#ifdef HISTO_WEIGHT_T
+#undef HISTO_WEIGHT_T
+#endif
+#define HISTO_WEIGHT_T float
+#ifdef HISTO_CUMUL_T
+#undef HISTO_CUMUL_T
+#endif
+#define HISTO_CUMUL_T float
+#include "histogramnd_template.c"
+
+#ifdef HISTO_SAMPLE_T
+#undef HISTO_SAMPLE_T
+#endif
+#define HISTO_SAMPLE_T float
+#ifdef HISTO_WEIGHT_T
+#undef HISTO_WEIGHT_T
+#endif
+#define HISTO_WEIGHT_T int32_t
+#ifdef HISTO_CUMUL_T
+#undef HISTO_CUMUL_T
+#endif
+#define HISTO_CUMUL_T float
+#include "histogramnd_template.c"
+
+/*=====================
+ * int32_t sample, float cumul
+ * =====================
+*/
+#ifdef HISTO_SAMPLE_T
+#undef HISTO_SAMPLE_T
+#endif
+#define HISTO_SAMPLE_T int32_t
+#ifdef HISTO_WEIGHT_T
+#undef HISTO_WEIGHT_T
+#endif
+#define HISTO_WEIGHT_T double
+#ifdef HISTO_CUMUL_T
+#undef HISTO_CUMUL_T
+#endif
+#define HISTO_CUMUL_T float
+#include "histogramnd_template.c"
+
+#ifdef HISTO_SAMPLE_T
+#undef HISTO_SAMPLE_T
+#endif
+#define HISTO_SAMPLE_T int32_t
+#ifdef HISTO_WEIGHT_T
+#undef HISTO_WEIGHT_T
+#endif
+#define HISTO_WEIGHT_T float
+#ifdef HISTO_CUMUL_T
+#undef HISTO_CUMUL_T
+#endif
+#define HISTO_CUMUL_T float
+#include "histogramnd_template.c"
+
+#ifdef HISTO_SAMPLE_T
+#undef HISTO_SAMPLE_T
+#endif
+#define HISTO_SAMPLE_T int32_t
+#ifdef HISTO_WEIGHT_T
+#undef HISTO_WEIGHT_T
+#endif
+#define HISTO_WEIGHT_T int32_t
+#ifdef HISTO_CUMUL_T
+#undef HISTO_CUMUL_T
+#endif
+#define HISTO_CUMUL_T float
+#include "histogramnd_template.c"
diff --git a/src/silx/math/histogramnd/src/histogramnd_template.c b/src/silx/math/histogramnd/src/histogramnd_template.c
new file mode 100644
index 0000000..0276bb4
--- /dev/null
+++ b/src/silx/math/histogramnd/src/histogramnd_template.c
@@ -0,0 +1,260 @@
+/*##########################################################################
+# Copyright (C) 2016 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
+
+#include "templates.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <math.h>
+#include <stdarg.h>
+
+#ifdef HISTO_SAMPLE_T
+#ifdef HISTO_WEIGHT_T
+#ifdef HISTO_CUMUL_T
+
+int TEMPLATE(histogramnd, HISTO_SAMPLE_T, HISTO_WEIGHT_T, HISTO_CUMUL_T)
+ (HISTO_SAMPLE_T *i_sample,
+ HISTO_WEIGHT_T *i_weights,
+ int i_n_dim,
+ int i_n_elem,
+ double *i_bin_ranges,
+ int *i_n_bins,
+ uint32_t *o_histo,
+ HISTO_CUMUL_T *o_cumul,
+ double *o_bin_edges,
+ int i_opt_flags,
+ HISTO_WEIGHT_T i_weight_min,
+ HISTO_WEIGHT_T i_weight_max)
+{
+ /* some counters */
+ int i = 0, j = 0;
+ long elem_idx = 0;
+
+ HISTO_WEIGHT_T * weight_ptr = 0;
+ HISTO_SAMPLE_T elem_coord = 0.;
+
+ /* computed bin index (i_sample -> grid) */
+ long bin_idx = 0;
+
+ double * g_min = 0;
+ double * g_max = 0;
+ double * range = 0;
+
+ /* ================================
+ * Parsing options, if any.
+ * ================================
+ */
+
+ int filt_min_weight = 0;
+ int filt_max_weight = 0;
+ int last_bin_closed = 0;
+
+ /* Testing the option flags */
+ if(i_opt_flags & HISTO_WEIGHT_MIN)
+ {
+ filt_min_weight = 1;
+ }
+
+ if(i_opt_flags & HISTO_WEIGHT_MAX)
+ {
+ filt_max_weight = 1;
+ }
+
+ if(i_opt_flags & HISTO_LAST_BIN_CLOSED)
+ {
+ last_bin_closed = 1;
+ }
+
+ /* storing the min & max bin coordinates in their own arrays because
+ * i_bin_ranges = [[min0, max0], [min1, max1], ...]
+ * (mostly for the sake of clarity)
+ * (maybe faster access too?)
+ */
+ g_min = (double *) malloc(i_n_dim *sizeof(double));
+ g_max = (double *) malloc(i_n_dim * sizeof(double));
+ /* range used to convert from i_coords to bin indices in the grid */
+ range = (double *) malloc(i_n_dim * sizeof(double));
+
+ if(!g_min || !g_max || !range)
+ {
+ free(g_min);
+ free(g_max);
+ free(range);
+ return HISTO_ERR_ALLOC;
+ }
+
+ j = 0;
+ for(i=0; i<i_n_dim; i++)
+ {
+ g_min[i] = i_bin_ranges[i*2];
+ g_max[i] = i_bin_ranges[i*2+1];
+ range[i] = g_max[i]-g_min[i];
+
+ for(bin_idx=0; bin_idx<i_n_bins[i]; j++, bin_idx++)
+ {
+ o_bin_edges[j] = g_min[i] +
+ bin_idx * (range[i] / i_n_bins[i]);
+ }
+ o_bin_edges[j++] = g_max[i];
+ }
+
+ weight_ptr = i_weights;
+
+ if(!i_weights)
+ {
+ /* if weights are not provided there no point in trying to filter them
+ * (!! careful if you change this, some code below relies on it !!)
+ */
+ filt_min_weight = 0;
+ filt_max_weight = 0;
+
+ /* If the weights array is not provided then there is no point
+ * updating the weighted histogram, only the bin counts (o_histo)
+ * will be filled.
+ * (!! careful if you change this, some code below relies on it !!)
+ */
+ o_cumul = 0;
+ }
+
+ /* tried to use pointers instead of indices here, but it didn't
+ * seem any faster (probably because the compiler
+ * optimizes stuff anyway),
+ * so i'm keeping the "indices" version, for the sake of clarity
+ */
+ for(elem_idx=0;
+ elem_idx<i_n_elem*i_n_dim;
+ elem_idx+=i_n_dim, weight_ptr++)
+ {
+ /* no testing the validity of weight_ptr here, because if it is NULL
+ * then filt_min_weight/filt_max_weight will be 0.
+ * (see code above)
+ */
+ if(filt_min_weight && *weight_ptr<i_weight_min)
+ {
+ continue;
+ }
+ if(filt_max_weight && *weight_ptr>i_weight_max)
+ {
+ continue;
+ }
+
+ bin_idx = 0;
+
+ for(i=0; i<i_n_dim; i++)
+ {
+ elem_coord = i_sample[elem_idx+i];
+
+ /* =====================
+ * Element is rejected if any of the following is NOT true :
+ * 1. coordinate is >= than the minimum value
+ * 2. coordinate is <= than the maximum value
+ * 3. coordinate==maximum value and last_bin_closed is True
+ * =====================
+ */
+ if(elem_coord<g_min[i])
+ {
+ bin_idx = -1;
+ break;
+ }
+
+ /* Here we make the assumption that most of the time
+ * there will be more coordinates inside the grid interval
+ * (one test)
+ * than coordinates higher or equal to the max
+ * (two tests)
+ */
+ if(elem_coord<g_max[i])
+ {
+ /* Warning : the following factorization seems to
+ * increase the effect of precision error.
+ * bin_idx = (long)floor(
+ * (bin_idx +
+ * (elem_coord-g_min[i])/range[i]) *
+ * i_n_bins[i]
+ * );
+ */
+
+ /* Not using floor to speed up things.
+ * We don't (?) need all the error checking provided by
+ * the built-in floor().
+ * Also the value is supposed to be always positive.
+ */
+ bin_idx = bin_idx * i_n_bins[i] +
+ (long)(
+ ((elem_coord-g_min[i]) * i_n_bins[i]) /
+ range[i]
+ );
+ }
+ else /* ===> elem_coord>=g_max[i] */
+ {
+ /* if equal and the last bin is closed :
+ * put it in the last bin
+ * else : discard
+ */
+ if(last_bin_closed && elem_coord==g_max[i])
+ {
+ bin_idx = (bin_idx + 1) * i_n_bins[i] - 1;
+ }
+ else
+ {
+ bin_idx = -1;
+ break;
+ }
+ } /* if(elem_coord<g_max[i]) */
+
+ } /* for(i=0; i<i_n_dim; i++) */
+
+ /* element is out of the grid */
+ if(bin_idx==-1)
+ {
+ continue;
+ }
+
+ if(o_histo)
+ {
+ o_histo[bin_idx] += 1;
+ }
+ if(o_cumul)
+ {
+ /* not testing the pointer since o_cumul is null if
+ * i_weights is null.
+ */
+ o_cumul[bin_idx] += (HISTO_CUMUL_T) *weight_ptr;
+ }
+
+ } /* for(elem_idx=0; elem_idx<i_n_elem*i_n_dim; elem_idx+=i_n_dim) */
+
+ free(g_min);
+ free(g_max);
+ free(range);
+
+ /* For now just returning 0 (OK) since all the checks are done in
+ * python. This might change later if people want to call this
+ * function directly from C (might have to implement error codes).
+ */
+ return HISTO_OK;
+}
+
+#endif
+#endif
+#endif
diff --git a/src/silx/math/histogramnd_c.pxd b/src/silx/math/histogramnd_c.pxd
new file mode 100644
index 0000000..35db529
--- /dev/null
+++ b/src/silx/math/histogramnd_c.pxd
@@ -0,0 +1,299 @@
+# coding: utf-8
+# /*##########################################################################
+# Copyright (C) 2016-2018 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
+
+__authors__ = ["D. Naudet"]
+__license__ = "MIT"
+__date__ = "01/02/2016"
+
+cimport numpy as cnumpy
+
+cdef extern from "histogramnd_c.h":
+
+ ctypedef enum histo_opt_type:
+ HISTO_NONE
+ HISTO_WEIGHT_MIN
+ HISTO_WEIGHT_MAX
+ HISTO_LAST_BIN_CLOSED
+
+ ctypedef enum histo_rc_t:
+ HISTO_OK
+ HISTO_ERR_ALLOC
+
+ # =====================
+ # double sample, double cumul
+ # =====================
+
+ int histogramnd_double_double_double(double *i_sample,
+ double *i_weigths,
+ int i_n_dim,
+ int i_n_elem,
+ double *i_bin_ranges,
+ int *i_n_bin,
+ cnumpy.uint32_t *o_histo,
+ double *o_cumul,
+ double * bin_edges,
+ int i_opt_flags,
+ double i_weight_min,
+ double i_weight_max) nogil
+
+ int histogramnd_double_float_double(double *i_sample,
+ float *i_weigths,
+ int i_n_dim,
+ int i_n_elem,
+ double *i_bin_ranges,
+ int *i_n_bin,
+ cnumpy.uint32_t *o_histo,
+ double *o_cumul,
+ double * bin_edges,
+ int i_opt_flags,
+ float i_weight_min,
+ float i_weight_max) nogil
+
+ int histogramnd_double_int32_t_double(double *i_sample,
+ cnumpy.int32_t *i_weigths,
+ int i_n_dim,
+ int i_n_elem,
+ double *i_bin_ranges,
+ int *i_n_bin,
+ cnumpy.uint32_t *o_histo,
+ double *o_cumul,
+ double * bin_edges,
+ int i_opt_flags,
+ cnumpy.int32_t i_weight_min,
+ cnumpy.int32_t i_weight_max) nogil
+
+ # =====================
+ # float sample, double cumul
+ # =====================
+
+ int histogramnd_float_double_double(float *i_sample,
+ double *i_weigths,
+ int i_n_dim,
+ int i_n_elem,
+ double *i_bin_ranges,
+ int *i_n_bin,
+ cnumpy.uint32_t *o_histo,
+ double *o_cumul,
+ double * bin_edges,
+ int i_opt_flags,
+ double i_weight_min,
+ double i_weight_max) nogil
+
+ int histogramnd_float_float_double(float *i_sample,
+ float *i_weigths,
+ int i_n_dim,
+ int i_n_elem,
+ double *i_bin_ranges,
+ int *i_n_bin,
+ cnumpy.uint32_t *o_histo,
+ double *o_cumul,
+ double * bin_edges,
+ int i_opt_flags,
+ float i_weight_min,
+ float i_weight_max) nogil
+
+ int histogramnd_float_int32_t_double(float *i_sample,
+ cnumpy.int32_t *i_weigths,
+ int i_n_dim,
+ int i_n_elem,
+ double *i_bin_ranges,
+ int *i_n_bin,
+ cnumpy.uint32_t *o_histo,
+ double *o_cumul,
+ double * bin_edges,
+ int i_opt_flags,
+ cnumpy.int32_t i_weight_min,
+ cnumpy.int32_t i_weight_max) nogil
+
+ # =====================
+ # numpy.int32_t sample, double cumul
+ # =====================
+
+ int histogramnd_int32_t_double_double(cnumpy.int32_t *i_sample,
+ double *i_weigths,
+ int i_n_dim,
+ int i_n_elem,
+ double *i_bin_ranges,
+ int *i_n_bin,
+ cnumpy.uint32_t *o_histo,
+ double *o_cumul,
+ double * bin_edges,
+ int i_opt_flags,
+ double i_weight_min,
+ double i_weight_max) nogil
+
+ int histogramnd_int32_t_float_double(cnumpy.int32_t *i_sample,
+ float *i_weigths,
+ int i_n_dim,
+ int i_n_elem,
+ double *i_bin_ranges,
+ int *i_n_bin,
+ cnumpy.uint32_t *o_histo,
+ double *o_cumul,
+ double * bin_edges,
+ int i_opt_flags,
+ float i_weight_min,
+ float i_weight_max) nogil
+
+ int histogramnd_int32_t_int32_t_double(cnumpy.int32_t *i_sample,
+ cnumpy.int32_t *i_weigths,
+ int i_n_dim,
+ int i_n_elem,
+ double *i_bin_ranges,
+ int *i_n_bin,
+ cnumpy.uint32_t *o_histo,
+ double *o_cumul,
+ double * bin_edges,
+ int i_opt_flags,
+ cnumpy.int32_t i_weight_min,
+ cnumpy.int32_t i_weight_max) nogil
+
+ # =====================
+ # double sample, float cumul
+ # =====================
+
+ int histogramnd_double_double_float(double *i_sample,
+ double *i_weigths,
+ int i_n_dim,
+ int i_n_elem,
+ double *i_bin_ranges,
+ int *i_n_bin,
+ cnumpy.uint32_t *o_histo,
+ float *o_cumul,
+ double * bin_edges,
+ int i_opt_flags,
+ double i_weight_min,
+ double i_weight_max) nogil
+
+ int histogramnd_double_float_float(double *i_sample,
+ float *i_weigths,
+ int i_n_dim,
+ int i_n_elem,
+ double *i_bin_ranges,
+ int *i_n_bin,
+ cnumpy.uint32_t *o_histo,
+ float *o_cumul,
+ double * bin_edges,
+ int i_opt_flags,
+ float i_weight_min,
+ float i_weight_max) nogil
+
+ int histogramnd_double_int32_t_float(double *i_sample,
+ cnumpy.int32_t *i_weigths,
+ int i_n_dim,
+ int i_n_elem,
+ double *i_bin_ranges,
+ int *i_n_bin,
+ cnumpy.uint32_t *o_histo,
+ float *o_cumul,
+ double * bin_edges,
+ int i_opt_flags,
+ cnumpy.int32_t i_weight_min,
+ cnumpy.int32_t i_weight_max) nogil
+
+ # =====================
+ # float sample, float cumul
+ # =====================
+
+ int histogramnd_float_double_float(float *i_sample,
+ double *i_weigths,
+ int i_n_dim,
+ int i_n_elem,
+ double *i_bin_ranges,
+ int *i_n_bin,
+ cnumpy.uint32_t *o_histo,
+ float *o_cumul,
+ double * bin_edges,
+ int i_opt_flags,
+ double i_weight_min,
+ double i_weight_max) nogil
+
+ int histogramnd_float_float_float(float *i_sample,
+ float *i_weigths,
+ int i_n_dim,
+ int i_n_elem,
+ double *i_bin_ranges,
+ int *i_n_bin,
+ cnumpy.uint32_t *o_histo,
+ float *o_cumul,
+ double * bin_edges,
+ int i_opt_flags,
+ float i_weight_min,
+ float i_weight_max) nogil
+
+ int histogramnd_float_int32_t_float(float *i_sample,
+ cnumpy.int32_t *i_weigths,
+ int i_n_dim,
+ int i_n_elem,
+ double *i_bin_ranges,
+ int *i_n_bin,
+ cnumpy.uint32_t *o_histo,
+ float *o_cumul,
+ double * bin_edges,
+ int i_opt_flags,
+ cnumpy.int32_t i_weight_min,
+ cnumpy.int32_t i_weight_max) nogil
+
+ # =====================
+ # numpy.int32_t sample, float cumul
+ # =====================
+
+ int histogramnd_int32_t_double_float(cnumpy.int32_t *i_sample,
+ double *i_weigths,
+ int i_n_dim,
+ int i_n_elem,
+ double *i_bin_ranges,
+ int *i_n_bin,
+ cnumpy.uint32_t *o_histo,
+ float *o_cumul,
+ double * bin_edges,
+ int i_opt_flags,
+ double i_weight_min,
+ double i_weight_max) nogil
+
+ int histogramnd_int32_t_float_float(cnumpy.int32_t *i_sample,
+ float *i_weigths,
+ int i_n_dim,
+ int i_n_elem,
+ double *i_bin_ranges,
+ int *i_n_bin,
+ cnumpy.uint32_t *o_histo,
+ float *o_cumul,
+ double * bin_edges,
+ int i_opt_flags,
+ float i_weight_min,
+ float i_weight_max) nogil
+
+ int histogramnd_int32_t_int32_t_float(cnumpy.int32_t *i_sample,
+ cnumpy.int32_t *i_weigths,
+ int i_n_dim,
+ int i_n_elem,
+ double *i_bin_ranges,
+ int *i_n_bin,
+ cnumpy.uint32_t *o_histo,
+ float *o_cumul,
+ double * bin_edges,
+ int i_opt_flags,
+ cnumpy.int32_t i_weight_min,
+ cnumpy.int32_t i_weight_max) nogil
diff --git a/src/silx/math/include/math_compatibility.h b/src/silx/math/include/math_compatibility.h
new file mode 100644
index 0000000..3d69c0c
--- /dev/null
+++ b/src/silx/math/include/math_compatibility.h
@@ -0,0 +1,53 @@
+# /*##########################################################################
+#
+# Copyright (c) 2017-2018 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+/* This header provides libc math functions and macros across platforms.
+
+ Needed as VisualStudio 2008 (i.e., Python2.7) is missing some functions/macros.
+*/
+
+#ifndef __MATH_COMPATIBILITY_H__
+#define __MATH_COMPATIBILITY_H__
+
+#include <math.h>
+
+#ifndef INFINITY
+#define INFINITY (DBL_MAX+DBL_MAX)
+#endif
+
+#ifndef NAN
+#define NAN (INFINITY-INFINITY)
+#endif
+
+#if (defined (_MSC_VER) && _MSC_VER < 1800)
+#include <float.h>
+
+/* Make sure asinh returns -inf rather than NaN for v=-inf */
+#define asinh(v) (v == -INFINITY ? v : log((v) + sqrt((v)*(v) + 1)))
+
+#define isnan(v) _isnan(v)
+#define isfinite(v) _finite(v)
+#define lrint(v) ((long int) (v))
+#endif
+
+#endif /*__MATH_COMPATIBILITY_H__*/
diff --git a/src/silx/math/interpolate.pyx b/src/silx/math/interpolate.pyx
new file mode 100644
index 0000000..c79224a
--- /dev/null
+++ b/src/silx/math/interpolate.pyx
@@ -0,0 +1,165 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2019 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
+"""This module provides :func:`interp3d` to perform trilinear interpolation.
+"""
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "11/07/2019"
+
+
+import cython
+from cython.parallel import prange
+import numpy
+
+cimport cython
+from libc.math cimport floor
+cimport numpy as cnumpy
+
+
+ctypedef fused _floating:
+ float
+ double
+
+ctypedef fused _floating_pts:
+ float
+ double
+
+
+@cython.initializedcheck(False)
+@cython.boundscheck(False)
+@cython.wraparound(False)
+cdef inline double trilinear_interpolation(
+ _floating[:, :, :] values,
+ _floating_pts pos0,
+ _floating_pts pos1,
+ _floating_pts pos2,
+ double fill_value) nogil:
+ """Evaluate the trilinear interpolation at a given position
+
+ :param values: 3D dataset from which to do the interpolation
+ :param pos0: Dimension 0 coordinate at which to evaluate the interpolation
+ :param pos1: Dimension 1 coordinate at which to evaluate the interpolation
+ :param pos2: Dimension 2 coordinate at which to evaluate the interpolation
+ :param fill_value: Value to return for points outside data
+ """
+ cdef:
+ int i0, i1, i2 # Indices
+ int i0_plus1, i1_plus1, i2_plus1 # Indices+1
+ double delta
+ double c00, c01, c10, c11, c0, c1
+ double c
+
+ if (pos0 < 0. or pos0 > (values.shape[0] -1) or
+ pos1 < 0. or pos1 > (values.shape[1] -1) or
+ pos2 < 0. or pos2 > (values.shape[2] -1)):
+ return fill_value
+
+ i0 = < int > floor(pos0)
+ i1 = < int > floor(pos1)
+ i2 = < int > floor(pos2)
+
+ # Clip i+1 indices to data volume
+ # In this case, corresponding dX is 0.
+ i0_plus1 = min(i0 + 1, values.shape[0] - 1)
+ i1_plus1 = min(i1 + 1, values.shape[1] - 1)
+ i2_plus1 = min(i2 + 1, values.shape[2] - 1)
+
+ if pos2 == i2: # Avoids multiplication by 0 (which yields to NaN with inf)
+ c00 = <double> values[i0, i1, i2]
+ c10 = <double> values[i0, i1_plus1, i2]
+ c01 = <double> values[i0_plus1, i1, i2]
+ c11 = <double> values[i0_plus1, i1_plus1, i2]
+ else:
+ delta = pos2 - i2
+ c00 = (<double> values[i0, i1, i2]) * (1. - delta) + (<double> values[i0, i1, i2_plus1]) * delta
+ c10 = (<double> values[i0, i1_plus1, i2]) * (1. - delta) + (<double> values[i0, i1_plus1, i2_plus1]) * delta
+ c01 = (<double> values[i0_plus1, i1, i2]) * (1. - delta) + (<double> values[i0_plus1, i1, i2_plus1]) * delta
+ c11 = (<double> values[i0_plus1, i1_plus1, i2]) * (1. - delta) + (<double> values[i0_plus1, i1_plus1, i2_plus1]) * delta
+
+ if pos1 == i1: # Avoids multiplication by 0 (which yields to NaN with inf)
+ c0 = c00
+ c1 = c01
+ else:
+ delta = pos1 - i1
+ c0 = c00 * (1. - delta) + c10 * delta
+ c1 = c01 * (1. - delta) + c11 * delta
+
+ if pos0 == i0: # Avoids multiplication by 0 (which yields to NaN with inf)
+ c = c0
+ else:
+ delta = pos0 - i0
+ c = c0 * (1 - delta) + c1 * delta
+
+ return c
+
+
+@cython.boundscheck(False)
+@cython.wraparound(False)
+def interp3d(_floating[:, :, :] values not None,
+ _floating_pts[:, :] xi not None,
+ str method='linear',
+ double fill_value=numpy.nan):
+ """Trilinear interpolation in a regular grid.
+
+ Perform trilinear interpolation of the 3D dataset at given points
+
+ :param numpy.ndarray values: 3D dataset of floating point values
+ :param numpy.ndarray xi: (N, 3) sampling points
+ :param str method: Interpolation method to use in:
+ - 'linear': Trilinear interpolation
+ - 'linear_omp': Trilinear interpolation with OpenMP parallelism
+ :param float fill_value:
+ Value to use for points outside the volume (default: nan)
+ :return: Values evaluated at given input points.
+ :rtype: numpy.ndarray
+ """
+ if _floating is cnumpy.float32_t:
+ dtype = numpy.float32
+ elif _floating is cnumpy.float64_t:
+ dtype = numpy.float64
+ else: # This should not happen
+ raise ValueError("Unsupported input dtype")
+
+ cdef:
+ int npoints = xi.shape[0]
+ _floating[:] result = numpy.empty((npoints,), dtype=dtype)
+ int index
+ double c_fill_value = fill_value
+
+ if method == 'linear':
+ with nogil:
+ for index in range(npoints):
+ result[index] = < _floating > trilinear_interpolation(
+ values, xi[index, 0], xi[index, 1], xi[index, 2], c_fill_value)
+
+ elif method == 'linear_omp':
+ for index in prange(npoints, nogil=True):
+ result[index] = < _floating > trilinear_interpolation(
+ values, xi[index, 0], xi[index, 1], xi[index, 2], c_fill_value)
+ else:
+ raise ValueError("Unsupported method: %s" % method)
+
+ return numpy.array(result, copy=False) \ No newline at end of file
diff --git a/src/silx/math/marchingcubes.pyx b/src/silx/math/marchingcubes.pyx
new file mode 100644
index 0000000..0409691
--- /dev/null
+++ b/src/silx/math/marchingcubes.pyx
@@ -0,0 +1,246 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2015-2018 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+"""This module provides marching cubes implementation.
+
+It provides a :class:`MarchingCubes` class allowing to build an isosurface
+from data provided as a 3D data set or slice by slice.
+"""
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "16/08/2017"
+
+
+import numpy
+cimport numpy as cnumpy
+cimport cython
+
+cimport silx.math.mc as mc
+
+
+# From numpy_common.pxi to avoid warnings while compiling C code
+# See this thread:
+# https://mail.python.org/pipermail//cython-devel/2012-March/002137.html
+cdef extern from *:
+ bint FALSE "0"
+ void import_array()
+ void import_umath()
+
+if FALSE:
+ import_array()
+ import_umath()
+
+
+cdef class MarchingCubes:
+ """Compute isosurface using marching cubes algorithm.
+
+ It builds a surface from a 3D scalar dataset as a 3D contour at a
+ given value.
+ The resulting surface is not topologically correct.
+
+ See: http://paulbourke.net/geometry/polygonise/
+
+ Lorensen, W. E. and Cline, H. E. Marching cubes: A high resolution 3D
+ surface construction algorithm. Computer Graphics, 21, 4 (July 1987).
+ ACM, 163-169.
+
+ Generated vertex and normal coordinates are in the same order
+ as input array, i.e., (dim 0, dim 1, dim 2).
+
+ Expected indices in memory of a (2, 2, 2) dataset:
+
+ dim 0 (depth)
+ |
+ |
+ 4 +------+ 5
+ /| /|
+ / | / |
+ 6 +------+ 7|
+ | | | |
+ |0 +---|--+ 1 --- dim 2 (width)
+ | / | /
+ |/ |/
+ 2 +------+ 3
+ /
+ /
+ dim 1 (height)
+
+ Example with a 3D data set:
+
+ >>> vertices, normals, indices = MarchingCubes(data, isolevel=1.)
+
+ Example of code for processing a list of images:
+
+ >>> mc = MarchingCubes(isolevel=1.) # Create object with iso-level=1
+ >>> previous_image = images[0]
+ >>> for image in images[1:]:
+ ... mc.process_image(previous_image, image) # Process one slice
+ ... previous_image = image
+
+ >>> vertices = mc.get_vertices() # Array of vertex positions
+ >>> normals = mc.get_normals() # Array of normals
+ >>> triangle_indices = mc.get_indices() # Array of indices of vertices
+
+ :param data: 3D dataset of float32 or None
+ :type data: numpy.ndarray of float32 of dimension 3
+ :param float isolevel: The value for which to generate the isosurface
+ :param bool invert_normals:
+ True (default) for normals oriented in direction of gradient descent
+ :param sampling: Sampling along each dimension (depth, height, width)
+ """
+ cdef mc.MarchingCubes[float, float] * c_mc # Pointer to the C++ instance
+
+ def __cinit__(self, data=None, isolevel=None,
+ invert_normals=True, sampling=(1, 1, 1)):
+ self.c_mc = new mc.MarchingCubes[float, float](isolevel)
+ self.c_mc.invert_normals = bool(invert_normals)
+ self.c_mc.sampling[0] = sampling[0]
+ self.c_mc.sampling[1] = sampling[1]
+ self.c_mc.sampling[2] = sampling[2]
+
+ if data is not None:
+ self.process(data)
+
+ def __dealloc__(self):
+ del self.c_mc
+
+ def __getitem__(self, key):
+ """Allows one to unpack object as a single liner:
+
+ vertices, normals, indices = MarchingCubes(...)
+ """
+ if key == 0:
+ return self.get_vertices()
+ elif key == 1:
+ return self.get_normals()
+ elif key == 2:
+ return self.get_indices()
+ else:
+ raise IndexError("Index out of range")
+
+ def process(self, data):
+ """Compute an isosurface from a 3D scalar field.
+
+ This builds vertices, normals and indices arrays.
+ Vertices and normals coordinates are in the same order as input array,
+ i.e., (dim 0, dim 1, dim 2).
+
+ :param numpy.ndarray data: 3D scalar field
+ """
+ # Make sure data is a 3D contiguous array of native endian float32
+ data = numpy.ascontiguousarray(data, dtype='=f4')
+ assert data.ndim == 3
+ cdef float[:] c_data = numpy.ravel(data)
+ cdef unsigned int depth, height, width
+
+ depth = data.shape[0]
+ height = data.shape[1]
+ width = data.shape[2]
+
+ self.c_mc.process(&c_data[0], depth, height, width)
+
+ def process_slice(self, slice0, slice1):
+ """Process a new slice to build the isosurface.
+
+ :param numpy.ndarray slice0: Slice previously provided as slice1.
+ :param numpy.ndarray slice1: Slice to process.
+ """
+ # Make sure slices are 2D contiguous arrays of native endian float32
+ slice0 = numpy.ascontiguousarray(slice0, dtype='=f4')
+ assert slice0.ndim == 2
+ slice1 = numpy.ascontiguousarray(slice1, dtype='=f4')
+ assert slice1.ndim == 2
+
+ assert slice0.shape[0] == slice1.shape[0]
+ assert slice0.shape[1] == slice1.shape[1]
+
+ cdef float[:] c_slice0 = numpy.ravel(slice0)
+ cdef float[:] c_slice1 = numpy.ravel(slice1)
+
+ if self.c_mc.depth == 0:
+ # Starts a new isosurface, bootstrap with slice size
+ self.c_mc.set_slice_size(slice1.shape[0], slice1.shape[1])
+
+ assert slice1.shape[0] == self.c_mc.height
+ assert slice1.shape[1] == self.c_mc.width
+
+ self.c_mc.process_slice(&c_slice0[0], &c_slice1[0])
+
+ def finish_process(self):
+ """Clear internal cache after processing slice by slice."""
+ self.c_mc.finish_process()
+
+ def reset(self):
+ """Reset internal resources including computed isosurface info."""
+ self.c_mc.reset()
+
+ @cython.embedsignature(False)
+ @property
+ def shape(self):
+ """The shape of the processed scalar field (depth, height, width)."""
+ return self.c_mc.depth, self.c_mc.height, self.c_mc.width
+
+ @cython.embedsignature(False)
+ @property
+ def sampling(self):
+ """The sampling over each dimension (depth, height, width).
+
+ Default: 1, 1, 1
+ """
+ return (self.c_mc.sampling[0],
+ self.c_mc.sampling[1],
+ self.c_mc.sampling[2])
+
+ @cython.embedsignature(False)
+ @property
+ def isolevel(self):
+ """The iso-level at which to generate the isosurface"""
+ return self.c_mc.isolevel
+
+ @cython.embedsignature(False)
+ @property
+ def invert_normals(self):
+ """True to use gradient descent as normals."""
+ return self.c_mc.invert_normals
+
+ def get_vertices(self):
+ """Vertices currently computed (ndarray of dim NbVertices x 3)
+
+ Order is dim0, dim1, dim2 (i.e., z, y, x if dim0 is depth).
+ """
+ return numpy.array(self.c_mc.vertices).reshape(-1, 3)
+
+ def get_normals(self):
+ """Normals currently computed (ndarray of dim NbVertices x 3)
+
+ Order is dim0, dim1, dim2 (i.e., z, y, x if dim0 is depth).
+ """
+ return numpy.array(self.c_mc.normals).reshape(-1, 3)
+
+ def get_indices(self):
+ """Triangle indices currently computed (ndarray of dim NbTriangles x 3)
+ """
+ return numpy.array(self.c_mc.indices,
+ dtype=numpy.uint32).reshape(-1, 3)
diff --git a/src/silx/math/marchingcubes/mc.hpp b/src/silx/math/marchingcubes/mc.hpp
new file mode 100644
index 0000000..82eced9
--- /dev/null
+++ b/src/silx/math/marchingcubes/mc.hpp
@@ -0,0 +1,724 @@
+/*##########################################################################
+#
+# Copyright (c) 2015-2016 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+#ifndef __mc_HPP__
+#define __mc_HPP__
+
+#include <iostream>
+#include <cmath>
+#include <map>
+#include <stdexcept>
+#include <vector>
+#include <assert.h>
+
+
+extern const int MCTriangleTable[256][16];
+extern const unsigned int MCEdgeIndexToCoordOffsets[12][4];
+
+#define DEPTH_IDX 0
+#define HEIGHT_IDX 1
+#define WIDTH_IDX 2
+
+/** Class Marching cubes
+ *
+ * Implements the marching cube algorithm and provides an API to process
+ * data image by image.
+ *
+ * Dimension convention used is (dim0, dim1, dim2) denoted as
+ * (depth, height, width) with dim2 (= width) being contiguous in memory.
+ *
+ * If data is provided as (depth, height, width), resulting vertices
+ * and normals will be stored as (z, y, x)
+ *
+ * Indices in memory for a single cube:
+ *
+ * dim 0 (depth)
+ * |
+ * |
+ * 4 +------+ 5
+ * /| /|
+ * / | / |
+ * 6 +------+ 7|
+ * | | | |
+ * |0 +---|--+ 1 --- dim 2 (width)
+ * | / | /
+ * |/ |/
+ * 2 +------+ 3
+ * /
+ * /
+ * dim 1 (height)
+ */
+template <typename FloatIn, typename FloatOut>
+class MarchingCubes {
+public:
+ /** Create a marching cube object.
+ *
+ * @param level Level at which to build the isosurface
+ */
+ MarchingCubes(const FloatIn level);
+
+ ~MarchingCubes();
+
+ /** Process a 3D scalar field
+ *
+ * @param data Pointer to the data set
+ * @param depth The 1st dimension of the data set
+ * @param height The 2nd dimension of the data set
+ * @param width The 3rd dimension of the data set
+ * (tightly packed in memory)
+ */
+ void process(const FloatIn * data,
+ const unsigned int depth,
+ const unsigned int height,
+ const unsigned int width);
+
+ /** Init dimension of slices
+ *
+ * @param height Height in pixels of the slices
+ * @param width Width in pixels of the slices
+ */
+ void set_slice_size(const unsigned int height,
+ const unsigned int width);
+
+ /** Process a slice (i.e., an image)
+ *
+ * The size of the images MUST match height and width provided to
+ * set_slice_size.
+ *
+ * The marching cube process 2 consecutive images at a time.
+ * A slice provided as next parameter MUST be provided as current
+ * parameter for the next call.
+ * Example with 3 images:
+ *
+ * float * img1;
+ * float * img2;
+ * float * img3;
+ * ...
+ * mc = MarchingCubes<float>(100.);
+ * mc.set_slice_size(10, 10);
+ * mc.process_slice(img1, img2);
+ * mc.process_slice(img2, img3);
+ * mc.finish_process();
+ *
+ * @param slice0 Pointer to the nth slice data
+ * @param slice1 Pointer to the (n+1)th slice of data
+ */
+ void process_slice(const FloatIn * slice0,
+ const FloatIn * slice1);
+
+ /** Clear marching cube processing internal cache. */
+ void finish_process();
+
+ /** Reset all internal data and counters. */
+ void reset();
+
+ /** Vertices of the isosurface (x, y, z) */
+ std::vector<FloatOut> vertices;
+
+ /** Approximation of normals at the vertices (nx, ny, nz)
+ *
+ * Current implementation provides coarse (but fast) normals computation
+ */
+ std::vector<FloatOut> normals;
+
+ /** Triangle indices */
+ std::vector<unsigned int> indices;
+
+ unsigned int depth; /**< Number of images currently processed */
+ unsigned int height; /**< Images height in pixels */
+ unsigned int width; /**< Images width in pixels */
+
+ /** Sampling of the data (depth, height, width)
+ *
+ * Default: 1, 1, 1
+ */
+ unsigned int sampling[3];
+
+ FloatIn isolevel; /**< Iso level to use */
+ bool invert_normals; /**< True to inverse gradient as normals */
+
+private:
+
+ /** Start to build isosurface starting with first slice
+ *
+ * Bootstrap cache edge_indices
+ *
+ * @param slice The first slice of the data
+ * @param next The second slice
+ */
+ void first_slice(const FloatIn * slice,
+ const FloatIn * next);
+
+ /** Process an edge
+ *
+ * @param value0 Data at 'begining' of edge
+ * @param value Data at 'end' of edge
+ * @param depth Depth coordinate of the edge position
+ * @param row Row coordinate of the edge
+ * @param col Column coordinate of the edge
+ * @param direction Direction of the edge: 0 for x, 1 for y and 2 for z
+ * @param previous
+ * @param current
+ * @param next
+ */
+ void process_edge(const FloatIn value0,
+ const FloatIn value,
+ const unsigned int depth,
+ const unsigned int row,
+ const unsigned int col,
+ const unsigned int direction,
+ const FloatIn * previous,
+ const FloatIn * current,
+ const FloatIn * next);
+
+ /** Return the bit mask of cube corners <= the iso-value.
+ *
+ * @param slice1 1st slice of the cube to consider
+ * @param slice2 2nd slice of the cube to consider
+ * @param row Row of the cube to consider
+ * @param col Column of the cube to consider
+ * @return The bit mask of cube corners <= the iso-value
+ */
+ unsigned char get_cell_code(const FloatIn * slice1,
+ const FloatIn * slice2,
+ const unsigned int row,
+ const unsigned int col);
+
+ /** Compute an edge index from position and edge direction.
+ *
+ * @param depth Depth of the origin of the edge
+ * @param row Row of the origin of the edge
+ * @param col Column of the origin of the edge
+ * @param direction 0 for x, 1 for y, 2 for z
+ * @return The (4D) index of the edge
+ */
+ unsigned int edge_index(const unsigned int depth,
+ const unsigned int row,
+ const unsigned int col,
+ const unsigned int direction);
+
+ /** For each dimension, a map from edge index to vertex index
+ *
+ * This caches indices for previously processed slice.
+ *
+ * Edge index is the linearized position of the edge using size + 1
+ * in all dimensions as coordinates plus the direction as 4th coord.
+ * WARNING: direction 0 for x, 1 for y and 2 for z
+ */
+ std::map<unsigned int, unsigned int> * edge_indices;
+};
+
+
+/* Implementation */
+
+template <typename FloatIn, typename FloatOut>
+MarchingCubes<FloatIn, FloatOut>::MarchingCubes(const FloatIn level)
+{
+ this->edge_indices = 0;
+ this->reset();
+ this->height = 0;
+ this->width = 0;
+ this->isolevel = level;
+ this->invert_normals = true;
+ this->sampling[0] = 1;
+ this->sampling[1] = 1;
+ this->sampling[2] = 1;
+}
+
+template <typename FloatIn, typename FloatOut>
+MarchingCubes<FloatIn, FloatOut>::~MarchingCubes()
+{
+}
+
+template <typename FloatIn, typename FloatOut>
+void
+MarchingCubes<FloatIn, FloatOut>::reset()
+{
+ this->depth = 0;
+ this->vertices.clear();
+ this->normals.clear();
+ this->indices.clear();
+ if (this->edge_indices != 0) {
+ delete this->edge_indices;
+ this->edge_indices = 0;
+ }
+}
+
+template <typename FloatIn, typename FloatOut>
+void
+MarchingCubes<FloatIn, FloatOut>::finish_process()
+{
+ if (this->edge_indices != 0) {
+ delete this->edge_indices;
+ this->edge_indices = 0;
+ }
+}
+
+
+template <typename FloatIn, typename FloatOut>
+void
+MarchingCubes<FloatIn, FloatOut>::process(const FloatIn * data,
+ const unsigned int depth,
+ const unsigned int height,
+ const unsigned int width)
+{
+ assert(data != NULL);
+ unsigned int size = height * width * this->sampling[DEPTH_IDX];
+
+ /* number of slices minus - 1 to process */
+ const unsigned int nb_slices = (depth - 1) / this->sampling[DEPTH_IDX];
+
+ this->reset();
+ this->set_slice_size(height, width);
+
+ for (unsigned int index=0; index < nb_slices; index++) {
+ const FloatIn * slice0 = data + (index * size);
+ const FloatIn * slice1 = slice0 + size;
+
+ this->process_slice(slice0, slice1);
+ }
+ this->finish_process();
+
+ this->depth = depth; /* Forced as it might be < depth otherwise */
+}
+
+
+template <typename FloatIn, typename FloatOut>
+void
+MarchingCubes<FloatIn, FloatOut>::set_slice_size(const unsigned int height,
+ const unsigned int width)
+{
+ this->reset();
+ this->height = height;
+ this->width = width;
+}
+
+
+template <typename FloatIn, typename FloatOut>
+void
+MarchingCubes<FloatIn, FloatOut>::process_slice(const FloatIn * slice0,
+ const FloatIn * slice1)
+{
+ assert(slice0 != NULL);
+ assert(slice1 != NULL);
+ unsigned int row, col;
+
+ if (this->edge_indices == 0) {
+ /* No previously processed slice, bootstrap */
+ this->first_slice(slice0, slice1);
+ }
+
+ /* Keep reference to cache from previous slice */
+ std::map<unsigned int, unsigned int> * previous_edge_indices =
+ this->edge_indices;
+
+ /* Init cache for this slice */
+ this->edge_indices = new std::map<unsigned int, unsigned int>();
+
+ /* Loop over slice to add vertices */
+ for (row=0; row < this->height; row += this->sampling[HEIGHT_IDX]) {
+ unsigned int line_index = row * this->width;
+
+ for (col=0; col < this->width; col += this->sampling[WIDTH_IDX]) {
+ unsigned int item_index = line_index + col;
+
+ FloatIn value0 = slice1[item_index];
+
+ /* Test forward edges and add vertices in the current slice plane */
+ if (col < (width - this->sampling[WIDTH_IDX])) {
+ FloatIn value = slice1[item_index + this->sampling[WIDTH_IDX]];
+
+ this->process_edge(value0, value, this->depth, row, col, 0,
+ slice0, slice1, 0);
+ }
+
+ if (row < (height - this->sampling[HEIGHT_IDX])) {
+ /* Value from next line*/
+ FloatIn value = slice1[item_index + this->width * this->sampling[HEIGHT_IDX]];
+
+ this->process_edge(value0, value, this->depth, row, col, 1,
+ slice0, slice1, 0);
+ }
+
+ /* Test backward edges and add vertices in z direction */
+ {
+ FloatIn value = slice0[item_index];
+
+ /* Expect forward edge, so pass: previous, current */
+ this->process_edge(value, value0,
+ this->depth - this->sampling[DEPTH_IDX],
+ row, col, 2,
+ 0, slice0, slice1);
+ }
+
+ }
+ }
+
+ /* Loop over cubes to add triangle indices */
+ for (row=0; row < this->height - this->sampling[HEIGHT_IDX]; row += this->sampling[HEIGHT_IDX]) {
+ for (col=0; col < this->width - this->sampling[WIDTH_IDX]; col += this->sampling[WIDTH_IDX]) {
+ unsigned char code = this->get_cell_code(slice0, slice1,
+ row, col);
+
+ if (code == 0) {
+ continue;
+ }
+
+ const int * edgeIndexPtr = &MCTriangleTable[code][0];
+ for (; *edgeIndexPtr >= 0; edgeIndexPtr++) {
+ const unsigned int * offsets = \
+ MCEdgeIndexToCoordOffsets[*edgeIndexPtr];
+
+ unsigned int edge_index = this->edge_index(
+ this->depth - this->sampling[DEPTH_IDX] + offsets[DEPTH_IDX] * this->sampling[DEPTH_IDX],
+ row + offsets[HEIGHT_IDX] * this->sampling[HEIGHT_IDX],
+ col + offsets[WIDTH_IDX] * this->sampling[WIDTH_IDX],
+ offsets[3]);
+
+ /* Add vertex index to the list of indices */
+ std::map<unsigned int, unsigned int>::iterator it, end;
+ if (offsets[DEPTH_IDX] == 0 && offsets[3] != 2) {
+ it = previous_edge_indices->find(edge_index);
+ end = previous_edge_indices->end();
+ } else {
+ it = this->edge_indices->find(edge_index);
+ end = this->edge_indices->end();
+ }
+ if (it == end) {
+ throw std::runtime_error(
+ "Internal error: cannot build triangle indices.");
+ }
+ else {
+ this->indices.push_back(it->second);
+ }
+ }
+
+ }
+ }
+
+ /* Clean-up previous slice cache */
+ delete previous_edge_indices;
+
+ this->depth += this->sampling[DEPTH_IDX];
+}
+
+
+template <typename FloatIn, typename FloatOut>
+void
+MarchingCubes<FloatIn, FloatOut>::first_slice(const FloatIn * slice,
+ const FloatIn * next)
+{
+ assert(slice != NULL);
+ assert(next != NULL);
+ /* Init cache for this slice */
+ this->edge_indices = new std::map<unsigned int, unsigned int>();
+
+ unsigned int row, col;
+
+ /* Loop over slice, and add isosurface vertices in the slice plane */
+ for (row=0; row < this->height; row += this->sampling[HEIGHT_IDX]) {
+ unsigned int line_index = row * this->width;
+
+ for (col=0; col < this->width; col += this->sampling[WIDTH_IDX]) {
+ unsigned int item_index = line_index + col;
+
+ /* For each point test forward edges */
+ FloatIn value0 = slice[item_index];
+
+ if (col < (width - this->sampling[WIDTH_IDX])) {
+ FloatIn value = slice[item_index + this->sampling[WIDTH_IDX]];
+
+ this->process_edge(value0, value, this->depth, row, col, 0,
+ 0, slice, next);
+ }
+
+ if (row < (height - this->sampling[HEIGHT_IDX])) {
+ /* Value from next line */
+ FloatIn value = slice[item_index + this->width * this->sampling[HEIGHT_IDX]];
+
+ this->process_edge(value0, value, this->depth, row, col, 1,
+ 0, slice, next);
+ }
+ }
+ }
+
+ this->depth += this->sampling[DEPTH_IDX];
+}
+
+
+template <typename FloatIn, typename FloatOut>
+inline unsigned int
+MarchingCubes<FloatIn, FloatOut>::edge_index(const unsigned int depth,
+ const unsigned int row,
+ const unsigned int col,
+ const unsigned int direction)
+{
+ return ((depth * (this->height + 1) + row) *
+ (this->width + 1) + col) * 3 + direction;
+}
+
+
+template <typename FloatIn, typename FloatOut>
+inline void
+MarchingCubes<FloatIn, FloatOut>::process_edge(const FloatIn value0,
+ const FloatIn value,
+ const unsigned int depth,
+ const unsigned int row,
+ const unsigned int col,
+ const unsigned int direction,
+ const FloatIn * previous,
+ const FloatIn * current,
+ const FloatIn * next)
+{
+ assert(current != NULL);
+
+ if ((value0 <= this->isolevel) ^ (value <= this->isolevel)) {
+
+ /* Crossing iso-surface, store it */
+ FloatIn offset = (this->isolevel - value0) / (value - value0);
+
+ /* Store edge to vertex index correspondance */
+ unsigned int edge_index = this->edge_index(depth, row, col, direction);
+ (*this->edge_indices)[edge_index] = this->vertices.size() / 3;
+
+ /* Store vertex as (z, y, x) */
+ if (direction == 0) {
+ this->vertices.push_back((FloatOut) depth);
+ this->vertices.push_back((FloatOut) row);
+ this->vertices.push_back(
+ (FloatOut) col + offset * this->sampling[WIDTH_IDX]);
+ }
+ else if (direction == 1) {
+ this->vertices.push_back((FloatOut) depth);
+ this->vertices.push_back(
+ (FloatOut) row + offset * this->sampling[HEIGHT_IDX]);
+ this->vertices.push_back((FloatOut) col);
+ }
+ else if (direction == 2) {
+ this->vertices.push_back(
+ (FloatOut) depth + offset * this->sampling[DEPTH_IDX]);
+ this->vertices.push_back((FloatOut) row);
+ this->vertices.push_back((FloatOut) col);
+ } else {
+ throw std::runtime_error(
+ "Internal error: dimension > 3, never event.");
+ }
+
+ /* Store normal as (nz, ny, nx) */
+ FloatOut nz, ny, nx;
+ const FloatIn * slice0 = (previous != 0) ? previous : current;
+ const FloatIn * slice1 = (previous != 0) ? current : next;
+
+ unsigned int row_offset = this->width * this->sampling[HEIGHT_IDX];
+
+ if (direction == 0) {
+ { /* nz */
+ unsigned int item, item_next_col;
+
+ item = row * this->width + col;
+ if (col >= this->width - this->sampling[WIDTH_IDX]) {
+ /* For last column, use previous column */
+ item -= this->sampling[WIDTH_IDX];
+ }
+ item_next_col = item + this->sampling[WIDTH_IDX];
+
+ nz = ((1. - offset) * (slice1[item] - slice0[item]) +
+ offset * (slice1[item_next_col] - slice0[item_next_col]));
+ }
+
+ { /* ny */
+ unsigned int item, item_next_col;
+
+ item = row * this->width + col;
+ if (row >= this->height - this->sampling[HEIGHT_IDX]) {
+ /* For last row, use previous row */
+ item -= row_offset;
+ }
+ if (col >= this->width - this->sampling[WIDTH_IDX]) {
+ /* For last column, use previous column */
+ item -= this->sampling[WIDTH_IDX];
+ }
+ item_next_col = item + this->sampling[WIDTH_IDX];
+
+ ny = ((1. - offset) * (current[item + row_offset] -
+ current[item]) +
+ offset * (current[item_next_col + row_offset] -
+ current[item_next_col]));
+ }
+
+ nx = value - value0;
+
+ } else if (direction == 1) {
+ { /* nz */
+ unsigned int item, item_next_row;
+
+ item = row * this->width + col;
+ if (row >= this->height - this->sampling[HEIGHT_IDX]) {
+ /* For last row, use previous row */
+ item -= row_offset;
+ }
+ item_next_row = item + row_offset;
+
+ nz = ((1. - offset) * (slice1[item] - slice0[item]) +
+ offset * (slice1[item_next_row] - slice0[item_next_row]));
+ }
+
+ ny = value - value0;
+
+ { /* nx */
+ unsigned int item, item_next_row;
+
+ item = row * this->width + col;
+ if (row >= this->height - this->sampling[HEIGHT_IDX]) {
+ /* For last row, use previous row */
+ item -= row_offset;
+ }
+ if (col >= this->width - this->sampling[WIDTH_IDX]) {
+ /* For last column, use previous column */
+ item -= this->sampling[WIDTH_IDX];
+ }
+
+ item_next_row = item + row_offset;
+
+ nx = ((1. - offset) * (current[item + this->sampling[WIDTH_IDX]] - current[item]) +
+ offset * (current[item_next_row + this->sampling[WIDTH_IDX]] - current[item_next_row]));
+ }
+
+ } else { /* direction == 2 */
+ assert(direction == 2);
+ /* Previous should always be 0, only here in case this changes */
+ const FloatIn * other_slice = (previous != 0) ? previous : next;
+
+ nz = value - value0;
+
+ { /* ny */
+ unsigned int item, item_next_row;
+
+ item = row * this->width + col;
+ if (row >= this->height - this->sampling[HEIGHT_IDX]) {
+ /* For last row, use previous row */
+ item -= row_offset;
+ }
+ item_next_row = item + row_offset;
+
+ ny = ((1. - offset) * (current[item_next_row] - current[item]) +
+ offset * (other_slice[item_next_row] - other_slice[item]));
+ }
+
+ { /* nx */
+ unsigned int item;
+
+ item = row * this->width + col;
+ if (col >= this->width - this->sampling[WIDTH_IDX]) {
+ /* For last column, use previous column */
+ item -= this->sampling[WIDTH_IDX];
+ }
+ const unsigned int item_next_col = item + this->sampling[WIDTH_IDX];
+
+ nx = ((1. - offset) * (current[item_next_col] - current[item]) +
+ offset * (other_slice[item_next_col] - other_slice[item]));
+ }
+ }
+
+ /* apply sampling scaling */
+ nz /= (FloatOut) this->sampling[0];
+ ny /= (FloatOut) this->sampling[1];
+ nx /= (FloatOut) this->sampling[2];
+
+ /* normalisation */
+ FloatOut norm = sqrt(nz * nz + ny * ny + nx * nx);
+ if (this->invert_normals) { /* Normal inversion */
+ norm *= -1.;
+ }
+
+ if (norm != 0) {
+ nz /= norm;
+ ny /= norm;
+ nx /= norm;
+ }
+ this->normals.push_back(nz);
+ this->normals.push_back(ny);
+ this->normals.push_back(nx);
+ }
+}
+
+
+template <typename FloatIn, typename FloatOut>
+inline unsigned char
+MarchingCubes<FloatIn, FloatOut>::get_cell_code(const FloatIn * slice1,
+ const FloatIn * slice2,
+ const unsigned int row,
+ const unsigned int col)
+{
+ assert(slice1 != NULL);
+ assert(slice2 != NULL);
+ unsigned int item = row * this->width + col;
+ unsigned int item_next_row = item + this->width * this->sampling[HEIGHT_IDX];
+ unsigned char code = 0;
+
+ /* Cube convention for cell code:
+ * WARNING: This differ from layout in memory
+ *
+ * 4 +------+ 5
+ * /| /|
+ * / | / |
+ * 7 +------+ 6|
+ * | | | |
+ * |0 +---|--+ 1
+ * | / | /
+ * |/ |/
+ * 3 +------+ 2
+ *
+ */
+ /* First slice */
+ if (slice1[item] <= this->isolevel) {
+ code |= 1 << 0;
+ }
+ if (slice1[item + this->sampling[WIDTH_IDX]] <= this->isolevel) {
+ code |= 1 << 1;
+ }
+ if (slice1[item_next_row + this->sampling[WIDTH_IDX]] <= this->isolevel) {
+ code |= 1 << 2;
+ }
+ if (slice1[item_next_row] <= this->isolevel) {
+ code |= 1 << 3;
+ }
+
+ /* Second slice */
+ if (slice2[item] <= this->isolevel) {
+ code |= 1 << 4;
+ }
+ if (slice2[item + this->sampling[WIDTH_IDX]] <= this->isolevel) {
+ code |= 1 << 5;
+ }
+ if (slice2[item_next_row + this->sampling[WIDTH_IDX]] <= this->isolevel) {
+ code |= 1 << 6;
+ }
+ if (slice2[item_next_row] <= this->isolevel) {
+ code |= 1 << 7;
+ }
+
+ return code;
+}
+
+#endif /*__mc_HPP__*/
diff --git a/src/silx/math/marchingcubes/mc_lut.cpp b/src/silx/math/marchingcubes/mc_lut.cpp
new file mode 100644
index 0000000..7998f1b
--- /dev/null
+++ b/src/silx/math/marchingcubes/mc_lut.cpp
@@ -0,0 +1,316 @@
+# /*##########################################################################
+#
+# Copyright (c) 2015-2016 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+
+#include "mc.hpp"
+
+
+/** Gives edge index of triangles vertices for each of the 256 possible cubes.
+ *
+ * Table taken from http://paulbourke.net/geometry/polygonise/
+ * Author: Cory Bloyd
+ * Originially this code is public domain,
+ * relicensed here as MIT to provide a license.
+ *
+ * The cube index is a bit mask of cube corners <= isoValue.
+ * See vertexOffset for the place of each corner in the bit mask.
+ */
+const int MCTriangleTable[256][16] = {
+ {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {0, 8, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {0, 1, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {1, 8, 3, 9, 8, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {1, 2, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {0, 8, 3, 1, 2, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {9, 2, 10, 0, 2, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {2, 8, 3, 2, 10, 8, 10, 9, 8, -1, -1, -1, -1, -1, -1, -1},
+ {3, 11, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {0, 11, 2, 8, 11, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {1, 9, 0, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {1, 11, 2, 1, 9, 11, 9, 8, 11, -1, -1, -1, -1, -1, -1, -1},
+ {3, 10, 1, 11, 10, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {0, 10, 1, 0, 8, 10, 8, 11, 10, -1, -1, -1, -1, -1, -1, -1},
+ {3, 9, 0, 3, 11, 9, 11, 10, 9, -1, -1, -1, -1, -1, -1, -1},
+ {9, 8, 10, 10, 8, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {4, 7, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {4, 3, 0, 7, 3, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {0, 1, 9, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {4, 1, 9, 4, 7, 1, 7, 3, 1, -1, -1, -1, -1, -1, -1, -1},
+ {1, 2, 10, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {3, 4, 7, 3, 0, 4, 1, 2, 10, -1, -1, -1, -1, -1, -1, -1},
+ {9, 2, 10, 9, 0, 2, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1},
+ {2, 10, 9, 2, 9, 7, 2, 7, 3, 7, 9, 4, -1, -1, -1, -1},
+ {8, 4, 7, 3, 11, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {11, 4, 7, 11, 2, 4, 2, 0, 4, -1, -1, -1, -1, -1, -1, -1},
+ {9, 0, 1, 8, 4, 7, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1},
+ {4, 7, 11, 9, 4, 11, 9, 11, 2, 9, 2, 1, -1, -1, -1, -1},
+ {3, 10, 1, 3, 11, 10, 7, 8, 4, -1, -1, -1, -1, -1, -1, -1},
+ {1, 11, 10, 1, 4, 11, 1, 0, 4, 7, 11, 4, -1, -1, -1, -1},
+ {4, 7, 8, 9, 0, 11, 9, 11, 10, 11, 0, 3, -1, -1, -1, -1},
+ {4, 7, 11, 4, 11, 9, 9, 11, 10, -1, -1, -1, -1, -1, -1, -1},
+ {9, 5, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {9, 5, 4, 0, 8, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {0, 5, 4, 1, 5, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {8, 5, 4, 8, 3, 5, 3, 1, 5, -1, -1, -1, -1, -1, -1, -1},
+ {1, 2, 10, 9, 5, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {3, 0, 8, 1, 2, 10, 4, 9, 5, -1, -1, -1, -1, -1, -1, -1},
+ {5, 2, 10, 5, 4, 2, 4, 0, 2, -1, -1, -1, -1, -1, -1, -1},
+ {2, 10, 5, 3, 2, 5, 3, 5, 4, 3, 4, 8, -1, -1, -1, -1},
+ {9, 5, 4, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {0, 11, 2, 0, 8, 11, 4, 9, 5, -1, -1, -1, -1, -1, -1, -1},
+ {0, 5, 4, 0, 1, 5, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1},
+ {2, 1, 5, 2, 5, 8, 2, 8, 11, 4, 8, 5, -1, -1, -1, -1},
+ {10, 3, 11, 10, 1, 3, 9, 5, 4, -1, -1, -1, -1, -1, -1, -1},
+ {4, 9, 5, 0, 8, 1, 8, 10, 1, 8, 11, 10, -1, -1, -1, -1},
+ {5, 4, 0, 5, 0, 11, 5, 11, 10, 11, 0, 3, -1, -1, -1, -1},
+ {5, 4, 8, 5, 8, 10, 10, 8, 11, -1, -1, -1, -1, -1, -1, -1},
+ {9, 7, 8, 5, 7, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {9, 3, 0, 9, 5, 3, 5, 7, 3, -1, -1, -1, -1, -1, -1, -1},
+ {0, 7, 8, 0, 1, 7, 1, 5, 7, -1, -1, -1, -1, -1, -1, -1},
+ {1, 5, 3, 3, 5, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {9, 7, 8, 9, 5, 7, 10, 1, 2, -1, -1, -1, -1, -1, -1, -1},
+ {10, 1, 2, 9, 5, 0, 5, 3, 0, 5, 7, 3, -1, -1, -1, -1},
+ {8, 0, 2, 8, 2, 5, 8, 5, 7, 10, 5, 2, -1, -1, -1, -1},
+ {2, 10, 5, 2, 5, 3, 3, 5, 7, -1, -1, -1, -1, -1, -1, -1},
+ {7, 9, 5, 7, 8, 9, 3, 11, 2, -1, -1, -1, -1, -1, -1, -1},
+ {9, 5, 7, 9, 7, 2, 9, 2, 0, 2, 7, 11, -1, -1, -1, -1},
+ {2, 3, 11, 0, 1, 8, 1, 7, 8, 1, 5, 7, -1, -1, -1, -1},
+ {11, 2, 1, 11, 1, 7, 7, 1, 5, -1, -1, -1, -1, -1, -1, -1},
+ {9, 5, 8, 8, 5, 7, 10, 1, 3, 10, 3, 11, -1, -1, -1, -1},
+ {5, 7, 0, 5, 0, 9, 7, 11, 0, 1, 0, 10, 11, 10, 0, -1},
+ {11, 10, 0, 11, 0, 3, 10, 5, 0, 8, 0, 7, 5, 7, 0, -1},
+ {11, 10, 5, 7, 11, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {10, 6, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {0, 8, 3, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {9, 0, 1, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {1, 8, 3, 1, 9, 8, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1},
+ {1, 6, 5, 2, 6, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {1, 6, 5, 1, 2, 6, 3, 0, 8, -1, -1, -1, -1, -1, -1, -1},
+ {9, 6, 5, 9, 0, 6, 0, 2, 6, -1, -1, -1, -1, -1, -1, -1},
+ {5, 9, 8, 5, 8, 2, 5, 2, 6, 3, 2, 8, -1, -1, -1, -1},
+ {2, 3, 11, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {11, 0, 8, 11, 2, 0, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1},
+ {0, 1, 9, 2, 3, 11, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1},
+ {5, 10, 6, 1, 9, 2, 9, 11, 2, 9, 8, 11, -1, -1, -1, -1},
+ {6, 3, 11, 6, 5, 3, 5, 1, 3, -1, -1, -1, -1, -1, -1, -1},
+ {0, 8, 11, 0, 11, 5, 0, 5, 1, 5, 11, 6, -1, -1, -1, -1},
+ {3, 11, 6, 0, 3, 6, 0, 6, 5, 0, 5, 9, -1, -1, -1, -1},
+ {6, 5, 9, 6, 9, 11, 11, 9, 8, -1, -1, -1, -1, -1, -1, -1},
+ {5, 10, 6, 4, 7, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {4, 3, 0, 4, 7, 3, 6, 5, 10, -1, -1, -1, -1, -1, -1, -1},
+ {1, 9, 0, 5, 10, 6, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1},
+ {10, 6, 5, 1, 9, 7, 1, 7, 3, 7, 9, 4, -1, -1, -1, -1},
+ {6, 1, 2, 6, 5, 1, 4, 7, 8, -1, -1, -1, -1, -1, -1, -1},
+ {1, 2, 5, 5, 2, 6, 3, 0, 4, 3, 4, 7, -1, -1, -1, -1},
+ {8, 4, 7, 9, 0, 5, 0, 6, 5, 0, 2, 6, -1, -1, -1, -1},
+ {7, 3, 9, 7, 9, 4, 3, 2, 9, 5, 9, 6, 2, 6, 9, -1},
+ {3, 11, 2, 7, 8, 4, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1},
+ {5, 10, 6, 4, 7, 2, 4, 2, 0, 2, 7, 11, -1, -1, -1, -1},
+ {0, 1, 9, 4, 7, 8, 2, 3, 11, 5, 10, 6, -1, -1, -1, -1},
+ {9, 2, 1, 9, 11, 2, 9, 4, 11, 7, 11, 4, 5, 10, 6, -1},
+ {8, 4, 7, 3, 11, 5, 3, 5, 1, 5, 11, 6, -1, -1, -1, -1},
+ {5, 1, 11, 5, 11, 6, 1, 0, 11, 7, 11, 4, 0, 4, 11, -1},
+ {0, 5, 9, 0, 6, 5, 0, 3, 6, 11, 6, 3, 8, 4, 7, -1},
+ {6, 5, 9, 6, 9, 11, 4, 7, 9, 7, 11, 9, -1, -1, -1, -1},
+ {10, 4, 9, 6, 4, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {4, 10, 6, 4, 9, 10, 0, 8, 3, -1, -1, -1, -1, -1, -1, -1},
+ {10, 0, 1, 10, 6, 0, 6, 4, 0, -1, -1, -1, -1, -1, -1, -1},
+ {8, 3, 1, 8, 1, 6, 8, 6, 4, 6, 1, 10, -1, -1, -1, -1},
+ {1, 4, 9, 1, 2, 4, 2, 6, 4, -1, -1, -1, -1, -1, -1, -1},
+ {3, 0, 8, 1, 2, 9, 2, 4, 9, 2, 6, 4, -1, -1, -1, -1},
+ {0, 2, 4, 4, 2, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {8, 3, 2, 8, 2, 4, 4, 2, 6, -1, -1, -1, -1, -1, -1, -1},
+ {10, 4, 9, 10, 6, 4, 11, 2, 3, -1, -1, -1, -1, -1, -1, -1},
+ {0, 8, 2, 2, 8, 11, 4, 9, 10, 4, 10, 6, -1, -1, -1, -1},
+ {3, 11, 2, 0, 1, 6, 0, 6, 4, 6, 1, 10, -1, -1, -1, -1},
+ {6, 4, 1, 6, 1, 10, 4, 8, 1, 2, 1, 11, 8, 11, 1, -1},
+ {9, 6, 4, 9, 3, 6, 9, 1, 3, 11, 6, 3, -1, -1, -1, -1},
+ {8, 11, 1, 8, 1, 0, 11, 6, 1, 9, 1, 4, 6, 4, 1, -1},
+ {3, 11, 6, 3, 6, 0, 0, 6, 4, -1, -1, -1, -1, -1, -1, -1},
+ {6, 4, 8, 11, 6, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {7, 10, 6, 7, 8, 10, 8, 9, 10, -1, -1, -1, -1, -1, -1, -1},
+ {0, 7, 3, 0, 10, 7, 0, 9, 10, 6, 7, 10, -1, -1, -1, -1},
+ {10, 6, 7, 1, 10, 7, 1, 7, 8, 1, 8, 0, -1, -1, -1, -1},
+ {10, 6, 7, 10, 7, 1, 1, 7, 3, -1, -1, -1, -1, -1, -1, -1},
+ {1, 2, 6, 1, 6, 8, 1, 8, 9, 8, 6, 7, -1, -1, -1, -1},
+ {2, 6, 9, 2, 9, 1, 6, 7, 9, 0, 9, 3, 7, 3, 9, -1},
+ {7, 8, 0, 7, 0, 6, 6, 0, 2, -1, -1, -1, -1, -1, -1, -1},
+ {7, 3, 2, 6, 7, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {2, 3, 11, 10, 6, 8, 10, 8, 9, 8, 6, 7, -1, -1, -1, -1},
+ {2, 0, 7, 2, 7, 11, 0, 9, 7, 6, 7, 10, 9, 10, 7, -1},
+ {1, 8, 0, 1, 7, 8, 1, 10, 7, 6, 7, 10, 2, 3, 11, -1},
+ {11, 2, 1, 11, 1, 7, 10, 6, 1, 6, 7, 1, -1, -1, -1, -1},
+ {8, 9, 6, 8, 6, 7, 9, 1, 6, 11, 6, 3, 1, 3, 6, -1},
+ {0, 9, 1, 11, 6, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {7, 8, 0, 7, 0, 6, 3, 11, 0, 11, 6, 0, -1, -1, -1, -1},
+ {7, 11, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {7, 6, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {3, 0, 8, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {0, 1, 9, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {8, 1, 9, 8, 3, 1, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1},
+ {10, 1, 2, 6, 11, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {1, 2, 10, 3, 0, 8, 6, 11, 7, -1, -1, -1, -1, -1, -1, -1},
+ {2, 9, 0, 2, 10, 9, 6, 11, 7, -1, -1, -1, -1, -1, -1, -1},
+ {6, 11, 7, 2, 10, 3, 10, 8, 3, 10, 9, 8, -1, -1, -1, -1},
+ {7, 2, 3, 6, 2, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {7, 0, 8, 7, 6, 0, 6, 2, 0, -1, -1, -1, -1, -1, -1, -1},
+ {2, 7, 6, 2, 3, 7, 0, 1, 9, -1, -1, -1, -1, -1, -1, -1},
+ {1, 6, 2, 1, 8, 6, 1, 9, 8, 8, 7, 6, -1, -1, -1, -1},
+ {10, 7, 6, 10, 1, 7, 1, 3, 7, -1, -1, -1, -1, -1, -1, -1},
+ {10, 7, 6, 1, 7, 10, 1, 8, 7, 1, 0, 8, -1, -1, -1, -1},
+ {0, 3, 7, 0, 7, 10, 0, 10, 9, 6, 10, 7, -1, -1, -1, -1},
+ {7, 6, 10, 7, 10, 8, 8, 10, 9, -1, -1, -1, -1, -1, -1, -1},
+ {6, 8, 4, 11, 8, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {3, 6, 11, 3, 0, 6, 0, 4, 6, -1, -1, -1, -1, -1, -1, -1},
+ {8, 6, 11, 8, 4, 6, 9, 0, 1, -1, -1, -1, -1, -1, -1, -1},
+ {9, 4, 6, 9, 6, 3, 9, 3, 1, 11, 3, 6, -1, -1, -1, -1},
+ {6, 8, 4, 6, 11, 8, 2, 10, 1, -1, -1, -1, -1, -1, -1, -1},
+ {1, 2, 10, 3, 0, 11, 0, 6, 11, 0, 4, 6, -1, -1, -1, -1},
+ {4, 11, 8, 4, 6, 11, 0, 2, 9, 2, 10, 9, -1, -1, -1, -1},
+ {10, 9, 3, 10, 3, 2, 9, 4, 3, 11, 3, 6, 4, 6, 3, -1},
+ {8, 2, 3, 8, 4, 2, 4, 6, 2, -1, -1, -1, -1, -1, -1, -1},
+ {0, 4, 2, 4, 6, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {1, 9, 0, 2, 3, 4, 2, 4, 6, 4, 3, 8, -1, -1, -1, -1},
+ {1, 9, 4, 1, 4, 2, 2, 4, 6, -1, -1, -1, -1, -1, -1, -1},
+ {8, 1, 3, 8, 6, 1, 8, 4, 6, 6, 10, 1, -1, -1, -1, -1},
+ {10, 1, 0, 10, 0, 6, 6, 0, 4, -1, -1, -1, -1, -1, -1, -1},
+ {4, 6, 3, 4, 3, 8, 6, 10, 3, 0, 3, 9, 10, 9, 3, -1},
+ {10, 9, 4, 6, 10, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {4, 9, 5, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {0, 8, 3, 4, 9, 5, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1},
+ {5, 0, 1, 5, 4, 0, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1},
+ {11, 7, 6, 8, 3, 4, 3, 5, 4, 3, 1, 5, -1, -1, -1, -1},
+ {9, 5, 4, 10, 1, 2, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1},
+ {6, 11, 7, 1, 2, 10, 0, 8, 3, 4, 9, 5, -1, -1, -1, -1},
+ {7, 6, 11, 5, 4, 10, 4, 2, 10, 4, 0, 2, -1, -1, -1, -1},
+ {3, 4, 8, 3, 5, 4, 3, 2, 5, 10, 5, 2, 11, 7, 6, -1},
+ {7, 2, 3, 7, 6, 2, 5, 4, 9, -1, -1, -1, -1, -1, -1, -1},
+ {9, 5, 4, 0, 8, 6, 0, 6, 2, 6, 8, 7, -1, -1, -1, -1},
+ {3, 6, 2, 3, 7, 6, 1, 5, 0, 5, 4, 0, -1, -1, -1, -1},
+ {6, 2, 8, 6, 8, 7, 2, 1, 8, 4, 8, 5, 1, 5, 8, -1},
+ {9, 5, 4, 10, 1, 6, 1, 7, 6, 1, 3, 7, -1, -1, -1, -1},
+ {1, 6, 10, 1, 7, 6, 1, 0, 7, 8, 7, 0, 9, 5, 4, -1},
+ {4, 0, 10, 4, 10, 5, 0, 3, 10, 6, 10, 7, 3, 7, 10, -1},
+ {7, 6, 10, 7, 10, 8, 5, 4, 10, 4, 8, 10, -1, -1, -1, -1},
+ {6, 9, 5, 6, 11, 9, 11, 8, 9, -1, -1, -1, -1, -1, -1, -1},
+ {3, 6, 11, 0, 6, 3, 0, 5, 6, 0, 9, 5, -1, -1, -1, -1},
+ {0, 11, 8, 0, 5, 11, 0, 1, 5, 5, 6, 11, -1, -1, -1, -1},
+ {6, 11, 3, 6, 3, 5, 5, 3, 1, -1, -1, -1, -1, -1, -1, -1},
+ {1, 2, 10, 9, 5, 11, 9, 11, 8, 11, 5, 6, -1, -1, -1, -1},
+ {0, 11, 3, 0, 6, 11, 0, 9, 6, 5, 6, 9, 1, 2, 10, -1},
+ {11, 8, 5, 11, 5, 6, 8, 0, 5, 10, 5, 2, 0, 2, 5, -1},
+ {6, 11, 3, 6, 3, 5, 2, 10, 3, 10, 5, 3, -1, -1, -1, -1},
+ {5, 8, 9, 5, 2, 8, 5, 6, 2, 3, 8, 2, -1, -1, -1, -1},
+ {9, 5, 6, 9, 6, 0, 0, 6, 2, -1, -1, -1, -1, -1, -1, -1},
+ {1, 5, 8, 1, 8, 0, 5, 6, 8, 3, 8, 2, 6, 2, 8, -1},
+ {1, 5, 6, 2, 1, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {1, 3, 6, 1, 6, 10, 3, 8, 6, 5, 6, 9, 8, 9, 6, -1},
+ {10, 1, 0, 10, 0, 6, 9, 5, 0, 5, 6, 0, -1, -1, -1, -1},
+ {0, 3, 8, 5, 6, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {10, 5, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {11, 5, 10, 7, 5, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {11, 5, 10, 11, 7, 5, 8, 3, 0, -1, -1, -1, -1, -1, -1, -1},
+ {5, 11, 7, 5, 10, 11, 1, 9, 0, -1, -1, -1, -1, -1, -1, -1},
+ {10, 7, 5, 10, 11, 7, 9, 8, 1, 8, 3, 1, -1, -1, -1, -1},
+ {11, 1, 2, 11, 7, 1, 7, 5, 1, -1, -1, -1, -1, -1, -1, -1},
+ {0, 8, 3, 1, 2, 7, 1, 7, 5, 7, 2, 11, -1, -1, -1, -1},
+ {9, 7, 5, 9, 2, 7, 9, 0, 2, 2, 11, 7, -1, -1, -1, -1},
+ {7, 5, 2, 7, 2, 11, 5, 9, 2, 3, 2, 8, 9, 8, 2, -1},
+ {2, 5, 10, 2, 3, 5, 3, 7, 5, -1, -1, -1, -1, -1, -1, -1},
+ {8, 2, 0, 8, 5, 2, 8, 7, 5, 10, 2, 5, -1, -1, -1, -1},
+ {9, 0, 1, 5, 10, 3, 5, 3, 7, 3, 10, 2, -1, -1, -1, -1},
+ {9, 8, 2, 9, 2, 1, 8, 7, 2, 10, 2, 5, 7, 5, 2, -1},
+ {1, 3, 5, 3, 7, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {0, 8, 7, 0, 7, 1, 1, 7, 5, -1, -1, -1, -1, -1, -1, -1},
+ {9, 0, 3, 9, 3, 5, 5, 3, 7, -1, -1, -1, -1, -1, -1, -1},
+ {9, 8, 7, 5, 9, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {5, 8, 4, 5, 10, 8, 10, 11, 8, -1, -1, -1, -1, -1, -1, -1},
+ {5, 0, 4, 5, 11, 0, 5, 10, 11, 11, 3, 0, -1, -1, -1, -1},
+ {0, 1, 9, 8, 4, 10, 8, 10, 11, 10, 4, 5, -1, -1, -1, -1},
+ {10, 11, 4, 10, 4, 5, 11, 3, 4, 9, 4, 1, 3, 1, 4, -1},
+ {2, 5, 1, 2, 8, 5, 2, 11, 8, 4, 5, 8, -1, -1, -1, -1},
+ {0, 4, 11, 0, 11, 3, 4, 5, 11, 2, 11, 1, 5, 1, 11, -1},
+ {0, 2, 5, 0, 5, 9, 2, 11, 5, 4, 5, 8, 11, 8, 5, -1},
+ {9, 4, 5, 2, 11, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {2, 5, 10, 3, 5, 2, 3, 4, 5, 3, 8, 4, -1, -1, -1, -1},
+ {5, 10, 2, 5, 2, 4, 4, 2, 0, -1, -1, -1, -1, -1, -1, -1},
+ {3, 10, 2, 3, 5, 10, 3, 8, 5, 4, 5, 8, 0, 1, 9, -1},
+ {5, 10, 2, 5, 2, 4, 1, 9, 2, 9, 4, 2, -1, -1, -1, -1},
+ {8, 4, 5, 8, 5, 3, 3, 5, 1, -1, -1, -1, -1, -1, -1, -1},
+ {0, 4, 5, 1, 0, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {8, 4, 5, 8, 5, 3, 9, 0, 5, 0, 3, 5, -1, -1, -1, -1},
+ {9, 4, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {4, 11, 7, 4, 9, 11, 9, 10, 11, -1, -1, -1, -1, -1, -1, -1},
+ {0, 8, 3, 4, 9, 7, 9, 11, 7, 9, 10, 11, -1, -1, -1, -1},
+ {1, 10, 11, 1, 11, 4, 1, 4, 0, 7, 4, 11, -1, -1, -1, -1},
+ {3, 1, 4, 3, 4, 8, 1, 10, 4, 7, 4, 11, 10, 11, 4, -1},
+ {4, 11, 7, 9, 11, 4, 9, 2, 11, 9, 1, 2, -1, -1, -1, -1},
+ {9, 7, 4, 9, 11, 7, 9, 1, 11, 2, 11, 1, 0, 8, 3, -1},
+ {11, 7, 4, 11, 4, 2, 2, 4, 0, -1, -1, -1, -1, -1, -1, -1},
+ {11, 7, 4, 11, 4, 2, 8, 3, 4, 3, 2, 4, -1, -1, -1, -1},
+ {2, 9, 10, 2, 7, 9, 2, 3, 7, 7, 4, 9, -1, -1, -1, -1},
+ {9, 10, 7, 9, 7, 4, 10, 2, 7, 8, 7, 0, 2, 0, 7, -1},
+ {3, 7, 10, 3, 10, 2, 7, 4, 10, 1, 10, 0, 4, 0, 10, -1},
+ {1, 10, 2, 8, 7, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {4, 9, 1, 4, 1, 7, 7, 1, 3, -1, -1, -1, -1, -1, -1, -1},
+ {4, 9, 1, 4, 1, 7, 0, 8, 1, 8, 7, 1, -1, -1, -1, -1},
+ {4, 0, 3, 7, 4, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {4, 8, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {9, 10, 8, 10, 11, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {3, 0, 9, 3, 9, 11, 11, 9, 10, -1, -1, -1, -1, -1, -1, -1},
+ {0, 1, 10, 0, 10, 8, 8, 10, 11, -1, -1, -1, -1, -1, -1, -1},
+ {3, 1, 10, 11, 3, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {1, 2, 11, 1, 11, 9, 9, 11, 8, -1, -1, -1, -1, -1, -1, -1},
+ {3, 0, 9, 3, 9, 11, 1, 2, 9, 2, 11, 9, -1, -1, -1, -1},
+ {0, 2, 11, 8, 0, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {3, 2, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {2, 3, 8, 2, 8, 10, 10, 8, 9, -1, -1, -1, -1, -1, -1, -1},
+ {9, 10, 2, 0, 9, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {2, 3, 8, 2, 8, 10, 0, 1, 8, 1, 10, 8, -1, -1, -1, -1},
+ {1, 10, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {1, 3, 8, 9, 1, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {0, 9, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {0, 3, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
+ {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}
+};
+
+
+/** List edge origin and direction for each edge index in [0-12).
+ *
+ * For each edge, gives the first vertices as 3 coordinates from the origin
+ * of the cube and the direction of the edge as the 4th value.
+ */
+const unsigned int MCEdgeIndexToCoordOffsets[12][4] = {
+ {0, 0, 0, 0},
+ {0, 0, 1, 1},
+ {0, 1, 0, 0},
+ {0, 0, 0, 1},
+ {1, 0, 0, 0},
+ {1, 0, 1, 1},
+ {1, 1, 0, 0},
+ {1, 0, 0, 1},
+ {0, 0, 0, 2},
+ {0, 0, 1, 2},
+ {0, 1, 1, 2},
+ {0, 1, 0, 2}
+};
diff --git a/src/silx/math/math_compatibility.pxd b/src/silx/math/math_compatibility.pxd
new file mode 100644
index 0000000..ddaa550
--- /dev/null
+++ b/src/silx/math/math_compatibility.pxd
@@ -0,0 +1,35 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2018 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
+
+# Provides Visual Studio 2008 missing math functions/macros
+
+cdef extern from "math_compatibility.h":
+ double asinh(double x) nogil
+ bint isnan(double x) nogil
+ bint isfinite(double x) nogil
+ long int lrint(double x) nogil
+
+ double INFINITY
+ double NAN
diff --git a/src/silx/math/mc.pxd b/src/silx/math/mc.pxd
new file mode 100644
index 0000000..b1c81e7
--- /dev/null
+++ b/src/silx/math/mc.pxd
@@ -0,0 +1,51 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2015-2016 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+
+from libcpp.vector cimport vector as std_vector
+from libcpp cimport bool
+
+cdef extern from "mc.hpp":
+ cdef cppclass MarchingCubes[FloatIn, FloatOut]:
+ MarchingCubes(FloatIn level) except +
+ void process(FloatIn * data,
+ unsigned int depth,
+ unsigned int height,
+ unsigned int width) except +
+ void set_slice_size(unsigned int height,
+ unsigned int width)
+ void process_slice(FloatIn * slice0,
+ FloatIn * slice1) except +
+ void finish_process()
+ void reset()
+
+ unsigned int depth
+ unsigned int height
+ unsigned int width
+ unsigned int sampling[3]
+ FloatIn isolevel
+ bool invert_normals
+ std_vector[FloatOut] vertices
+ std_vector[FloatOut] normals
+ std_vector[unsigned int] indices
diff --git a/src/silx/math/medianfilter/__init__.py b/src/silx/math/medianfilter/__init__.py
new file mode 100644
index 0000000..2b05f06
--- /dev/null
+++ b/src/silx/math/medianfilter/__init__.py
@@ -0,0 +1,30 @@
+# coding: utf-8
+# /*##########################################################################
+# Copyright (C) 2016 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
+
+__authors__ = ["H. Payno"]
+__license__ = "MIT"
+__date__ = "02/05/2017"
+
+
+from .medianfilter import (medfilt, medfilt1d, medfilt2d)
diff --git a/src/silx/math/medianfilter/include/median_filter.hpp b/src/silx/math/medianfilter/include/median_filter.hpp
new file mode 100644
index 0000000..7e42980
--- /dev/null
+++ b/src/silx/math/medianfilter/include/median_filter.hpp
@@ -0,0 +1,284 @@
+/*##########################################################################
+#
+# Copyright (c) 2017-2019 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+// __authors__ = ["H. Payno"]
+// __license__ = "MIT"
+// __date__ = "10/02/2017"
+
+#ifndef MEDIAN_FILTER
+#define MEDIAN_FILTER
+
+#include <vector>
+#include <assert.h>
+#include <algorithm>
+#include <signal.h>
+#include <iostream>
+#include <cmath>
+#include <cfloat>
+
+/* Needed for pytohn2.7 on Windows... */
+#ifndef INFINITY
+#define INFINITY (DBL_MAX+DBL_MAX)
+#endif
+
+#ifndef NAN
+#define NAN (INFINITY-INFINITY)
+#endif
+
+// Modes for the median filter
+enum MODE{
+ NEAREST=0,
+ REFLECT=1,
+ MIRROR=2,
+ SHRINK=3,
+ CONSTANT=4,
+};
+
+// Simple function browsing a deque and registering the min and max values
+// and if those values are unique or not
+template<typename T>
+void getMinMax(std::vector<T>& v, T& min, T&max,
+ typename std::vector<T>::const_iterator end){
+ // init min and max values
+ typename std::vector<T>::const_iterator it = v.begin();
+ if (v.size() == 0){
+ raise(SIGINT);
+ }else{
+ min = max = *it;
+ }
+ it++;
+
+ // Browse all the deque
+ while(it!=end){
+ // check if repeated (should always be before min/max setting)
+ T value = *it;
+ if(value > max) max = value;
+ if(value < min) min = value;
+
+ it++;
+ }
+}
+
+
+// apply the median filter only on limited part of the vector
+// In case of even number of elements (either due to NaNs in the window
+// or for image borders in shrink mode):
+// the highest of the 2 central values is returned
+template<typename T>
+inline T median(std::vector<T>& v, int window_size) {
+ int pivot = window_size / 2;
+ std::nth_element(v.begin(), v.begin() + pivot, v.begin()+window_size);
+ return v[pivot];
+}
+
+
+// return the index into 0, (length_max - 1) in reflect mode
+inline int reflect(int index, int length_max){
+ int res = index;
+ // if the index is negative get the positive symmetrical value
+ if(res < 0){
+ res += 1;
+ res = -res;
+ }
+ // then apply the reflect algorithm. Frequency is 2 max length
+ res = res % (2*length_max);
+ if(res >= length_max){
+ res = 2*length_max - res -1;
+ res = res % length_max;
+ }
+ return res;
+}
+
+// return the index into 0, (length_max - 1) in mirror mode
+inline int mirror(int index, int length_max){
+ int res = index;
+ // if the index is negative get the positive symmetrical value
+ if(res < 0){
+ res = -res;
+ }
+ int rightLimit = length_max -1;
+ // apply the redundancy each two right limit
+ res = res % (2*rightLimit);
+ if(res >= length_max){
+ int distToRedundancy = (2*rightLimit) - res;
+ res = distToRedundancy;
+ }
+ return res;
+}
+
+/* Provide a way to access NaN that also works for integers*/
+
+template<typename T>
+inline T NotANumber(void) {
+ assert(false); //This should never be called
+ return 0;
+}
+
+template<>
+inline float NotANumber<float>(void) { return NAN; }
+
+template<>
+inline double NotANumber<double>(void) { return NAN; }
+
+
+// Browse the column of pixel_x
+template<typename T>
+void median_filter(
+ const T* input,
+ T* output,
+ int* kernel_dim, // two values : 0:width, 1:height
+ int* image_dim, // two values : 0:width, 1:height
+ int y_pixel, // the x pixel to process
+ int x_pixel_range_min,
+ int x_pixel_range_max,
+ bool conditional,
+ int pMode,
+ T cval) {
+
+ assert(kernel_dim[0] > 0);
+ assert(kernel_dim[1] > 0);
+ assert(y_pixel >= 0);
+ assert(image_dim[0] > 0);
+ assert(image_dim[1] > 0);
+ assert(y_pixel >= 0);
+ assert(y_pixel < image_dim[0]);
+ assert(x_pixel_range_max < image_dim[1]);
+ assert(x_pixel_range_min <= x_pixel_range_max);
+ // kernel odd assertion
+ assert((kernel_dim[0] - 1)%2 == 0);
+ assert((kernel_dim[1] - 1)%2 == 0);
+
+ // # this should be move up to avoid calculation each time
+ int halfKernel_x = (kernel_dim[1] - 1) / 2;
+ int halfKernel_y = (kernel_dim[0] - 1) / 2;
+
+ MODE mode = static_cast<MODE>(pMode);
+
+ // init buffer
+ std::vector<T> window_values(kernel_dim[0]*kernel_dim[1]);
+
+ bool not_horizontal_border = (y_pixel >= halfKernel_y && y_pixel < image_dim[0] - halfKernel_y);
+
+ for(int x_pixel=x_pixel_range_min; x_pixel <= x_pixel_range_max; x_pixel ++ ){
+ typename std::vector<T>::iterator it = window_values.begin();
+ // fill the vector
+
+ if (not_horizontal_border &&
+ x_pixel >= halfKernel_x && x_pixel < image_dim[1] - halfKernel_x) {
+ //This is not a border, just fill it
+ for(int win_y=y_pixel-halfKernel_y; win_y<= y_pixel+halfKernel_y; win_y++) {
+ for(int win_x = x_pixel-halfKernel_x; win_x <= x_pixel+halfKernel_x; win_x++){
+ T value = input[win_y*image_dim[1] + win_x];
+ if (value == value) { // Ignore NaNs
+ *it = value;
+ ++it;
+ }
+ }
+ }
+
+ } else { // This is a border, handle the special case
+ for(int win_y=y_pixel-halfKernel_y; win_y<= y_pixel+halfKernel_y; win_y++)
+ {
+ for(int win_x = x_pixel-halfKernel_x; win_x <= x_pixel+halfKernel_x; win_x++)
+ {
+ T value = 0;
+ int index_x = win_x;
+ int index_y = win_y;
+
+ switch(mode){
+ case NEAREST:
+ index_x = std::min(std::max(win_x, 0), image_dim[1] - 1);
+ index_y = std::min(std::max(win_y, 0), image_dim[0] - 1);
+ value = input[index_y*image_dim[1] + index_x];
+ break;
+
+ case REFLECT:
+ index_x = reflect(win_x, image_dim[1]);
+ index_y = reflect(win_y, image_dim[0]);
+ value = input[index_y*image_dim[1] + index_x];
+ break;
+
+ case MIRROR:
+ index_x = mirror(win_x, image_dim[1]);
+ // deal with 1d case
+ if(win_y == 0 && image_dim[0] == 1){
+ index_y = 0;
+ }else{
+ index_y = mirror(win_y, image_dim[0]);
+ }
+ value = input[index_y*image_dim[1] + index_x];
+ break;
+
+ case SHRINK:
+ if ((index_x < 0) || (index_x > image_dim[1] -1) ||
+ (index_y < 0) || (index_y > image_dim[0] -1)) {
+ continue;
+ }
+ value = input[index_y*image_dim[1] + index_x];
+ break;
+ case CONSTANT:
+ if ((index_x < 0) || (index_x > image_dim[1] -1) ||
+ (index_y < 0) || (index_y > image_dim[0] -1)) {
+ value = cval;
+ } else {
+ value = input[index_y*image_dim[1] + index_x];
+ }
+ break;
+ }
+
+ if (value == value) { // Ignore NaNs
+ *it = value;
+ ++it;
+ }
+ }
+ }
+ }
+
+ //window_size can be smaller than kernel size in shrink mode or if there is NaNs
+ int window_size = std::distance(window_values.begin(), it);
+
+ if (window_size == 0) {
+ // Window is empty, this is the case when all values are NaNs
+ output[image_dim[1]*y_pixel + x_pixel] = NotANumber<T>();
+ } else {
+ // apply the median value if needed for this pixel
+ const T currentPixelValue = input[image_dim[1]*y_pixel + x_pixel];
+ if (conditional == true){
+ typename std::vector<T>::iterator window_end = window_values.begin() + window_size;
+ T min = 0;
+ T max = 0;
+ getMinMax(window_values, min, max, window_end);
+ // NaNs are propagated through unchanged
+ if ((currentPixelValue == max) || (currentPixelValue == min)){
+ output[image_dim[1]*y_pixel + x_pixel] = median<T>(window_values, window_size);
+ }else{
+ output[image_dim[1]*y_pixel + x_pixel] = currentPixelValue;
+ }
+ }else{
+ output[image_dim[1]*y_pixel + x_pixel] = median<T>(window_values, window_size);
+ }
+ }
+ }
+}
+
+#endif // MEDIAN_FILTER
diff --git a/src/silx/math/medianfilter/median_filter.pxd b/src/silx/math/medianfilter/median_filter.pxd
new file mode 100644
index 0000000..2fc0283
--- /dev/null
+++ b/src/silx/math/medianfilter/median_filter.pxd
@@ -0,0 +1,42 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2015-2018 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+
+from libcpp cimport bool
+
+# pyx
+cdef extern from "median_filter.hpp":
+ cdef extern void median_filter[T](const T* image,
+ T* output,
+ int* kernel_dim,
+ int* image_dim,
+ int x_pixel_range_min,
+ int x_pixel_range_max,
+ int y_pixel_range_min,
+ int y_pixel_range_max,
+ bool conditional,
+ T cval) nogil;
+
+ cdef extern int reflect(int index, int length_max);
+ cdef extern int mirror(int index, int length_max);
diff --git a/src/silx/math/medianfilter/medianfilter.pyx b/src/silx/math/medianfilter/medianfilter.pyx
new file mode 100644
index 0000000..fe05a78
--- /dev/null
+++ b/src/silx/math/medianfilter/medianfilter.pyx
@@ -0,0 +1,496 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2015-2018 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+"""This module provides median filter function for 1D and 2D arrays.
+"""
+
+__authors__ = ["H. Payno", "J. Kieffer"]
+__license__ = "MIT"
+__date__ = "02/05/2017"
+
+
+from cython.parallel import prange
+cimport cython
+cimport silx.math.medianfilter.median_filter as median_filter
+import numpy
+cimport numpy as cnumpy
+from libcpp cimport bool
+
+import numbers
+
+ctypedef unsigned long uint64
+ctypedef unsigned int uint32
+ctypedef unsigned short uint16
+
+
+MODES = {'nearest': 0, 'reflect': 1, 'mirror': 2, 'shrink': 3, 'constant': 4}
+
+
+def medfilt1d(data,
+ kernel_size=3,
+ bool conditional=False,
+ mode='nearest',
+ cval=0):
+ """Function computing the median filter of the given input.
+
+ Behavior at boundaries: the algorithm is reducing the size of the
+ window/kernel for pixels at boundaries (there is no mirroring).
+
+ Not-a-Number (NaN) float values are ignored.
+ If the window only contains NaNs, it evaluates to NaN.
+
+ In event of an even number of valid values in the window (either
+ because of NaN values or on image border in shrink mode),
+ the highest of the 2 central sorted values is taken.
+
+ :param numpy.ndarray data: the array for which we want to apply
+ the median filter. Should be 1d.
+ :param kernel_size: the dimension of the kernel.
+ :type kernel_size: int
+ :param bool conditional: True if we want to apply a conditional median
+ filtering.
+ :param str mode: the algorithm used to determine how values at borders
+ are determined: 'nearest', 'reflect', 'mirror', 'shrink', 'constant'
+ :param cval: Value used outside borders in 'constant' mode
+
+ :returns: the array with the median value for each pixel.
+ """
+ return medfilt(data, kernel_size, conditional, mode, cval)
+
+
+def medfilt2d(image,
+ kernel_size=3,
+ bool conditional=False,
+ mode='nearest',
+ cval=0):
+ """Function computing the median filter of the given input.
+ Behavior at boundaries: the algorithm is reducing the size of the
+ window/kernel for pixels at boundaries (there is no mirroring).
+
+ Not-a-Number (NaN) float values are ignored.
+ If the window only contains NaNs, it evaluates to NaN.
+
+ In event of an even number of valid values in the window (either
+ because of NaN values or on image border in shrink mode),
+ the highest of the 2 central sorted values is taken.
+
+ :param numpy.ndarray data: the array for which we want to apply
+ the median filter. Should be 2d.
+ :param kernel_size: the dimension of the kernel.
+ :type kernel_size: For 1D should be an int for 2D should be a tuple or
+ a list of (kernel_height, kernel_width)
+ :param bool conditional: True if we want to apply a conditional median
+ filtering.
+ :param str mode: the algorithm used to determine how values at borders
+ are determined: 'nearest', 'reflect', 'mirror', 'shrink', 'constant'
+ :param cval: Value used outside borders in 'constant' mode
+
+ :returns: the array with the median value for each pixel.
+ """
+ return medfilt(image, kernel_size, conditional, mode, cval)
+
+
+def medfilt(data,
+ kernel_size=3,
+ bool conditional=False,
+ mode='nearest',
+ cval=0):
+ """Function computing the median filter of the given input.
+ Behavior at boundaries: the algorithm is reducing the size of the
+ window/kernel for pixels at boundaries (there is no mirroring).
+
+ Not-a-Number (NaN) float values are ignored.
+ If the window only contains NaNs, it evaluates to NaN.
+
+ In event of an even number of valid values in the window (either
+ because of NaN values or on image border in shrink mode),
+ the highest of the 2 central sorted values is taken.
+
+ :param numpy.ndarray data: the array for which we want to apply
+ the median filter. Should be 1d or 2d.
+ :param kernel_size: the dimension of the kernel.
+ :type kernel_size: For 1D should be an int for 2D should be a tuple or
+ a list of (kernel_height, kernel_width)
+ :param bool conditional: True if we want to apply a conditional median
+ filtering.
+ :param str mode: the algorithm used to determine how values at borders
+ are determined: 'nearest', 'reflect', 'mirror', 'shrink', 'constant'
+ :param cval: Value used outside borders in 'constant' mode
+
+ :returns: the array with the median value for each pixel.
+ """
+ if mode not in MODES:
+ err = 'Requested mode %s is unknown.' % mode
+ raise ValueError(err)
+
+ if data.ndim > 2:
+ raise ValueError(
+ "Invalid data shape. Dimension of the array should be 1 or 2")
+
+ # Handle case of scalar kernel size
+ if isinstance(kernel_size, numbers.Integral):
+ kernel_size = [kernel_size] * data.ndim
+
+ assert len(kernel_size) == data.ndim
+
+ # Convert 1D arrays to 2D
+ reshaped = False
+ if len(data.shape) == 1:
+ data = data.reshape(1, data.shape[0])
+ kernel_size = [1, kernel_size[0]]
+ reshaped = True
+
+ # simple median filter apply into a 2D buffer
+ output_buffer = numpy.zeros_like(data)
+ check(data, output_buffer)
+
+ ker_dim = numpy.array(kernel_size, dtype=numpy.int32)
+
+ if data.dtype == numpy.float64:
+ medfilterfc = _median_filter_float64
+ elif data.dtype == numpy.float32:
+ medfilterfc = _median_filter_float32
+ elif data.dtype == numpy.int64:
+ medfilterfc = _median_filter_int64
+ elif data.dtype == numpy.uint64:
+ medfilterfc = _median_filter_uint64
+ elif data.dtype == numpy.int32:
+ medfilterfc = _median_filter_int32
+ elif data.dtype == numpy.uint32:
+ medfilterfc = _median_filter_uint32
+ elif data.dtype == numpy.int16:
+ medfilterfc = _median_filter_int16
+ elif data.dtype == numpy.uint16:
+ medfilterfc = _median_filter_uint16
+ else:
+ raise ValueError("%s type is not managed by the median filter" % data.dtype)
+
+ medfilterfc(input_buffer=data,
+ output_buffer=output_buffer,
+ kernel_size=ker_dim,
+ conditional=conditional,
+ mode=MODES[mode],
+ cval=cval)
+
+ if reshaped:
+ output_buffer.shape = -1 # Convert to 1D array
+
+ return output_buffer
+
+
+def check(input_buffer, output_buffer):
+ """Simple check on the two buffers to make sure we can apply the median filter
+ """
+ if (input_buffer.flags['C_CONTIGUOUS'] is False):
+ raise ValueError('<input_buffer> must be a C_CONTIGUOUS numpy array.')
+
+ if (output_buffer.flags['C_CONTIGUOUS'] is False):
+ raise ValueError('<output_buffer> must be a C_CONTIGUOUS numpy array.')
+
+ if not (len(input_buffer.shape) <= 2):
+ raise ValueError('<input_buffer> dimension must mo higher than 2.')
+
+ if not (len(output_buffer.shape) <= 2):
+ raise ValueError('<output_buffer> dimension must mo higher than 2.')
+
+ if not(input_buffer.dtype == output_buffer.dtype):
+ raise ValueError('input buffer and output_buffer must be of the same type')
+
+ if not (input_buffer.shape == output_buffer.shape):
+ raise ValueError('input buffer and output_buffer must be of the same dimension and same dimension')
+
+
+######### implementations of the include/median_filter.hpp function ############
+@cython.cdivision(True)
+@cython.boundscheck(False)
+@cython.wraparound(False)
+@cython.initializedcheck(False)
+def reflect(int index, int length_max):
+ """find the correct index into [0, length_max-1] for index in reflect mode
+
+ :param int index: the index to move into [0, length_max-1] in reflect mode
+ :param int length_max: the higher bound limit
+ """
+ return median_filter.reflect(index, length_max)
+
+
+@cython.cdivision(True)
+@cython.boundscheck(False)
+@cython.wraparound(False)
+@cython.initializedcheck(False)
+def mirror(int index, int length_max):
+ """find the correct index into [0, length_max-1] for index in mirror mode
+
+ :param int index: the index to move into [0, length_max-1] in mirror mode
+ :param int length_max: the higher bound limit
+ """
+ return median_filter.mirror(index, length_max)
+
+
+@cython.cdivision(True)
+@cython.boundscheck(False)
+@cython.wraparound(False)
+@cython.initializedcheck(False)
+def _median_filter_float32(float[:, ::1] input_buffer not None,
+ float[:, ::1] output_buffer not None,
+ cnumpy.int32_t[::1] kernel_size not None,
+ bool conditional,
+ int mode,
+ float cval):
+
+ cdef:
+ int y = 0
+ int image_dim = input_buffer.shape[1] - 1
+ int[2] buffer_shape
+ buffer_shape[0] = input_buffer.shape[0]
+ buffer_shape[1] = input_buffer.shape[1]
+
+ for y in prange(input_buffer.shape[0], nogil=True):
+ median_filter.median_filter[float](<float*> & input_buffer[0,0],
+ <float*> & output_buffer[0,0],
+ <int*>& kernel_size[0],
+ <int*>buffer_shape,
+ y,
+ 0,
+ image_dim,
+ conditional,
+ mode,
+ cval)
+
+
+@cython.cdivision(True)
+@cython.boundscheck(False)
+@cython.wraparound(False)
+@cython.initializedcheck(False)
+def _median_filter_float64(double[:, ::1] input_buffer not None,
+ double[:, ::1] output_buffer not None,
+ cnumpy.int32_t[::1] kernel_size not None,
+ bool conditional,
+ int mode,
+ double cval):
+
+ cdef:
+ int y = 0
+ int image_dim = input_buffer.shape[1] - 1
+ int[2] buffer_shape
+ buffer_shape[0] = input_buffer.shape[0]
+ buffer_shape[1] = input_buffer.shape[1]
+
+ for y in prange(input_buffer.shape[0], nogil=True):
+ median_filter.median_filter[double](<double*> & input_buffer[0, 0],
+ <double*> & output_buffer[0, 0],
+ <int*>&kernel_size[0],
+ <int*>buffer_shape,
+ y,
+ 0,
+ image_dim,
+ conditional,
+ mode,
+ cval)
+
+
+@cython.cdivision(True)
+@cython.boundscheck(False)
+@cython.wraparound(False)
+@cython.initializedcheck(False)
+def _median_filter_int64(cnumpy.int64_t[:, ::1] input_buffer not None,
+ cnumpy.int64_t[:, ::1] output_buffer not None,
+ cnumpy.int32_t[::1] kernel_size not None,
+ bool conditional,
+ int mode,
+ cnumpy.int64_t cval):
+
+ cdef:
+ int y = 0
+ int image_dim = input_buffer.shape[1] - 1
+ int[2] buffer_shape
+ buffer_shape[0] = input_buffer.shape[0]
+ buffer_shape[1] = input_buffer.shape[1]
+
+ for y in prange(input_buffer.shape[0], nogil=True):
+ median_filter.median_filter[long](<long*> & input_buffer[0,0],
+ <long*> & output_buffer[0, 0],
+ <int*>&kernel_size[0],
+ <int*>buffer_shape,
+ y,
+ 0,
+ image_dim,
+ conditional,
+ mode,
+ cval)
+
+@cython.cdivision(True)
+@cython.boundscheck(False)
+@cython.wraparound(False)
+@cython.initializedcheck(False)
+def _median_filter_uint64(cnumpy.uint64_t[:, ::1] input_buffer not None,
+ cnumpy.uint64_t[:, ::1] output_buffer not None,
+ cnumpy.int32_t[::1] kernel_size not None,
+ bool conditional,
+ int mode,
+ cnumpy.uint64_t cval):
+
+ cdef:
+ int y = 0
+ int image_dim = input_buffer.shape[1] - 1
+ int[2] buffer_shape
+ buffer_shape[0] = input_buffer.shape[0]
+ buffer_shape[1] = input_buffer.shape[1]
+
+ for y in prange(input_buffer.shape[0], nogil=True):
+ median_filter.median_filter[uint64](<uint64*> & input_buffer[0,0],
+ <uint64*> & output_buffer[0, 0],
+ <int*>&kernel_size[0],
+ <int*>buffer_shape,
+ y,
+ 0,
+ image_dim,
+ conditional,
+ mode,
+ cval)
+
+
+@cython.cdivision(True)
+@cython.boundscheck(False)
+@cython.wraparound(False)
+@cython.initializedcheck(False)
+def _median_filter_int32(cnumpy.int32_t[:, ::1] input_buffer not None,
+ cnumpy.int32_t[:, ::1] output_buffer not None,
+ cnumpy.int32_t[::1] kernel_size not None,
+ bool conditional,
+ int mode,
+ cnumpy.int32_t cval):
+
+ cdef:
+ int y = 0
+ int image_dim = input_buffer.shape[1] - 1
+ int[2] buffer_shape
+ buffer_shape[0] = input_buffer.shape[0]
+ buffer_shape[1] = input_buffer.shape[1]
+
+ for y in prange(input_buffer.shape[0], nogil=True):
+ median_filter.median_filter[int](<int*> & input_buffer[0,0],
+ <int*> & output_buffer[0, 0],
+ <int*>&kernel_size[0],
+ <int*>buffer_shape,
+ y,
+ 0,
+ image_dim,
+ conditional,
+ mode,
+ cval)
+
+
+@cython.cdivision(True)
+@cython.boundscheck(False)
+@cython.wraparound(False)
+@cython.initializedcheck(False)
+def _median_filter_uint32(cnumpy.uint32_t[:, ::1] input_buffer not None,
+ cnumpy.uint32_t[:, ::1] output_buffer not None,
+ cnumpy.int32_t[::1] kernel_size not None,
+ bool conditional,
+ int mode,
+ cnumpy.uint32_t cval):
+
+ cdef:
+ int y = 0
+ int image_dim = input_buffer.shape[1] - 1
+ int[2] buffer_shape
+ buffer_shape[0] = input_buffer.shape[0]
+ buffer_shape[1] = input_buffer.shape[1]
+
+ for y in prange(input_buffer.shape[0], nogil=True):
+ median_filter.median_filter[uint32](<uint32*> & input_buffer[0,0],
+ <uint32*> & output_buffer[0, 0],
+ <int*>&kernel_size[0],
+ <int*>buffer_shape,
+ y,
+ 0,
+ image_dim,
+ conditional,
+ mode,
+ cval)
+
+
+@cython.cdivision(True)
+@cython.boundscheck(False)
+@cython.wraparound(False)
+@cython.initializedcheck(False)
+def _median_filter_int16(cnumpy.int16_t[:, ::1] input_buffer not None,
+ cnumpy.int16_t[:, ::1] output_buffer not None,
+ cnumpy.int32_t[::1] kernel_size not None,
+ bool conditional,
+ int mode,
+ cnumpy.int16_t cval):
+
+ cdef:
+ int y = 0
+ int image_dim = input_buffer.shape[1] - 1
+ int[2] buffer_shape
+ buffer_shape[0] = input_buffer.shape[0]
+ buffer_shape[1] = input_buffer.shape[1]
+
+ for y in prange(input_buffer.shape[0], nogil=True):
+ median_filter.median_filter[short](<short*> & input_buffer[0,0],
+ <short*> & output_buffer[0, 0],
+ <int*>&kernel_size[0],
+ <int*>buffer_shape,
+ y,
+ 0,
+ image_dim,
+ conditional,
+ mode,
+ cval)
+
+
+@cython.cdivision(True)
+@cython.boundscheck(False)
+@cython.wraparound(False)
+@cython.initializedcheck(False)
+def _median_filter_uint16(
+ cnumpy.uint16_t[:, ::1] input_buffer not None,
+ cnumpy.uint16_t[:, ::1] output_buffer not None,
+ cnumpy.int32_t[::1] kernel_size not None,
+ bool conditional,
+ int mode,
+ cnumpy.uint16_t cval):
+
+ cdef:
+ int y = 0
+ int image_dim = input_buffer.shape[1] - 1
+ int[2] buffer_shape,
+ buffer_shape[0] = input_buffer.shape[0]
+ buffer_shape[1] = input_buffer.shape[1]
+
+ for y in prange(input_buffer.shape[0], nogil=True):
+ median_filter.median_filter[uint16](<uint16*> & input_buffer[0, 0],
+ <uint16*> & output_buffer[0, 0],
+ <int*>&kernel_size[0],
+ <int*>buffer_shape,
+ y,
+ 0,
+ image_dim,
+ conditional,
+ mode,
+ cval)
diff --git a/src/silx/math/medianfilter/setup.py b/src/silx/math/medianfilter/setup.py
new file mode 100644
index 0000000..d228357
--- /dev/null
+++ b/src/silx/math/medianfilter/setup.py
@@ -0,0 +1,59 @@
+# coding: utf-8
+# /*##########################################################################
+# Copyright (C) 2016-2017 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
+
+__authors__ = ["D. Naudet"]
+__license__ = "MIT"
+__date__ = "02/05/2017"
+
+
+import numpy
+
+from numpy.distutils.misc_util import Configuration
+
+
+def configuration(parent_package='', top_path=None):
+ config = Configuration('medianfilter', parent_package, top_path)
+ config.add_subpackage('test')
+
+ # =====================================
+ # median filter
+ # =====================================
+ medfilt_src = ['medianfilter.pyx']
+ medfilt_inc = ['include', numpy.get_include()]
+ extra_link_args = ['-fopenmp']
+ extra_compile_args = ['-fopenmp']
+ config.add_extension('medianfilter',
+ sources=medfilt_src,
+ include_dirs=[medfilt_inc],
+ language='c++',
+ extra_link_args=extra_link_args,
+ extra_compile_args=extra_compile_args)
+
+ return config
+
+
+if __name__ == "__main__":
+ from numpy.distutils.core import setup
+
+ setup(configuration=configuration) \ No newline at end of file
diff --git a/src/silx/math/medianfilter/test/__init__.py b/src/silx/math/medianfilter/test/__init__.py
new file mode 100644
index 0000000..71f8e95
--- /dev/null
+++ b/src/silx/math/medianfilter/test/__init__.py
@@ -0,0 +1,23 @@
+# coding: utf-8
+# /*##########################################################################
+# Copyright (C) 2016-2018 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
diff --git a/src/silx/math/medianfilter/test/benchmark.py b/src/silx/math/medianfilter/test/benchmark.py
new file mode 100644
index 0000000..81e893e
--- /dev/null
+++ b/src/silx/math/medianfilter/test/benchmark.py
@@ -0,0 +1,122 @@
+# coding: utf-8
+# /*##########################################################################
+# Copyright (C) 2017-2021 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
+"""Tests of the median filter"""
+
+__authors__ = ["H. Payno"]
+__license__ = "MIT"
+__date__ = "02/05/2017"
+
+from silx.gui import qt
+from silx.math.medianfilter import medfilt2d as medfilt2d_silx
+import numpy
+import numpy.random
+from timeit import Timer
+from silx.gui.plot import Plot1D
+import logging
+
+try:
+ import scipy
+except:
+ scipy = None
+else:
+ import scipy.ndimage
+
+try:
+ import PyMca5.PyMca as pymca
+except:
+ pymca = None
+else:
+ from PyMca5.PyMca.median import medfilt2d as medfilt2d_pymca
+
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.INFO)
+
+
+class BenchmarkMedianFilter(object):
+ """Simple benchmark of the median fiter silx vs scipy"""
+
+ NB_ITER = 3
+
+ def __init__(self, imageWidth, kernels):
+ self.img = numpy.random.rand(imageWidth, imageWidth)
+ self.kernels = kernels
+
+ self.run()
+
+ def run(self):
+ self.execTime = {}
+ for kernel in self.kernels:
+ self.execTime[kernel] = self.bench(kernel)
+
+ def bench(self, width):
+ def execSilx():
+ medfilt2d_silx(self.img, width)
+
+ def execScipy():
+ scipy.ndimage.median_filter(input=self.img,
+ size=width,
+ mode='nearest')
+
+ def execPymca():
+ medfilt2d_pymca(self.img, width)
+
+ execTime = {}
+
+ t = Timer(execSilx)
+ execTime["silx"] = t.timeit(BenchmarkMedianFilter.NB_ITER)
+ logger.info(
+ 'exec time silx (kernel size = %s) is %s' % (width, execTime["silx"]))
+
+ if scipy is not None:
+ t = Timer(execScipy)
+ execTime["scipy"] = t.timeit(BenchmarkMedianFilter.NB_ITER)
+ logger.info(
+ 'exec time scipy (kernel size = %s) is %s' % (width, execTime["scipy"]))
+ if pymca is not None:
+ t = Timer(execPymca)
+ execTime["pymca"] = t.timeit(BenchmarkMedianFilter.NB_ITER)
+ logger.info(
+ 'exec time pymca (kernel size = %s) is %s' % (width, execTime["pymca"]))
+
+ return execTime
+
+ def getExecTimeFor(self, id):
+ res = []
+ for k in self.kernels:
+ res.append(self.execTime[k][id])
+ return res
+
+
+app = qt.QApplication([])
+kernels = [3, 5, 7, 11, 15]
+benchmark = BenchmarkMedianFilter(imageWidth=1000, kernels=kernels)
+plot = Plot1D()
+plot.addCurve(x=kernels, y=benchmark.getExecTimeFor("silx"), legend='silx')
+if scipy is not None:
+ plot.addCurve(x=kernels, y=benchmark.getExecTimeFor("scipy"), legend='scipy')
+if pymca is not None:
+ plot.addCurve(x=kernels, y=benchmark.getExecTimeFor("pymca"), legend='pymca')
+plot.show()
+app.exec()
+del app
diff --git a/src/silx/math/medianfilter/test/test_medianfilter.py b/src/silx/math/medianfilter/test/test_medianfilter.py
new file mode 100644
index 0000000..a4e3021
--- /dev/null
+++ b/src/silx/math/medianfilter/test/test_medianfilter.py
@@ -0,0 +1,722 @@
+# coding: utf-8
+# ##########################################################################
+# Copyright (C) 2017-2018 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################
+"""Tests of the median filter"""
+
+__authors__ = ["H. Payno"]
+__license__ = "MIT"
+__date__ = "17/01/2018"
+
+import unittest
+import numpy
+from silx.math.medianfilter import medfilt2d, medfilt1d
+from silx.math.medianfilter.medianfilter import reflect, mirror
+from silx.math.medianfilter.medianfilter import MODES as silx_mf_modes
+from silx.utils.testutils import ParametricTestCase
+try:
+ import scipy
+ import scipy.misc
+except:
+ scipy = None
+else:
+ import scipy.ndimage
+
+import logging
+_logger = logging.getLogger(__name__)
+
+RANDOM_FLOAT_MAT = numpy.array([
+ [0.05564293, 0.62717157, 0.75002406, 0.40555336, 0.70278975],
+ [0.76532598, 0.02839148, 0.05272484, 0.65166994, 0.42161216],
+ [0.23067427, 0.74219128, 0.56049024, 0.44406320, 0.28773158],
+ [0.81025249, 0.20303021, 0.68382382, 0.46372299, 0.81281709],
+ [0.94691602, 0.07813661, 0.81651256, 0.84220106, 0.33623165]])
+
+RANDOM_INT_MAT = numpy.array([
+ [0, 5, 2, 6, 1],
+ [2, 3, 1, 7, 1],
+ [9, 8, 6, 7, 8],
+ [5, 6, 8, 2, 4]])
+
+
+class TestMedianFilterNearest(ParametricTestCase):
+ """Unit tests for the median filter in nearest mode"""
+
+ def testFilter3_100(self):
+ """Test median filter on a 10x10 matrix with a 3x3 kernel."""
+ dataIn = numpy.arange(100, dtype=numpy.int32)
+ dataIn = dataIn.reshape((10, 10))
+
+ dataOut = medfilt2d(image=dataIn,
+ kernel_size=(3, 3),
+ conditional=False,
+ mode='nearest')
+ self.assertTrue(dataOut[0, 0] == 1)
+ self.assertTrue(dataOut[9, 0] == 90)
+ self.assertTrue(dataOut[9, 9] == 98)
+
+ self.assertTrue(dataOut[0, 9] == 9)
+ self.assertTrue(dataOut[0, 4] == 5)
+ self.assertTrue(dataOut[9, 4] == 93)
+ self.assertTrue(dataOut[4, 4] == 44)
+
+ def testFilter3_9(self):
+ "Test median filter on a 3x3 matrix with a 3x3 kernel."
+ dataIn = numpy.array([0, -1, 1,
+ 12, 6, -2,
+ 100, 4, 12],
+ dtype=numpy.int16)
+ dataIn = dataIn.reshape((3, 3))
+ dataOut = medfilt2d(image=dataIn,
+ kernel_size=(3, 3),
+ conditional=False,
+ mode='nearest')
+ self.assertTrue(dataOut.shape == dataIn.shape)
+ self.assertTrue(dataOut[1, 1] == 4)
+ self.assertTrue(dataOut[0, 0] == 0)
+ self.assertTrue(dataOut[0, 1] == 0)
+ self.assertTrue(dataOut[1, 0] == 6)
+
+ def testFilterWidthOne(self):
+ """Make sure a filter of one by one give the same result as the input
+ """
+ dataIn = numpy.arange(100, dtype=numpy.int32)
+ dataIn = dataIn.reshape((10, 10))
+
+ dataOut = medfilt2d(image=dataIn,
+ kernel_size=(1, 1),
+ conditional=False,
+ mode='nearest')
+
+ self.assertTrue(numpy.array_equal(dataIn, dataOut))
+
+ def testFilter3_1d(self):
+ """Test binding and result of the 1d filter"""
+ self.assertTrue(numpy.array_equal(
+ medfilt1d(RANDOM_INT_MAT[0], kernel_size=3, conditional=False,
+ mode='nearest'),
+ [0, 2, 5, 2, 1])
+ )
+
+ def testFilter3Conditionnal(self):
+ """Test that the conditional filter apply correctly in a 10x10 matrix
+ with a 3x3 kernel
+ """
+ dataIn = numpy.arange(100, dtype=numpy.int32)
+ dataIn = dataIn.reshape((10, 10))
+
+ dataOut = medfilt2d(image=dataIn,
+ kernel_size=(3, 3),
+ conditional=True,
+ mode='nearest')
+ self.assertTrue(dataOut[0, 0] == 1)
+ self.assertTrue(dataOut[0, 1] == 1)
+ self.assertTrue(numpy.array_equal(dataOut[1:8, 1:8], dataIn[1:8, 1:8]))
+ self.assertTrue(dataOut[9, 9] == 98)
+
+ def testFilter3_1D(self):
+ """Simple test of a 3x3 median filter on a 1D array"""
+ dataIn = numpy.arange(100, dtype=numpy.int32)
+
+ dataOut = medfilt2d(image=dataIn,
+ kernel_size=(5),
+ conditional=False,
+ mode='nearest')
+
+ self.assertTrue(dataOut[0] == 0)
+ self.assertTrue(dataOut[9] == 9)
+ self.assertTrue(dataOut[99] == 99)
+
+ def testNaNs(self):
+ """Test median filter on image with NaNs in nearest mode"""
+ # Data with a NaN in first corner
+ nan_corner = numpy.arange(100.).reshape(10, 10)
+ nan_corner[0, 0] = numpy.nan
+ output = medfilt2d(
+ nan_corner, kernel_size=3, conditional=False, mode='nearest')
+ self.assertEqual(output[0, 0], 10)
+ self.assertEqual(output[0, 1], 2)
+ self.assertEqual(output[1, 0], 11)
+ self.assertEqual(output[1, 1], 12)
+
+ # Data with some NaNs
+ some_nans = numpy.arange(100.).reshape(10, 10)
+ some_nans[0, 1] = numpy.nan
+ some_nans[1, 1] = numpy.nan
+ some_nans[1, 0] = numpy.nan
+ output = medfilt2d(
+ some_nans, kernel_size=3, conditional=False, mode='nearest')
+ self.assertEqual(output[0, 0], 0)
+ self.assertEqual(output[0, 1], 2)
+ self.assertEqual(output[1, 0], 20)
+ self.assertEqual(output[1, 1], 20)
+
+
+class TestMedianFilterReflect(ParametricTestCase):
+ """Unit test for the median filter in reflect mode"""
+
+ def testArange9(self):
+ """Test from a 3x3 window to RANDOM_FLOAT_MAT"""
+ img = numpy.arange(9, dtype=numpy.int32)
+ img = img.reshape(3, 3)
+ kernel = (3, 3)
+ res = medfilt2d(image=img,
+ kernel_size=kernel,
+ conditional=False,
+ mode='reflect')
+ self.assertTrue(
+ numpy.array_equal(res.ravel(), [1, 2, 2, 3, 4, 5, 6, 6, 7]))
+
+ def testRandom10(self):
+ """Test a (5, 3) window to a RANDOM_FLOAT_MAT"""
+ kernel = (5, 3)
+
+ thRes = numpy.array([
+ [0.23067427, 0.56049024, 0.56049024, 0.4440632, 0.42161216],
+ [0.23067427, 0.62717157, 0.56049024, 0.56049024, 0.46372299],
+ [0.62717157, 0.62717157, 0.56049024, 0.56049024, 0.4440632],
+ [0.76532598, 0.68382382, 0.56049024, 0.56049024, 0.42161216],
+ [0.81025249, 0.68382382, 0.56049024, 0.68382382, 0.46372299]])
+
+ res = medfilt2d(image=RANDOM_FLOAT_MAT,
+ kernel_size=kernel,
+ conditional=False,
+ mode='reflect')
+
+ self.assertTrue(numpy.array_equal(thRes, res))
+
+ def testApplyReflect1D(self):
+ """Test the reflect function used for the median filter in reflect mode
+ """
+ # test for inside values
+ self.assertTrue(reflect(2, 3) == 2)
+ # test for boundaries values
+ self.assertTrue(reflect(3, 3) == 2)
+ self.assertTrue(reflect(4, 3) == 1)
+ self.assertTrue(reflect(5, 3) == 0)
+ self.assertTrue(reflect(6, 3) == 0)
+ self.assertTrue(reflect(7, 3) == 1)
+ self.assertTrue(reflect(-1, 3) == 0)
+ self.assertTrue(reflect(-2, 3) == 1)
+ self.assertTrue(reflect(-3, 3) == 2)
+ self.assertTrue(reflect(-4, 3) == 2)
+ self.assertTrue(reflect(-5, 3) == 1)
+ self.assertTrue(reflect(-6, 3) == 0)
+ self.assertTrue(reflect(-7, 3) == 0)
+
+ def testRandom10Conditionnal(self):
+ """Test the median filter in reflect mode and with the conditionnal
+ option"""
+ kernel = (3, 1)
+
+ thRes = numpy.array([
+ [0.05564293, 0.62717157, 0.75002406, 0.40555336, 0.70278975],
+ [0.23067427, 0.62717157, 0.56049024, 0.44406320, 0.42161216],
+ [0.76532598, 0.20303021, 0.56049024, 0.46372299, 0.42161216],
+ [0.81025249, 0.20303021, 0.68382382, 0.46372299, 0.33623165],
+ [0.94691602, 0.07813661, 0.81651256, 0.84220106, 0.33623165]])
+
+ res = medfilt2d(image=RANDOM_FLOAT_MAT,
+ kernel_size=kernel,
+ conditional=True,
+ mode='reflect')
+ self.assertTrue(numpy.array_equal(thRes, res))
+
+ def testNaNs(self):
+ """Test median filter on image with NaNs in reflect mode"""
+ # Data with a NaN in first corner
+ nan_corner = numpy.arange(100.).reshape(10, 10)
+ nan_corner[0, 0] = numpy.nan
+ output = medfilt2d(
+ nan_corner, kernel_size=3, conditional=False, mode='reflect')
+ self.assertEqual(output[0, 0], 10)
+ self.assertEqual(output[0, 1], 2)
+ self.assertEqual(output[1, 0], 11)
+ self.assertEqual(output[1, 1], 12)
+
+ # Data with some NaNs
+ some_nans = numpy.arange(100.).reshape(10, 10)
+ some_nans[0, 1] = numpy.nan
+ some_nans[1, 1] = numpy.nan
+ some_nans[1, 0] = numpy.nan
+ output = medfilt2d(
+ some_nans, kernel_size=3, conditional=False, mode='reflect')
+ self.assertEqual(output[0, 0], 0)
+ self.assertEqual(output[0, 1], 2)
+ self.assertEqual(output[1, 0], 20)
+ self.assertEqual(output[1, 1], 20)
+
+ def testFilter3_1d(self):
+ """Test binding and result of the 1d filter"""
+ self.assertTrue(numpy.array_equal(
+ medfilt1d(RANDOM_INT_MAT[0], kernel_size=5, conditional=False,
+ mode='reflect'),
+ [2, 2, 2, 2, 2])
+ )
+
+
+class TestMedianFilterMirror(ParametricTestCase):
+ """Unit test for the median filter in mirror mode
+ """
+
+ def testApplyMirror1D(self):
+ """Test the reflect function used for the median filter in mirror mode
+ """
+ # test for inside values
+ self.assertTrue(mirror(2, 3) == 2)
+ # test for boundaries values
+ self.assertTrue(mirror(4, 4) == 2)
+ self.assertTrue(mirror(5, 4) == 1)
+ self.assertTrue(mirror(6, 4) == 0)
+ self.assertTrue(mirror(7, 4) == 1)
+ self.assertTrue(mirror(8, 4) == 2)
+ self.assertTrue(mirror(-1, 4) == 1)
+ self.assertTrue(mirror(-2, 4) == 2)
+ self.assertTrue(mirror(-3, 4) == 3)
+ self.assertTrue(mirror(-4, 4) == 2)
+ self.assertTrue(mirror(-5, 4) == 1)
+ self.assertTrue(mirror(-6, 4) == 0)
+
+ def testRandom10(self):
+ """Test a (5, 3) window to a random array"""
+ kernel = (3, 5)
+
+ thRes = numpy.array([
+ [0.05272484, 0.40555336, 0.42161216, 0.42161216, 0.42161216],
+ [0.56049024, 0.56049024, 0.4440632, 0.4440632, 0.4440632],
+ [0.56049024, 0.46372299, 0.46372299, 0.46372299, 0.46372299],
+ [0.68382382, 0.56049024, 0.56049024, 0.46372299, 0.56049024],
+ [0.68382382, 0.46372299, 0.68382382, 0.46372299, 0.68382382]])
+
+ res = medfilt2d(image=RANDOM_FLOAT_MAT,
+ kernel_size=kernel,
+ conditional=False,
+ mode='mirror')
+
+ self.assertTrue(numpy.array_equal(thRes, res))
+
+ def testRandom10Conditionnal(self):
+ """Test the median filter in reflect mode and with the conditionnal
+ option"""
+ kernel = (1, 3)
+
+ thRes = numpy.array([
+ [0.62717157, 0.62717157, 0.62717157, 0.70278975, 0.40555336],
+ [0.02839148, 0.05272484, 0.05272484, 0.42161216, 0.65166994],
+ [0.74219128, 0.56049024, 0.56049024, 0.44406320, 0.44406320],
+ [0.20303021, 0.68382382, 0.46372299, 0.68382382, 0.46372299],
+ [0.07813661, 0.81651256, 0.81651256, 0.81651256, 0.84220106]])
+
+ res = medfilt2d(image=RANDOM_FLOAT_MAT,
+ kernel_size=kernel,
+ conditional=True,
+ mode='mirror')
+
+ self.assertTrue(numpy.array_equal(thRes, res))
+
+ def testNaNs(self):
+ """Test median filter on image with NaNs in mirror mode"""
+ # Data with a NaN in first corner
+ nan_corner = numpy.arange(100.).reshape(10, 10)
+ nan_corner[0, 0] = numpy.nan
+ output = medfilt2d(
+ nan_corner, kernel_size=3, conditional=False, mode='mirror')
+ self.assertEqual(output[0, 0], 11)
+ self.assertEqual(output[0, 1], 11)
+ self.assertEqual(output[1, 0], 11)
+ self.assertEqual(output[1, 1], 12)
+
+ # Data with some NaNs
+ some_nans = numpy.arange(100.).reshape(10, 10)
+ some_nans[0, 1] = numpy.nan
+ some_nans[1, 1] = numpy.nan
+ some_nans[1, 0] = numpy.nan
+ output = medfilt2d(
+ some_nans, kernel_size=3, conditional=False, mode='mirror')
+ self.assertEqual(output[0, 0], 0)
+ self.assertEqual(output[0, 1], 12)
+ self.assertEqual(output[1, 0], 21)
+ self.assertEqual(output[1, 1], 20)
+
+ def testFilter3_1d(self):
+ """Test binding and result of the 1d filter"""
+ self.assertTrue(numpy.array_equal(
+ medfilt1d(RANDOM_INT_MAT[0], kernel_size=5, conditional=False,
+ mode='mirror'),
+ [2, 5, 2, 5, 2])
+ )
+
+class TestMedianFilterShrink(ParametricTestCase):
+ """Unit test for the median filter in mirror mode
+ """
+
+ def testRandom_3x3(self):
+ """Test the median filter in shrink mode and with the conditionnal
+ option"""
+ kernel = (3, 3)
+
+ thRes = numpy.array([
+ [0.62717157, 0.62717157, 0.62717157, 0.65166994, 0.65166994],
+ [0.62717157, 0.56049024, 0.56049024, 0.44406320, 0.44406320],
+ [0.74219128, 0.56049024, 0.46372299, 0.46372299, 0.46372299],
+ [0.74219128, 0.68382382, 0.56049024, 0.56049024, 0.46372299],
+ [0.81025249, 0.81025249, 0.68382382, 0.81281709, 0.81281709]])
+
+ res = medfilt2d(image=RANDOM_FLOAT_MAT,
+ kernel_size=kernel,
+ conditional=False,
+ mode='shrink')
+
+ self.assertTrue(numpy.array_equal(thRes, res))
+
+ def testBounds(self):
+ """Test the median filter in shrink mode with 3 different kernels
+ which should return the same result due to the large values of kernels
+ used.
+ """
+ kernel1 = (1, 9)
+ kernel2 = (1, 11)
+ kernel3 = (1, 21)
+
+ thRes = numpy.array([[2, 2, 2, 2, 2],
+ [2, 2, 2, 2, 2],
+ [8, 8, 8, 8, 8],
+ [5, 5, 5, 5, 5]])
+
+ resK1 = medfilt2d(image=RANDOM_INT_MAT,
+ kernel_size=kernel1,
+ conditional=False,
+ mode='shrink')
+
+ resK2 = medfilt2d(image=RANDOM_INT_MAT,
+ kernel_size=kernel2,
+ conditional=False,
+ mode='shrink')
+
+ resK3 = medfilt2d(image=RANDOM_INT_MAT,
+ kernel_size=kernel3,
+ conditional=False,
+ mode='shrink')
+
+ self.assertTrue(numpy.array_equal(resK1, thRes))
+ self.assertTrue(numpy.array_equal(resK2, resK1))
+ self.assertTrue(numpy.array_equal(resK3, resK1))
+
+ def testRandom_3x3Conditionnal(self):
+ """Test the median filter in reflect mode and with the conditionnal
+ option"""
+ kernel = (3, 3)
+
+ thRes = numpy.array([
+ [0.05564293, 0.62717157, 0.62717157, 0.40555336, 0.65166994],
+ [0.62717157, 0.56049024, 0.05272484, 0.65166994, 0.42161216],
+ [0.23067427, 0.74219128, 0.56049024, 0.44406320, 0.46372299],
+ [0.81025249, 0.20303021, 0.68382382, 0.46372299, 0.81281709],
+ [0.81025249, 0.81025249, 0.81651256, 0.81281709, 0.81281709]])
+
+ res = medfilt2d(image=RANDOM_FLOAT_MAT,
+ kernel_size=kernel,
+ conditional=True,
+ mode='shrink')
+
+ self.assertTrue(numpy.array_equal(res, thRes))
+
+ def testRandomInt(self):
+ """Test 3x3 kernel on RANDOM_INT_MAT
+ """
+ kernel = (3, 3)
+
+ thRes = numpy.array([[3, 2, 5, 2, 6],
+ [5, 3, 6, 6, 7],
+ [6, 6, 6, 6, 7],
+ [8, 8, 7, 7, 7]])
+
+ resK1 = medfilt2d(image=RANDOM_INT_MAT,
+ kernel_size=kernel,
+ conditional=False,
+ mode='shrink')
+
+ self.assertTrue(numpy.array_equal(resK1, thRes))
+
+ def testNaNs(self):
+ """Test median filter on image with NaNs in shrink mode"""
+ # Data with a NaN in first corner
+ nan_corner = numpy.arange(100.).reshape(10, 10)
+ nan_corner[0, 0] = numpy.nan
+ output = medfilt2d(
+ nan_corner, kernel_size=3, conditional=False, mode='shrink')
+ self.assertEqual(output[0, 0], 10)
+ self.assertEqual(output[0, 1], 10)
+ self.assertEqual(output[1, 0], 11)
+ self.assertEqual(output[1, 1], 12)
+
+ # Data with some NaNs
+ some_nans = numpy.arange(100.).reshape(10, 10)
+ some_nans[0, 1] = numpy.nan
+ some_nans[1, 1] = numpy.nan
+ some_nans[1, 0] = numpy.nan
+ output = medfilt2d(
+ some_nans, kernel_size=3, conditional=False, mode='shrink')
+ self.assertEqual(output[0, 0], 0)
+ self.assertEqual(output[0, 1], 2)
+ self.assertEqual(output[1, 0], 20)
+ self.assertEqual(output[1, 1], 20)
+
+ def testFilter3_1d(self):
+ """Test binding and result of the 1d filter"""
+ self.assertTrue(numpy.array_equal(
+ medfilt1d(RANDOM_INT_MAT[0], kernel_size=3, conditional=False,
+ mode='shrink'),
+ [5, 2, 5, 2, 6])
+ )
+
+class TestMedianFilterConstant(ParametricTestCase):
+ """Unit test for the median filter in constant mode
+ """
+
+ def testRandom10(self):
+ """Test a (5, 3) window to a random array"""
+ kernel = (3, 5)
+
+ thRes = numpy.array([
+ [0., 0.02839148, 0.05564293, 0.02839148, 0.],
+ [0.05272484, 0.40555336, 0.4440632, 0.42161216, 0.28773158],
+ [0.05272484, 0.44406320, 0.46372299, 0.42161216, 0.28773158],
+ [0.20303021, 0.46372299, 0.56049024, 0.44406320, 0.33623165],
+ [0., 0.07813661, 0.33623165, 0.07813661, 0.]])
+
+ res = medfilt2d(image=RANDOM_FLOAT_MAT,
+ kernel_size=kernel,
+ conditional=False,
+ mode='constant')
+
+ self.assertTrue(numpy.array_equal(thRes, res))
+
+ RANDOM_FLOAT_MAT = numpy.array([
+ [0.05564293, 0.62717157, 0.75002406, 0.40555336, 0.70278975],
+ [0.76532598, 0.02839148, 0.05272484, 0.65166994, 0.42161216],
+ [0.23067427, 0.74219128, 0.56049024, 0.44406320, 0.28773158],
+ [0.81025249, 0.20303021, 0.68382382, 0.46372299, 0.81281709],
+ [0.94691602, 0.07813661, 0.81651256, 0.84220106, 0.33623165]])
+
+ def testRandom10Conditionnal(self):
+ """Test the median filter in reflect mode and with the conditionnal
+ option"""
+ kernel = (1, 3)
+
+ print(RANDOM_FLOAT_MAT)
+
+ thRes = numpy.array([
+ [0.05564293, 0.62717157, 0.62717157, 0.70278975, 0.40555336],
+ [0.02839148, 0.05272484, 0.05272484, 0.42161216, 0.42161216],
+ [0.23067427, 0.56049024, 0.56049024, 0.44406320, 0.28773158],
+ [0.20303021, 0.68382382, 0.46372299, 0.68382382, 0.46372299],
+ [0.07813661, 0.81651256, 0.81651256, 0.81651256, 0.33623165]])
+
+ res = medfilt2d(image=RANDOM_FLOAT_MAT,
+ kernel_size=kernel,
+ conditional=True,
+ mode='constant')
+
+ self.assertTrue(numpy.array_equal(thRes, res))
+
+ def testNaNs(self):
+ """Test median filter on image with NaNs in constant mode"""
+ # Data with a NaN in first corner
+ nan_corner = numpy.arange(100.).reshape(10, 10)
+ nan_corner[0, 0] = numpy.nan
+ output = medfilt2d(nan_corner,
+ kernel_size=3,
+ conditional=False,
+ mode='constant',
+ cval=0)
+ self.assertEqual(output[0, 0], 0)
+ self.assertEqual(output[0, 1], 2)
+ self.assertEqual(output[1, 0], 10)
+ self.assertEqual(output[1, 1], 12)
+
+ # Data with some NaNs
+ some_nans = numpy.arange(100.).reshape(10, 10)
+ some_nans[0, 1] = numpy.nan
+ some_nans[1, 1] = numpy.nan
+ some_nans[1, 0] = numpy.nan
+ output = medfilt2d(some_nans,
+ kernel_size=3,
+ conditional=False,
+ mode='constant',
+ cval=0)
+ self.assertEqual(output[0, 0], 0)
+ self.assertEqual(output[0, 1], 0)
+ self.assertEqual(output[1, 0], 0)
+ self.assertEqual(output[1, 1], 20)
+
+ def testFilter3_1d(self):
+ """Test binding and result of the 1d filter"""
+ self.assertTrue(numpy.array_equal(
+ medfilt1d(RANDOM_INT_MAT[0], kernel_size=5, conditional=False,
+ mode='constant'),
+ [0, 2, 2, 2, 1])
+ )
+
+class TestGeneralExecution(ParametricTestCase):
+ """Some general test on median filter application"""
+
+ def testTypes(self):
+ """Test that all needed types have their implementation of the median
+ filter
+ """
+ for mode in silx_mf_modes:
+ for testType in [numpy.float32, numpy.float64, numpy.int16,
+ numpy.uint16, numpy.int32, numpy.int64,
+ numpy.uint64]:
+ with self.subTest(mode=mode, type=testType):
+ data = (numpy.random.rand(10, 10) * 65000).astype(testType)
+ out = medfilt2d(image=data,
+ kernel_size=(3, 3),
+ conditional=False,
+ mode=mode)
+ self.assertTrue(out.dtype.type is testType)
+
+ def testInputDataIsNotModify(self):
+ """Make sure input data is not modify by the median filter"""
+ dataIn = numpy.arange(100, dtype=numpy.int32)
+ dataIn = dataIn.reshape((10, 10))
+ dataInCopy = dataIn.copy()
+
+ for mode in silx_mf_modes:
+ with self.subTest(mode=mode):
+ medfilt2d(image=dataIn,
+ kernel_size=(3, 3),
+ conditional=False,
+ mode=mode)
+ self.assertTrue(numpy.array_equal(dataIn, dataInCopy))
+
+ def testAllNaNs(self):
+ """Test median filter on image all NaNs"""
+ all_nans = numpy.empty((10, 10), dtype=numpy.float32)
+ all_nans[:] = numpy.nan
+
+ for mode in silx_mf_modes:
+ for conditional in (True, False):
+ with self.subTest(mode=mode, conditional=conditional):
+ output = medfilt2d(
+ all_nans,
+ kernel_size=3,
+ conditional=conditional,
+ mode=mode,
+ cval=numpy.nan)
+ self.assertTrue(numpy.all(numpy.isnan(output)))
+
+ def testConditionalWithNaNs(self):
+ """Test that NaNs are propagated through conditional median filter"""
+ for mode in silx_mf_modes:
+ with self.subTest(mode=mode):
+ image = numpy.ones((10, 10), dtype=numpy.float32)
+ nan_mask = numpy.zeros_like(image, dtype=bool)
+ nan_mask[0, 0] = True
+ nan_mask[4, :] = True
+ nan_mask[6, 4] = True
+ image[nan_mask] = numpy.nan
+ output = medfilt2d(
+ image,
+ kernel_size=3,
+ conditional=True,
+ mode=mode)
+ out_isnan = numpy.isnan(output)
+ self.assertTrue(numpy.all(out_isnan[nan_mask]))
+ self.assertFalse(
+ numpy.any(out_isnan[numpy.logical_not(nan_mask)]))
+
+
+def _getScipyAndSilxCommonModes():
+ """return the mode which are comparable between silx and scipy"""
+ modes = silx_mf_modes.copy()
+ del modes['shrink']
+ return modes
+
+
+@unittest.skipUnless(scipy is not None, "scipy not available")
+class TestVsScipy(ParametricTestCase):
+ """Compare scipy.ndimage.median_filter vs silx.math.medianfilter
+ on comparable
+ """
+ def testWithArange(self):
+ """Test vs scipy with different kernels on arange matrix"""
+ data = numpy.arange(10000, dtype=numpy.int32)
+ data = data.reshape(100, 100)
+
+ kernels = [(3, 7), (7, 5), (1, 1), (3, 3)]
+ modesToTest = _getScipyAndSilxCommonModes()
+ for kernel in kernels:
+ for mode in modesToTest:
+ with self.subTest(kernel=kernel, mode=mode):
+ resScipy = scipy.ndimage.median_filter(input=data,
+ size=kernel,
+ mode=mode)
+ resSilx = medfilt2d(image=data,
+ kernel_size=kernel,
+ conditional=False,
+ mode=mode)
+
+ self.assertTrue(numpy.array_equal(resScipy, resSilx))
+
+ def testRandomMatrice(self):
+ """Test vs scipy with different kernels on RANDOM_FLOAT_MAT"""
+ kernels = [(3, 7), (7, 5), (1, 1), (3, 3)]
+ modesToTest = _getScipyAndSilxCommonModes()
+ for kernel in kernels:
+ for mode in modesToTest:
+ with self.subTest(kernel=kernel, mode=mode):
+ resScipy = scipy.ndimage.median_filter(input=RANDOM_FLOAT_MAT,
+ size=kernel,
+ mode=mode)
+
+ resSilx = medfilt2d(image=RANDOM_FLOAT_MAT,
+ kernel_size=kernel,
+ conditional=False,
+ mode=mode)
+
+ self.assertTrue(numpy.array_equal(resScipy, resSilx))
+
+ def testAscentOrLena(self):
+ """Test vs scipy with """
+ if hasattr(scipy.misc, 'ascent'):
+ img = scipy.misc.ascent()
+ else:
+ img = scipy.misc.lena()
+
+ kernels = [(3, 1), (3, 5), (5, 9), (9, 3)]
+ modesToTest = _getScipyAndSilxCommonModes()
+
+ for kernel in kernels:
+ for mode in modesToTest:
+ with self.subTest(kernel=kernel, mode=mode):
+ resScipy = scipy.ndimage.median_filter(input=img,
+ size=kernel,
+ mode=mode)
+
+ resSilx = medfilt2d(image=img,
+ kernel_size=kernel,
+ conditional=False,
+ mode=mode)
+
+ self.assertTrue(numpy.array_equal(resScipy, resSilx))
diff --git a/src/silx/math/setup.py b/src/silx/math/setup.py
new file mode 100644
index 0000000..1c30e6e
--- /dev/null
+++ b/src/silx/math/setup.py
@@ -0,0 +1,99 @@
+# coding: utf-8
+# /*##########################################################################
+# Copyright (C) 2016-2021 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
+
+__authors__ = ["D. Naudet"]
+__license__ = "MIT"
+__date__ = "27/03/2017"
+
+import os.path
+
+import numpy
+
+from numpy.distutils.misc_util import Configuration
+
+
+def configuration(parent_package='', top_path=None):
+ config = Configuration('math', parent_package, top_path)
+ config.add_subpackage('test')
+ config.add_subpackage('fit')
+ config.add_subpackage('medianfilter')
+ config.add_subpackage('fft')
+
+ # =====================================
+ # histogramnd
+ # =====================================
+ histo_src = [os.path.join('histogramnd', 'src', 'histogramnd_c.c'),
+ 'chistogramnd.pyx']
+ histo_inc = [os.path.join('histogramnd', 'include'),
+ numpy.get_include()]
+
+ config.add_extension('chistogramnd',
+ sources=histo_src,
+ include_dirs=histo_inc,
+ language='c')
+
+ # =====================================
+ # histogramnd_lut
+ # =====================================
+ config.add_extension('chistogramnd_lut',
+ sources=['chistogramnd_lut.pyx'],
+ include_dirs=histo_inc,
+ language='c')
+ # =====================================
+ # marching cubes
+ # =====================================
+ mc_src = [os.path.join('marchingcubes', 'mc_lut.cpp'),
+ 'marchingcubes.pyx']
+ config.add_extension('marchingcubes',
+ sources=mc_src,
+ include_dirs=['marchingcubes', numpy.get_include()],
+ language='c++')
+
+ # min/max
+ config.add_extension('combo',
+ sources=['combo.pyx'],
+ include_dirs=['include'],
+ language='c')
+
+ config.add_extension('_colormap',
+ sources=["_colormap.pyx"],
+ language='c',
+ include_dirs=['include', numpy.get_include()],
+ extra_link_args=['-fopenmp'],
+ extra_compile_args=['-fopenmp'])
+
+ config.add_extension('interpolate',
+ sources=["interpolate.pyx"],
+ language='c',
+ include_dirs=['include', numpy.get_include()],
+ extra_link_args=['-fopenmp'],
+ extra_compile_args=['-fopenmp'])
+
+ return config
+
+
+if __name__ == "__main__":
+ from numpy.distutils.core import setup
+
+ setup(configuration=configuration)
diff --git a/src/silx/math/test/__init__.py b/src/silx/math/test/__init__.py
new file mode 100644
index 0000000..ad9836c
--- /dev/null
+++ b/src/silx/math/test/__init__.py
@@ -0,0 +1,23 @@
+# coding: utf-8
+# /*##########################################################################
+# Copyright (C) 2016-2019 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
diff --git a/src/silx/math/test/benchmark_combo.py b/src/silx/math/test/benchmark_combo.py
new file mode 100644
index 0000000..c12f590
--- /dev/null
+++ b/src/silx/math/test/benchmark_combo.py
@@ -0,0 +1,192 @@
+# coding: utf-8
+# /*##########################################################################
+# Copyright (C) 2016-2017 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
+"""Benchmarks of the combo module"""
+
+from __future__ import division
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "17/01/2018"
+
+
+import logging
+import os.path
+import time
+import unittest
+
+import numpy
+
+from silx.test.utils import temp_dir
+from silx.utils.testutils import ParametricTestCase
+
+from silx.math import combo
+
+_logger = logging.getLogger(__name__)
+_logger.setLevel(logging.DEBUG)
+
+
+class TestBenchmarkMinMax(ParametricTestCase):
+ """Benchmark of min max combo"""
+
+ DTYPES = ('float32', 'float64',
+ 'int8', 'int16', 'int32', 'int64',
+ 'uint8', 'uint16', 'uint32', 'uint64')
+
+ ARANGE = 'ascent', 'descent', 'random'
+
+ EXPONENT = 3, 4, 5, 6, 7
+
+ def test_benchmark_min_max(self):
+ """Benchmark min_max without min positive.
+
+ Compares with:
+
+ - numpy.nanmin, numpy.nanmax and
+ - numpy.argmin, numpy.argmax
+
+ It runs bench for different types, different data size and 3
+ data sets: increasing , decreasing and random data.
+ """
+ durations = {'min/max': [], 'argmin/max': [], 'combo': []}
+
+ _logger.info('Benchmark against argmin/argmax and nanmin/nanmax')
+
+ for dtype in self.DTYPES:
+ for arange in self.ARANGE:
+ for exponent in self.EXPONENT:
+ size = 10**exponent
+ with self.subTest(dtype=dtype, size=size, arange=arange):
+ if arange == 'ascent':
+ data = numpy.arange(0, size, 1, dtype=dtype)
+ elif arange == 'descent':
+ data = numpy.arange(size, 0, -1, dtype=dtype)
+ else:
+ if dtype in ('float32', 'float64'):
+ data = numpy.random.random(size)
+ else:
+ data = numpy.random.randint(10**6, size=size)
+ data = numpy.array(data, dtype=dtype)
+
+ start = time.time()
+ ref_min = numpy.nanmin(data)
+ ref_max = numpy.nanmax(data)
+ durations['min/max'].append(time.time() - start)
+
+ start = time.time()
+ ref_argmin = numpy.argmin(data)
+ ref_argmax = numpy.argmax(data)
+ durations['argmin/max'].append(time.time() - start)
+
+ start = time.time()
+ result = combo.min_max(data, min_positive=False)
+ durations['combo'].append(time.time() - start)
+
+ _logger.info(
+ '%s-%s-10**%d\tx%.2f argmin/max x%.2f min/max',
+ dtype, arange, exponent,
+ durations['argmin/max'][-1] / durations['combo'][-1],
+ durations['min/max'][-1] / durations['combo'][-1])
+
+ self.assertEqual(result.minimum, ref_min)
+ self.assertEqual(result.maximum, ref_max)
+ self.assertEqual(result.argmin, ref_argmin)
+ self.assertEqual(result.argmax, ref_argmax)
+
+ self.show_results('min/max', durations, 'combo')
+
+ def test_benchmark_min_pos(self):
+ """Benchmark min_max wit min positive.
+
+ Compares with:
+
+ - numpy.nanmin(data[data > 0]); numpy.nanmin(pos); numpy.nanmax(pos)
+
+ It runs bench for different types, different data size and 3
+ data sets: increasing , decreasing and random data.
+ """
+ durations = {'min/max': [], 'combo': []}
+
+ _logger.info('Benchmark against min, max, positive min')
+
+ for dtype in self.DTYPES:
+ for arange in self.ARANGE:
+ for exponent in self.EXPONENT:
+ size = 10**exponent
+ with self.subTest(dtype=dtype, size=size, arange=arange):
+ if arange == 'ascent':
+ data = numpy.arange(0, size, 1, dtype=dtype)
+ elif arange == 'descent':
+ data = numpy.arange(size, 0, -1, dtype=dtype)
+ else:
+ if dtype in ('float32', 'float64'):
+ data = numpy.random.random(size)
+ else:
+ data = numpy.random.randint(10**6, size=size)
+ data = numpy.array(data, dtype=dtype)
+
+ start = time.time()
+ ref_min_positive = numpy.nanmin(data[data > 0])
+ ref_min = numpy.nanmin(data)
+ ref_max = numpy.nanmax(data)
+ durations['min/max'].append(time.time() - start)
+
+ start = time.time()
+ result = combo.min_max(data, min_positive=True)
+ durations['combo'].append(time.time() - start)
+
+ _logger.info(
+ '%s-%s-10**%d\tx%.2f min/minpos/max',
+ dtype, arange, exponent,
+ durations['min/max'][-1] / durations['combo'][-1])
+
+ self.assertEqual(result.min_positive, ref_min_positive)
+ self.assertEqual(result.minimum, ref_min)
+ self.assertEqual(result.maximum, ref_max)
+
+ self.show_results('min/max/min positive', durations, 'combo')
+
+ def show_results(self, title, durations, ref_key):
+ try:
+ from matplotlib import pyplot
+ except ImportError:
+ _logger.warning('matplotlib not available')
+ return
+
+ pyplot.title(title)
+ pyplot.xlabel('-'.join(self.DTYPES))
+ pyplot.ylabel('duration (sec)')
+ for label, values in durations.items():
+ pyplot.semilogy(values, label=label)
+ pyplot.legend()
+ pyplot.show()
+
+ pyplot.title(title)
+ pyplot.xlabel('-'.join(self.DTYPES))
+ pyplot.ylabel('Duration ratio')
+ ref = numpy.array(durations[ref_key])
+ for label, values in durations.items():
+ values = numpy.array(values)
+ pyplot.plot(values/ref, label=label + ' / ' + ref_key)
+ pyplot.legend()
+ pyplot.show()
diff --git a/src/silx/math/test/histo_benchmarks.py b/src/silx/math/test/histo_benchmarks.py
new file mode 100644
index 0000000..7d3216d
--- /dev/null
+++ b/src/silx/math/test/histo_benchmarks.py
@@ -0,0 +1,269 @@
+# coding: utf-8
+# /*##########################################################################
+# Copyright (C) 2016 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
+"""
+histogramnd benchmarks, vs numpy.histogramdd (bin counts and weights).
+"""
+
+import numpy as np
+
+import time
+
+from silx.math import histogramnd
+
+
+def print_times(t0s, t1s, t2s, t3s):
+ c_times = t1s - t0s
+ np_times = t2s - t1s
+ np_w_times = t3s - t2s
+
+ time_txt = 'min : {0: <7.3f}; max : {1: <7.3f}; avg : {2: <7.3f}'
+
+ print('\tTimes :')
+ print('\tC : ' + time_txt.format(c_times.min(),
+ c_times.max(),
+ c_times.mean()))
+ print('\tNP : ' + time_txt.format(np_times.min(),
+ np_times.max(),
+ np_times.mean()))
+ print('\tNP(W) : ' + time_txt.format(np_w_times.min(),
+ np_w_times.max(),
+ np_w_times.mean()))
+
+
+def commpare_results(txt,
+ times,
+ result_c,
+ result_np,
+ result_np_w,
+ sample,
+ weights,
+ raise_ex=False):
+
+ if result_np:
+ hits_cmp = np.array_equal(result_c[0], result_np[0])
+ else:
+ hits_cmp = None
+
+ if result_np_w and result_c[1] is not None:
+ weights_cmp = np.array_equal(result_c[1], result_np_w[0])
+ else:
+ weights_cmp = None
+
+ if((hits_cmp is not None and not hits_cmp) or
+ (weights_cmp is not None and not weights_cmp)):
+ err_txt = (txt + ' : results arent the same : '
+ 'hits : {0}, '
+ 'weights : {1}.'
+ ''.format('OK' if hits_cmp else 'NOK',
+ 'OK' if weights_cmp else 'NOK'))
+ print('\t' + err_txt)
+ if raise_ex:
+ raise ValueError(err_txt)
+ return False
+
+ result_txt = ' : results OK. c : {0: <7.3f};'.format(times[0])
+ if result_np or result_np_w:
+ result_txt += (' np : {0: <7.3f}; '
+ 'np (weights) {1: <7.3f}.'
+ ''.format(times[1], times[2]))
+ print('\t' + txt + result_txt)
+ return True
+
+
+def benchmark(n_loops,
+ sample_shape,
+ sample_rng,
+ weights_rng,
+ histo_range,
+ n_bins,
+ weight_min,
+ weight_max,
+ last_bin_closed,
+ dtype=np.double,
+ do_weights=True,
+ do_numpy=True):
+
+ int_min = 0
+ int_max = 100000
+
+ sample = np.random.randint(int_min,
+ high=int_max,
+ size=sample_shape).astype(np.double)
+ sample = (sample_rng[0] +
+ (sample - int_min) *
+ (sample_rng[1] - sample_rng[0]) /
+ (int_max - int_min))
+ sample = sample.astype(dtype)
+
+ if do_weights:
+ weights = np.random.randint(int_min,
+ high=int_max,
+ size=(ssetup.pyample_shape[0],))
+ weights = weights.astype(np.double)
+ weights = (weights_rng[0] +
+ (weights - int_min) *
+ (weights_rng[1] - weights_rng[0]) /
+ (int_max - int_min))
+ else:
+ weights = None
+
+ t0s = []
+ t1s = []
+ t2s = []
+ t3s = []
+
+ for i in range(n_loops):
+ t0s.append(time.time())
+ result_c = histogramnd(sample,
+ histo_range,
+ n_bins,
+ weights=weights,
+ weight_min=weight_min,
+ weight_max=weight_max,
+ last_bin_closed=last_bin_closed)
+ t1s.append(time.time())
+ if do_numpy:
+ result_np = np.histogramdd(sample,
+ bins=n_bins,
+ range=histo_range)
+ t2s.append(time.time())
+ result_np_w = np.histogramdd(sample,
+ bins=n_bins,
+ range=histo_range,
+ weights=weights)
+ t3s.append(time.time())
+ else:
+ result_np = None
+ result_np_w = None
+ t2s.append(0)
+ t3s.append(0)
+
+ commpare_results('Run {0}'.format(i),
+ [t1s[-1] - t0s[-1], t2s[-1] - t1s[-1], t3s[-1] - t2s[-1]],
+ result_c,
+ result_np,
+ result_np_w,
+ sample,
+ weights)
+
+ print_times(np.array(t0s), np.array(t1s), np.array(t2s), np.array(t3s))
+
+
+def run_benchmark(dtype=np.double,
+ do_weights=True,
+ do_numpy=True):
+ n_loops = 5
+
+ weights_rng = [0., 100.]
+ sample_rng = [0., 100.]
+
+ weight_min = None
+ weight_max = None
+ last_bin_closed = True
+
+ # ====================================================
+ # ====================================================
+ # 1D
+ # ====================================================
+ # ====================================================
+
+ print('==========================')
+ print(' 1D [{0}]'.format(dtype))
+ print('==========================')
+ sample_shape = (10**7,)
+ histo_range = [[0., 100.]]
+ n_bins = 30
+
+ benchmark(n_loops,
+ sample_shape,
+ sample_rng,
+ weights_rng,
+ histo_range,
+ n_bins,
+ weight_min,
+ weight_max,
+ last_bin_closed,
+ dtype=dtype,
+ do_weights=True,
+ do_numpy=do_numpy)
+
+ # ====================================================
+ # ====================================================
+ # 2D
+ # ====================================================
+ # ====================================================
+
+ print('==========================')
+ print(' 2D [{0}]'.format(dtype))
+ print('==========================')
+ sample_shape = (10**7, 2)
+ histo_range = [[0., 100.], [0., 100.]]
+ n_bins = 30
+
+ benchmark(n_loops,
+ sample_shape,
+ sample_rng,
+ weights_rng,
+ histo_range,
+ n_bins,
+ weight_min,
+ weight_max,
+ last_bin_closed,
+ dtype=dtype,
+ do_weights=True,
+ do_numpy=do_numpy)
+
+ # ====================================================
+ # ====================================================
+ # 3D
+ # ====================================================
+ # ====================================================
+
+ print('==========================')
+ print(' 3D [{0}]'.format(dtype))
+ print('==========================')
+ sample_shape = (10**7, 3)
+ histo_range = np.array([[0., 100.], [0., 100.], [0., 100.]])
+ n_bins = 30
+
+ benchmark(n_loops,
+ sample_shape,
+ sample_rng,
+ weights_rng,
+ histo_range,
+ n_bins,
+ weight_min,
+ weight_max,
+ last_bin_closed,
+ dtype=dtype,
+ do_weights=True,
+ do_numpy=do_numpy)
+
+if __name__ == '__main__':
+ types = (np.double, np.int32, np.float32,)
+
+ for t in types:
+ run_benchmark(t,
+ do_weights=True,
+ do_numpy=True)
diff --git a/src/silx/math/test/test_HistogramndLut_nominal.py b/src/silx/math/test/test_HistogramndLut_nominal.py
new file mode 100644
index 0000000..52e003c
--- /dev/null
+++ b/src/silx/math/test/test_HistogramndLut_nominal.py
@@ -0,0 +1,571 @@
+# coding: utf-8
+# /*##########################################################################
+# Copyright (C) 2016-2019 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
+"""
+Nominal tests of the HistogramndLut function.
+"""
+
+import unittest
+
+import numpy as np
+
+from silx.math import HistogramndLut
+
+
+def _get_bin_edges(histo_range, n_bins, n_dims):
+ edges = []
+ for i_dim in range(n_dims):
+ edges.append(histo_range[i_dim, 0] +
+ np.arange(n_bins[i_dim] + 1) *
+ (histo_range[i_dim, 1] - histo_range[i_dim, 0]) /
+ n_bins[i_dim])
+ return tuple(edges)
+
+
+# ==============================================================
+# ==============================================================
+# ==============================================================
+
+
+class _TestHistogramndLut_nominal(unittest.TestCase):
+ """
+ Unit tests of the HistogramndLut class.
+ """
+ __test__ = False # ignore abstract class
+
+ ndims = None
+
+ def setUp(self):
+ ndims = self.ndims
+ if ndims is None:
+ self.skipTest("Abstract class")
+ self.tested_dim = ndims-1
+
+ if ndims is None:
+ raise ValueError('ndims class member not set.')
+
+ sample = np.array([5.5, -3.3,
+ 0., -0.5,
+ 3.3, 8.8,
+ -7.7, 6.0,
+ -4.0])
+
+ weights = np.array([500.5, -300.3,
+ 0.01, -0.5,
+ 300.3, 800.8,
+ -700.7, 600.6,
+ -400.4])
+
+ n_elems = len(sample)
+
+ if ndims == 1:
+ shape = (n_elems,)
+ else:
+ shape = (n_elems, ndims)
+
+ self.sample = np.zeros(shape=shape, dtype=sample.dtype)
+ if ndims == 1:
+ self.sample = sample
+ else:
+ self.sample[..., ndims-1] = sample
+
+ self.weights = weights
+
+ # the tests are performed along one dimension,
+ # all the other bins indices along the other dimensions
+ # are expected to be 2
+ # (e.g : when testing a 2D sample : [0, x] will go into
+ # bin [2, y] because of the bin ranges [-2, 2] and n_bins = 4
+ # for the first dimension)
+ self.other_axes_index = 2
+ self.histo_range = np.repeat([[-2., 2.]], ndims, axis=0)
+ self.histo_range[ndims-1] = [-4., 6.]
+
+ self.n_bins = np.array([4]*ndims)
+ self.n_bins[ndims-1] = 5
+
+ if ndims == 1:
+ def fill_histo(h, v, dim, op=None):
+ if op:
+ h[:] = op(h[:], v)
+ else:
+ h[:] = v
+ self.fill_histo = fill_histo
+ else:
+ def fill_histo(h, v, dim, op=None):
+ idx = [self.other_axes_index]*len(h.shape)
+ idx[dim] = slice(0, None)
+ idx = tuple(idx)
+ if op:
+ h[idx] = op(h[idx], v)
+ else:
+ h[idx] = v
+ self.fill_histo = fill_histo
+
+ def test_nominal_bin_edges(self):
+
+ instance = HistogramndLut(self.sample,
+ self.histo_range,
+ self.n_bins)
+
+ bin_edges = instance.bins_edges
+
+ expected_edges = _get_bin_edges(self.histo_range,
+ self.n_bins,
+ self.ndims)
+
+ for i_edges, edges in enumerate(expected_edges):
+ self.assertTrue(np.array_equal(bin_edges[i_edges],
+ expected_edges[i_edges]),
+ msg='Testing bin_edges for dim {0}'
+ ''.format(i_edges+1))
+
+ def test_nominal_histo_range(self):
+
+ instance = HistogramndLut(self.sample,
+ self.histo_range,
+ self.n_bins)
+
+ histo_range = instance.histo_range
+
+ self.assertTrue(np.array_equal(histo_range, self.histo_range))
+
+ def test_nominal_last_bin_closed(self):
+
+ instance = HistogramndLut(self.sample,
+ self.histo_range,
+ self.n_bins)
+
+ last_bin_closed = instance.last_bin_closed
+
+ self.assertEqual(last_bin_closed, False)
+
+ instance = HistogramndLut(self.sample,
+ self.histo_range,
+ self.n_bins,
+ last_bin_closed=True)
+
+ last_bin_closed = instance.last_bin_closed
+
+ self.assertEqual(last_bin_closed, True)
+
+ instance = HistogramndLut(self.sample,
+ self.histo_range,
+ self.n_bins,
+ last_bin_closed=False)
+
+ last_bin_closed = instance.last_bin_closed
+
+ self.assertEqual(last_bin_closed, False)
+
+ def test_nominal_n_bins_array(self):
+
+ test_n_bins = np.arange(self.ndims) + 10
+ instance = HistogramndLut(self.sample,
+ self.histo_range,
+ test_n_bins)
+
+ n_bins = instance.n_bins
+
+ self.assertTrue(np.array_equal(test_n_bins, n_bins))
+
+ def test_nominal_n_bins_scalar(self):
+
+ test_n_bins = 10
+ expected_n_bins = np.array([test_n_bins] * self.ndims)
+ instance = HistogramndLut(self.sample,
+ self.histo_range,
+ test_n_bins)
+
+ n_bins = instance.n_bins
+
+ self.assertTrue(np.array_equal(expected_n_bins, n_bins))
+
+ def test_nominal_histo_ref(self):
+ """
+ """
+ expected_h_tpl = np.array([2, 1, 1, 1, 1])
+ expected_c_tpl = np.array([-700.7, -0.5, 0.01, 300.3, 500.5])
+
+ expected_h = np.zeros(shape=self.n_bins, dtype=np.double)
+ expected_c = np.zeros(shape=self.n_bins, dtype=np.double)
+
+ self.fill_histo(expected_h, expected_h_tpl, self.ndims-1)
+ self.fill_histo(expected_c, expected_c_tpl, self.ndims-1)
+
+ instance = HistogramndLut(self.sample,
+ self.histo_range,
+ self.n_bins)
+
+ instance.accumulate(self.weights)
+
+ histo = instance.histo()
+ w_histo = instance.weighted_histo()
+ histo_ref = instance.histo(copy=False)
+ w_histo_ref = instance.weighted_histo(copy=False)
+
+ self.assertTrue(np.array_equal(histo, expected_h))
+ self.assertTrue(np.array_equal(w_histo, expected_c))
+ self.assertTrue(np.array_equal(histo_ref, expected_h))
+ self.assertTrue(np.array_equal(w_histo_ref, expected_c))
+
+ histo_ref[0, ...] = histo_ref[0, ...] + 10
+ w_histo_ref[0, ...] = w_histo_ref[0, ...] + 20
+
+ self.assertTrue(np.array_equal(histo, expected_h))
+ self.assertTrue(np.array_equal(w_histo, expected_c))
+ self.assertFalse(np.array_equal(histo_ref, expected_h))
+ self.assertFalse(np.array_equal(w_histo_ref, expected_c))
+
+ histo_2 = instance.histo()
+ w_histo_2 = instance.weighted_histo()
+
+ self.assertFalse(np.array_equal(histo_2, expected_h))
+ self.assertFalse(np.array_equal(w_histo_2, expected_c))
+ self.assertTrue(np.array_equal(histo_2, histo_ref))
+ self.assertTrue(np.array_equal(w_histo_2, w_histo_ref))
+
+ def test_nominal_accumulate_once(self):
+ """
+ """
+ expected_h_tpl = np.array([2, 1, 1, 1, 1])
+ expected_c_tpl = np.array([-700.7, -0.5, 0.01, 300.3, 500.5])
+
+ expected_h = np.zeros(shape=self.n_bins, dtype=np.double)
+ expected_c = np.zeros(shape=self.n_bins, dtype=np.double)
+
+ self.fill_histo(expected_h, expected_h_tpl, self.ndims-1)
+ self.fill_histo(expected_c, expected_c_tpl, self.ndims-1)
+
+ instance = HistogramndLut(self.sample,
+ self.histo_range,
+ self.n_bins)
+
+ instance.accumulate(self.weights)
+
+ histo = instance.histo()
+ w_histo = instance.weighted_histo()
+
+ self.assertEqual(w_histo.dtype, np.float64)
+ self.assertEqual(histo.dtype, np.uint32)
+ self.assertTrue(np.array_equal(histo, expected_h))
+ self.assertTrue(np.array_equal(w_histo, expected_c))
+ self.assertTrue(np.array_equal(instance.histo(), expected_h))
+ self.assertTrue(np.array_equal(instance.weighted_histo(),
+ expected_c))
+
+ def test_nominal_accumulate_twice(self):
+ """
+ """
+ expected_h_tpl = np.array([2, 1, 1, 1, 1])
+ expected_c_tpl = np.array([-700.7, -0.5, 0.01, 300.3, 500.5])
+
+ expected_h = np.zeros(shape=self.n_bins, dtype=np.double)
+ expected_c = np.zeros(shape=self.n_bins, dtype=np.double)
+
+ self.fill_histo(expected_h, expected_h_tpl, self.ndims-1)
+ self.fill_histo(expected_c, expected_c_tpl, self.ndims-1)
+
+ # calling accumulate twice
+ expected_h *= 2
+ expected_c *= 2
+
+ instance = HistogramndLut(self.sample,
+ self.histo_range,
+ self.n_bins)
+
+ instance.accumulate(self.weights)
+
+ instance.accumulate(self.weights)
+
+ histo = instance.histo()
+ w_histo = instance.weighted_histo()
+
+ self.assertEqual(w_histo.dtype, np.float64)
+ self.assertEqual(histo.dtype, np.uint32)
+ self.assertTrue(np.array_equal(histo, expected_h))
+ self.assertTrue(np.array_equal(w_histo, expected_c))
+ self.assertTrue(np.array_equal(instance.histo(), expected_h))
+ self.assertTrue(np.array_equal(instance.weighted_histo(),
+ expected_c))
+
+ def test_nominal_apply_lut_once(self):
+ """
+ """
+ expected_h_tpl = np.array([2, 1, 1, 1, 1])
+ expected_c_tpl = np.array([-700.7, -0.5, 0.01, 300.3, 500.5])
+
+ expected_h = np.zeros(shape=self.n_bins, dtype=np.double)
+ expected_c = np.zeros(shape=self.n_bins, dtype=np.double)
+
+ self.fill_histo(expected_h, expected_h_tpl, self.ndims-1)
+ self.fill_histo(expected_c, expected_c_tpl, self.ndims-1)
+
+ instance = HistogramndLut(self.sample,
+ self.histo_range,
+ self.n_bins)
+
+ histo, w_histo = instance.apply_lut(self.weights)
+
+ self.assertEqual(w_histo.dtype, np.float64)
+ self.assertEqual(histo.dtype, np.uint32)
+ self.assertTrue(np.array_equal(histo, expected_h))
+ self.assertTrue(np.array_equal(w_histo, expected_c))
+ self.assertEqual(instance.histo(), None)
+ self.assertEqual(instance.weighted_histo(), None)
+
+ def test_nominal_apply_lut_twice(self):
+ """
+ """
+ expected_h_tpl = np.array([2, 1, 1, 1, 1])
+ expected_c_tpl = np.array([-700.7, -0.5, 0.01, 300.3, 500.5])
+
+ expected_h = np.zeros(shape=self.n_bins, dtype=np.double)
+ expected_c = np.zeros(shape=self.n_bins, dtype=np.double)
+
+ self.fill_histo(expected_h, expected_h_tpl, self.ndims-1)
+ self.fill_histo(expected_c, expected_c_tpl, self.ndims-1)
+
+ # calling apply_lut twice
+ expected_h *= 2
+ expected_c *= 2
+
+ instance = HistogramndLut(self.sample,
+ self.histo_range,
+ self.n_bins)
+
+ histo, w_histo = instance.apply_lut(self.weights)
+ histo_2, w_histo_2 = instance.apply_lut(self.weights,
+ histo=histo,
+ weighted_histo=w_histo)
+
+ self.assertEqual(id(histo), id(histo_2))
+ self.assertEqual(id(w_histo), id(w_histo_2))
+ self.assertEqual(w_histo.dtype, np.float64)
+ self.assertEqual(histo.dtype, np.uint32)
+ self.assertTrue(np.array_equal(histo, expected_h))
+ self.assertTrue(np.array_equal(w_histo, expected_c))
+ self.assertEqual(instance.histo(), None)
+ self.assertEqual(instance.weighted_histo(), None)
+
+ def test_nominal_accumulate_last_bin_closed(self):
+ """
+ """
+ expected_h_tpl = np.array([2, 1, 1, 1, 2])
+ expected_c_tpl = np.array([-700.7, -0.5, 0.01, 300.3, 1101.1])
+
+ expected_h = np.zeros(shape=self.n_bins, dtype=np.double)
+ expected_c = np.zeros(shape=self.n_bins, dtype=np.double)
+
+ self.fill_histo(expected_h, expected_h_tpl, self.ndims-1)
+ self.fill_histo(expected_c, expected_c_tpl, self.ndims-1)
+
+ instance = HistogramndLut(self.sample,
+ self.histo_range,
+ self.n_bins,
+ last_bin_closed=True)
+
+ instance.accumulate(self.weights)
+
+ histo = instance.histo()
+ w_histo = instance.weighted_histo()
+
+ self.assertEqual(w_histo.dtype, np.float64)
+ self.assertEqual(histo.dtype, np.uint32)
+ self.assertTrue(np.array_equal(histo, expected_h))
+ self.assertTrue(np.array_equal(w_histo, expected_c))
+
+ def test_nominal_accumulate_weight_min_max(self):
+ """
+ """
+ weight_min = -299.9
+ weight_max = 499.9
+
+ expected_h_tpl = np.array([0, 1, 1, 1, 0])
+ expected_c_tpl = np.array([0., -0.5, 0.01, 300.3, 0.])
+
+ expected_h = np.zeros(shape=self.n_bins, dtype=np.double)
+ expected_c = np.zeros(shape=self.n_bins, dtype=np.double)
+
+ self.fill_histo(expected_h, expected_h_tpl, self.ndims-1)
+ self.fill_histo(expected_c, expected_c_tpl, self.ndims-1)
+
+ instance = HistogramndLut(self.sample,
+ self.histo_range,
+ self.n_bins)
+
+ instance.accumulate(self.weights,
+ weight_min=weight_min,
+ weight_max=weight_max)
+
+ histo = instance.histo()
+ w_histo = instance.weighted_histo()
+
+ self.assertEqual(w_histo.dtype, np.float64)
+ self.assertEqual(histo.dtype, np.uint32)
+ self.assertTrue(np.array_equal(histo, expected_h))
+ self.assertTrue(np.array_equal(w_histo, expected_c))
+
+ def test_nominal_accumulate_forced_int32(self):
+ """
+ double weights, int32 weighted_histogram
+ """
+ expected_h_tpl = np.array([2, 1, 1, 1, 1])
+ expected_c_tpl = np.array([-700, 0, 0, 300, 500])
+
+ expected_h = np.zeros(shape=self.n_bins, dtype=np.double)
+ expected_c = np.zeros(shape=self.n_bins, dtype=np.double)
+
+ self.fill_histo(expected_h, expected_h_tpl, self.ndims-1)
+ self.fill_histo(expected_c, expected_c_tpl, self.ndims-1)
+
+ instance = HistogramndLut(self.sample,
+ self.histo_range,
+ self.n_bins,
+ dtype=np.int32)
+
+ instance.accumulate(self.weights)
+
+ histo = instance.histo()
+ w_histo = instance.weighted_histo()
+
+ self.assertEqual(w_histo.dtype, np.int32)
+ self.assertEqual(histo.dtype, np.uint32)
+ self.assertTrue(np.array_equal(histo, expected_h))
+ self.assertTrue(np.array_equal(w_histo, expected_c))
+
+ def test_nominal_accumulate_forced_float32(self):
+ """
+ int32 weights, float32 weighted_histogram
+ """
+ expected_h_tpl = np.array([2, 1, 1, 1, 1])
+ expected_c_tpl = np.array([-700., 0., 0., 300., 500.])
+
+ expected_h = np.zeros(shape=self.n_bins, dtype=np.double)
+ expected_c = np.zeros(shape=self.n_bins, dtype=np.float32)
+
+ self.fill_histo(expected_h, expected_h_tpl, self.ndims-1)
+ self.fill_histo(expected_c, expected_c_tpl, self.ndims-1)
+
+ instance = HistogramndLut(self.sample,
+ self.histo_range,
+ self.n_bins,
+ dtype=np.float32)
+
+ instance.accumulate(self.weights.astype(np.int32))
+
+ histo = instance.histo()
+ w_histo = instance.weighted_histo()
+
+ self.assertEqual(w_histo.dtype, np.float32)
+ self.assertEqual(histo.dtype, np.uint32)
+ self.assertTrue(np.array_equal(histo, expected_h))
+ self.assertTrue(np.array_equal(w_histo, expected_c))
+
+ def test_nominal_accumulate_int32(self):
+ """
+ int32 weights
+ """
+ expected_h_tpl = np.array([2, 1, 1, 1, 1])
+ expected_c_tpl = np.array([-700, 0, 0, 300, 500])
+
+ expected_h = np.zeros(shape=self.n_bins, dtype=np.double)
+ expected_c = np.zeros(shape=self.n_bins, dtype=np.int32)
+
+ self.fill_histo(expected_h, expected_h_tpl, self.ndims-1)
+ self.fill_histo(expected_c, expected_c_tpl, self.ndims-1)
+
+ instance = HistogramndLut(self.sample,
+ self.histo_range,
+ self.n_bins)
+
+ instance.accumulate(self.weights.astype(np.int32))
+
+ histo = instance.histo()
+ w_histo = instance.weighted_histo()
+
+ self.assertEqual(w_histo.dtype, np.int32)
+ self.assertEqual(histo.dtype, np.uint32)
+ self.assertTrue(np.array_equal(histo, expected_h))
+ self.assertTrue(np.array_equal(w_histo, expected_c))
+
+ def test_nominal_accumulate_int32_double(self):
+ """
+ int32 weights
+ """
+ expected_h_tpl = np.array([2, 1, 1, 1, 1])
+ expected_c_tpl = np.array([-700, 0, 0, 300, 500])
+
+ expected_h = np.zeros(shape=self.n_bins, dtype=np.double)
+ expected_c = np.zeros(shape=self.n_bins, dtype=np.int32)
+
+ self.fill_histo(expected_h, expected_h_tpl, self.ndims-1)
+ self.fill_histo(expected_c, expected_c_tpl, self.ndims-1)
+
+ instance = HistogramndLut(self.sample,
+ self.histo_range,
+ self.n_bins)
+
+ instance.accumulate(self.weights.astype(np.int32))
+ instance.accumulate(self.weights)
+
+ histo = instance.histo()
+ w_histo = instance.weighted_histo()
+
+ expected_h *= 2
+ expected_c *= 2
+
+ self.assertEqual(w_histo.dtype, np.int32)
+ self.assertEqual(histo.dtype, np.uint32)
+ self.assertTrue(np.array_equal(histo, expected_h))
+ self.assertTrue(np.array_equal(w_histo, expected_c))
+
+ def testNoneNativeTypes(self):
+ type = self.sample.dtype.newbyteorder("B")
+ sampleB = self.sample.astype(type)
+
+ type = self.sample.dtype.newbyteorder("L")
+ sampleL = self.sample.astype(type)
+
+ histo_inst = HistogramndLut(sampleB,
+ self.histo_range,
+ self.n_bins)
+
+ histo_inst = HistogramndLut(sampleL,
+ self.histo_range,
+ self.n_bins)
+
+
+class TestHistogramndLut_nominal_1d(_TestHistogramndLut_nominal):
+ __test__ = True # because _TestHistogramndLut_nominal is ignored
+ ndims = 1
+
+
+class TestHistogramndLut_nominal_2d(_TestHistogramndLut_nominal):
+ __test__ = True # because _TestHistogramndLut_nominal is ignored
+ ndims = 2
+
+
+class TestHistogramndLut_nominal_3d(_TestHistogramndLut_nominal):
+ __test__ = True # because _TestHistogramndLut_nominal is ignored
+ ndims = 3
diff --git a/src/silx/math/test/test_calibration.py b/src/silx/math/test/test_calibration.py
new file mode 100644
index 0000000..7158293
--- /dev/null
+++ b/src/silx/math/test/test_calibration.py
@@ -0,0 +1,145 @@
+# coding: utf-8
+# /*##########################################################################
+# Copyright (C) 2018 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
+"""Tests of the calibration module"""
+
+from __future__ import division
+
+__authors__ = ["P. Knobel"]
+__license__ = "MIT"
+__date__ = "14/05/2018"
+
+
+import unittest
+
+import numpy
+
+from silx.math.calibration import NoCalibration, LinearCalibration, \
+ ArrayCalibration, FunctionCalibration
+
+
+X = numpy.array([3.14, 2.73, 1337])
+
+
+class TestNoCalibration(unittest.TestCase):
+ def setUp(self):
+ self.calib = NoCalibration()
+
+ def testIsAffine(self):
+ self.assertTrue(self.calib.is_affine())
+
+ def testSlope(self):
+ self.assertEqual(self.calib.get_slope(), 1.)
+
+ def testYIntercept(self):
+ self.assertEqual(self.calib(0.),
+ 0.)
+
+ def testCall(self):
+ self.assertTrue(numpy.array_equal(self.calib(X), X))
+
+
+class TestLinearCalibration(unittest.TestCase):
+ def setUp(self):
+ self.y_intercept = 1.5
+ self.slope = 2.5
+ self.calib = LinearCalibration(y_intercept=self.y_intercept,
+ slope=self.slope)
+
+ def testIsAffine(self):
+ self.assertTrue(self.calib.is_affine())
+
+ def testSlope(self):
+ self.assertEqual(self.calib.get_slope(), self.slope)
+
+ def testYIntercept(self):
+ self.assertEqual(self.calib(0.),
+ self.y_intercept)
+
+ def testCall(self):
+ self.assertTrue(numpy.array_equal(self.calib(X),
+ self.y_intercept + self.slope * X))
+
+
+class TestArrayCalibration(unittest.TestCase):
+ def setUp(self):
+ self.arr = numpy.array([45.2, 25.3, 666., -8.])
+ self.calib = ArrayCalibration(self.arr)
+ self.affine_calib = ArrayCalibration([0.1, 0.2, 0.3])
+
+ def testIsAffine(self):
+ self.assertFalse(self.calib.is_affine())
+ self.assertTrue(self.affine_calib.is_affine())
+
+ def testSlope(self):
+ with self.assertRaises(AttributeError):
+ self.calib.get_slope()
+ self.assertEqual(self.affine_calib.get_slope(),
+ 0.1)
+
+ def testYIntercept(self):
+ self.assertEqual(self.calib(0),
+ self.arr[0])
+
+ def testCall(self):
+ with self.assertRaises(ValueError):
+ # X is an array with a different shape
+ self.calib(X)
+
+ with self.assertRaises(ValueError):
+ # floats are not valid indices
+ self.calib(3.14)
+
+ self.assertTrue(
+ numpy.array_equal(self.calib([1, 2, 3, 4]),
+ self.arr))
+
+ for idx, value in enumerate(self.arr):
+ self.assertEqual(self.calib(idx), value)
+
+
+class TestFunctionCalibration(unittest.TestCase):
+ def setUp(self):
+ self.non_affine_fun = numpy.sin
+ self.non_affine_calib = FunctionCalibration(self.non_affine_fun)
+
+ self.affine_fun = lambda x: 52. * x + 0.01
+ self.affine_calib = FunctionCalibration(self.affine_fun,
+ is_affine=True)
+
+ def testIsAffine(self):
+ self.assertFalse(self.non_affine_calib.is_affine())
+ self.assertTrue(self.affine_calib.is_affine())
+
+ def testSlope(self):
+ with self.assertRaises(AttributeError):
+ self.non_affine_calib.get_slope()
+ self.assertAlmostEqual(self.affine_calib.get_slope(),
+ 52.)
+
+ def testCall(self):
+ for x in X:
+ self.assertAlmostEqual(self.non_affine_calib(x),
+ self.non_affine_fun(x))
+ self.assertAlmostEqual(self.affine_calib(x),
+ self.affine_fun(x))
diff --git a/src/silx/math/test/test_colormap.py b/src/silx/math/test/test_colormap.py
new file mode 100644
index 0000000..0b0ec59
--- /dev/null
+++ b/src/silx/math/test/test_colormap.py
@@ -0,0 +1,269 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2018-2021 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
+"""Test for colormap mapping implementation"""
+
+from __future__ import division
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "16/05/2018"
+
+
+import logging
+import sys
+
+import numpy
+
+from silx.utils.testutils import ParametricTestCase
+from silx.math import colormap
+
+
+_logger = logging.getLogger(__name__)
+
+
+class TestNormalization(ParametricTestCase):
+ """Test silx.math.colormap.Normalization sub classes"""
+
+ def _testCodec(self, normalization, rtol=1e-5):
+ """Test apply/revert for normalizations"""
+ test_data = (numpy.arange(1, 10, dtype=numpy.int32),
+ numpy.linspace(1., 100., 1000, dtype=numpy.float32),
+ numpy.linspace(-1., 1., 100, dtype=numpy.float32),
+ 1.,
+ 1)
+
+ for index in range(len(test_data)):
+ with self.subTest(normalization=normalization, data_index=index):
+ data = test_data[index]
+ normalized = normalization.apply(data, 1., 100.)
+ result = normalization.revert(normalized, 1., 100.)
+
+ self.assertTrue(numpy.array_equal(
+ numpy.isnan(normalized), numpy.isnan(result)))
+
+ if isinstance(data, numpy.ndarray):
+ notNaN = numpy.logical_not(numpy.isnan(result))
+ data = data[notNaN]
+ result = result[notNaN]
+ self.assertTrue(numpy.allclose(data, result, rtol=rtol))
+
+ def testLinearNormalization(self):
+ """Test for LinearNormalization"""
+ normalization = colormap.LinearNormalization()
+ self._testCodec(normalization)
+
+ def testLogarithmicNormalization(self):
+ """Test for LogarithmicNormalization"""
+ normalization = colormap.LogarithmicNormalization()
+ # relative tolerance is higher because of the log approximation
+ self._testCodec(normalization, rtol=1e-3)
+
+ # Specific extra tests
+ self.assertTrue(numpy.isnan(normalization.apply(-1., 1., 100.)))
+ self.assertTrue(numpy.isnan(normalization.apply(numpy.nan, 1., 100.)))
+ self.assertEqual(normalization.apply(numpy.inf, 1., 100.), numpy.inf)
+ self.assertEqual(normalization.apply(0, 1., 100.), - numpy.inf)
+
+ def testArcsinhNormalization(self):
+ """Test for ArcsinhNormalization"""
+ self._testCodec(colormap.ArcsinhNormalization())
+
+ def testSqrtNormalization(self):
+ """Test for SqrtNormalization"""
+ normalization = colormap.SqrtNormalization()
+ self._testCodec(normalization)
+
+ # Specific extra tests
+ self.assertTrue(numpy.isnan(normalization.apply(-1., 0., 100.)))
+ self.assertTrue(numpy.isnan(normalization.apply(numpy.nan, 0., 100.)))
+ self.assertEqual(normalization.apply(numpy.inf, 0., 100.), numpy.inf)
+ self.assertEqual(normalization.apply(0, 0., 100.), 0.)
+
+
+class TestColormap(ParametricTestCase):
+ """Test silx.math.colormap.cmap"""
+
+ NORMALIZATIONS = (
+ 'linear',
+ 'log',
+ 'arcsinh',
+ 'sqrt',
+ colormap.LinearNormalization(),
+ colormap.LogarithmicNormalization(),
+ colormap.GammaNormalization(2.),
+ colormap.GammaNormalization(0.5))
+
+ @staticmethod
+ def ref_colormap(data, colors, vmin, vmax, normalization, nan_color):
+ """Reference implementation of colormap
+
+ :param numpy.ndarray data: Data to convert
+ :param numpy.ndarray colors: Color look-up-table
+ :param float vmin: Lower bound of the colormap range
+ :param float vmax: Upper bound of the colormap range
+ :param str normalization: Normalization to use
+ :param Union[numpy.ndarray, None] nan_color: Color to use for NaN
+ """
+ norm_functions = {'linear': lambda v: v,
+ 'log': numpy.log10,
+ 'arcsinh': numpy.arcsinh,
+ 'sqrt': numpy.sqrt}
+
+ if isinstance(normalization, str):
+ norm_function = norm_functions[normalization]
+ else:
+ def norm_function(value):
+ return normalization.apply(value, vmin, vmax)
+
+ with numpy.errstate(divide='ignore', invalid='ignore'):
+ # Ignore divide by zero and invalid value encountered in log10, sqrt
+ norm_data, vmin, vmax = map(norm_function, (data, vmin, vmax))
+
+ if normalization == 'arcsinh' and sys.platform == 'win32':
+ # There is a difference of behavior of numpy.arcsinh
+ # between Windows and other OS for results of infinite values
+ # This makes Windows behaves as Linux and MacOS
+ norm_data[data == numpy.inf] = numpy.inf
+ norm_data[data == -numpy.inf] = -numpy.inf
+
+ nb_colors = len(colors)
+ scale = nb_colors / (vmax - vmin)
+
+ # Substraction must be done in float to avoid overflow with uint
+ indices = numpy.clip(scale * (norm_data - float(vmin)),
+ 0, nb_colors - 1)
+ indices[numpy.isnan(indices)] = nb_colors # Use an extra index for NaN
+ indices = indices.astype('uint')
+
+ # Add NaN color to array
+ if nan_color is None:
+ nan_color = (0,) * colors.shape[-1]
+ colors = numpy.append(colors, numpy.atleast_2d(nan_color), axis=0)
+
+ return colors[indices]
+
+ def _test(self, data, colors, vmin, vmax, normalization, nan_color):
+ """Run test of colormap against alternative implementation
+
+ :param numpy.ndarray data: Data to convert
+ :param numpy.ndarray colors: Color look-up-table
+ :param float vmin: Lower bound of the colormap range
+ :param float vmax: Upper bound of the colormap range
+ :param str normalization: Normalization to use
+ :param Union[numpy.ndarray, None] nan_color: Color to use for NaN
+ """
+ image = colormap.cmap(
+ data, colors, vmin, vmax, normalization, nan_color)
+
+ ref_image = self.ref_colormap(
+ data, colors, vmin, vmax, normalization, nan_color)
+
+ self.assertTrue(numpy.allclose(ref_image, image))
+ self.assertEqual(image.dtype, colors.dtype)
+ self.assertEqual(image.shape, data.shape + (colors.shape[-1],))
+
+ def test(self):
+ """Test all dtypes with finite data
+
+ Test all supported types and endianness
+ """
+ colors = numpy.zeros((256, 4), dtype=numpy.uint8)
+ colors[:, 0] = numpy.arange(len(colors))
+ colors[:, 3] = 255
+
+ # Generates (u)int and floats types
+ dtypes = [e + k + i for e in '<>' for k in 'uif' for i in '1248'
+ if k != 'f' or i != '1']
+ dtypes.append(numpy.dtype(numpy.longdouble).name) # Add long double
+
+ for normalization in self.NORMALIZATIONS:
+ for dtype in dtypes:
+ with self.subTest(dtype=dtype, normalization=normalization):
+ _logger.info('normalization: %s, dtype: %s',
+ normalization, dtype)
+ data = numpy.arange(-5, 15, dtype=dtype).reshape(4, 5)
+
+ self._test(data, colors, 1, 10, normalization, None)
+
+ def test_not_finite(self):
+ """Test float data with not finite values"""
+ colors = numpy.zeros((256, 4), dtype=numpy.uint8)
+ colors[:, 0] = numpy.arange(len(colors))
+ colors[:, 3] = 255
+
+ test_data = { # message: data
+ 'no finite values': (float('inf'), float('-inf'), float('nan')),
+ 'only NaN': (float('nan'), float('nan'), float('nan')),
+ 'mix finite/not finite': (float('inf'), float('-inf'), 1., float('nan')),
+ }
+
+ for normalization in self.NORMALIZATIONS:
+ for msg, data in test_data.items():
+ with self.subTest(msg, normalization=normalization):
+ _logger.info('normalization: %s, %s', normalization, msg)
+ data = numpy.array(data, dtype=numpy.float64)
+ self._test(data, colors, 1, 10, normalization, (0, 0, 0, 0))
+
+ def test_errors(self):
+ """Test raising exception for bad vmin, vmax, normalization parameters
+ """
+ colors = numpy.zeros((256, 4), dtype=numpy.uint8)
+ colors[:, 0] = numpy.arange(len(colors))
+ colors[:, 3] = 255
+
+ data = numpy.arange(10, dtype=numpy.float64)
+
+ test_params = [ # (vmin, vmax, normalization)
+ (-1., 2., 'log'),
+ (0., 1., 'log'),
+ (1., 0., 'log'),
+ (-1., 1., 'sqrt'),
+ (1., -1., 'sqrt'),
+ ]
+
+ for vmin, vmax, normalization in test_params:
+ with self.subTest(
+ vmin=vmin, vmax=vmax, normalization=normalization):
+ _logger.info('normalization: %s, range: [%f, %f]',
+ normalization, vmin, vmax)
+ with self.assertRaises(ValueError):
+ self._test(data, colors, vmin, vmax, normalization, None)
+
+
+def test_apply_colormap():
+ """Basic test of silx.math.colormap.apply_colormap"""
+ data = numpy.arange(256)
+ expected_colors = numpy.empty((256, 4), dtype=numpy.uint8)
+ expected_colors[:, :3] = numpy.arange(256, dtype=numpy.uint8).reshape(256, 1)
+ expected_colors[:, 3] = 255
+ colors = colormap.apply_colormap(
+ data,
+ colormap="gray",
+ norm="linear",
+ autoscale="minmax",
+ vmin=None,
+ vmax=None,
+ gamma=1.0)
+ assert numpy.array_equal(colors, expected_colors)
diff --git a/src/silx/math/test/test_combo.py b/src/silx/math/test/test_combo.py
new file mode 100644
index 0000000..9a96923
--- /dev/null
+++ b/src/silx/math/test/test_combo.py
@@ -0,0 +1,207 @@
+# coding: utf-8
+# /*##########################################################################
+# Copyright (C) 2016-2020 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
+"""Tests of the combo module"""
+
+from __future__ import division
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "17/01/2018"
+
+
+import unittest
+
+import numpy
+
+from silx.utils.testutils import ParametricTestCase
+
+from silx.math.combo import min_max
+
+
+class TestMinMax(ParametricTestCase):
+ """Tests of min max combo"""
+
+ FLOATING_DTYPES = 'float32', 'float64'
+ if hasattr(numpy, "float128"):
+ FLOATING_DTYPES += ('float128',)
+ SIGNED_INT_DTYPES = 'int8', 'int16', 'int32', 'int64'
+ UNSIGNED_INT_DTYPES = 'uint8', 'uint16', 'uint32', 'uint64'
+ DTYPES = FLOATING_DTYPES + SIGNED_INT_DTYPES + UNSIGNED_INT_DTYPES
+
+ def _numpy_min_max(self, data, min_positive=False, finite=False):
+ """Reference numpy implementation of min_max
+
+ :param numpy.ndarray data: Data set to use for test
+ :param bool min_positive: True to test with positive min
+ :param bool finite: True to only test finite values
+ """
+ data = numpy.array(data, copy=False)
+ if data.size == 0:
+ raise ValueError('Zero-sized array')
+
+ minimum = None
+ argmin = None
+ maximum = None
+ argmax = None
+ min_pos = None
+ argmin_pos = None
+
+ if finite:
+ filtered_data = data[numpy.isfinite(data)]
+ else:
+ filtered_data = data
+
+ if filtered_data.size > 0:
+ if numpy.all(numpy.isnan(filtered_data)):
+ minimum = numpy.nan
+ argmin = 0
+ maximum = numpy.nan
+ argmax = 0
+ else:
+ minimum = numpy.nanmin(filtered_data)
+ # nanargmin equivalent
+ argmin = numpy.where(data == minimum)[0][0]
+ maximum = numpy.nanmax(filtered_data)
+ # nanargmax equivalent
+ argmax = numpy.where(data == maximum)[0][0]
+
+ if min_positive:
+ with numpy.errstate(invalid='ignore'):
+ # Ignore invalid value encountered in greater
+ pos_data = filtered_data[filtered_data > 0]
+ if pos_data.size > 0:
+ min_pos = numpy.min(pos_data)
+ argmin_pos = numpy.where(data == min_pos)[0][0]
+
+ return minimum, min_pos, maximum, argmin, argmin_pos, argmax
+
+ def _test_min_max(self, data, min_positive, finite=False):
+ """Compare min_max with numpy for the given dataset
+
+ :param numpy.ndarray data: Data set to use for test
+ :param bool min_positive: True to test with positive min
+ :param bool finite: True to only test finite values
+ """
+ minimum, min_pos, maximum, argmin, argmin_pos, argmax = \
+ self._numpy_min_max(data, min_positive, finite)
+
+ result = min_max(data, min_positive, finite)
+
+ self.assertSimilar(minimum, result.minimum)
+ self.assertSimilar(min_pos, result.min_positive)
+ self.assertSimilar(maximum, result.maximum)
+ self.assertSimilar(argmin, result.argmin)
+ self.assertSimilar(argmin_pos, result.argmin_positive)
+ self.assertSimilar(argmax, result.argmax)
+
+ def assertSimilar(self, a, b):
+ """Assert that a and b are both None or NaN or that a == b."""
+ self.assertTrue((a is None and b is None) or
+ (numpy.isnan(a) and numpy.isnan(b)) or
+ a == b)
+
+ def test_different_datasets(self):
+ """Test min_max with different numpy.arange datasets."""
+ size = 1000
+
+ for dtype in self.DTYPES:
+
+ tests = {
+ '0 to N': (0, 1),
+ 'N-1 to 0': (size - 1, -1)}
+ if dtype not in self.UNSIGNED_INT_DTYPES:
+ tests['N/2 to -N/2'] = size // 2, -1
+ tests['0 to -N'] = 0, -1
+
+ for name, (start, step) in tests.items():
+ for min_positive in (True, False):
+ with self.subTest(dtype=dtype,
+ min_positive=min_positive,
+ data=name):
+ data = numpy.arange(
+ start, start + step * size, step, dtype=dtype)
+
+ self._test_min_max(data, min_positive)
+
+ def test_nodata(self):
+ """Test min_max with None and empty array"""
+ for dtype in self.DTYPES:
+ with self.subTest(dtype=dtype):
+ with self.assertRaises(TypeError):
+ min_max(None)
+
+ data = numpy.array((), dtype=dtype)
+ with self.assertRaises(ValueError):
+ min_max(data)
+
+ NAN_TEST_DATA = [
+ (float('nan'), float('nan')), # All NaNs
+ (float('nan'), 1.0), # NaN first and positive
+ (float('nan'), -1.0), # NaN first and negative
+ (1.0, 2.0, float('nan')), # NaN last and positive
+ (-1.0, -2.0, float('nan')), # NaN last and negative
+ (1.0, float('nan'), -1.0), # Some NaN
+ ]
+
+ def test_nandata(self):
+ """Test min_max with NaN in data"""
+ for dtype in self.FLOATING_DTYPES:
+ for data in self.NAN_TEST_DATA:
+ with self.subTest(dtype=dtype, data=data):
+ data = numpy.array(data, dtype=dtype)
+ self._test_min_max(data, min_positive=True)
+
+ INF_TEST_DATA = [
+ [float('inf')] * 3, # All +inf
+ [float('-inf')] * 3, # All -inf
+ (float('inf'), float('-inf')), # + and - inf
+ (float('inf'), float('-inf'), float('nan')), # +/-inf, nan last
+ (float('nan'), float('-inf'), float('inf')), # +/-inf, nan first
+ (float('inf'), float('nan'), float('-inf')), # +/-inf, nan center
+ ]
+
+ def test_infdata(self):
+ """Test min_max with inf."""
+ for dtype in self.FLOATING_DTYPES:
+ for data in self.INF_TEST_DATA:
+ with self.subTest(dtype=dtype, data=data):
+ data = numpy.array(data, dtype=dtype)
+ self._test_min_max(data, min_positive=True)
+
+ def test_finite(self):
+ """Test min_max with finite=True"""
+ tests = [
+ (-1., 2., 0.), # Basic test
+ (float('nan'), float('inf'), float('-inf')), # NaN + Inf
+ (float('nan'), float('inf'), -2, float('-inf')), # NaN + Inf + 1 value
+ (float('inf'), -3, -2), # values + inf
+ ]
+ tests += self.INF_TEST_DATA
+ tests += self.NAN_TEST_DATA
+
+ for dtype in self.FLOATING_DTYPES:
+ for data in tests:
+ with self.subTest(dtype=dtype, data=data):
+ data = numpy.array(data, dtype=dtype)
+ self._test_min_max(data, min_positive=True, finite=True)
diff --git a/src/silx/math/test/test_histogramnd_error.py b/src/silx/math/test/test_histogramnd_error.py
new file mode 100644
index 0000000..22304cb
--- /dev/null
+++ b/src/silx/math/test/test_histogramnd_error.py
@@ -0,0 +1,519 @@
+# coding: utf-8
+# /*##########################################################################
+# Copyright (C) 2016 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
+
+__authors__ = ["D. Naudet"]
+__license__ = "MIT"
+__date__ = "01/02/2016"
+
+"""
+Tests of the histogramnd function, error cases.
+"""
+import sys
+import platform
+import unittest
+
+import numpy as np
+
+from silx.math.chistogramnd import chistogramnd as histogramnd
+from silx.math import Histogramnd
+
+
+# ==============================================================
+# ==============================================================
+# ==============================================================
+
+
+class _Test_chistogramnd_errors(unittest.TestCase):
+ """
+ Unit tests of the chistogramnd error cases.
+ """
+ __test__ = False # ignore abstract class
+
+ def setUp(self):
+ self.skipTest("Abstract class")
+
+ def test_weights_shape(self):
+ """
+ """
+
+ for err_w_shape in self.err_weights_shapes:
+ test_msg = ('Testing invalid weights shape : {0}'
+ ''.format(err_w_shape))
+
+ err_weights = np.random.randint(0,
+ high=10,
+ size=err_w_shape)
+ err_weights = err_weights.astype(np.double)
+
+ ex_str = None
+ try:
+ histo, cumul = histogramnd(self.sample,
+ self.histo_range,
+ self.n_bins,
+ weights=err_weights)[0:2]
+ except ValueError as ex:
+ ex_str = str(ex)
+
+ self.assertIsNotNone(ex_str, msg=test_msg)
+ self.assertEqual(ex_str,
+ '<weights> must be an array whose length '
+ 'is equal to the number of samples.')
+
+ def test_histo_range_shape(self):
+ """
+ """
+ n_dims = 1 if len(self.s_shape) == 1 else self.s_shape[1]
+ expected_txt_tpl = ('<histo_range> error : expected {n_dims} sets '
+ 'of lower and upper bin edges, '
+ 'got the following instead : {histo_range}. '
+ '(provided <sample> contains '
+ '{n_dims}D values)')
+
+ for err_histo_range in self.err_histo_range_shapes:
+ test_msg = ('Testing invalid histo_range shape : {0}'
+ ''.format(err_histo_range))
+
+ expected_txt = expected_txt_tpl.format(histo_range=err_histo_range,
+ n_dims=n_dims)
+
+ ex_str = None
+ try:
+ histo, cumul = histogramnd(self.sample,
+ err_histo_range,
+ self.n_bins,
+ weights=self.weights)[0:2]
+ except ValueError as ex:
+ ex_str = str(ex)
+
+ self.assertIsNotNone(ex_str, msg=test_msg)
+ self.assertEqual(ex_str, expected_txt, msg=test_msg)
+
+ def test_nbins_shape(self):
+ """
+ """
+
+ expected_txt = ('n_bins must be either a scalar (same number '
+ 'of bins for all dimensions) or '
+ 'an array (number of bins for each '
+ 'dimension).')
+
+ for err_n_bins in self.err_n_bins_shapes:
+ test_msg = ('Testing invalid n_bins shape : {0}'
+ ''.format(err_n_bins))
+
+ ex_str = None
+ try:
+ histo, cumul = histogramnd(self.sample,
+ self.histo_range,
+ err_n_bins,
+ weights=self.weights)[0:2]
+ except ValueError as ex:
+ ex_str = str(ex)
+
+ self.assertIsNotNone(ex_str, msg=test_msg)
+ self.assertEqual(ex_str, expected_txt, msg=test_msg)
+
+ def test_nbins_values(self):
+ """
+ """
+ expected_txt = ('<n_bins> : only positive values allowed.')
+
+ for err_n_bins in self.err_n_bins_values:
+ test_msg = ('Testing invalid n_bins value : {0}'
+ ''.format(err_n_bins))
+
+ ex_str = None
+ try:
+ histo, cumul = histogramnd(self.sample,
+ self.histo_range,
+ err_n_bins,
+ weights=self.weights)[0:2]
+ except ValueError as ex:
+ ex_str = str(ex)
+
+ self.assertIsNotNone(ex_str, msg=test_msg)
+ self.assertEqual(ex_str, expected_txt, msg=test_msg)
+
+ def test_histo_shape(self):
+ """
+ """
+ for err_h_shape in self.err_histo_shapes:
+
+ # windows & python 2.7 : numpy shapes are long values
+ if platform.system() == 'Windows':
+ version = (sys.version_info.major, sys.version_info.minor)
+ if version <= (2, 7):
+ err_h_shape = tuple([long(val) for val in err_h_shape])
+
+ test_msg = ('Testing invalid histo shape : {0}'
+ ''.format(err_h_shape))
+
+ expected_txt = ('Provided <histo> array doesn\'t have '
+ 'a shape compatible with <n_bins> '
+ ': should be {0} instead of {1}.'
+ ''.format(self.h_shape, err_h_shape))
+
+ histo = np.zeros(shape=err_h_shape, dtype=np.uint32)
+
+ ex_str = None
+ try:
+ histo, cumul = histogramnd(self.sample,
+ self.histo_range,
+ self.n_bins,
+ weights=self.weights,
+ histo=histo)[0:2]
+ except ValueError as ex:
+ ex_str = str(ex)
+
+ self.assertIsNotNone(ex_str, msg=test_msg)
+ self.assertEqual(ex_str, expected_txt, msg=test_msg)
+
+ def test_histo_dtype(self):
+ """
+ """
+ for err_h_dtype in self.err_histo_dtypes:
+ test_msg = ('Testing invalid histo dtype : {0}'
+ ''.format(err_h_dtype))
+
+ histo = np.zeros(shape=self.h_shape, dtype=err_h_dtype)
+
+ expected_txt = ('Provided <histo> array doesn\'t have '
+ 'the expected type '
+ ': should be {0} instead of {1}.'
+ ''.format(np.uint32, histo.dtype))
+
+ ex_str = None
+ try:
+ histo, cumul = histogramnd(self.sample,
+ self.histo_range,
+ self.n_bins,
+ weights=self.weights,
+ histo=histo)[0:2]
+ except ValueError as ex:
+ ex_str = str(ex)
+
+ self.assertIsNotNone(ex_str, msg=test_msg)
+ self.assertEqual(ex_str, expected_txt, msg=test_msg)
+
+ def test_weighted_histo_shape(self):
+ """
+ """
+ # using the same values as histo
+ for err_h_shape in self.err_histo_shapes:
+
+ # windows & python 2.7 : numpy shapes are long values
+ if platform.system() == 'Windows':
+ version = (sys.version_info.major, sys.version_info.minor)
+ if version <= (2, 7):
+ err_h_shape = tuple([long(val) for val in err_h_shape])
+
+ test_msg = ('Testing invalid weighted_histo shape : {0}'
+ ''.format(err_h_shape))
+
+ expected_txt = ('Provided <weighted_histo> array doesn\'t have '
+ 'a shape compatible with <n_bins> '
+ ': should be {0} instead of {1}.'
+ ''.format(self.h_shape, err_h_shape))
+
+ cumul = np.zeros(shape=err_h_shape, dtype=np.double)
+
+ ex_str = None
+ try:
+ histo, cumul = histogramnd(self.sample,
+ self.histo_range,
+ self.n_bins,
+ weights=self.weights,
+ weighted_histo=cumul)[0:2]
+ except ValueError as ex:
+ ex_str = str(ex)
+
+ self.assertIsNotNone(ex_str, msg=test_msg)
+ self.assertEqual(ex_str, expected_txt, msg=test_msg)
+
+ def test_cumul_dtype(self):
+ """
+ """
+ # using the same values as histo
+ for err_h_dtype in self.err_histo_dtypes:
+ test_msg = ('Testing invalid weighted_histo dtype : {0}'
+ ''.format(err_h_dtype))
+
+ cumul = np.zeros(shape=self.h_shape, dtype=err_h_dtype)
+
+ expected_txt = ('Provided <weighted_histo> array doesn\'t have '
+ 'the expected type '
+ ': should be {0} or {1} instead of {2}.'
+ ''.format(np.float64, np.float32, cumul.dtype))
+
+ ex_str = None
+ try:
+ histo, cumul = histogramnd(self.sample,
+ self.histo_range,
+ self.n_bins,
+ weights=self.weights,
+ weighted_histo=cumul)[0:2]
+ except ValueError as ex:
+ ex_str = str(ex)
+
+ self.assertIsNotNone(ex_str, msg=test_msg)
+ self.assertEqual(ex_str, expected_txt, msg=test_msg)
+
+ def test_wh_histo_dtype(self):
+ """
+ """
+ # using the same values as histo
+ for err_h_dtype in self.err_histo_dtypes:
+ test_msg = ('Testing invalid wh_dtype dtype : {0}'
+ ''.format(err_h_dtype))
+
+ expected_txt = ('<wh_dtype> type not supported : {0}.'
+ ''.format(err_h_dtype))
+
+ ex_str = None
+ try:
+ histo, cumul = histogramnd(self.sample,
+ self.histo_range,
+ self.n_bins,
+ weights=self.weights,
+ wh_dtype=err_h_dtype)[0:2]
+ except ValueError as ex:
+ ex_str = str(ex)
+
+ self.assertIsNotNone(ex_str, msg=test_msg)
+ self.assertEqual(ex_str, expected_txt, msg=test_msg)
+
+ def test_unmanaged_dtypes(self):
+ """
+ """
+ for err_unmanaged_dtype in self.err_unmanaged_dtypes:
+ test_msg = ('Testing unmanaged dtypes : {0}'
+ ''.format(err_unmanaged_dtype))
+
+ sample = self.sample.astype(err_unmanaged_dtype[0])
+ weights = self.weights.astype(err_unmanaged_dtype[1])
+
+ expected_txt = ('Case not supported - sample:{0} '
+ 'and weights:{1}.'
+ ''.format(sample.dtype,
+ weights.dtype))
+
+ ex_str = None
+ try:
+ histogramnd(sample,
+ self.histo_range,
+ self.n_bins,
+ weights=weights)
+ except TypeError as ex:
+ ex_str = str(ex)
+
+ self.assertIsNotNone(ex_str, msg=test_msg)
+ self.assertEqual(ex_str, expected_txt, msg=test_msg)
+
+ def test_uncontiguous_histo(self):
+ """
+ """
+ # non contiguous array
+ shape = np.array(self.n_bins, ndmin=1)
+ shape[0] *= 2
+ histo_tmp = np.zeros(shape)
+ histo = histo_tmp[::2, ...]
+
+ expected_txt = ('<histo> must be a C_CONTIGUOUS numpy array.')
+
+ ex_str = None
+ try:
+ histogramnd(self.sample,
+ self.histo_range,
+ self.n_bins,
+ weights=self.weights,
+ histo=histo)
+ except ValueError as ex:
+ ex_str = str(ex)
+
+ self.assertIsNotNone(ex_str)
+ self.assertEqual(ex_str, expected_txt)
+
+ def test_uncontiguous_weighted_histo(self):
+ """
+ """
+ # non contiguous array
+ shape = np.array(self.n_bins, ndmin=1)
+ shape[0] *= 2
+ cumul_tmp = np.zeros(shape)
+ cumul = cumul_tmp[::2, ...]
+
+ expected_txt = ('<weighted_histo> must be a C_CONTIGUOUS numpy array.')
+
+ ex_str = None
+ try:
+ histogramnd(self.sample,
+ self.histo_range,
+ self.n_bins,
+ weights=self.weights,
+ weighted_histo=cumul)
+ except ValueError as ex:
+ ex_str = str(ex)
+
+ self.assertIsNotNone(ex_str)
+ self.assertEqual(ex_str, expected_txt)
+
+
+class Test_chistogramnd_1D_errors(_Test_chistogramnd_errors):
+ """
+ Unit tests of the 1D histogramnd error cases.
+ """
+ __test__ = True # because _Test_chistogramnd_errors is ignored
+
+ def setUp(self):
+ # nominal values
+ self.n_elements = 1000
+ self.s_shape = (self.n_elements,)
+ self.w_shape = (self.n_elements,)
+
+ self.histo_range = [0., 100.]
+ self.n_bins = 10
+
+ self.h_shape = (self.n_bins,)
+
+ self.sample = np.random.randint(0,
+ high=10,
+ size=self.s_shape)
+ self.sample = self.sample.astype(np.double)
+
+ self.weights = np.random.randint(0,
+ high=10,
+ size=self.w_shape)
+ self.weights = self.weights.astype(np.double)
+
+ self.err_weights_shapes = ((self.n_elements+1,),
+ (self.n_elements-1,),
+ (self.n_elements-1, 3))
+ self.err_histo_range_shapes = ([0.],
+ [0., 1., 2.],
+ [[0.], [1.]])
+ self.err_n_bins_shapes = ([10, 2],
+ [[10], [2]])
+ self.err_n_bins_values = (0,
+ [-10],
+ None)
+ self.err_histo_shapes = ((self.n_bins+1,),
+ (self.n_bins-1,),
+ (self.n_bins, self.n_bins))
+ # these are used for testing the histo parameter as well
+ # as the weighted_histo parameter.
+ self.err_histo_dtypes = (np.uint16,
+ np.float16)
+
+ self.err_unmanaged_dtypes = ((np.double, np.uint16),
+ (np.uint16, np.double),
+ (np.uint16, np.uint16))
+
+class Test_chistogramnd_ND_range(unittest.TestCase):
+ """
+
+ """
+
+ def test_invalid_histo_range(self):
+ data = np.random.random((60, 60))
+ nbins = 10
+
+ with self.assertRaises(ValueError):
+ histo_range = data.min(), np.inf
+
+ Histogramnd(sample=data.ravel(),
+ histo_range=histo_range,
+ n_bins=nbins)
+
+ histo_range = data.min(), np.nan
+
+ Histogramnd(sample=data.ravel(),
+ histo_range=histo_range,
+ n_bins=nbins)
+
+
+class Test_chistogramnd_ND_errors(_Test_chistogramnd_errors):
+ """
+ Unit tests of the 3D histogramnd error cases.
+ """
+ __test__ = True # because _Test_chistogramnd_errors is ignored
+
+ def setUp(self):
+ # nominal values
+ self.n_elements = 1000
+ self.s_shape = (self.n_elements, 3)
+ self.w_shape = (self.n_elements,)
+
+ self.histo_range = [[0., 100.], [0., 100.], [0., 100.]]
+ self.n_bins = (10, 20, 30)
+
+ self.h_shape = self.n_bins
+
+ self.sample = np.random.randint(0,
+ high=10,
+ size=self.s_shape)
+ self.sample = self.sample.astype(np.double)
+
+ self.weights = np.random.randint(0,
+ high=10,
+ size=self.w_shape)
+ self.weights = self.weights.astype(np.double)
+
+ self.err_weights_shapes = ((self.n_elements+1,),
+ (self.n_elements-1,),
+ (self.n_elements-1, 3))
+ self.err_histo_range_shapes = ([0.],
+ [0., 1.],
+ [[0., 10.], [0., 10.]],
+ [0., 10., 0, 10., 0, 10.])
+ self.err_n_bins_shapes = ([10, 2],
+ [[10], [20], [30]])
+ self.err_n_bins_values = (0,
+ [-10],
+ [10, 20, -4],
+ None,
+ [10, None, 30])
+ self.err_histo_shapes = ((self.n_bins[0]+1,
+ self.n_bins[1],
+ self.n_bins[2]),
+ (self.n_bins[0],
+ self.n_bins[1],
+ self.n_bins[2]-1),
+ (self.n_bins[0],
+ self.n_bins[1]),
+ (self.n_bins[1],
+ self.n_bins[0],
+ self.n_bins[2]),
+ (self.n_bins[0],
+ self.n_bins[1],
+ self.n_bins[2],
+ 10)
+ )
+ # these are used for testing the histo parameter as well
+ # as the weighted_histo parameter.
+ self.err_histo_dtypes = (np.uint16,
+ np.float16)
+
+ self.err_unmanaged_dtypes = ((np.double, np.uint16),
+ (np.uint16, np.double),
+ (np.uint16, np.uint16))
diff --git a/src/silx/math/test/test_histogramnd_nominal.py b/src/silx/math/test/test_histogramnd_nominal.py
new file mode 100644
index 0000000..031a772
--- /dev/null
+++ b/src/silx/math/test/test_histogramnd_nominal.py
@@ -0,0 +1,937 @@
+# coding: utf-8
+# /*##########################################################################
+# Copyright (C) 2016-2021 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
+"""
+Nominal tests of the histogramnd function.
+"""
+
+import unittest
+import pytest
+
+import numpy as np
+
+from silx.math.chistogramnd import chistogramnd as histogramnd
+from silx.math import Histogramnd
+
+
+def _get_bin_edges(histo_range, n_bins, n_dims):
+ edges = []
+ for i_dim in range(n_dims):
+ edges.append(histo_range[i_dim, 0] +
+ np.arange(n_bins[i_dim] + 1) *
+ (histo_range[i_dim, 1] - histo_range[i_dim, 0]) /
+ n_bins[i_dim])
+ return tuple(edges)
+
+
+# ==============================================================
+# ==============================================================
+# ==============================================================
+
+
+class _Test_chistogramnd_nominal(unittest.TestCase):
+ """
+ Unit tests of the histogramnd function.
+ """
+ __test__ = False # ignore abstract classe
+
+ ndims = None
+
+ def setUp(self):
+ if type(self).__name__.startswith("_"):
+ self.skipTest("Abstract class")
+ ndims = self.ndims
+ self.tested_dim = ndims-1
+
+ if ndims is None:
+ raise ValueError('ndims class member not set.')
+
+ sample = np.array([5.5, -3.3,
+ 0., -0.5,
+ 3.3, 8.8,
+ -7.7, 6.0,
+ -4.0])
+
+ weights = np.array([500.5, -300.3,
+ 0.01, -0.5,
+ 300.3, 800.8,
+ -700.7, 600.6,
+ -400.4])
+
+ n_elems = len(sample)
+
+ if ndims == 1:
+ shape = (n_elems,)
+ else:
+ shape = (n_elems, ndims)
+
+ self.sample = np.zeros(shape=shape, dtype=sample.dtype)
+ if ndims == 1:
+ self.sample = sample
+ else:
+ self.sample[..., ndims-1] = sample
+
+ self.weights = weights
+
+ # the tests are performed along one dimension,
+ # all the other bins indices along the other dimensions
+ # are expected to be 2
+ # (e.g : when testing a 2D sample : [0, x] will go into
+ # bin [2, y] because of the bin ranges [-2, 2] and n_bins = 4
+ # for the first dimension)
+ self.other_axes_index = 2
+ self.histo_range = np.repeat([[-2., 2.]], ndims, axis=0)
+ self.histo_range[ndims-1] = [-4., 6.]
+
+ self.n_bins = np.array([4]*ndims)
+ self.n_bins[ndims-1] = 5
+
+ if ndims == 1:
+ def fill_histo(h, v, dim, op=None):
+ if op:
+ h[:] = op(h[:], v)
+ else:
+ h[:] = v
+ self.fill_histo = fill_histo
+ else:
+ def fill_histo(h, v, dim, op=None):
+ idx = [self.other_axes_index]*len(h.shape)
+ idx[dim] = slice(0, None)
+ idx = tuple(idx)
+ if op:
+ h[idx] = op(h[idx], v)
+ else:
+ h[idx] = v
+ self.fill_histo = fill_histo
+
+ def test_nominal(self):
+ """
+ """
+ expected_h_tpl = np.array([2, 1, 1, 1, 1])
+ expected_c_tpl = np.array([-700.7, -0.5, 0.01, 300.3, 500.5])
+
+ expected_h = np.zeros(shape=self.n_bins, dtype=np.double)
+ expected_c = np.zeros(shape=self.n_bins, dtype=np.double)
+
+ self.fill_histo(expected_h, expected_h_tpl, self.ndims-1)
+ self.fill_histo(expected_c, expected_c_tpl, self.ndims-1)
+
+ histo, cumul, bin_edges = histogramnd(self.sample,
+ self.histo_range,
+ self.n_bins,
+ weights=self.weights)
+
+ expected_edges = _get_bin_edges(self.histo_range,
+ self.n_bins,
+ self.ndims)
+
+ self.assertEqual(cumul.dtype, np.float64)
+ self.assertEqual(histo.dtype, np.uint32)
+ self.assertTrue(np.array_equal(histo, expected_h))
+ self.assertTrue(np.array_equal(cumul, expected_c))
+
+ for i_edges, edges in enumerate(expected_edges):
+ self.assertTrue(np.array_equal(bin_edges[i_edges],
+ expected_edges[i_edges]),
+ msg='Testing bin_edges for dim {0}'
+ ''.format(i_edges+1))
+
+ def test_nominal_wh_dtype(self):
+ """
+ """
+ expected_h_tpl = np.array([2, 1, 1, 1, 1])
+ expected_c_tpl = np.array([-700.7, -0.5, 0.01, 300.3, 500.5])
+
+ expected_h = np.zeros(shape=self.n_bins, dtype=np.double)
+ expected_c = np.zeros(shape=self.n_bins, dtype=np.float32)
+
+ self.fill_histo(expected_h, expected_h_tpl, self.ndims-1)
+ self.fill_histo(expected_c, expected_c_tpl, self.ndims-1)
+
+ histo, cumul, bin_edges = histogramnd(self.sample,
+ self.histo_range,
+ self.n_bins,
+ weights=self.weights,
+ wh_dtype=np.float32)
+
+ self.assertEqual(cumul.dtype, np.float32)
+ self.assertTrue(np.array_equal(histo, expected_h))
+ self.assertTrue(np.allclose(cumul, expected_c))
+
+ def test_nominal_uncontiguous_sample(self):
+ """
+ """
+ expected_h_tpl = np.array([2, 1, 1, 1, 1])
+ expected_c_tpl = np.array([-700.7, -0.5, 0.01, 300.3, 500.5])
+
+ expected_h = np.zeros(shape=self.n_bins, dtype=np.double)
+ expected_c = np.zeros(shape=self.n_bins, dtype=np.double)
+
+ self.fill_histo(expected_h, expected_h_tpl, self.ndims-1)
+ self.fill_histo(expected_c, expected_c_tpl, self.ndims-1)
+
+ shape = list(self.sample.shape)
+ shape[0] *= 2
+ sample = np.zeros(shape, dtype=self.sample.dtype)
+ uncontig_sample = sample[::2, ...]
+ uncontig_sample[:] = self.sample
+
+ self.assertFalse(uncontig_sample.flags['C_CONTIGUOUS'],
+ msg='Making sure the array is not contiguous.')
+
+ histo, cumul, bin_edges = histogramnd(uncontig_sample,
+ self.histo_range,
+ self.n_bins,
+ weights=self.weights)
+
+ self.assertEqual(cumul.dtype, np.float64)
+ self.assertEqual(histo.dtype, np.uint32)
+ self.assertTrue(np.array_equal(histo, expected_h))
+ self.assertTrue(np.array_equal(cumul, expected_c))
+
+ def test_nominal_uncontiguous_weights(self):
+ """
+ """
+ expected_h_tpl = np.array([2, 1, 1, 1, 1])
+ expected_c_tpl = np.array([-700.7, -0.5, 0.01, 300.3, 500.5])
+
+ expected_h = np.zeros(shape=self.n_bins, dtype=np.double)
+ expected_c = np.zeros(shape=self.n_bins, dtype=np.double)
+
+ self.fill_histo(expected_h, expected_h_tpl, self.ndims-1)
+ self.fill_histo(expected_c, expected_c_tpl, self.ndims-1)
+
+ shape = list(self.weights.shape)
+ shape[0] *= 2
+ weights = np.zeros(shape, dtype=self.weights.dtype)
+ uncontig_weights = weights[::2, ...]
+ uncontig_weights[:] = self.weights
+
+ self.assertFalse(uncontig_weights.flags['C_CONTIGUOUS'],
+ msg='Making sure the array is not contiguous.')
+
+ histo, cumul, bin_edges = histogramnd(self.sample,
+ self.histo_range,
+ self.n_bins,
+ weights=uncontig_weights)
+
+ self.assertEqual(cumul.dtype, np.float64)
+ self.assertEqual(histo.dtype, np.uint32)
+ self.assertTrue(np.array_equal(histo, expected_h))
+ self.assertTrue(np.array_equal(cumul, expected_c))
+
+ def test_nominal_wo_weights(self):
+ """
+ """
+ expected_h_tpl = np.array([2, 1, 1, 1, 1])
+
+ expected_h = np.zeros(shape=self.n_bins, dtype=np.double)
+
+ self.fill_histo(expected_h, expected_h_tpl, self.ndims-1)
+
+ histo, cumul = histogramnd(self.sample,
+ self.histo_range,
+ self.n_bins,
+ weights=None)[0:2]
+
+ self.assertTrue(np.array_equal(histo, expected_h))
+ self.assertTrue(cumul is None)
+
+ def test_nominal_wo_weights_w_cumul(self):
+ """
+ """
+ expected_h_tpl = np.array([2, 1, 1, 1, 1])
+
+ expected_h = np.zeros(shape=self.n_bins, dtype=np.double)
+
+ # creating an array of ones just to make sure that
+ # it is not cleared by histogramnd
+ cumul_in = np.ones(self.n_bins, dtype=np.double)
+
+ self.fill_histo(expected_h, expected_h_tpl, self.ndims-1)
+
+ histo, cumul = histogramnd(self.sample,
+ self.histo_range,
+ self.n_bins,
+ weights=None,
+ weighted_histo=cumul_in)[0:2]
+
+ self.assertTrue(np.array_equal(histo, expected_h))
+ self.assertTrue(cumul is None)
+ self.assertTrue(np.array_equal(cumul_in,
+ np.ones(shape=self.n_bins,
+ dtype=np.double)))
+
+ def test_nominal_wo_weights_w_histo(self):
+ """
+ """
+ expected_h_tpl = np.array([2, 1, 1, 1, 1])
+
+ expected_h = np.zeros(shape=self.n_bins, dtype=np.double)
+
+ # creating an array of ones just to make sure that
+ # it is not cleared by histogramnd
+ histo_in = np.ones(self.n_bins, dtype=np.uint32)
+
+ self.fill_histo(expected_h, expected_h_tpl, self.ndims-1)
+
+ histo, cumul = histogramnd(self.sample,
+ self.histo_range,
+ self.n_bins,
+ weights=None,
+ histo=histo_in)[0:2]
+
+ self.assertTrue(np.array_equal(histo, expected_h + 1))
+ self.assertTrue(cumul is None)
+ self.assertEqual(id(histo), id(histo_in))
+
+ def test_nominal_last_bin_closed(self):
+ """
+ """
+ expected_h_tpl = np.array([2, 1, 1, 1, 2])
+ expected_c_tpl = np.array([-700.7, -0.5, 0.01, 300.3, 1101.1])
+
+ expected_h = np.zeros(shape=self.n_bins, dtype=np.double)
+ expected_c = np.zeros(shape=self.n_bins, dtype=np.double)
+
+ self.fill_histo(expected_h, expected_h_tpl, self.ndims-1)
+ self.fill_histo(expected_c, expected_c_tpl, self.ndims-1)
+
+ histo, cumul = histogramnd(self.sample,
+ self.histo_range,
+ self.n_bins,
+ weights=self.weights,
+ last_bin_closed=True)[0:2]
+
+ self.assertTrue(np.array_equal(histo, expected_h))
+ self.assertTrue(np.array_equal(cumul, expected_c))
+
+ def test_int32_weights_double_weights_range(self):
+ """
+ """
+ weight_min = -299.9 # ===> will be cast to -299
+ weight_max = 499.9 # ===> will be cast to 499
+
+ expected_h_tpl = np.array([0, 1, 1, 1, 0])
+ expected_c_tpl = np.array([0., 0., 0., 300., 0.])
+
+ expected_h = np.zeros(shape=self.n_bins, dtype=np.double)
+ expected_c = np.zeros(shape=self.n_bins, dtype=np.double)
+
+ self.fill_histo(expected_h, expected_h_tpl, self.ndims-1)
+ self.fill_histo(expected_c, expected_c_tpl, self.ndims-1)
+
+ histo, cumul = histogramnd(self.sample,
+ self.histo_range,
+ self.n_bins,
+ weights=self.weights.astype(np.int32),
+ weight_min=weight_min,
+ weight_max=weight_max)[0:2]
+
+ self.assertTrue(np.array_equal(histo, expected_h))
+ self.assertTrue(np.array_equal(cumul, expected_c))
+
+ def test_reuse_histo(self):
+ """
+ """
+
+ expected_h_tpl = np.array([2, 3, 2, 2, 2])
+ expected_c_tpl = np.array([0.0, -7007, -5.0, 0.1, 3003.0])
+
+ expected_h = np.zeros(shape=self.n_bins, dtype=np.double)
+ expected_c = np.zeros(shape=self.n_bins, dtype=np.double)
+
+ self.fill_histo(expected_h, expected_h_tpl, self.ndims-1)
+ self.fill_histo(expected_c, expected_c_tpl, self.ndims-1)
+
+ histo, cumul = histogramnd(self.sample,
+ self.histo_range,
+ self.n_bins,
+ weights=self.weights)[0:2]
+
+ sample_2 = self.sample[:]
+ if len(sample_2.shape) == 1:
+ idx = (slice(0, None),)
+ else:
+ idx = slice(0, None), self.tested_dim
+
+ sample_2[idx] += 2
+
+ histo_2, cumul = histogramnd(sample_2, # <==== !!
+ self.histo_range,
+ self.n_bins,
+ weights=10 * self.weights, # <==== !!
+ histo=histo)[0:2]
+
+ self.assertTrue(np.array_equal(histo, expected_h))
+ self.assertTrue(np.array_equal(cumul, expected_c))
+ self.assertEqual(id(histo), id(histo_2))
+
+ def test_reuse_cumul(self):
+ """
+ """
+
+ expected_h_tpl = np.array([0, 2, 1, 1, 1])
+ expected_c_tpl = np.array([-700.7, -7007.5, -4.99, 300.4, 3503.5])
+
+ expected_h = np.zeros(shape=self.n_bins, dtype=np.double)
+ expected_c = np.zeros(shape=self.n_bins, dtype=np.double)
+
+ self.fill_histo(expected_h, expected_h_tpl, self.ndims-1)
+ self.fill_histo(expected_c, expected_c_tpl, self.ndims-1)
+
+ histo, cumul = histogramnd(self.sample,
+ self.histo_range,
+ self.n_bins,
+ weights=self.weights)[0:2]
+
+ sample_2 = self.sample[:]
+ if len(sample_2.shape) == 1:
+ idx = (slice(0, None),)
+ else:
+ idx = slice(0, None), self.tested_dim
+
+ sample_2[idx] += 2
+
+ histo, cumul_2 = histogramnd(sample_2, # <==== !!
+ self.histo_range,
+ self.n_bins,
+ weights=10 * self.weights, # <==== !!
+ weighted_histo=cumul)[0:2]
+
+ self.assertEqual(cumul.dtype, np.float64)
+ self.assertTrue(np.array_equal(histo, expected_h))
+ self.assertTrue(np.allclose(cumul, expected_c, rtol=10e-15))
+ self.assertEqual(id(cumul), id(cumul_2))
+
+ def test_reuse_cumul_float(self):
+ """
+ """
+
+ expected_h_tpl = np.array([0, 2, 1, 1, 1])
+ expected_c_tpl = np.array([-700.7, -7007.5, -4.99, 300.4, 3503.5],
+ dtype=np.float32)
+
+ expected_h = np.zeros(shape=self.n_bins, dtype=np.double)
+ expected_c = np.zeros(shape=self.n_bins, dtype=np.double)
+
+ self.fill_histo(expected_h, expected_h_tpl, self.ndims-1)
+ self.fill_histo(expected_c, expected_c_tpl, self.ndims-1)
+
+ histo, cumul = histogramnd(self.sample,
+ self.histo_range,
+ self.n_bins,
+ weights=self.weights)[0:2]
+
+ # converting the cumul array to float
+ cumul = cumul.astype(np.float32)
+
+ sample_2 = self.sample[:]
+ if len(sample_2.shape) == 1:
+ idx = (slice(0, None),)
+ else:
+ idx = slice(0, None), self.tested_dim
+
+ sample_2[idx] += 2
+
+ histo, cumul_2 = histogramnd(sample_2, # <==== !!
+ self.histo_range,
+ self.n_bins,
+ weights=10 * self.weights, # <==== !!
+ weighted_histo=cumul)[0:2]
+
+ self.assertEqual(cumul.dtype, np.float32)
+ self.assertTrue(np.array_equal(histo, expected_h))
+ self.assertEqual(id(cumul), id(cumul_2))
+ self.assertTrue(np.allclose(cumul, expected_c, rtol=10e-15))
+
+class _Test_Histogramnd_nominal(unittest.TestCase):
+ """
+ Unit tests of the Histogramnd class.
+ """
+ __test__ = False # ignore abstract class
+
+ ndims = None
+
+ def setUp(self):
+ ndims = self.ndims
+ if ndims is None:
+ self.skipTest("Abstract class")
+ self.tested_dim = ndims-1
+
+ if ndims is None:
+ raise ValueError('ndims class member not set.')
+
+ sample = np.array([5.5, -3.3,
+ 0., -0.5,
+ 3.3, 8.8,
+ -7.7, 6.0,
+ -4.0])
+
+ weights = np.array([500.5, -300.3,
+ 0.01, -0.5,
+ 300.3, 800.8,
+ -700.7, 600.6,
+ -400.4])
+
+ n_elems = len(sample)
+
+ if ndims == 1:
+ shape = (n_elems,)
+ else:
+ shape = (n_elems, ndims)
+
+ self.sample = np.zeros(shape=shape, dtype=sample.dtype)
+ if ndims == 1:
+ self.sample = sample
+ else:
+ self.sample[..., ndims-1] = sample
+
+ self.weights = weights
+
+ # the tests are performed along one dimension,
+ # all the other bins indices along the other dimensions
+ # are expected to be 2
+ # (e.g : when testing a 2D sample : [0, x] will go into
+ # bin [2, y] because of the bin ranges [-2, 2] and n_bins = 4
+ # for the first dimension)
+ self.other_axes_index = 2
+ self.histo_range = np.repeat([[-2., 2.]], ndims, axis=0)
+ self.histo_range[ndims-1] = [-4., 6.]
+
+ self.n_bins = np.array([4]*ndims)
+ self.n_bins[ndims-1] = 5
+
+ if ndims == 1:
+ def fill_histo(h, v, dim, op=None):
+ if op:
+ h[:] = op(h[:], v)
+ else:
+ h[:] = v
+ self.fill_histo = fill_histo
+ else:
+ def fill_histo(h, v, dim, op=None):
+ idx = [self.other_axes_index]*len(h.shape)
+ idx[dim] = slice(0, None)
+ idx = tuple(idx)
+ if op:
+ h[idx] = op(h[idx], v)
+ else:
+ h[idx] = v
+ self.fill_histo = fill_histo
+
+ def test_nominal(self):
+ """
+ """
+ expected_h_tpl = np.array([2, 1, 1, 1, 1])
+ expected_c_tpl = np.array([-700.7, -0.5, 0.01, 300.3, 500.5])
+
+ expected_h = np.zeros(shape=self.n_bins, dtype=np.double)
+ expected_c = np.zeros(shape=self.n_bins, dtype=np.double)
+
+ self.fill_histo(expected_h, expected_h_tpl, self.ndims-1)
+ self.fill_histo(expected_c, expected_c_tpl, self.ndims-1)
+
+ histo = Histogramnd(self.sample,
+ self.histo_range,
+ self.n_bins,
+ weights=self.weights)
+
+ histo, cumul, bin_edges = histo
+
+ expected_edges = _get_bin_edges(self.histo_range,
+ self.n_bins,
+ self.ndims)
+
+ self.assertEqual(cumul.dtype, np.float64)
+ self.assertEqual(histo.dtype, np.uint32)
+ self.assertTrue(np.array_equal(histo, expected_h))
+ self.assertTrue(np.array_equal(cumul, expected_c))
+
+ for i_edges, edges in enumerate(expected_edges):
+ self.assertTrue(np.array_equal(bin_edges[i_edges],
+ expected_edges[i_edges]),
+ msg='Testing bin_edges for dim {0}'
+ ''.format(i_edges+1))
+
+ def test_nominal_wh_dtype(self):
+ """
+ """
+ expected_h_tpl = np.array([2, 1, 1, 1, 1])
+ expected_c_tpl = np.array([-700.7, -0.5, 0.01, 300.3, 500.5])
+
+ expected_h = np.zeros(shape=self.n_bins, dtype=np.double)
+ expected_c = np.zeros(shape=self.n_bins, dtype=np.float32)
+
+ self.fill_histo(expected_h, expected_h_tpl, self.ndims-1)
+ self.fill_histo(expected_c, expected_c_tpl, self.ndims-1)
+
+ histo, cumul, bin_edges = Histogramnd(self.sample,
+ self.histo_range,
+ self.n_bins,
+ weights=self.weights,
+ wh_dtype=np.float32)
+
+ self.assertEqual(cumul.dtype, np.float32)
+ self.assertTrue(np.array_equal(histo, expected_h))
+ self.assertTrue(np.allclose(cumul, expected_c))
+
+ def test_nominal_uncontiguous_sample(self):
+ """
+ """
+ expected_h_tpl = np.array([2, 1, 1, 1, 1])
+ expected_c_tpl = np.array([-700.7, -0.5, 0.01, 300.3, 500.5])
+
+ expected_h = np.zeros(shape=self.n_bins, dtype=np.double)
+ expected_c = np.zeros(shape=self.n_bins, dtype=np.double)
+
+ self.fill_histo(expected_h, expected_h_tpl, self.ndims-1)
+ self.fill_histo(expected_c, expected_c_tpl, self.ndims-1)
+
+ shape = list(self.sample.shape)
+ shape[0] *= 2
+ sample = np.zeros(shape, dtype=self.sample.dtype)
+ uncontig_sample = sample[::2, ...]
+ uncontig_sample[:] = self.sample
+
+ self.assertFalse(uncontig_sample.flags['C_CONTIGUOUS'],
+ msg='Making sure the array is not contiguous.')
+
+ histo, cumul, bin_edges = Histogramnd(uncontig_sample,
+ self.histo_range,
+ self.n_bins,
+ weights=self.weights)
+
+ self.assertEqual(cumul.dtype, np.float64)
+ self.assertEqual(histo.dtype, np.uint32)
+ self.assertTrue(np.array_equal(histo, expected_h))
+ self.assertTrue(np.array_equal(cumul, expected_c))
+
+ def test_nominal_uncontiguous_weights(self):
+ """
+ """
+ expected_h_tpl = np.array([2, 1, 1, 1, 1])
+ expected_c_tpl = np.array([-700.7, -0.5, 0.01, 300.3, 500.5])
+
+ expected_h = np.zeros(shape=self.n_bins, dtype=np.double)
+ expected_c = np.zeros(shape=self.n_bins, dtype=np.double)
+
+ self.fill_histo(expected_h, expected_h_tpl, self.ndims-1)
+ self.fill_histo(expected_c, expected_c_tpl, self.ndims-1)
+
+ shape = list(self.weights.shape)
+ shape[0] *= 2
+ weights = np.zeros(shape, dtype=self.weights.dtype)
+ uncontig_weights = weights[::2, ...]
+ uncontig_weights[:] = self.weights
+
+ self.assertFalse(uncontig_weights.flags['C_CONTIGUOUS'],
+ msg='Making sure the array is not contiguous.')
+
+ histo, cumul, bin_edges = Histogramnd(self.sample,
+ self.histo_range,
+ self.n_bins,
+ weights=uncontig_weights)
+
+ self.assertEqual(cumul.dtype, np.float64)
+ self.assertEqual(histo.dtype, np.uint32)
+ self.assertTrue(np.array_equal(histo, expected_h))
+ self.assertTrue(np.array_equal(cumul, expected_c))
+
+ def test_nominal_wo_weights(self):
+ """
+ """
+ expected_h_tpl = np.array([2, 1, 1, 1, 1])
+
+ expected_h = np.zeros(shape=self.n_bins, dtype=np.double)
+
+ self.fill_histo(expected_h, expected_h_tpl, self.ndims-1)
+
+ histo, cumul = Histogramnd(self.sample,
+ self.histo_range,
+ self.n_bins,
+ weights=None)[0:2]
+
+ self.assertTrue(np.array_equal(histo, expected_h))
+ self.assertTrue(cumul is None)
+
+ def test_nominal_last_bin_closed(self):
+ """
+ """
+ expected_h_tpl = np.array([2, 1, 1, 1, 2])
+ expected_c_tpl = np.array([-700.7, -0.5, 0.01, 300.3, 1101.1])
+
+ expected_h = np.zeros(shape=self.n_bins, dtype=np.double)
+ expected_c = np.zeros(shape=self.n_bins, dtype=np.double)
+
+ self.fill_histo(expected_h, expected_h_tpl, self.ndims-1)
+ self.fill_histo(expected_c, expected_c_tpl, self.ndims-1)
+
+ histo, cumul = Histogramnd(self.sample,
+ self.histo_range,
+ self.n_bins,
+ weights=self.weights,
+ last_bin_closed=True)[0:2]
+
+ self.assertTrue(np.array_equal(histo, expected_h))
+ self.assertTrue(np.array_equal(cumul, expected_c))
+
+ def test_int32_weights_double_weights_range(self):
+ """
+ """
+ weight_min = -299.9 # ===> will be cast to -299
+ weight_max = 499.9 # ===> will be cast to 499
+
+ expected_h_tpl = np.array([0, 1, 1, 1, 0])
+ expected_c_tpl = np.array([0., 0., 0., 300., 0.])
+
+ expected_h = np.zeros(shape=self.n_bins, dtype=np.double)
+ expected_c = np.zeros(shape=self.n_bins, dtype=np.double)
+
+ self.fill_histo(expected_h, expected_h_tpl, self.ndims-1)
+ self.fill_histo(expected_c, expected_c_tpl, self.ndims-1)
+
+ histo, cumul = Histogramnd(self.sample,
+ self.histo_range,
+ self.n_bins,
+ weights=self.weights.astype(np.int32),
+ weight_min=weight_min,
+ weight_max=weight_max)[0:2]
+
+ self.assertTrue(np.array_equal(histo, expected_h))
+ self.assertTrue(np.array_equal(cumul, expected_c))
+
+ def test_nominal_no_sample(self):
+ """
+ """
+
+ histo_inst = Histogramnd(None,
+ self.histo_range,
+ self.n_bins)
+
+ histo, weighted_histo, edges = histo_inst
+
+ self.assertIsNone(histo)
+ self.assertIsNone(weighted_histo)
+ self.assertIsNone(edges)
+ self.assertIsNone(histo_inst.histo)
+ self.assertIsNone(histo_inst.weighted_histo)
+ self.assertIsNone(histo_inst.edges)
+
+ def test_empty_init_accumulate(self):
+ """
+ """
+ expected_h_tpl = np.array([2, 1, 1, 1, 1])
+ expected_c_tpl = np.array([-700.7, -0.5, 0.01, 300.3, 500.5])
+
+ expected_h = np.zeros(shape=self.n_bins, dtype=np.double)
+ expected_c = np.zeros(shape=self.n_bins, dtype=np.double)
+
+ self.fill_histo(expected_h, expected_h_tpl, self.ndims-1)
+ self.fill_histo(expected_c, expected_c_tpl, self.ndims-1)
+
+ histo_inst = Histogramnd(None,
+ self.histo_range,
+ self.n_bins)
+
+ histo_inst.accumulate(self.sample,
+ weights=self.weights)
+
+ histo = histo_inst.histo
+ cumul = histo_inst.weighted_histo
+ bin_edges = histo_inst.edges
+
+ expected_edges = _get_bin_edges(self.histo_range,
+ self.n_bins,
+ self.ndims)
+
+ self.assertEqual(cumul.dtype, np.float64)
+ self.assertEqual(histo.dtype, np.uint32)
+ self.assertTrue(np.array_equal(histo, expected_h))
+ self.assertTrue(np.array_equal(cumul, expected_c))
+
+ for i_edges, edges in enumerate(expected_edges):
+ self.assertTrue(np.array_equal(bin_edges[i_edges],
+ expected_edges[i_edges]),
+ msg='Testing bin_edges for dim {0}'
+ ''.format(i_edges+1))
+
+ def test_accumulate(self):
+ """
+ """
+
+ expected_h_tpl = np.array([2, 3, 2, 2, 2])
+ expected_c_tpl = np.array([-700.7, -7007.5, -4.99, 300.4, 3503.5])
+
+ expected_h = np.zeros(shape=self.n_bins, dtype=np.double)
+ expected_c = np.zeros(shape=self.n_bins, dtype=np.double)
+
+ self.fill_histo(expected_h, expected_h_tpl, self.ndims-1)
+ self.fill_histo(expected_c, expected_c_tpl, self.ndims-1)
+
+ histo_inst = Histogramnd(self.sample,
+ self.histo_range,
+ self.n_bins,
+ weights=self.weights)
+
+ sample_2 = self.sample[:]
+ if len(sample_2.shape) == 1:
+ idx = (slice(0, None),)
+ else:
+ idx = slice(0, None), self.tested_dim
+
+ sample_2[idx] += 2
+
+ histo_inst.accumulate(sample_2, # <==== !!
+ weights=10 * self.weights) # <==== !!
+
+ histo = histo_inst.histo
+ cumul = histo_inst.weighted_histo
+ bin_edges = histo_inst.edges
+
+ self.assertEqual(cumul.dtype, np.float64)
+ self.assertTrue(np.array_equal(histo, expected_h))
+ self.assertTrue(np.allclose(cumul, expected_c, rtol=10e-15))
+
+ def test_accumulate_no_weights(self):
+ """
+ """
+
+ expected_h_tpl = np.array([2, 3, 2, 2, 2])
+ expected_c_tpl = np.array([-700.7, -0.5, 0.01, 300.3, 500.5])
+
+ expected_h = np.zeros(shape=self.n_bins, dtype=np.double)
+ expected_c = np.zeros(shape=self.n_bins, dtype=np.double)
+
+ self.fill_histo(expected_h, expected_h_tpl, self.ndims-1)
+ self.fill_histo(expected_c, expected_c_tpl, self.ndims-1)
+
+ histo_inst = Histogramnd(self.sample,
+ self.histo_range,
+ self.n_bins,
+ weights=self.weights)
+
+ sample_2 = self.sample[:]
+ if len(sample_2.shape) == 1:
+ idx = (slice(0, None),)
+ else:
+ idx = slice(0, None), self.tested_dim
+
+ sample_2[idx] += 2
+
+ histo_inst.accumulate(sample_2) # <==== !!
+
+ histo = histo_inst.histo
+ cumul = histo_inst.weighted_histo
+ bin_edges = histo_inst.edges
+
+ self.assertEqual(cumul.dtype, np.float64)
+ self.assertTrue(np.array_equal(histo, expected_h))
+ self.assertTrue(np.allclose(cumul, expected_c, rtol=10e-15))
+
+ def test_accumulate_no_weights_at_init(self):
+ """
+ """
+
+ expected_h_tpl = np.array([2, 3, 2, 2, 2])
+ expected_c_tpl = np.array([0.0, -700.7, -0.5, 0.01, 300.3])
+
+ expected_h = np.zeros(shape=self.n_bins, dtype=np.double)
+ expected_c = np.zeros(shape=self.n_bins, dtype=np.double)
+
+ self.fill_histo(expected_h, expected_h_tpl, self.ndims-1)
+ self.fill_histo(expected_c, expected_c_tpl, self.ndims-1)
+
+ histo_inst = Histogramnd(self.sample,
+ self.histo_range,
+ self.n_bins,
+ weights=None) # <==== !!
+
+ cumul = histo_inst.weighted_histo
+ self.assertIsNone(cumul)
+
+ sample_2 = self.sample[:]
+ if len(sample_2.shape) == 1:
+ idx = (slice(0, None),)
+ else:
+ idx = slice(0, None), self.tested_dim
+
+ sample_2[idx] += 2
+
+ histo_inst.accumulate(sample_2,
+ weights=self.weights) # <==== !!
+
+ histo = histo_inst.histo
+ cumul = histo_inst.weighted_histo
+ bin_edges = histo_inst.edges
+
+ self.assertEqual(cumul.dtype, np.float64)
+ self.assertTrue(np.array_equal(histo, expected_h))
+ self.assertTrue(np.array_equal(cumul, expected_c))
+
+ def testNoneNativeTypes(self):
+ type = self.sample.dtype.newbyteorder("B")
+ sampleB = self.sample.astype(type)
+
+ type = self.sample.dtype.newbyteorder("L")
+ sampleL = self.sample.astype(type)
+
+ histo_inst = Histogramnd(sampleB,
+ self.histo_range,
+ self.n_bins,
+ weights=self.weights)
+
+ histo_inst = Histogramnd(sampleL,
+ self.histo_range,
+ self.n_bins,
+ weights=self.weights)
+
+
+class Test_chistogram_nominal_1d(_Test_chistogramnd_nominal):
+ __test__ = True # because _Test_chistogramnd_nominal is ignored
+ ndims = 1
+
+
+class Test_chistogram_nominal_2d(_Test_chistogramnd_nominal):
+ __test__ = True # because _Test_chistogramnd_nominal is ignored
+ ndims = 2
+
+
+class Test_chistogram_nominal_3d(_Test_chistogramnd_nominal):
+ __test__ = True # because _Test_chistogramnd_nominal is ignored
+ ndims = 3
+
+
+class Test_Histogramnd_nominal_1d(_Test_Histogramnd_nominal):
+ __test__ = True # because _Test_chistogramnd_nominal is ignored
+ ndims = 1
+
+
+class Test_Histogramnd_nominal_2d(_Test_Histogramnd_nominal):
+ __test__ = True # because _Test_chistogramnd_nominal is ignored
+ ndims = 2
+
+
+class Test_Histogramnd_nominal_3d(_Test_Histogramnd_nominal):
+ __test__ = True # because _Test_chistogramnd_nominal is ignored
+ ndims = 3
diff --git a/src/silx/math/test/test_histogramnd_vs_np.py b/src/silx/math/test/test_histogramnd_vs_np.py
new file mode 100644
index 0000000..d6a8d19
--- /dev/null
+++ b/src/silx/math/test/test_histogramnd_vs_np.py
@@ -0,0 +1,826 @@
+# coding: utf-8
+# /*##########################################################################
+# Copyright (C) 2016-2021 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
+"""
+Tests for the histogramnd function.
+Results are compared to numpy's histogramdd.
+"""
+
+import unittest
+import operator
+
+import numpy as np
+
+from silx.math.chistogramnd import chistogramnd as histogramnd
+
+# ==============================================================
+# ==============================================================
+# ==============================================================
+
+_RTOL_DICT = {np.float64: 10**-13,
+ np.float32: 10**-5}
+
+# ==============================================================
+# ==============================================================
+# ==============================================================
+
+
+def _add_values_to_array_if_missing(array, values, n_values):
+ max_in_col = np.any(array[:, ...] == values, axis=0)
+
+ if len(array.shape) == 1:
+ if not max_in_col:
+ rnd_idx = np.random.randint(0,
+ high=len(array)-1,
+ size=(n_values,))
+ array[rnd_idx] = values
+ else:
+ for i in range(len(max_in_col)):
+ if not max_in_col[i]:
+ rnd_idx = np.random.randint(0,
+ high=len(array)-1,
+ size=(n_values,))
+ array[rnd_idx, i] = values[i]
+
+
+def _get_values_index(array, values, op=operator.lt):
+ idx = op(array[:, ...], values)
+ if array.ndim > 1:
+ idx = np.all(idx, axis=1)
+ return np.where(idx)[0]
+
+
+def _get_in_range_indices(array,
+ minvalues,
+ maxvalues,
+ minop=operator.ge,
+ maxop=operator.lt):
+ idx = np.logical_and(minop(array, minvalues),
+ maxop(array, maxvalues))
+ if array.ndim > 1:
+ idx = np.all(idx, axis=1)
+ return np.where(idx)[0]
+
+
+class _TestHistogramnd(unittest.TestCase):
+ """
+ Unit tests of the histogramnd function.
+ """
+ __test__ = False # ignore abstract class
+
+ sample_rng = None
+ weights_rng = None
+ n_dims = None
+
+ filter_min = None
+ filter_max = None
+
+ histo_range = None
+ n_bins = None
+
+ dtype_sample = None
+ dtype_weights = None
+
+ def generate_data(self):
+
+ self.longMessage = True
+
+ int_min = 0
+ int_max = 100000
+ n_elements = 10**5
+
+ if self.n_dims == 1:
+ shape = (n_elements,)
+ else:
+ shape = (n_elements, self.n_dims,)
+
+ self.rng_state = np.random.get_state()
+
+ self.state_msg = ('Current RNG state :\n'
+ '{0}'.format(self.rng_state))
+
+ sample = np.random.randint(int_min,
+ high=int_max,
+ size=shape)
+
+ sample = sample.astype(self.dtype_sample)
+ sample = (self.sample_rng[0] +
+ (sample-int_min) *
+ (self.sample_rng[1]-self.sample_rng[0]) /
+ (int_max-int_min)).astype(self.dtype_sample)
+
+ weights = np.random.randint(int_min,
+ high=int_max,
+ size=(n_elements,))
+ weights = weights.astype(self.dtype_weights)
+ weights = (self.weights_rng[0] +
+ (weights-int_min) *
+ (self.weights_rng[1]-self.weights_rng[0]) /
+ (int_max-int_min)).astype(self.dtype_weights)
+
+ # !!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ # !!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ # the bins range are cast to the same type as the sample
+ # in order to get the same results as numpy
+ # (which doesnt cast the range)
+ self.histo_range = np.array(self.histo_range).astype(self.dtype_sample)
+
+ # adding some values that are equal to the max
+ # in order to test the opened/closed last bin
+ bins_max = [b[1] for b in self.histo_range]
+ _add_values_to_array_if_missing(sample,
+ bins_max,
+ 100)
+
+ # adding some values that are equal to the min weight value
+ # in order to test the filters
+ _add_values_to_array_if_missing(weights,
+ self.weights_rng[0],
+ 100)
+
+ # adding some values that are equal to the max weight value
+ # in order to test the filters
+ _add_values_to_array_if_missing(weights,
+ self.weights_rng[1],
+ 100)
+
+ return sample, weights
+
+ def setUp(self):
+ if type(self).__name__.startswith("_"):
+ self.skipTest("Abstract class")
+ self.sample, self.weights = self.generate_data()
+ self.rtol = _RTOL_DICT.get(self.dtype_weights, None)
+
+ def array_compare(self, ar_a, ar_b):
+ if self.rtol is None:
+ return np.array_equal(ar_a, ar_b)
+ return np.allclose(ar_a, ar_b, self.rtol)
+
+ def test_bin_ranges(self):
+ """
+
+ """
+ result_c = histogramnd(self.sample,
+ self.histo_range,
+ self.n_bins,
+ weights=self.weights,
+ last_bin_closed=True)
+
+ result_np = np.histogramdd(self.sample,
+ bins=self.n_bins,
+ range=self.histo_range)
+
+ for i_edges, edges in enumerate(result_c[2]):
+ # allclose for now until I can try with the latest version (TBD)
+ # of numpy
+ self.assertTrue(np.allclose(edges,
+ result_np[1][i_edges]),
+ msg='{0}. Testing bin_edges for dim {1}.'
+ ''.format(self.state_msg, i_edges+1))
+
+ def test_last_bin_closed(self):
+ """
+
+ """
+ result_c = histogramnd(self.sample,
+ self.histo_range,
+ self.n_bins,
+ weights=self.weights,
+ last_bin_closed=True)
+
+ result_np = np.histogramdd(self.sample,
+ bins=self.n_bins,
+ range=self.histo_range)
+
+ result_np_w = np.histogramdd(self.sample,
+ bins=self.n_bins,
+ range=self.histo_range,
+ weights=self.weights)
+
+ # comparing "hits"
+ hits_cmp = np.array_equal(result_c[0],
+ result_np[0])
+ # comparing weights
+ weights_cmp = np.array_equal(result_c[1],
+ result_np_w[0])
+
+ self.assertTrue(hits_cmp, msg=self.state_msg)
+ self.assertTrue(weights_cmp, msg=self.state_msg)
+
+ bins_min = [rng[0] for rng in self.histo_range]
+ bins_max = [rng[1] for rng in self.histo_range]
+ inrange_idx = _get_in_range_indices(self.sample,
+ bins_min,
+ bins_max,
+ minop=operator.ge,
+ maxop=operator.le)
+
+ self.assertEqual(result_c[0].sum(), inrange_idx.shape[0],
+ msg=self.state_msg)
+
+ # we have to sum the weights using the same precision as the
+ # histogramnd function
+ weights_sum = self.weights[inrange_idx].astype(result_c[1].dtype).sum()
+ self.assertTrue(self.array_compare(result_c[1].sum(), weights_sum),
+ msg=self.state_msg)
+
+ def test_last_bin_open(self):
+ """
+
+ """
+ result_c = histogramnd(self.sample,
+ self.histo_range,
+ self.n_bins,
+ weights=self.weights,
+ last_bin_closed=False)
+
+ bins_max = [rng[1] for rng in self.histo_range]
+ filtered_idx = _get_values_index(self.sample, bins_max)
+
+ result_np = np.histogramdd(self.sample[filtered_idx],
+ bins=self.n_bins,
+ range=self.histo_range)
+
+ result_np_w = np.histogramdd(self.sample[filtered_idx],
+ bins=self.n_bins,
+ range=self.histo_range,
+ weights=self.weights[filtered_idx])
+
+ # comparing "hits"
+ hits_cmp = np.array_equal(result_c[0], result_np[0])
+ # comparing weights
+ weights_cmp = np.array_equal(result_c[1],
+ result_np_w[0])
+
+ self.assertTrue(hits_cmp, msg=self.state_msg)
+ self.assertTrue(weights_cmp, msg=self.state_msg)
+
+ bins_min = [rng[0] for rng in self.histo_range]
+ bins_max = [rng[1] for rng in self.histo_range]
+ inrange_idx = _get_in_range_indices(self.sample,
+ bins_min,
+ bins_max,
+ minop=operator.ge,
+ maxop=operator.lt)
+
+ self.assertEqual(result_c[0].sum(), len(inrange_idx),
+ msg=self.state_msg)
+ # we have to sum the weights using the same precision as the
+ # histogramnd function
+ weights_sum = self.weights[inrange_idx].astype(result_c[1].dtype).sum()
+ self.assertTrue(self.array_compare(result_c[1].sum(), weights_sum),
+ msg=self.state_msg)
+
+ def test_filter_min(self):
+ """
+
+ """
+ result_c = histogramnd(self.sample,
+ self.histo_range,
+ self.n_bins,
+ weights=self.weights,
+ last_bin_closed=True,
+ weight_min=self.filter_min)
+
+ # !!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ filter_min = self.dtype_weights(self.filter_min)
+
+ weight_idx = _get_values_index(self.weights,
+ filter_min, # <------ !!!
+ operator.ge)
+
+ result_np = np.histogramdd(self.sample[weight_idx],
+ bins=self.n_bins,
+ range=self.histo_range)
+
+ result_np_w = np.histogramdd(self.sample[weight_idx],
+ bins=self.n_bins,
+ range=self.histo_range,
+ weights=self.weights[weight_idx])
+
+ # comparing "hits"
+ hits_cmp = np.array_equal(result_c[0],
+ result_np[0])
+ # comparing weights
+ weights_cmp = np.array_equal(result_c[1], result_np_w[0])
+
+ self.assertTrue(hits_cmp, msg=self.state_msg)
+ self.assertTrue(weights_cmp, msg=self.state_msg)
+
+ bins_min = [rng[0] for rng in self.histo_range]
+ bins_max = [rng[1] for rng in self.histo_range]
+ inrange_idx = _get_in_range_indices(self.sample[weight_idx],
+ bins_min,
+ bins_max,
+ minop=operator.ge,
+ maxop=operator.le)
+
+ inrange_idx = weight_idx[inrange_idx]
+
+ self.assertEqual(result_c[0].sum(), len(inrange_idx),
+ msg=self.state_msg)
+
+ # we have to sum the weights using the same precision as the
+ # histogramnd function
+ weights_sum = self.weights[inrange_idx].astype(result_c[1].dtype).sum()
+ self.assertTrue(self.array_compare(result_c[1].sum(), weights_sum),
+ msg=self.state_msg)
+
+ def test_filter_max(self):
+ """
+
+ """
+ result_c = histogramnd(self.sample,
+ self.histo_range,
+ self.n_bins,
+ weights=self.weights,
+ last_bin_closed=True,
+ weight_max=self.filter_max)
+
+ # !!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ filter_max = self.dtype_weights(self.filter_max)
+
+ weight_idx = _get_values_index(self.weights,
+ filter_max, # <------ !!!
+ operator.le)
+
+ result_np = np.histogramdd(self.sample[weight_idx],
+ bins=self.n_bins,
+ range=self.histo_range)
+
+ result_np_w = np.histogramdd(self.sample[weight_idx],
+ bins=self.n_bins,
+ range=self.histo_range,
+ weights=self.weights[weight_idx])
+
+ # comparing "hits"
+ hits_cmp = np.array_equal(result_c[0],
+ result_np[0])
+ # comparing weights
+ weights_cmp = np.array_equal(result_c[1], result_np_w[0])
+
+ self.assertTrue(hits_cmp, msg=self.state_msg)
+ self.assertTrue(weights_cmp, msg=self.state_msg)
+
+ bins_min = [rng[0] for rng in self.histo_range]
+ bins_max = [rng[1] for rng in self.histo_range]
+ inrange_idx = _get_in_range_indices(self.sample[weight_idx],
+ bins_min,
+ bins_max,
+ minop=operator.ge,
+ maxop=operator.le)
+
+ inrange_idx = weight_idx[inrange_idx]
+
+ self.assertEqual(result_c[0].sum(), len(inrange_idx),
+ msg=self.state_msg)
+
+ # we have to sum the weights using the same precision as the
+ # histogramnd function
+ weights_sum = self.weights[inrange_idx].astype(result_c[1].dtype).sum()
+ self.assertTrue(self.array_compare(result_c[1].sum(), weights_sum),
+ msg=self.state_msg)
+
+ def test_filter_minmax(self):
+ """
+
+ """
+ result_c = histogramnd(self.sample,
+ self.histo_range,
+ self.n_bins,
+ weights=self.weights,
+ last_bin_closed=True,
+ weight_min=self.filter_min,
+ weight_max=self.filter_max)
+
+ # !!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ filter_min = self.dtype_weights(self.filter_min)
+ filter_max = self.dtype_weights(self.filter_max)
+
+ weight_idx = _get_in_range_indices(self.weights,
+ filter_min, # <------ !!!
+ filter_max, # <------ !!!
+ minop=operator.ge,
+ maxop=operator.le)
+
+ result_np = np.histogramdd(self.sample[weight_idx],
+ bins=self.n_bins,
+ range=self.histo_range)
+
+ result_np_w = np.histogramdd(self.sample[weight_idx],
+ bins=self.n_bins,
+ range=self.histo_range,
+ weights=self.weights[weight_idx])
+
+ # comparing "hits"
+ hits_cmp = np.array_equal(result_c[0],
+ result_np[0])
+ # comparing weights
+ weights_cmp = np.array_equal(result_c[1], result_np_w[0])
+
+ self.assertTrue(hits_cmp)
+ self.assertTrue(weights_cmp)
+
+ bins_min = [rng[0] for rng in self.histo_range]
+ bins_max = [rng[1] for rng in self.histo_range]
+ inrange_idx = _get_in_range_indices(self.sample[weight_idx],
+ bins_min,
+ bins_max,
+ minop=operator.ge,
+ maxop=operator.le)
+
+ inrange_idx = weight_idx[inrange_idx]
+
+ self.assertEqual(result_c[0].sum(), len(inrange_idx),
+ msg=self.state_msg)
+
+ # we have to sum the weights using the same precision as the
+ # histogramnd function
+ weights_sum = self.weights[inrange_idx].astype(result_c[1].dtype).sum()
+ self.assertTrue(self.array_compare(result_c[1].sum(), weights_sum),
+ msg=self.state_msg)
+
+ def test_reuse_histo(self):
+ """
+
+ """
+ result_c_1 = histogramnd(self.sample,
+ self.histo_range,
+ self.n_bins,
+ weights=self.weights,
+ last_bin_closed=True)
+
+ result_np_1 = np.histogramdd(self.sample,
+ bins=self.n_bins,
+ range=self.histo_range)
+
+ np.histogramdd(self.sample,
+ bins=self.n_bins,
+ range=self.histo_range,
+ weights=self.weights)
+
+ sample_2, weights_2 = self.generate_data()
+
+ result_c_2 = histogramnd(sample_2,
+ self.histo_range,
+ self.n_bins,
+ weights=weights_2,
+ last_bin_closed=True,
+ histo=result_c_1[0])
+
+ result_np_2 = np.histogramdd(sample_2,
+ bins=self.n_bins,
+ range=self.histo_range)
+
+ result_np_w_2 = np.histogramdd(sample_2,
+ bins=self.n_bins,
+ range=self.histo_range,
+ weights=weights_2)
+
+ # comparing "hits"
+ hits_cmp = np.array_equal(result_c_2[0],
+ result_np_1[0] +
+ result_np_2[0])
+ # comparing weights
+ weights_cmp = np.array_equal(result_c_2[1],
+ result_np_w_2[0])
+
+ self.assertTrue(hits_cmp, msg=self.state_msg)
+ self.assertTrue(weights_cmp, msg=self.state_msg)
+
+ def test_reuse_cumul(self):
+ """
+
+ """
+ result_c = histogramnd(self.sample,
+ self.histo_range,
+ self.n_bins,
+ weights=self.weights,
+ last_bin_closed=True)
+
+ np.histogramdd(self.sample,
+ bins=self.n_bins,
+ range=self.histo_range)
+
+ result_np_w = np.histogramdd(self.sample,
+ bins=self.n_bins,
+ range=self.histo_range,
+ weights=self.weights)
+
+ sample_2, weights_2 = self.generate_data()
+
+ result_c_2 = histogramnd(sample_2,
+ self.histo_range,
+ self.n_bins,
+ weights=weights_2,
+ last_bin_closed=True,
+ weighted_histo=result_c[1])
+
+ result_np_2 = np.histogramdd(sample_2,
+ bins=self.n_bins,
+ range=self.histo_range)
+
+ result_np_w_2 = np.histogramdd(sample_2,
+ bins=self.n_bins,
+ range=self.histo_range,
+ weights=weights_2)
+
+ # comparing "hits"
+ hits_cmp = np.array_equal(result_c_2[0],
+ result_np_2[0])
+ # comparing weights
+
+ self.assertTrue(hits_cmp, msg=self.state_msg)
+ self.assertTrue(self.array_compare(result_c_2[1],
+ result_np_w[0] + result_np_w_2[0]),
+ msg=self.state_msg)
+
+ def test_reuse_cumul_float(self):
+ """
+
+ """
+ n_bins = np.array(self.n_bins, ndmin=1)
+ if len(self.sample.shape) == 2:
+ if len(n_bins) == self.sample.shape[1]:
+ shp = tuple([x for x in n_bins])
+ else:
+ shp = (self.n_bins,) * self.sample.shape[1]
+ cumul = np.zeros(shp, dtype=np.float32)
+ else:
+ shp = (self.n_bins,)
+ cumul = np.zeros(shp, dtype=np.float32)
+
+ result_c_1 = histogramnd(self.sample,
+ self.histo_range,
+ self.n_bins,
+ weights=self.weights,
+ last_bin_closed=True,
+ weighted_histo=cumul)
+
+ result_np_1 = np.histogramdd(self.sample,
+ bins=self.n_bins,
+ range=self.histo_range)
+
+ result_np_w_1 = np.histogramdd(self.sample,
+ bins=self.n_bins,
+ range=self.histo_range,
+ weights=self.weights)
+
+ # comparing "hits"
+ hits_cmp = np.array_equal(result_c_1[0],
+ result_np_1[0])
+
+ self.assertTrue(hits_cmp, msg=self.state_msg)
+ self.assertEqual(result_c_1[1].dtype, np.float32, msg=self.state_msg)
+
+ bins_min = [rng[0] for rng in self.histo_range]
+ bins_max = [rng[1] for rng in self.histo_range]
+ inrange_idx = _get_in_range_indices(self.sample,
+ bins_min,
+ bins_max,
+ minop=operator.ge,
+ maxop=operator.le)
+ weights_sum = \
+ self.weights[inrange_idx].astype(np.float32).sum(dtype=np.float64)
+ self.assertTrue(np.allclose(result_c_1[1].sum(dtype=np.float64),
+ weights_sum), msg=self.state_msg)
+ self.assertTrue(np.allclose(result_c_1[1].sum(dtype=np.float64),
+ result_np_w_1[0].sum(dtype=np.float64)),
+ msg=self.state_msg)
+
+
+class _TestHistogramnd_1d(_TestHistogramnd):
+ """
+ Unit tests of the 1D histogramnd function.
+ """
+ sample_rng = [-55., 100.]
+ weights_rng = [-70., 150.]
+ n_dims = 1
+ filter_min = -15.6
+ filter_max = 85.7
+
+ histo_range = [[-30.2, 90.3]]
+ n_bins = 30
+
+ dtype = None
+
+
+class _TestHistogramnd_2d(_TestHistogramnd):
+ """
+ Unit tests of the 1D histogramnd function.
+ """
+ sample_rng = [-50.2, 100.99]
+ weights_rng = [70., 150.]
+ n_dims = 2
+ filter_min = 81.7
+ filter_max = 135.3
+
+ histo_range = [[10., 90.], [20., 70.]]
+ n_bins = 30
+
+ dtype = None
+
+
+class _TestHistogramnd_3d(_TestHistogramnd):
+ """
+ Unit tests of the 1D histogramnd function.
+ """
+ sample_rng = [10.2, 200.9]
+ weights_rng = [0., 100.]
+ n_dims = 3
+ filter_min = 31.5
+ filter_max = 83.7
+
+ histo_range = [[30.8, 150.2], [20.1, 90.9], [10.1, 195.]]
+ n_bins = 30
+
+ dtype = None
+
+
+# ################################################################
+# ################################################################
+# ################################################################
+# ################################################################
+
+
+class TestHistogramnd_1d_double_double(_TestHistogramnd_1d):
+ __test__ = True # because _TestHistogramnd is ignored
+ dtype_sample = np.double
+ dtype_weights = np.double
+
+
+class TestHistogramnd_1d_double_float(_TestHistogramnd_1d):
+ __test__ = True # because _TestHistogramnd is ignored
+ dtype_sample = np.double
+ dtype_weights = np.float32
+
+
+class TestHistogramnd_1d_double_int32(_TestHistogramnd_1d):
+ __test__ = True # because _TestHistogramnd is ignored
+ dtype_sample = np.double
+ dtype_weights = np.int32
+
+
+class TestHistogramnd_1d_float_double(_TestHistogramnd_1d):
+ __test__ = True # because _TestHistogramnd is ignored
+ dtype_sample = np.float32
+ dtype_weights = np.double
+
+
+class TestHistogramnd_1d_float_float(_TestHistogramnd_1d):
+ __test__ = True # because _TestHistogramnd is ignored
+ dtype_sample = np.float32
+ dtype_weights = np.float32
+
+
+class TestHistogramnd_1d_float_int32(_TestHistogramnd_1d):
+ __test__ = True # because _TestHistogramnd is ignored
+ dtype_sample = np.float32
+ dtype_weights = np.int32
+
+
+class TestHistogramnd_1d_int32_double(_TestHistogramnd_1d):
+ __test__ = True # because _TestHistogramnd is ignored
+ dtype_sample = np.int32
+ dtype_weights = np.double
+
+
+class TestHistogramnd_1d_int32_float(_TestHistogramnd_1d):
+ __test__ = True # because _TestHistogramnd is ignored
+ dtype_sample = np.int32
+ dtype_weights = np.float32
+
+
+class TestHistogramnd_1d_int32_int32(_TestHistogramnd_1d):
+ __test__ = True # because _TestHistogramnd is ignored
+ dtype_sample = np.int32
+ dtype_weights = np.int32
+
+
+class TestHistogramnd_2d_double_double(_TestHistogramnd_2d):
+ __test__ = True # because _TestHistogramnd is ignored
+ dtype_sample = np.double
+ dtype_weights = np.double
+
+
+class TestHistogramnd_2d_double_float(_TestHistogramnd_2d):
+ __test__ = True # because _TestHistogramnd is ignored
+ dtype_sample = np.double
+ dtype_weights = np.float32
+
+
+class TestHistogramnd_2d_double_int32(_TestHistogramnd_2d):
+ __test__ = True # because _TestHistogramnd is ignored
+ dtype_sample = np.double
+ dtype_weights = np.int32
+
+
+class TestHistogramnd_2d_float_double(_TestHistogramnd_2d):
+ __test__ = True # because _TestHistogramnd is ignored
+ dtype_sample = np.float32
+ dtype_weights = np.double
+
+
+class TestHistogramnd_2d_float_float(_TestHistogramnd_2d):
+ __test__ = True # because _TestHistogramnd is ignored
+ dtype_sample = np.float32
+ dtype_weights = np.float32
+
+
+class TestHistogramnd_2d_float_int32(_TestHistogramnd_2d):
+ __test__ = True # because _TestHistogramnd is ignored
+ dtype_sample = np.float32
+ dtype_weights = np.int32
+
+
+class TestHistogramnd_2d_int32_double(_TestHistogramnd_2d):
+ __test__ = True # because _TestHistogramnd is ignored
+ dtype_sample = np.int32
+ dtype_weights = np.double
+
+
+class TestHistogramnd_2d_int32_float(_TestHistogramnd_2d):
+ __test__ = True # because _TestHistogramnd is ignored
+ dtype_sample = np.int32
+ dtype_weights = np.float32
+
+
+class TestHistogramnd_2d_int32_int32(_TestHistogramnd_2d):
+ __test__ = True # because _TestHistogramnd is ignored
+ dtype_sample = np.int32
+ dtype_weights = np.int32
+
+
+class TestHistogramnd_3d_double_double(_TestHistogramnd_3d):
+ __test__ = True # because _TestHistogramnd is ignored
+ dtype_sample = np.double
+ dtype_weights = np.double
+
+
+class TestHistogramnd_3d_double_float(_TestHistogramnd_3d):
+ __test__ = True # because _TestHistogramnd is ignored
+ dtype_sample = np.double
+ dtype_weights = np.float32
+
+
+class TestHistogramnd_3d_double_int32(_TestHistogramnd_3d):
+ __test__ = True # because _TestHistogramnd is ignored
+ dtype_sample = np.double
+ dtype_weights = np.int32
+
+
+class TestHistogramnd_3d_float_double(_TestHistogramnd_3d):
+ __test__ = True # because _TestHistogramnd is ignored
+ dtype_sample = np.float32
+ dtype_weights = np.double
+
+
+class TestHistogramnd_3d_float_float(_TestHistogramnd_3d):
+ __test__ = True # because _TestHistogramnd is ignored
+ dtype_sample = np.float32
+ dtype_weights = np.float32
+
+
+class TestHistogramnd_3d_float_int32(_TestHistogramnd_3d):
+ __test__ = True # because _TestHistogramnd is ignored
+ dtype_sample = np.float32
+ dtype_weights = np.int32
+
+
+class TestHistogramnd_3d_int32_double(_TestHistogramnd_3d):
+ __test__ = True # because _TestHistogramnd is ignored
+ dtype_sample = np.int32
+ dtype_weights = np.double
+
+
+class TestHistogramnd_3d_int32_float(_TestHistogramnd_3d):
+ __test__ = True # because _TestHistogramnd is ignored
+ dtype_sample = np.int32
+ dtype_weights = np.float32
+
+
+class TestHistogramnd_3d_int32_int32(_TestHistogramnd_3d):
+ __test__ = True # because _TestHistogramnd is ignored
+ dtype_sample = np.int32
+ dtype_weights = np.int32
diff --git a/src/silx/math/test/test_interpolate.py b/src/silx/math/test/test_interpolate.py
new file mode 100644
index 0000000..146449d
--- /dev/null
+++ b/src/silx/math/test/test_interpolate.py
@@ -0,0 +1,125 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2019 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
+"""Test for interpolate module"""
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "11/07/2019"
+
+
+import unittest
+
+import numpy
+try:
+ from scipy.interpolate import interpn
+except ImportError:
+ interpn = None
+
+from silx.utils.testutils import ParametricTestCase
+from silx.math import interpolate
+
+
+@unittest.skipUnless(interpn is not None, "scipy missing")
+class TestInterp3d(ParametricTestCase):
+ """Test silx.math.interpolate.interp3d"""
+
+ @staticmethod
+ def ref_interp3d(data, points):
+ """Reference implementation of interp3d based on scipy
+
+ :param numpy.ndarray data: 3D floating dataset
+ :param numpy.ndarray points: Array of points of shape (N, 3)
+ """
+ return interpn(
+ [numpy.arange(dim, dtype=data.dtype) for dim in data.shape],
+ data,
+ points,
+ method='linear')
+
+ def test_random_data(self):
+ """Test interp3d with random data"""
+ size = 32
+ npoints = 10
+
+ ref_data = numpy.random.random((size, size, size))
+ ref_points = numpy.random.random(npoints*3).reshape(npoints, 3) * (size -1)
+
+ for dtype in (numpy.float32, numpy.float64):
+ data = ref_data.astype(dtype)
+ points = ref_points.astype(dtype)
+ ref_result = self.ref_interp3d(data, points)
+
+ for method in (u'linear', u'linear_omp'):
+ with self.subTest(method=method):
+ result = interpolate.interp3d(data, points, method=method)
+ self.assertTrue(numpy.allclose(ref_result, result))
+
+ def test_notfinite_data(self):
+ """Test interp3d with NaN and inf"""
+ data = numpy.ones((3, 3, 3), dtype=numpy.float64)
+ data[0, 0, 0] = numpy.nan
+ data[2, 2, 2] = numpy.inf
+ points = numpy.array([(0.5, 0.5, 0.5),
+ (1.5, 1.5, 1.5)])
+
+ for method in (u'linear', u'linear_omp'):
+ with self.subTest(method=method):
+ result = interpolate.interp3d(
+ data, points, method=method)
+ self.assertTrue(numpy.isnan(result[0]))
+ self.assertTrue(result[1] == numpy.inf)
+
+ def test_points_outside(self):
+ """Test interp3d with points outside the volume"""
+ data = numpy.ones((4, 4, 4), dtype=numpy.float64)
+ points = numpy.array([(-0.1, -0.1, -0.1),
+ (3.1, 3.1, 3.1),
+ (-0.1, 1., 1.),
+ (1., 1., 3.1)])
+
+ for method in (u'linear', u'linear_omp'):
+ for fill_value in (numpy.nan, 0., -1.):
+ with self.subTest(method=method):
+ result = interpolate.interp3d(
+ data, points, method=method, fill_value=fill_value)
+ if numpy.isnan(fill_value):
+ self.assertTrue(numpy.all(numpy.isnan(result)))
+ else:
+ self.assertTrue(numpy.all(numpy.equal(result, fill_value)))
+
+ def test_integer_points(self):
+ """Test interp3d with integer points coord"""
+ data = numpy.arange(4**3, dtype=numpy.float64).reshape(4, 4, 4)
+ points = numpy.array([(0., 0., 0.),
+ (0., 0., 1.),
+ (2., 3., 0.),
+ (3., 3., 3.)])
+
+ ref_result = data[tuple(points.T.astype(numpy.int32))]
+
+ for method in (u'linear', u'linear_omp'):
+ with self.subTest(method=method):
+ result = interpolate.interp3d(data, points, method=method)
+ self.assertTrue(numpy.allclose(ref_result, result))
diff --git a/src/silx/math/test/test_marchingcubes.py b/src/silx/math/test/test_marchingcubes.py
new file mode 100644
index 0000000..5e2b193
--- /dev/null
+++ b/src/silx/math/test/test_marchingcubes.py
@@ -0,0 +1,174 @@
+# coding: utf-8
+# /*##########################################################################
+# Copyright (C) 2016 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ############################################################################*/
+"""Tests of the marchingcubes module"""
+
+from __future__ import division
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "17/01/2018"
+
+import unittest
+
+import numpy
+
+from silx.utils.testutils import ParametricTestCase
+
+from silx.math import marchingcubes
+
+
+class TestMarchingCubes(ParametricTestCase):
+ """Tests of marching cubes"""
+
+ def assertAllClose(self, array1, array2, msg=None,
+ rtol=1e-05, atol=1e-08):
+ """Assert that the 2 numpy.ndarrays are almost equal.
+
+ :param str msg: Message to provide when assert fails
+ :param float rtol: Relative tolerance, see :func:`numpy.allclose`
+ :param float atol: Absolute tolerance, see :func:`numpy.allclose`
+ """
+ if not numpy.allclose(array1, array2, rtol, atol):
+ raise self.failureException(msg)
+
+ def test_cube(self):
+ """Unit tests with a single cube"""
+
+ # No isosurface
+ cube_zero = numpy.zeros((2, 2, 2), dtype=numpy.float32)
+
+ result = marchingcubes.MarchingCubes(cube_zero, 1.)
+ self.assertEqual(result.shape, cube_zero.shape)
+ self.assertEqual(result.isolevel, 1.)
+ self.assertEqual(result.invert_normals, True)
+
+ vertices, normals, indices = result
+ self.assertEqual(len(vertices), 0)
+ self.assertEqual(len(normals), 0)
+ self.assertEqual(len(indices), 0)
+
+ # Cube array dimensions: shape = (dim 0, dim 1, dim2)
+ #
+ # dim 0 (Z)
+ # ^
+ # |
+ # 4 +------+ 5
+ # /| /|
+ # / | / |
+ # 6 +------+ 7|
+ # | | | |
+ # |0 +---|--+ 1 -> dim 2 (X)
+ # | / | /
+ # |/ |/
+ # 2 +------+ 3
+ # /
+ # dim 1 (Y)
+
+ # isosurface perpendicular to dim 0 (Z)
+ cube = numpy.array(
+ (((0., 0.), (0., 0.)),
+ ((1., 1.), (1., 1.))), dtype=numpy.float32)
+ level = 0.5
+ vertices, normals, indices = marchingcubes.MarchingCubes(
+ cube, level, invert_normals=False)
+ self.assertAllClose(vertices[:, 0], level)
+ self.assertAllClose(normals, (1., 0., 0.))
+ self.assertEqual(len(indices), 2)
+
+ # isosurface perpendicular to dim 1 (Y)
+ cube = numpy.array(
+ (((0., 0.), (1., 1.)),
+ ((0., 0.), (1., 1.))), dtype=numpy.float32)
+ level = 0.2
+ vertices, normals, indices = marchingcubes.MarchingCubes(cube, level)
+ self.assertAllClose(vertices[:, 1], level)
+ self.assertAllClose(normals, (0., -1., 0.))
+ self.assertEqual(len(indices), 2)
+
+ # isosurface perpendicular to dim 2 (X)
+ cube = numpy.array(
+ (((0., 1.), (0., 1.)),
+ ((0., 1.), (0., 1.))), dtype=numpy.float32)
+ level = 0.9
+ vertices, normals, indices = marchingcubes.MarchingCubes(
+ cube, level, invert_normals=False)
+ self.assertAllClose(vertices[:, 2], level)
+ self.assertAllClose(normals, (0., 0., 1.))
+ self.assertEqual(len(indices), 2)
+
+ # isosurface normal in dim1, dim 0 (Y, Z) plane
+ cube = numpy.array(
+ (((0., 0.), (0., 0.)),
+ ((0., 0.), (1., 1.))), dtype=numpy.float32)
+ level = 0.5
+ vertices, normals, indices = marchingcubes.MarchingCubes(cube, level)
+ self.assertAllClose(normals[:, 2], 0.)
+ self.assertEqual(len(indices), 2)
+
+ def test_sampling(self):
+ """Test different sampling, comparing to reference without sampling"""
+ isolevel = 0.5
+ size = 9
+ chessboard = numpy.zeros((size, size, size), dtype=numpy.float32)
+ chessboard.reshape(-1)[::2] = 1 # OK as long as dimensions are odd
+
+ ref_result = marchingcubes.MarchingCubes(chessboard, isolevel)
+
+ samplings = [
+ (2, 1, 1),
+ (1, 2, 1),
+ (1, 1, 2),
+ (2, 2, 2),
+ (3, 3, 3),
+ (1, 3, 1),
+ (1, 1, 3),
+ ]
+
+ for sampling in samplings:
+ with self.subTest(sampling=sampling):
+ sampling = numpy.array(sampling)
+
+ data = 1e6 * numpy.ones(
+ sampling * size, dtype=numpy.float32)
+ # Copy ref chessboard in data according to sampling
+ data[::sampling[0], ::sampling[1], ::sampling[2]] = chessboard
+
+ result = marchingcubes.MarchingCubes(data, isolevel,
+ sampling=sampling)
+ # Compare vertices normalized with shape
+ self.assertAllClose(
+ ref_result.get_vertices() / ref_result.shape,
+ result.get_vertices() / result.shape,
+ atol=0., rtol=0.)
+
+ # Compare normals
+ # This comparison only works for normals aligned with axes
+ # otherwise non uniform sampling would make different normals
+ self.assertAllClose(ref_result.get_normals(),
+ result.get_normals(),
+ atol=0., rtol=0.)
+
+ self.assertAllClose(ref_result.get_indices(),
+ result.get_indices(),
+ atol=0., rtol=0.)