Source code for xopto.mcbase.mcutil.axis

# -*- coding: utf-8 -*-
################################ Begin license #################################
# Copyright (C) Laboratory of Imaging technologies,
#               Faculty of Electrical Engineering,
#               University of Ljubljana.
#
# This file is part of PyXOpto.
#
# PyXOpto 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 3 of the License, or
# (at your option) any later version.
#
# PyXOpto 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 PyXOpto. If not, see <https://www.gnu.org/licenses/>.
################################# End license ##################################

from typing import Tuple

import numpy as np


[docs]class Axis: ''' An accumulator/detector axis with linearly or logarithmically spaced points/bins. ''' def __init__(self, start: float=0.0, stop: float=1.0, n: int=1, logscale: bool=False): ''' Creates a linearly or logarithmically spaced accumulator/detector axis. The parameters and data points of the axis can be accessed through several properties. Parameters ---------- start: float Start coordinate (left edge) of the accumulator. stop: float Stop coordinate (right edge) of the accumulator. n: int Number of bins in the accumulator. logscale: bool Scale of the accumulator. Linear if False (default) or logarithmic if True. Note that values of the start and stop parameters are threated as unscaled even if logscale is True. Note ---- Note that only the r-axis of RT accumulators supports log-scale. Detailed description of the class properties: - start: float Left edge of the first bin. - scaled_start: float Logarithm of the start point if bins are distributed logarithmically, else equals start. - stop: float Right edge of the final bin. - scaled_stop: float Logarithm of the stop point if bins are distributed logarithmically, else equals stop. - span: tuple of two float Full range of the accumulator axis as a tuple (start, stop). - scaled_span: tuple of two float Full scaled range of the accumulator axis as a tuple (scaled_start, scaled_stop). - n: int Number of bins along the axis. - logscale: bool True, if bins are distributed logarithmically. - edges: np.ndarray vector Numpy vector of bin edges (number of points is n + 1). - centers: np.ndarray vector Central positions of the bins. ''' if isinstance(start, Axis): axis = start start = axis.start stop = axis.stop n = axis.n logscale = axis.logscale else: start = float(start) stop = float(stop) n = int(n) logscale = bool(logscale) self._logscale = logscale self._n = n if self._logscale: start = max(start, float(np.finfo(np.float64).eps)) self._edges = np.logspace(np.log(start), np.log(stop), n + 1, base=np.e) self._step = (np.log(stop) - np.log(start))/self._n else: self._edges = np.linspace(start, stop, n + 1) self._step = (stop - start)/self._n self._span = np.array((start, stop), dtype=np.float64) if self._logscale: self._scaled_span = np.log(self._span) else: self._scaled_span = self._span self._centers = 0.5*(self._edges[:-1] + self._edges[1:])
[docs] def todict(self): ''' Export object toa dict. ''' return {'start':self._span[0], 'stop':self._span[1], 'n':self._n, 'logscale':self._logscale, 'type':'Axis'}
[docs] @classmethod def fromdict(cls, data: dict) -> 'Axis': ''' Create a new object from dict. The dict keys must match the parameter names defined by the constructor. ''' data_ = dict(data) type_name = data_.pop('type') if type_name != cls.__name__: raise TypeError('Expected "{}" type bot got "{}"!'.format( cls.__name__, type_name)) return cls(**data_)
def _get_logscale(self) -> bool: return self._logscale logscale = property(_get_logscale, None, None, 'Accumulator scale.') def _get_step(self) -> float: return self._step step = property(_get_step, None, None, 'Accumulator step.') def _get_n(self) -> int: return self._n n = property(_get_n, None, None, 'Number of accumulators along the axis.') def _get_span(self): return self._span span = property(_get_span, None, None, 'Accumulator span as [min, max].') def _get_start(self) -> float: return self._span[0] start = property(_get_start, None, None, 'Accumulator start coordinate.') def _get_stop(self) -> float: return self._span[1] stop = property(_get_stop, None, None, 'Accumulator stop coordinate.') def _get_scaled_span(self) -> Tuple[float, float]: return self._scaled_span scaled_span = property(_get_scaled_span, None, None, 'Scaled accumulator span as [min, max].') def _get_scaled_start(self) -> float: return self._scaled_span[0] scaled_start = property(_get_scaled_start, None, None, 'Scaled accumulator start coordinate.') def _get_scaled_stop(self) -> float: return self._scaled_span[1] scaled_stop = property(_get_scaled_stop, None, None, 'Scaled accumulator stop coordinate.') def _get_edges(self) -> np.ndarray: return self._edges edges = property(_get_edges, None, None, 'Edges of the accumulators.') def _get_centers(self) -> np.ndarray: return self._centers centers = property(_get_centers, None, None, 'Center points of the accumulators.') def __str__(self): return 'Axis(start={}, stop={}, n={}, logscale={})'.format( self._span[0], self._span[1], self._n, self._logscale) def __repr__(self): return self.__str__() + \ ' # object at 0x{:>08X}.'.format(id(self))
[docs]class RadialAxis(Axis): ''' A radial accumulator/detector axis with linearly or logarithmically spaced points/bins and corrected accumulator centers. ''' def __init__(self, start: float=0.0, stop: float=1.0, n: int=1, logscale: bool=False): ''' Radial axis with corrected bin centers. The central point of each bin is computed as: :math:`2/3(r_{i}^2 + r_{i}*r_{i + 1} + r_{i + 1}^2)/(r_{i} + r_{i + 1})` For a detailed description of parameters see the Axis documentation. Note ---- Correction is based on the assumption that for linear functions of reflectance :math:`f(r) = a + b r` the integral of :math:`f(r) 2 \\pi r dr` from :math:`r_i` to :math:`r_{i + 1}` is equal to the integral of :math:`f(r_center) 2 \pi r dr` from :math:`r_i` to :math:`r_{i + 1}`, where :math:`r_i < r_center < r_{i + 1}`. Computing the two integrals and equating the terms at coefficients :math:`a` and :math:`b` yields the "equivalent" center point of the bin :math:`[r_i, r_{i + 1})`. Note that the two integrals represent surface integral of reflectace. Warning ------- The spacing of points in the RadialAxis is uneven even if logscale is set to False, since correction of the bin centers depends on the absolute position of the bin. If using data from RadialAxis with the simpson integration method make sure to use a version that does not require a fixed step (specify x instead of dx in a call to scipy.integrate.simps). If using the :py:meth:`xopto.util.hankel.discrete_simpson` method make sure to set the value of the uneven parameter to True. ''' Axis.__init__(self, start, stop, n, logscale) edges = self.edges self._true_centers = (2.0/3.0)*(edges[:-1]**2 + edges[:-1]*edges[1:] + edges[1:]**2)/(edges[:-1] + edges[1:]) def _get_centers(self) -> np.ndarray: return self._true_centers centers = property(_get_centers, None, None, 'Corrected centers of the accumulator bins.')
[docs] def todict(self) -> dict: ''' Export object to a dict. ''' data = Axis.todict(self) data['type'] = 'RadialAxis'
[docs] @classmethod def fromdict(cls, data:dict) -> 'RadialAxis': ''' Create a new object from dict. The dict keys must match the parameter names defined by the constructor. ''' return cls(**data)
[docs]class SymmetricAxis: ''' An accumulator/detector axis with linearly or logarithmically spaced points/bins placed symmetrically around a center. ''' def __init__(self, center: float=0.0, range: float=1.0, n_half: int=1000, logscale: bool=False): ''' Creates a linearly or logarithmically spaced symmetric accumulator axis. The parameters and data points of the axis can be accessed through several properties. Parameters ---------- center: float Position of the axis/accumulator origin. range: float Extent of the axis as [center - range, center + range]. n_half: int Number of bins in the interval [center, center + range]. The total number of bins equals 2*n_half. logscale: bool Scale of the accumulator. Linear if False (default) or logarithmic if True. Note that the values of the center and range parameters are threated as unscaled even if logscale is set to True. Note ---- Note that only some RT classes support the SymmetricAxis class. Detailed description of the class properties: - center: float Coordinate of the center/origin of the symmetric accumulator. - range: float Range of the symmetric accumulator such that the full span of the symmetric accumulator is [center - range, center + range]. - start: float Left edge of the first bin of the positive half interval. Note that for logarithmically spaced accumulators the start does not equal center but is instead slightly (eps) larger than the center. - stop: float Right edge of the final bin of the positive interval. - offset: float Offset of the left edge of the first bin along the positive interval. This value is 0 for linearly spaced accumulators but is a small nonzero value (eps) for logarithmically spaced values. - scaled_offset: float Logarithm of the offset of the left edge of the first bin along the positive interval. This value is 0 for linearly spaced accumulators but is a logarithm of a small nonzero value (ln(eps)) for logarithmically spaced accumulators. - span: tuple of two float Full range of the accumulators (start, stop). - n_half:int Number of bins along the positive axis [center, center + range]. - n: int Number of bins (2*n_half) along the full axis range [center - range, center + range]. - logscale: bool True, if bins are distributed logarithmically. - edges: np.ndarray vector Numpy vector of bin edges (number of points is n + 1) in the full axis interval [center - range, center + range]. - centers: np.ndarray vector Central positions of the bins in the full axis interval [center - range, center + range]. ''' if isinstance(center, SymmetricAxis): axis = center range = axis.range center = axis.center n_half = axis.n_half logscale = axis.logscale else: range = float(range) center = float(center) n_half = int(n_half) logscale = bool(logscale) self._range = range self._center = center self._logscale = logscale self._n_half = n_half self._n = 2*n_half if self._logscale: eps = np.finfo(np.float64).eps tmp = center + np.logspace( np.log(eps), np.log(range), n_half + 1, base=np.e) edges_right = tmp[1:] edges_left = -edges_right[::-1] self._offset = eps self._edges = np.hstack((edges_left, [center], edges_right)) self._step = (np.log(range) - np.log(eps))/n_half else: self._offset = 0.0 self._edges = np.linspace( center - range, center + range, 2*n_half + 1) self._step = (self._edges[-1] - self._edges[0])/self._n self._span = np.array( (self._center + self._offset, self._center + self._range), dtype=np.float64) if self._logscale: self._scaled_offset = np.log(self._offset) else: self._scaled_offset = self._offset self._centers = 0.5*(self._edges[:-1] + self._edges[1:])
[docs] def todict(self) -> dict: ''' Export object to a dict. ''' return {'range':self._range, 'center':self._center, 'n_half':self._n_half, 'logscale':self._logscale, 'type':'SymmetricAxis'}
[docs] @classmethod def from_dict(cls, data: dict) -> 'SymmetricAxis': ''' Create a new object from dict. The dict keys must match the parameter names defined by the constructor. ''' return cls(**data)
def _get_logscale(self) -> bool: return self._logscale logscale = property(_get_logscale, None, None, 'True if the accumulator is defined in log scale.') def _get_center(self) -> np.ndarray: return self._center center = property(_get_center, None, None, 'Axis/accumulator origin.') def _get_range(self) -> float: return self._range range = property(_get_range, None, None, 'Axis/accumulator range.') def _get_step(self) -> float: return self._step step = property(_get_step, None, None, 'Accumulator step/bin size.') def _get_n_half(self) -> int: return self._n_half n_half = property(_get_n_half, None, None, 'Total number of accumulator bins along '\ 'the positive half axis.') def _get_n(self) -> int: return self._n n = property(_get_n, None, None, 'Total number of accumulator bins along the axis.') def _get_span(self) -> Tuple[float, float]: return self._span span = property(_get_span, None, None, 'Accumulator positive half axis span as [min, max].') def _get_start(self) -> float: return self._span[0] start = property(_get_start, None, None, 'Accumulator positive half interval start coordinate.') def _get_stop(self) -> float: return self._span[1] stop = property(_get_stop, None, None, 'Accumulator positive half interval stop coordinate.') def _get_scaled_offset(self) -> float: return self._scaled_offset scaled_offset = property(_get_scaled_offset, None, None, 'Scaled offset of the first bin ' \ 'of the positive half interval '\ 'from the center of the accumulator.') def _get_edges(self) -> np.ndarray: return self._edges edges = property(_get_edges, None, None, 'Edges of the accumulator bins.') def _get_centers(self) -> np.ndarray: return self._centers centers = property(_get_centers, None, None, 'Central points of the accumulator bins.') def __str__(self): return 'SymmetricAxis(range={}, center={}, n_half={}, '\ 'logscale={})'.format( self._range, self._center, self._n_half, self._logscale) def __repr__(self): return self.__str__() + \ ' # object at 0x{:>08X}.'.format(id(self))