# Copyright 2018 United Kingdom Research and Innovation
# Copyright 2018 The University of Manchester
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Authors:
# CIL Developers, listed at: https://github.com/TomographicImaging/CIL/blob/master/NOTICE.txt
import copy
import ctypes
import warnings
from functools import reduce
from numbers import Number
import numpy
from .cilacc import cilacc
from cil.utilities.multiprocessing import NUM_THREADS
[docs]
class DataContainer(object):
'''Generic class to hold data
Data is currently held in a numpy arrays'''
@property
def geometry(self):
return None
@geometry.setter
def geometry(self, val):
if val is not None:
raise TypeError("DataContainers cannot hold a geometry, use ImageData or AcquisitionData instead")
@property
def dimension_labels(self):
if self._dimension_labels is None:
default_labels = [0]*self.number_of_dimensions
for i in range(self.number_of_dimensions):
default_labels[i] = 'dimension_{0:02}'.format(i)
return tuple(default_labels)
else:
return self._dimension_labels
@dimension_labels.setter
def dimension_labels(self, val):
if val is None:
self._dimension_labels = None
elif len(val_tuple := tuple(val)) == self.number_of_dimensions:
self._dimension_labels = val_tuple
else:
raise ValueError("dimension_labels expected a list containing {0} strings got {1}".format(self.number_of_dimensions, val))
@property
def shape(self):
'''Returns the shape of the DataContainer'''
return self.array.shape
@shape.setter
def shape(self, val):
print("Deprecated - shape will be set automatically")
@property
def ndim(self):
'''Returns the ndim of the DataContainer'''
return self.array.ndim
@property
def number_of_dimensions(self):
'''Returns the shape of the of the DataContainer'''
return len(self.array.shape)
@property
def dtype(self):
'''Returns the dtype of the data array.'''
return self.array.dtype
@property
def size(self):
'''Returns the number of elements of the DataContainer'''
return self.array.size
__container_priority__ = 1
def __init__ (self, array, deep_copy=True, dimension_labels=None,
**kwargs):
if type(array) == numpy.ndarray:
if deep_copy:
self.array = array.copy()
else:
self.array = array
else:
raise TypeError('Array must be NumpyArray, passed {0}'\
.format(type(array)))
#Don't set for derived classes
if type(self) is DataContainer:
self.dimension_labels = dimension_labels
# finally copy the geometry, and force dtype of the geometry of the data = the dype of the data
if 'geometry' in kwargs.keys():
self.geometry = kwargs['geometry']
try:
self.geometry.dtype = self.dtype
except:
pass
def get_dimension_size(self, dimension_label):
if dimension_label in self.dimension_labels:
i = self.dimension_labels.index(dimension_label)
return self.shape[i]
else:
raise ValueError('Unknown dimension {0}. Should be one of {1}'.format(dimension_label,
self.dimension_labels))
[docs]
def get_dimension_axis(self, dimension_label):
"""
Returns the axis index of the DataContainer array if the specified dimension_label(s) match
any dimension_labels of the DataContainer or their indices
Parameters
----------
dimension_label: string or int or tuple of strings or ints
Specify dimension_label(s) or index of the DataContainer from which to check and return the axis index
Returns
-------
int or tuple of ints
The axis index of the DataContainer matching the specified dimension_label
"""
if isinstance(dimension_label,(tuple,list)):
return tuple(self.get_dimension_axis(x) for x in dimension_label)
if dimension_label in self.dimension_labels:
return self.dimension_labels.index(dimension_label)
elif isinstance(dimension_label, int) and dimension_label >= 0 and dimension_label < self.ndim:
return dimension_label
else:
raise ValueError('Unknown dimension {0}. Should be one of {1}, or an integer in range {2} - {3}'.format(dimension_label,
self.dimension_labels, 0, self.ndim))
[docs]
def as_array(self):
'''Returns the pointer to the array.
'''
return self.array
[docs]
def get_slice(self, **kw):
'''
Returns a new DataContainer containing a single slice in the requested direction. \
Pass keyword arguments <dimension label>=index
'''
# Force is not relevant for a DataContainer:
kw.pop('force', None)
new_array = None
#get ordered list of current dimensions
dimension_labels_list = list(self.dimension_labels)
#remove axes from array and labels
for key, value in kw.items():
if value is not None:
axis = dimension_labels_list.index(key)
dimension_labels_list.remove(key)
if new_array is None:
new_array = self.as_array()
new_array = new_array.take(indices=value, axis=axis)
if new_array.ndim > 1:
return DataContainer(new_array, False, dimension_labels_list, suppress_warning=True)
from .vector_data import VectorData
return VectorData(new_array, dimension_labels=dimension_labels_list)
[docs]
def reorder(self, order):
'''
reorders the data in memory as requested.
:param order: ordered list of labels from self.dimension_labels
:type order: list, sting
'''
try:
if len(order) != len(self.shape):
raise ValueError('The axes list for resorting must have {0} dimensions. Got {1}'.format(len(self.shape), len(order)))
except TypeError as ae:
raise ValueError('The order must be an iterable with __len__ implemented, like a list or a tuple. Got {}'.format(type(order)))
correct = True
for el in order:
correct = correct and el in self.dimension_labels
if not correct:
raise ValueError('The axes list for resorting must contain the dimension_labels {0} got {1}'.format(self.dimension_labels, order))
new_order = [0]*len(self.shape)
dimension_labels_new = [0]*len(self.shape)
for i, axis in enumerate(order):
new_order[i] = self.dimension_labels.index(axis)
dimension_labels_new[i] = axis
self.array = numpy.ascontiguousarray(numpy.transpose(self.array, new_order))
if self.geometry is None:
self.dimension_labels = dimension_labels_new
else:
self.geometry.set_labels(dimension_labels_new)
[docs]
def fill(self, array, **dimension):
'''fills the internal data array with the DataContainer, numpy array or number provided
:param array: number, numpy array or DataContainer to copy into the DataContainer
:type array: DataContainer or subclasses, numpy array or number
:param dimension: dictionary, optional
if the passed numpy array points to the same array that is contained in the DataContainer,
it just returns
In case a DataContainer or subclass is passed, there will be a check of the geometry,
if present, and the array will be resorted if the data is not in the appropriate order.
User may pass a named parameter to specify in which axis the fill should happen:
dc.fill(some_data, vertical=1, horizontal_x=32)
will copy the data in some_data into the data container.
'''
if id(array) == id(self.array):
return
if dimension == {}:
if isinstance(array, numpy.ndarray):
numpy.copyto(self.array, array)
elif isinstance(array, Number):
self.array.fill(array)
elif issubclass(array.__class__ , DataContainer):
try:
if self.dimension_labels != array.dimension_labels:
raise ValueError('Input array is not in the same order as destination array. Use "array.reorder()"')
except AttributeError:
pass
if self.array.shape == array.shape:
numpy.copyto(self.array, array.array)
else:
raise ValueError('Cannot fill with the provided array.' + \
'Expecting shape {0} got {1}'.format(
self.shape,array.shape))
else:
raise TypeError('Can fill only with number, numpy array or DataContainer and subclasses. Got {}'.format(type(array)))
else:
axis = [':']* self.number_of_dimensions
dimension_labels = tuple(self.dimension_labels)
for k,v in dimension.items():
i = dimension_labels.index(k)
axis[i] = v
command = 'self.array['
i = 0
for el in axis:
if i > 0:
command += ','
command += str(el)
i+=1
if isinstance(array, numpy.ndarray):
command = command + "] = array[:]"
elif issubclass(array.__class__, DataContainer):
command = command + "] = array.as_array()[:]"
elif isinstance (array, Number):
command = command + "] = array"
else:
raise TypeError('Can fill only with number, numpy array or DataContainer and subclasses. Got {}'.format(type(array)))
exec(command)
def check_dimensions(self, other):
return self.shape == other.shape
## algebra
def __add__(self, other):
return self.add(other)
def __mul__(self, other):
return self.multiply(other)
def __sub__(self, other):
return self.subtract(other)
def __div__(self, other):
return self.divide(other)
def __truediv__(self, other):
return self.divide(other)
def __pow__(self, other):
return self.power(other)
# reverse operand
def __radd__(self, other):
return self + other
# __radd__
def __rsub__(self, other):
return (-1 * self) + other
# __rsub__
def __rmul__(self, other):
return self * other
# __rmul__
def __rdiv__(self, other):
tmp = self.power(-1)
tmp *= other
return tmp
# __rdiv__
def __rtruediv__(self, other):
return self.__rdiv__(other)
def __rpow__(self, other):
if isinstance(other, Number) :
fother = numpy.ones(numpy.shape(self.array)) * other
return type(self)(fother ** self.array ,
dimension_labels=self.dimension_labels,
geometry=self.geometry)
# __rpow__
# in-place arithmetic operators:
# (+=, -=, *=, /= , //=,
# must return self
def __iadd__(self, other):
kw = {'out':self}
return self.add(other, **kw)
def __imul__(self, other):
kw = {'out':self}
return self.multiply(other, **kw)
def __isub__(self, other):
kw = {'out':self}
return self.subtract(other, **kw)
def __idiv__(self, other):
kw = {'out':self}
return self.divide(other, **kw)
def __itruediv__(self, other):
kw = {'out':self}
return self.divide(other, **kw)
def __neg__(self):
'''negation operator'''
return -1 * self
def __str__ (self, representation=False):
repres = ""
repres += "Number of dimensions: {0}\n".format(self.number_of_dimensions)
repres += "Shape: {0}\n".format(self.shape)
repres += "Axis labels: {0}\n".format(self.dimension_labels)
if representation:
repres += "Representation: \n{0}\n".format(self.array)
return repres
[docs]
def get_data_axes_order(self,new_order=None):
'''returns the axes label of self as a list
If new_order is None returns the labels of the axes as a sorted-by-key list.
If new_order is a list of length number_of_dimensions, returns a list
with the indices of the axes in new_order with respect to those in
self.dimension_labels: i.e.
>>> self.dimension_labels = {0:'horizontal',1:'vertical'}
>>> new_order = ['vertical','horizontal']
returns [1,0]
'''
if new_order is None:
return self.dimension_labels
else:
if len(new_order) == self.number_of_dimensions:
axes_order = [0]*len(self.shape)
for i, axis in enumerate(new_order):
axes_order[i] = self.dimension_labels.index(axis)
return axes_order
else:
raise ValueError(f"Expecting {len(self.shape)} axes, got {len(new_order)}")
[docs]
def clone(self):
'''returns a copy of DataContainer'''
return copy.deepcopy(self)
[docs]
def copy(self):
'''alias of clone'''
return self.clone()
## binary operations
def pixel_wise_binary(self, pwop, x2, *args, **kwargs):
out = kwargs.get('out', None)
if out is None:
if isinstance(x2, Number):
out = pwop(self.as_array() , x2 , *args, **kwargs )
elif issubclass(x2.__class__ , DataContainer):
out = pwop(self.as_array() , x2.as_array() , *args, **kwargs )
elif isinstance(x2, numpy.ndarray):
out = pwop(self.as_array() , x2 , *args, **kwargs )
else:
raise TypeError('Expected x2 type as number or DataContainer, got {}'.format(type(x2)))
geom = self.geometry
if geom is not None:
geom = self.geometry.copy()
return type(self)(out,
deep_copy=False,
dimension_labels=self.dimension_labels,
geometry= None if self.geometry is None else self.geometry.copy(),
suppress_warning=True)
elif issubclass(type(out), DataContainer) and issubclass(type(x2), DataContainer):
if self.check_dimensions(out) and self.check_dimensions(x2):
kwargs['out'] = out.as_array()
pwop(self.as_array(), x2.as_array(), *args, **kwargs )
#return type(self)(out.as_array(),
# deep_copy=False,
# dimension_labels=self.dimension_labels,
# geometry=self.geometry)
return out
raise ValueError(f"Wrong size for data memory: out {out.shape} x2 {x2.shape} expected {self.shape}")
elif issubclass(type(out), DataContainer) and \
isinstance(x2, (Number, numpy.ndarray)):
if self.check_dimensions(out):
if isinstance(x2, numpy.ndarray) and\
not (x2.shape == self.shape and x2.dtype == self.dtype):
raise ValueError(f"Wrong size for data memory: out {out.shape} x2 {x2.shape} expected {self.shape}")
kwargs['out']=out.as_array()
pwop(self.as_array(), x2, *args, **kwargs )
return out
raise ValueError(f"Wrong size for data memory: {out.shape} {self.shape}")
elif issubclass(type(out), numpy.ndarray):
if self.array.shape == out.shape and self.array.dtype == out.dtype:
kwargs['out'] = out
pwop(self.as_array(), x2, *args, **kwargs)
#return type(self)(out,
# deep_copy=False,
# dimension_labels=self.dimension_labels,
# geometry=self.geometry)
else:
raise ValueError(f"incompatible class: {pwop.__name__} {type(out)}")
def add(self, other, *args, **kwargs):
if hasattr(other, '__container_priority__') and \
self.__class__.__container_priority__ < other.__class__.__container_priority__:
return other.add(self, *args, **kwargs)
return self.pixel_wise_binary(numpy.add, other, *args, **kwargs)
def subtract(self, other, *args, **kwargs):
if hasattr(other, '__container_priority__') and \
self.__class__.__container_priority__ < other.__class__.__container_priority__:
return other.subtract(self, *args, **kwargs)
return self.pixel_wise_binary(numpy.subtract, other, *args, **kwargs)
def multiply(self, other, *args, **kwargs):
if hasattr(other, '__container_priority__') and \
self.__class__.__container_priority__ < other.__class__.__container_priority__:
return other.multiply(self, *args, **kwargs)
return self.pixel_wise_binary(numpy.multiply, other, *args, **kwargs)
def divide(self, other, *args, **kwargs):
if hasattr(other, '__container_priority__') and \
self.__class__.__container_priority__ < other.__class__.__container_priority__:
return other.divide(self, *args, **kwargs)
return self.pixel_wise_binary(numpy.divide, other, *args, **kwargs)
def power(self, other, *args, **kwargs):
return self.pixel_wise_binary(numpy.power, other, *args, **kwargs)
def maximum(self, x2, *args, **kwargs):
return self.pixel_wise_binary(numpy.maximum, x2, *args, **kwargs)
def minimum(self,x2, out=None, *args, **kwargs):
return self.pixel_wise_binary(numpy.minimum, x2=x2, out=out, *args, **kwargs)
[docs]
def sapyb(self, a, y, b, out=None, num_threads=NUM_THREADS):
'''performs a*self + b * y. Can be done in-place
Parameters
----------
a : multiplier for self, can be a number or a numpy array or a DataContainer
y : DataContainer
b : multiplier for y, can be a number or a numpy array or a DataContainer
out : return DataContainer, if None a new DataContainer is returned, default None.
out can be self or y.
num_threads : number of threads to use during the calculation, using the CIL C library
It will try to use the CIL C library and default to numpy operations, in case the C library does not handle the types.
Example
-------
>>> a = 2
>>> b = 3
>>> ig = ImageGeometry(10,11)
>>> x = ig.allocate(1)
>>> y = ig.allocate(2)
>>> out = x.sapyb(a,y,b)
'''
if out is None:
out = self * 0.
if out.dtype in [ numpy.float32, numpy.float64 ]:
# handle with C-lib _axpby
try:
self._axpby(a, b, y, out, out.dtype, num_threads)
return out
except RuntimeError as rte:
warnings.warn("sapyb defaulting to Python due to: {}".format(rte))
except TypeError as te:
warnings.warn("sapyb defaulting to Python due to: {}".format(te))
finally:
pass
# cannot be handled by _axpby
ax = self * a
y.multiply(b, out=out)
out.add(ax, out=out)
return out
def _axpby(self, a, b, y, out, dtype=numpy.float32, num_threads=NUM_THREADS):
'''performs axpby with cilacc C library, can be done in-place.
Does the operation .. math:: a*x+b*y and stores the result in out, where x is self
:param a: scalar
:type a: float
:param b: scalar
:type b: float
:param y: DataContainer
:param out: DataContainer instance to store the result
:param dtype: data type of the DataContainers
:type dtype: numpy type, optional, default numpy.float32
:param num_threads: number of threads to run on
:type num_threads: int, optional, default 1/2 CPU of the system
'''
c_float_p = ctypes.POINTER(ctypes.c_float)
c_double_p = ctypes.POINTER(ctypes.c_double)
#convert a and b to numpy arrays and get the reference to the data (length = 1 or ndx.size)
try:
nda = a.as_array()
except:
nda = numpy.asarray(a)
try:
ndb = b.as_array()
except:
ndb = numpy.asarray(b)
a_vec = 0
if nda.size > 1:
a_vec = 1
b_vec = 0
if ndb.size > 1:
b_vec = 1
# get the reference to the data
ndx = self.as_array()
ndy = y.as_array()
ndout = out.as_array()
if ndout.dtype != dtype:
raise Warning("out array of type {0} does not match requested dtype {1}. Using {0}".format(ndout.dtype, dtype))
dtype = ndout.dtype
if ndx.dtype != dtype:
ndx = ndx.astype(dtype, casting='safe')
if ndy.dtype != dtype:
ndy = ndy.astype(dtype, casting='safe')
if nda.dtype != dtype:
nda = nda.astype(dtype, casting='same_kind')
if ndb.dtype != dtype:
ndb = ndb.astype(dtype, casting='same_kind')
if dtype == numpy.float32:
x_p = ndx.ctypes.data_as(c_float_p)
y_p = ndy.ctypes.data_as(c_float_p)
out_p = ndout.ctypes.data_as(c_float_p)
a_p = nda.ctypes.data_as(c_float_p)
b_p = ndb.ctypes.data_as(c_float_p)
f = cilacc.saxpby
elif dtype == numpy.float64:
x_p = ndx.ctypes.data_as(c_double_p)
y_p = ndy.ctypes.data_as(c_double_p)
out_p = ndout.ctypes.data_as(c_double_p)
a_p = nda.ctypes.data_as(c_double_p)
b_p = ndb.ctypes.data_as(c_double_p)
f = cilacc.daxpby
else:
raise TypeError('Unsupported type {}. Expecting numpy.float32 or numpy.float64'.format(dtype))
#out = numpy.empty_like(a)
# int psaxpby(float * x, float * y, float * out, float a, float b, long size)
cilacc.saxpby.argtypes = [ctypes.POINTER(ctypes.c_float), # pointer to the first array
ctypes.POINTER(ctypes.c_float), # pointer to the second array
ctypes.POINTER(ctypes.c_float), # pointer to the third array
ctypes.POINTER(ctypes.c_float), # pointer to A
ctypes.c_int, # type of type of A selector (int)
ctypes.POINTER(ctypes.c_float), # pointer to B
ctypes.c_int, # type of type of B selector (int)
ctypes.c_longlong, # type of size of first array
ctypes.c_int] # number of threads
cilacc.daxpby.argtypes = [ctypes.POINTER(ctypes.c_double), # pointer to the first array
ctypes.POINTER(ctypes.c_double), # pointer to the second array
ctypes.POINTER(ctypes.c_double), # pointer to the third array
ctypes.POINTER(ctypes.c_double), # type of A (c_double)
ctypes.c_int, # type of type of A selector (int)
ctypes.POINTER(ctypes.c_double), # type of B (c_double)
ctypes.c_int, # type of type of B selector (int)
ctypes.c_longlong, # type of size of first array
ctypes.c_int] # number of threads
if f(x_p, y_p, out_p, a_p, a_vec, b_p, b_vec, ndx.size, num_threads) != 0:
raise RuntimeError('axpby execution failed')
## unary operations
def pixel_wise_unary(self, pwop, *args, **kwargs):
out = kwargs.get('out', None)
if out is None:
out = pwop(self.as_array() , *args, **kwargs )
return type(self)(out,
deep_copy=False,
dimension_labels=self.dimension_labels,
geometry=self.geometry,
suppress_warning=True)
elif issubclass(type(out), DataContainer):
if self.check_dimensions(out):
kwargs['out'] = out.as_array()
pwop(self.as_array(), *args, **kwargs )
else:
raise ValueError(f"Wrong size for data memory: {out.shape} {self.shape}")
elif issubclass(type(out), numpy.ndarray):
if self.array.shape == out.shape and self.array.dtype == out.dtype:
kwargs['out'] = out
pwop(self.as_array(), *args, **kwargs)
else:
raise ValueError("incompatible class: {pwop.__name__} {type(out)}")
def abs(self, *args, **kwargs):
return self.pixel_wise_unary(numpy.abs, *args, **kwargs)
def sign(self, *args, **kwargs):
return self.pixel_wise_unary(numpy.sign, *args, **kwargs)
def sqrt(self, *args, **kwargs):
return self.pixel_wise_unary(numpy.sqrt, *args, **kwargs)
def conjugate(self, *args, **kwargs):
return self.pixel_wise_unary(numpy.conjugate, *args, **kwargs)
[docs]
def exp(self, *args, **kwargs):
'''Applies exp pixel-wise to the DataContainer'''
return self.pixel_wise_unary(numpy.exp, *args, **kwargs)
[docs]
def log(self, *args, **kwargs):
'''Applies log pixel-wise to the DataContainer'''
return self.pixel_wise_unary(numpy.log, *args, **kwargs)
## reductions
[docs]
def squared_norm(self, **kwargs):
'''return the squared euclidean norm of the DataContainer viewed as a vector'''
#shape = self.shape
#size = reduce(lambda x,y:x*y, shape, 1)
#y = numpy.reshape(self.as_array(), (size, ))
return self.dot(self)
#return self.dot(self)
[docs]
def norm(self, **kwargs):
'''return the euclidean norm of the DataContainer viewed as a vector'''
return numpy.sqrt(self.squared_norm(**kwargs))
[docs]
def dot(self, other, *args, **kwargs):
'''returns the inner product of 2 DataContainers viewed as vectors. Suitable for real and complex data.
For complex data, the dot method returns a.dot(b.conjugate())
'''
method = kwargs.get('method', 'numpy')
if method not in ['numpy','reduce']:
raise ValueError('dot: specified method not valid. Expecting numpy or reduce got {} '.format(
method))
if self.shape == other.shape:
if method == 'numpy':
return numpy.dot(self.as_array().ravel(), other.as_array().ravel().conjugate())
elif method == 'reduce':
# see https://github.com/vais-ral/CCPi-Framework/pull/273
# notice that Python seems to be smart enough to use
# the appropriate type to hold the result of the reduction
sf = reduce(lambda x,y: x + y[0]*y[1],
zip(self.as_array().ravel(),
other.as_array().ravel().conjugate()),
0)
return sf
else:
raise ValueError('Shapes are not aligned: {} != {}'.format(self.shape, other.shape))
def _directional_reduction_unary(self, reduction_function, axis=None, out=None, *args, **kwargs):
"""
Returns the result of a unary function, considering the direction from an axis argument to the function
Parameters
----------
reduction_function : function
The unary function to be evaluated
axis : string or tuple of strings or int or tuple of ints, optional
Specify the axis or axes to calculate 'reduction_function' along. Can be specified as
string(s) of dimension_labels or int(s) of indices
Default None calculates the function over the whole array
out: ndarray or DataContainer, optional
Provide an object in which to place the result. The object must have the correct dimensions and
(for DataContainers) the correct dimension_labels, but the type will be cast if necessary. See
`Output type determination <https://numpy.org/doc/stable/user/basics.ufuncs.html#ufuncs-output-type>`_ for more details.
Default is None
Returns
-------
scalar or ndarray
The result of the unary function
"""
if axis is not None:
axis = self.get_dimension_axis(axis)
if out is None:
result = reduction_function(self.as_array(), axis=axis, *args, **kwargs)
if isinstance(result, numpy.ndarray):
new_dimensions = numpy.array(self.dimension_labels)
new_dimensions = numpy.delete(new_dimensions, axis)
return DataContainer(result, dimension_labels=new_dimensions)
else:
return result
else:
if hasattr(out,'array'):
out_arr = out.array
else:
out_arr = out
reduction_function(self.as_array(), out=out_arr, axis=axis, *args, **kwargs)
[docs]
def sum(self, axis=None, out=None, *args, **kwargs):
"""
Returns the sum of values in the DataContainer
Parameters
----------
axis : string or tuple of strings or int or tuple of ints, optional
Specify the axis or axes to calculate 'sum' along. Can be specified as
string(s) of dimension_labels or int(s) of indices
Default None calculates the function over the whole array
out : ndarray or DataContainer, optional
Provide an object in which to place the result. The object must have the correct dimensions and
(for DataContainers) the correct dimension_labels, but the type will be cast if necessary. See
`Output type determination <https://numpy.org/doc/stable/user/basics.ufuncs.html#ufuncs-output-type>`_ for more details.
Default is None
Returns
-------
scalar or DataContainer
The sum as a scalar or inside a DataContainer with reduced dimension_labels
Default is to accumulate and return data as float64 or complex128
"""
if kwargs.get('dtype') is not None:
warnings.warn("dtype is ignored (auto-using float64 or complex128)", DeprecationWarning, stacklevel=2)
if numpy.isrealobj(self.array):
kwargs['dtype'] = numpy.float64
else:
kwargs['dtype'] = numpy.complex128
return self._directional_reduction_unary(numpy.sum, axis=axis, out=out, *args, **kwargs)
[docs]
def min(self, axis=None, out=None, *args, **kwargs):
"""
Returns the minimum pixel value in the DataContainer
Parameters
----------
axis : string or tuple of strings or int or tuple of ints, optional
Specify the axis or axes to calculate 'min' along. Can be specified as
string(s) of dimension_labels or int(s) of indices
Default None calculates the function over the whole array
out : ndarray or DataContainer, optional
Provide an object in which to place the result. The object must have the correct dimensions and
(for DataContainers) the correct dimension_labels, but the type will be cast if necessary. See
`Output type determination <https://numpy.org/doc/stable/user/basics.ufuncs.html#ufuncs-output-type>`_ for more details.
Default is None
Returns
-------
scalar or DataContainer
The min as a scalar or inside a DataContainer with reduced dimension_labels
"""
return self._directional_reduction_unary(numpy.min, axis=axis, out=out, *args, **kwargs)
[docs]
def max(self, axis=None, out=None, *args, **kwargs):
"""
Returns the maximum pixel value in the DataContainer
Parameters
----------
axis : string or tuple of strings or int or tuple of ints, optional
Specify the axis or axes to calculate 'max' along. Can be specified as
string(s) of dimension_labels or int(s) of indices
Default None calculates the function over the whole array
out : ndarray or DataContainer, optional
Provide an object in which to place the result. The object must have the correct dimensions and
(for DataContainers) the correct dimension_labels, but the type will be cast if necessary. See
`Output type determination <https://numpy.org/doc/stable/user/basics.ufuncs.html#ufuncs-output-type>`_ for more details.
Default is None
Returns
-------
scalar or DataContainer
The max as a scalar or inside a DataContainer with reduced dimension_labels
"""
return self._directional_reduction_unary(numpy.max, axis=axis, out=out, *args, **kwargs)
[docs]
def mean(self, axis=None, out=None, *args, **kwargs):
"""
Returns the mean pixel value of the DataContainer
Parameters
----------
axis : string or tuple of strings or int or tuple of ints, optional
Specify the axis or axes to calculate 'mean' along. Can be specified as
string(s) of dimension_labels or int(s) of indices
Default None calculates the function over the whole array
out : ndarray or DataContainer, optional
Provide an object in which to place the result. The object must have the correct dimensions and
(for DataContainers) the correct dimension_labels, but the type will be cast if necessary. See
`Output type determination <https://numpy.org/doc/stable/user/basics.ufuncs.html#ufuncs-output-type>`_ for more details.
Default is None
Returns
-------
scalar or DataContainer
The mean as a scalar or inside a DataContainer with reduced dimension_labels
Default is to accumulate and return data as float64 or complex128
"""
if kwargs.get('dtype') is not None:
warnings.warn("dtype is ignored (auto-using float64 or complex128)", DeprecationWarning, stacklevel=2)
if numpy.isrealobj(self.array):
kwargs['dtype'] = numpy.float64
else:
kwargs['dtype'] = numpy.complex128
return self._directional_reduction_unary(numpy.mean, axis=axis, out=out, *args, **kwargs)
# Logic operators between DataContainers and floats
def __le__(self, other):
'''Returns boolean array of DataContainer less or equal than DataContainer/float'''
if isinstance(other, DataContainer):
return self.as_array()<=other.as_array()
return self.as_array()<=other
def __lt__(self, other):
'''Returns boolean array of DataContainer less than DataContainer/float'''
if isinstance(other, DataContainer):
return self.as_array()<other.as_array()
return self.as_array()<other
def __ge__(self, other):
'''Returns boolean array of DataContainer greater or equal than DataContainer/float'''
if isinstance(other, DataContainer):
return self.as_array()>=other.as_array()
return self.as_array()>=other
def __gt__(self, other):
'''Returns boolean array of DataContainer greater than DataContainer/float'''
if isinstance(other, DataContainer):
return self.as_array()>other.as_array()
return self.as_array()>other
def __eq__(self, other):
'''Returns boolean array of DataContainer equal to DataContainer/float'''
if isinstance(other, DataContainer):
return self.as_array()==other.as_array()
return self.as_array()==other
def __ne__(self, other):
'''Returns boolean array of DataContainer negative to DataContainer/float'''
if isinstance(other, DataContainer):
return self.as_array()!=other.as_array()
return self.as_array()!=other