# This file is part of xrayutilities.
#
# xrayutilities is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, see <http://www.gnu.org/licenses/>.
#
# Copyright (C) 2009-2010, 2013
# Eugen Wintersberger <eugen.wintersberger@desy.de>
# Copyright (C) 2009 Mario Keplinger <mario.keplinger@jku.at>
# Copyright (c) 2009-2021, 2023 Dominik Kriegner <dominik.kriegner@gmail.com>
import abc
import enum
import numpy
from . import config, cxrayutilities, utilities
from .exception import InputError
[docs]
class GridderFlags(enum.IntFlag):
NO_DATA_INIT = 1
NO_NORMALIZATION = 4
VERBOSE = 16
[docs]
def delta(min_value, max_value, n):
"""
Compute the stepsize along an axis of a grid.
Parameters
----------
min_value : axis minimum value
max_value : axis maximum value
n : number of steps
"""
if n != 1:
return (float(max_value) - float(min_value)) / float(n - 1)
return numpy.inf
[docs]
def axis(min_value, max_value, n):
"""
Compute the a grid axis.
Parameters
----------
min_value : float
axis minimum value
max_value : float
axis maximum value
n : int
number of steps
"""
if n != 1:
d = delta(min_value, max_value, n)
a = min_value + d * numpy.arange(0, n)
else:
a = (min_value + max_value) / 2.
return a
[docs]
def ones(*args):
"""
Compute ones for matrix generation. The shape is determined by the number
of input arguments.
"""
return numpy.ones(args, dtype=numpy.double)
[docs]
class Gridder(utilities.ABC):
"""
Basis class for gridders in xrayutilities. A gridder is a function mapping
irregular spaced data onto a regular grid by binning the data into equally
sized elements.
There are different ways of defining the regular grid of a Gridder. In
xrayutilities the data bins extend beyond the data range in the input data,
but the given position being the center of these bins, extends from the
minimum to the maximum of the data! The main motivation for this was to
create a Gridder, which when feeded with N equidistant data points and
gridded with N bins would not change the data position (not the case with
numpy.histogramm functions!). Of course this leads to the fact that for
homogeneous point density the first and last bin in any direction are not
filled as the other bins.
A different definition is used by numpy histogram functions where the bins
extend only to the end of the data range. (see numpy histogram,
histrogram2d, ...)
"""
[docs]
def __init__(self):
"""
Constructor defining default properties of any Gridder class
"""
# flags represent a way to transmit options to the C-code
# no data initialization necessary in c-code
self.flags = GridderFlags.NO_DATA_INIT
# by default every call to gridder will start a new gridding
self.keep_data = False
self.normalize = True
# flag to allow for sequential gridding with fixed data range
self.fixed_range = False
if not hasattr(self, '_gdata'):
self._gdata = numpy.empty(0)
if not hasattr(self, '_gnorm'):
self._gnorm = numpy.empty(0)
if config.VERBOSITY >= config.INFO_ALL:
self.flags |= GridderFlags.VERBOSE
[docs]
@abc.abstractmethod
def __call__(self):
"""
abstract call method which every implementation of a Gridder has to
override
"""
[docs]
def Normalize(self, bool):
"""
set or unset the normalization flag. Normalization needs to be done to
obtain proper gridding but may want to be disabled in certain cases
when sequential gridding is performed
"""
if bool not in [False, True]:
raise TypeError("Normalize flag must be a boolan value "
"(True/False)!")
self.normalize = bool
if bool: # Note this is usually done in Python anyways!
self.flags &= ~GridderFlags.NO_NORMALIZATION
else:
self.flags |= GridderFlags.NO_NORMALIZATION
[docs]
def KeepData(self, bool):
if bool not in [False, True]:
raise TypeError("Keep Data flag must be a boolan value"
"(True/False)!")
self.keep_data = bool
def __get_data(self):
"""
return gridded data (performs normalization if switched on)
"""
if self.normalize:
tmp = numpy.copy(self._gdata)
mask = (self._gnorm != 0)
tmp[mask] /= self._gnorm[mask].astype(numpy.double)
return tmp
return self._gdata.copy()
data = property(__get_data)
def _prepare_array(self, a):
"""
prepare array for passing to c-code
"""
if isinstance(a, (list, tuple, float, int)):
a = numpy.asarray(a)
return a.reshape(a.size)
[docs]
def Clear(self):
"""
Clear so far gridded data to reuse this instance of the Gridder
"""
self._gdata[...] = 0
self._gnorm[...] = 0
[docs]
class Gridder1D(Gridder):
[docs]
def __init__(self, nx):
Gridder.__init__(self)
if nx <= 0:
raise InputError('nx must be a positiv integer!')
self.nx = nx
self.xmin = 0
self.xmax = 0
self._gdata = numpy.zeros(nx, dtype=numpy.double)
self._gnorm = numpy.zeros(nx, dtype=numpy.double)
[docs]
def savetxt(self, filename, header=''):
"""
save gridded data to a txt file with two columns. The first column is
the data coordinate and the second the corresponding data value
Parameters
----------
filename : str
output filename
header : str, optional
optional header for the data file.
"""
numpy.savetxt(filename, numpy.vstack((self.xaxis, self.data)).T,
header=header, fmt='%.6g %.4g')
def __get_xaxis(self):
"""
Returns the xaxis of the gridder
the returned values correspond to the center of the data bins used by
the gridding algorithm
"""
return axis(self.xmin, self.xmax, self.nx)
xaxis = property(__get_xaxis)
[docs]
def dataRange(self, min, max, fixed=True):
"""
define minimum and maximum data range, usually this is deduced
from the given data automatically, however, for sequential
gridding it is useful to set this before the first call of the
gridder. data outside the range are simply ignored
Parameters
----------
min : float
minimum value of the gridding range
max : float
maximum value of the gridding range
fixed : bool, optional
flag to turn fixed range gridding on (True (default)) or off
(False)
"""
self.fixed_range = fixed
self.xmin = min
self.xmax = max
def _checktransinput(self, x, data):
"""
common checks and reshape commands for the input data. This function
checks the data type and shape of the input data.
"""
if not self.keep_data:
self.Clear()
x = self._prepare_array(x)
data = self._prepare_array(data)
if x.size != data.size:
raise InputError(f"XU.{self.__class__.__name__}: size of given "
"datasets (x, data) is not equal!")
if not self.fixed_range:
# assume that with setting keep_data the user wants to call the
# gridder more often and obtain a reasonable result
self.dataRange(x.min(), x.max(), self.keep_data)
return x, data
[docs]
def __call__(self, x, data):
"""
Perform gridding on a set of data. After running the gridder
the 'data' object in the class is holding the gridded data.
Parameters
----------
x : ndarray
numpy array of arbitrary shape with x positions
data : ndarray
numpy array of arbitrary shape with data values
"""
x, data = self._checktransinput(x, data)
# remove normalize flag for C-code, normalization is always performed
# in python
flags = self.flags | GridderFlags.NO_NORMALIZATION
cxrayutilities.gridder1d(x, data, self.nx, self.xmin, self.xmax,
self._gdata, self._gnorm, flags)
[docs]
class FuzzyGridder1D(Gridder1D):
"""
An 1D binning class considering every data point to have a finite width.
If necessary one data point will be split fractionally over different
data bins. This is numerically more effort but represents better the
typical case of a experimental data, which do not represent a mathematical
point but have a finite width (e.g. X-ray data from a 1D detector).
"""
[docs]
def __call__(self, x, data, width=None):
"""
Perform gridding on a set of data. After running the gridder
the 'data' object in the class is holding the gridded data.
Parameters
----------
x : ndarray
numpy array of arbitrary shape with x positions
data : ndarray
numpy array of arbitrary shape with data values
width : float, optional
width of one data point. If not given half the bin size will be
used.
"""
x, data = self._checktransinput(x, data)
if not width:
width = delta(self.xmin, self.xmax, self.nx) / 2.
# remove normalize flag for C-code, normalization is always performed
# in python
flags = self.flags | GridderFlags.NO_NORMALIZATION
cxrayutilities.fuzzygridder1d(x, data, self.nx, self.xmin, self.xmax,
self._gdata, self._gnorm, width, flags)
[docs]
class npyGridder1D(Gridder1D):
def __get_xaxis(self):
"""
Returns the xaxis of the gridder
the returned values correspond to the center of the data bins used by
the numpy.histogram function
"""
# no -1 here to be consistent with numpy.histogram
dx = (float(self.xmax - self.xmin)) / float(self.nx)
ax = self.xmin + dx * numpy.arange(0, self.nx) + dx / 2.
return ax
xaxis = property(__get_xaxis)
[docs]
def __call__(self, x, data):
"""
Perform gridding on a set of data. After running the gridder
the 'data' object in the class is holding the gridded data.
Parameters
----------
x : ndarray
numpy array of arbitrary shape with x positions
data : ndarray
numpy array of arbitrary shape with data values
"""
x, data = self._checktransinput(x, data)
# use only non-NaN data values
mask = numpy.invert(numpy.isnan(data))
ldata = data[mask]
lx = x[mask]
if not self.fixed_range:
# assume that with setting keep_data the user wants to call the
# gridder more often and obtain a reasonable result
self.dataRange(lx.min(), lx.max(), self.keep_data)
# grid the data using numpy histogram
tmpgdata, _ = numpy.histogram(lx, weights=ldata, bins=self.nx,
range=(self.xmin, self.xmax))
tmpgnorm, _ = numpy.histogram(lx, bins=self.nx,
range=(self.xmin, self.xmax))
if self.keep_data:
self._gnorm += tmpgnorm
self._gdata += tmpgdata
else:
self._gnorm = tmpgnorm
self._gdata = tmpgdata