def __init__(self, domain, range=None, ran_shp=None, **kwargs): """Initialize a new instance. Parameters ---------- domain : uniform `DiscreteLp` Uniformly discretized space, the operator can be applied to its elements. range : uniform `DiscreteLp`, optional Uniformly discretized space in which the result of the application of this operator lies. For the default ``None``, a space with the same attributes as ``domain`` is used, except for its shape, which is set to ``ran_shp``. ran_shp : sequence of ints, optional Shape of the range of this operator. This can be provided instead of ``range`` and is mandatory if ``range`` is ``None``. offset : int or sequence of ints, optional Number of cells to add to/remove from the left of ``domain.partition``. By default, the difference is distributed evenly, with preference for left in case of ambiguity. This option is can only be used together with ``ran_shp``. pad_mode : string, optional Method to be used to fill in missing values in an enlarged array. ``'constant'``: Fill with ``pad_const``. ``'symmetric'``: Reflect at the boundaries, not doubling the outmost values. This requires left and right padding sizes to be strictly smaller than the original array shape. ``'periodic'``: Fill in values from the other side, keeping the order. This requires left and right padding sizes to be at most as large as the original array shape. ``'order0'``: Extend constantly with the outmost values (ensures continuity). ``'order1'``: Extend with constant slope (ensures continuity of the first derivative). This requires at least 2 values along each axis where padding is applied. pad_const : scalar, optional Value to be used in the ``'constant'`` padding mode. discr_kwargs: dict, optional Keyword arguments passed to the `uniform_discr` constructor. Examples -------- The simplest way of initializing a resizing operator is by providing ``ran_shp`` and, optionally, parameters for the padding variant to be used. The range is inferred from ``domain`` and the supplied parameters. If no ``offset`` is given, the difference in size is evenly distributed to both sides: >>> space = odl.uniform_discr([0, 0], [1, 1], (2, 4)) >>> resize_op = odl.ResizingOperator(space, ran_shp=(4, 4)) >>> resize_op.range uniform_discr([-0.5, 0. ], [ 1.5, 1. ], (4, 4)) Testing different padding methods in the first axis (zero padding is the default): >>> x = [[1, 2, 3, 4], ... [5, 6, 7, 8]] >>> resize_op = odl.ResizingOperator(space, ran_shp=(4, 4)) >>> print(resize_op(x)) [[ 0., 0., 0., 0.], [ 1., 2., 3., 4.], [ 5., 6., 7., 8.], [ 0., 0., 0., 0.]] >>> >>> resize_op = odl.ResizingOperator(space, ran_shp=(4, 4), ... offset=(0, 0), ... pad_mode='periodic') >>> print(resize_op(x)) [[ 1., 2., 3., 4.], [ 5., 6., 7., 8.], [ 1., 2., 3., 4.], [ 5., 6., 7., 8.]] >>> >>> resize_op = odl.ResizingOperator(space, ran_shp=(4, 4), ... offset=(0, 0), ... pad_mode='order0') >>> print(resize_op(x)) [[ 1., 2., 3., 4.], [ 5., 6., 7., 8.], [ 5., 6., 7., 8.], [ 5., 6., 7., 8.]] Alternatively, the range of the operator can be provided directly. This requires that the partitions match, i.e. that the cell sizes are the same and there is no shift: >>> # Same space as in the first example, see above >>> large_spc = odl.uniform_discr([-0.5, 0], [1.5, 1], (4, 4)) >>> resize_op = odl.ResizingOperator(space, large_spc, ... pad_mode='periodic') >>> print(resize_op(x)) [[ 5., 6., 7., 8.], [ 1., 2., 3., 4.], [ 5., 6., 7., 8.], [ 1., 2., 3., 4.]] """ # Swap names to be able to use the range iterator without worries import builtins ran, range = range, builtins.range if not isinstance(domain, DiscreteLp): raise TypeError('`domain` must be a `DiscreteLp` instance, ' 'got {!r}'.format(domain)) offset = kwargs.pop('offset', None) discr_kwargs = kwargs.pop('discr_kwargs', {}) if ran is None: if ran_shp is None: raise ValueError('either `ran` or `ran_shp` must be ' 'given') offset = normalized_scalar_param_list(offset, domain.ndim, param_conv=safe_int_conv, keep_none=True) ran = _resize_discr(domain, ran_shp, offset, discr_kwargs) self.__offset = tuple(_offset_from_spaces(domain, ran)) elif ran_shp is None: if offset is not None: raise ValueError('`offset` can only be combined with ' '`ran_shp`') for i in range(domain.ndim): if (ran.is_uniform_byaxis[i] and domain.is_uniform_byaxis[i] and not np.isclose( ran.cell_sides[i], domain.cell_sides[i])): raise ValueError( 'in axis {}: cell sides of domain and range differ ' 'significantly: (difference {})' ''.format(i, ran.cell_sides[i] - domain.cell_sides[i])) self.__offset = _offset_from_spaces(domain, ran) else: raise ValueError('cannot combine `range` with `ran_shape`') pad_mode = kwargs.pop('pad_mode', 'constant') pad_mode, pad_mode_in = str(pad_mode).lower(), pad_mode if pad_mode not in _SUPPORTED_RESIZE_PAD_MODES: raise ValueError("`pad_mode` '{}' not understood" "".format(pad_mode_in)) self.__pad_mode = pad_mode # Store constant in a way that ensures safe casting (one-element array) self.__pad_const = np.array(kwargs.pop('pad_const', 0), dtype=ran.dtype) # padding mode 'constant' with `pad_const != 0` is not linear linear = (self.pad_mode != 'constant' or self.pad_const == 0.0) super(ResizingOperatorBase, self).__init__(domain, ran, linear=linear)
def nonuniform_partition(*coord_vecs, **kwargs): """Return a partition with un-equally sized cells. Parameters ---------- coord_vecs1, ... coord_vecsN : `array-like` Arrays of coordinates of the mid-points of the partition cells. min_pt, max_pt : float or sequence of floats, optional Vectors defining the lower/upper limits of the intervals in an `IntervalProd` (a rectangular box). ``None`` entries mean "compute the value". nodes_on_bdry : bool or sequence, optional If a sequence is provided, it determines per axis whether to place the last grid point on the boundary (``True``) or shift it by half a cell size into the interior (``False``). In each axis, an entry may consist in a single bool or a 2-tuple of bool. In the latter case, the first tuple entry decides for the left, the second for the right boundary. The length of the sequence must be ``array.ndim``. A single boolean is interpreted as a global choice for all boundaries. Cannot be given with both min_pt and max_pt since they determine the same thing. Default: ``False`` See Also -------- uniform_partition : uniformly spaced points uniform_partition_fromintv : partition an existing set uniform_partition_fromgrid : use an existing grid as basis Examples -------- With uniformly spaced points the result is the same as a uniform partition: >>> odl.nonuniform_partition([0, 1, 2, 3]) uniform_partition(-0.5, 3.5, 4) >>> odl.nonuniform_partition([0, 1, 2, 3], [1, 2]) uniform_partition([-0.5, 0.5], [3.5, 2.5], (4, 2)) If the points are not uniformly spaced, a nonuniform partition is created. Note that the containing interval is calculated by assuming that the points are in the middle of the sub-intervals: >>> odl.nonuniform_partition([0, 1, 3]) nonuniform_partition( [0.0, 1.0, 3.0] ) Higher dimensional partitions are created by specifying the gridpoints along each dimension: >>> odl.nonuniform_partition([0, 1, 3], [1, 2]) nonuniform_partition( [0.0, 1.0, 3.0], [1.0, 2.0] ) If the endpoints should be on the boundary, the ``nodes_on_bdry`` parameter can be used: >>> odl.nonuniform_partition([0, 1, 3], nodes_on_bdry=True) nonuniform_partition( [0.0, 1.0, 3.0], nodes_on_bdry=True ) Users can also manually specify the containing intervals dimensions by using the ``min_pt`` and ``max_pt`` arguments: >>> odl.nonuniform_partition([0, 1, 3], min_pt=-2, max_pt=3) nonuniform_partition( [0.0, 1.0, 3.0], min_pt=-2.0, max_pt=3.0 ) """ # Get parameters from kwargs min_pt = kwargs.pop('min_pt', None) max_pt = kwargs.pop('max_pt', None) nodes_on_bdry = kwargs.pop('nodes_on_bdry', False) # np.size(None) == 1 sizes = [len(coord_vecs)] + [np.size(p) for p in (min_pt, max_pt)] ndim = int(np.max(sizes)) min_pt = normalized_scalar_param_list(min_pt, ndim, param_conv=float, keep_none=True) max_pt = normalized_scalar_param_list(max_pt, ndim, param_conv=float, keep_none=True) nodes_on_bdry = normalized_nodes_on_bdry(nodes_on_bdry, ndim) # Calculate the missing parameters in min_pt, max_pt for i, (xmin, xmax, (bdry_l, bdry_r), coords) in enumerate(zip(min_pt, max_pt, nodes_on_bdry, coord_vecs)): # Check input for redundancy if xmin is not None and bdry_l: raise ValueError('in axis {}: got both `min_pt` and ' '`nodes_on_bdry=True`'.format(i)) if xmax is not None and bdry_r: raise ValueError('in axis {}: got both `max_pt` and ' '`nodes_on_bdry=True`'.format(i)) # Compute boundary position if not given by user if xmin is None: if bdry_l: min_pt[i] = coords[0] else: min_pt[i] = coords[0] - (coords[1] - coords[0]) / 2.0 if xmax is None: if bdry_r: max_pt[i] = coords[-1] else: max_pt[i] = coords[-1] + (coords[-1] - coords[-2]) / 2.0 interval = IntervalProd(min_pt, max_pt) grid = RectGrid(*coord_vecs) return RectPartition(interval, grid)
def kaczmarz(ops, x, rhs, niter, omega=1, projection=None, callback=None): """Optimized implementation of Kaczmarz's method. Solves the inverse problem given by the set of equations:: A_n(x) = rhs_n This is also known as the Landweber-Kaczmarz's method, since the method coincides with the Landweber method for a single operator. Parameters ---------- ops : sequence of `Operator`'s Operators in the inverse problem. ``op[i].derivative(x).adjoint`` must be well-defined for ``x`` in the operator domain and for all ``i``. x : ``op.domain`` element Element to which the result is written. Its initial value is used as starting point of the iteration, and its values are updated in each iteration step. rhs : sequence of ``ops[i].range`` elements Right-hand side of the equation defining the inverse problem. niter : int Number of iterations. omega : positive float or sequence of positive floats, optional Relaxation parameter in the iteration. If a single float is given the same step is used for all operators, otherwise separate steps are used. projection : callable, optional Function that can be used to modify the iterates in each iteration, for example enforcing positivity. The function should take one argument and modify it in-place. callback : callable, optional Object executing code per iteration, e.g. plotting each iterate. Notes ----- This method calculates an approximate least-squares solution of the inverse problem of the first kind .. math:: \mathcal{A}_i (x) = y_i \\quad 1 \\leq i \\leq n, for a given :math:`y_n \\in \mathcal{Y}_n`, i.e. an approximate solution :math:`x^*` to .. math:: \min_{x\\in \mathcal{X}} \\sum_{i=1}^n \| \mathcal{A}_i(x) - y_i \|_{\mathcal{Y}_i}^2 for a (Frechet-) differentiable operator :math:`\mathcal{A}: \mathcal{X} \\to \mathcal{Y}` between Hilbert spaces :math:`\mathcal{X}` and :math:`\mathcal{Y}`. The method starts from an initial guess :math:`x_0` and uses the iteration .. math:: x_{k+1} = x_k - \omega_{[k]} \ \partial \mathcal{A}_{[k]}(x_k)^* (\mathcal{A}_{[k]}(x_k) - y_{[k]}), where :math:`\partial \mathcal{A}_{[k]}(x_k)` is the Frechet derivative of :math:`\mathcal{A}_{[k]}` at :math:`x_k`, :math:`\omega_{[k]}` is a relaxation parameter and :math:`[k] := k \\text{ mod } n`. For linear problems, a choice :math:`0 < \omega_i < 2/\\lVert \mathcal{A}_{i}^2\\rVert` guarantees convergence, where :math:`\|\mathcal{A}_{i}\|` stands for the operator norm of :math:`\mathcal{A}_{i}`. This implementation uses a minimum amount of memory copies by applying re-usable temporaries and in-place evaluation. The method is also described in a `Wikipedia article <https://en.wikipedia.org/wiki/Kaczmarz_method>`_. and in Natterer, F. Mathematical Methods in Image Reconstruction, section 5.3.2. See Also -------- landweber """ domain = ops[0].domain if any(domain != opi.domain for opi in ops): raise ValueError('`opi[i].domain` are not all equal') if x not in domain: raise TypeError('`x` {!r} is not in the domain of `ops` {!r}' ''.format(x, domain)) if len(ops) != len(rhs): raise ValueError('`number of `ops` {} does not match number of ' '`rhs` {}'.format(len(ops), len(rhs))) omega = normalized_scalar_param_list(omega, len(ops), param_conv=float) # Reusable elements in the range, one per type of space ranges = [opi.range for opi in ops] unique_ranges = set(ranges) tmp_rans = {ran: ran.element() for ran in unique_ranges} # Single reusable element in the domain tmp_dom = domain.element() # Iteratively find solution for _ in range(niter): for i in range(len(ops)): # Find residual tmp_ran = tmp_rans[ops[i].range] ops[i](x, out=tmp_ran) tmp_ran -= rhs[i] # Update x ops[i].derivative(x).adjoint(tmp_ran, out=tmp_dom) x.lincomb(1, x, -omega[i], tmp_dom) if projection is not None: projection(x) if callback is not None: callback(x)
def nonuniform_partition(*coord_vecs, **kwargs): """Return a partition with un-equally sized cells. Parameters ---------- coord_vecs1, ... coord_vecsN : `array-like` Arrays of coordinates of the mid-points of the partition cells. min_pt, max_pt : float or sequence of floats, optional Vectors defining the lower/upper limits of the intervals in an `IntervalProd` (a rectangular box). ``None`` entries mean "compute the value". nodes_on_bdry : bool or sequence, optional If a sequence is provided, it determines per axis whether to place the last grid point on the boundary (``True``) or shift it by half a cell size into the interior (``False``). In each axis, an entry may consist in a single bool or a 2-tuple of bool. In the latter case, the first tuple entry decides for the left, the second for the right boundary. The length of the sequence must be ``array.ndim``. A single boolean is interpreted as a global choice for all boundaries. Cannot be given with both min_pt and max_pt since they determine the same thing. Default: ``False`` See Also -------- uniform_partition : uniformly spaced points uniform_partition_fromintv : partition an existing set uniform_partition_fromgrid : use an existing grid as basis Examples -------- With uniformly spaced points the result is the same as a uniform partition: >>> odl.nonuniform_partition([0, 1, 2, 3]) uniform_partition(-0.5, 3.5, 4) >>> odl.nonuniform_partition([0, 1, 2, 3], [1, 2]) uniform_partition([-0.5, 0.5], [3.5, 2.5], (4, 2)) If the points are not uniformly spaced, a nonuniform partition is created. Note that the containing interval is calculated by assuming that the points are in the middle of the sub-intervals: >>> odl.nonuniform_partition([0, 1, 3]) nonuniform_partition( [0.0, 1.0, 3.0] ) Higher dimensional partitions are created by specifying the gridpoints along each dimension: >>> odl.nonuniform_partition([0, 1, 3], [1, 2]) nonuniform_partition( [0.0, 1.0, 3.0], [1.0, 2.0] ) If the endpoints should be on the boundary, the ``nodes_on_bdry`` parameter can be used: >>> odl.nonuniform_partition([0, 1, 3], nodes_on_bdry=True) nonuniform_partition( [0.0, 1.0, 3.0], nodes_on_bdry=True ) Users can also manually specify the containing intervals dimensions by using the ``min_pt`` and ``max_pt`` arguments: >>> odl.nonuniform_partition([0, 1, 3], min_pt=-2, max_pt=3) nonuniform_partition( [0.0, 1.0, 3.0], min_pt=-2.0, max_pt=3.0 ) """ # Get parameters from kwargs min_pt = kwargs.pop('min_pt', None) max_pt = kwargs.pop('max_pt', None) nodes_on_bdry = kwargs.pop('nodes_on_bdry', False) # np.size(None) == 1 sizes = [len(coord_vecs)] + [np.size(p) for p in (min_pt, max_pt)] ndim = int(np.max(sizes)) min_pt = normalized_scalar_param_list(min_pt, ndim, param_conv=float, keep_none=True) max_pt = normalized_scalar_param_list(max_pt, ndim, param_conv=float, keep_none=True) nodes_on_bdry = normalized_nodes_on_bdry(nodes_on_bdry, ndim) # Calculate the missing parameters in min_pt, max_pt for i, (xmin, xmax, (bdry_l, bdry_r), coords) in enumerate( zip(min_pt, max_pt, nodes_on_bdry, coord_vecs)): # Check input for redundancy if xmin is not None and bdry_l: raise ValueError('in axis {}: got both `min_pt` and ' '`nodes_on_bdry=True`'.format(i)) if xmax is not None and bdry_r: raise ValueError('in axis {}: got both `max_pt` and ' '`nodes_on_bdry=True`'.format(i)) # Compute boundary position if not given by user if xmin is None: if bdry_l: min_pt[i] = coords[0] else: min_pt[i] = coords[0] - (coords[1] - coords[0]) / 2.0 if xmax is None: if bdry_r: max_pt[i] = coords[-1] else: max_pt[i] = coords[-1] + (coords[-1] - coords[-2]) / 2.0 interval = IntervalProd(min_pt, max_pt) grid = RectGrid(*coord_vecs) return RectPartition(interval, grid)
def uniform_partition(min_pt=None, max_pt=None, shape=None, cell_sides=None, nodes_on_bdry=False): """Return a partition with equally sized cells. Parameters ---------- min_pt, max_pt : float or sequence of float, optional Vectors defining the lower/upper limits of the intervals in an `IntervalProd` (a rectangular box). ``None`` entries mean "compute the value". shape : int or sequence of ints, optional Number of nodes per axis. ``None`` entries mean "compute the value". cell_sides : float or sequence of floats, optional Side length of the partition cells per axis. ``None`` entries mean "compute the value". nodes_on_bdry : bool or sequence, optional If a sequence is provided, it determines per axis whether to place the last grid point on the boundary (``True``) or shift it by half a cell size into the interior (``False``). In each axis, an entry may consist in a single bool or a 2-tuple of bool. In the latter case, the first tuple entry decides for the left, the second for the right boundary. The length of the sequence must be ``array.ndim``. A single boolean is interpreted as a global choice for all boundaries. Notes ----- In each axis, 3 of the 4 possible parameters ``min_pt``, ``max_pt``, ``shape`` and ``cell_sides`` must be given. If all four are provided, they are checked for consistency. See Also -------- uniform_partition_fromintv : partition an existing set uniform_partition_fromgrid : use an existing grid as basis Examples -------- Any combination of three of the four parameters can be used for creation of a partition: >>> part = odl.uniform_partition(min_pt=0, max_pt=2, shape=4) >>> part.cell_boundary_vecs (array([ 0. , 0.5, 1. , 1.5, 2. ]),) >>> part = odl.uniform_partition(min_pt=0, shape=4, cell_sides=0.5) >>> part.cell_boundary_vecs (array([ 0. , 0.5, 1. , 1.5, 2. ]),) >>> part = odl.uniform_partition(max_pt=2, shape=4, cell_sides=0.5) >>> part.cell_boundary_vecs (array([ 0. , 0.5, 1. , 1.5, 2. ]),) >>> part = odl.uniform_partition(min_pt=0, max_pt=2, cell_sides=0.5) >>> part.cell_boundary_vecs (array([ 0. , 0.5, 1. , 1.5, 2. ]),) In higher dimensions, the parameters can be given differently in each axis. Where ``None`` is given, the value will be computed: >>> part = odl.uniform_partition(min_pt=[0, 0], max_pt=[1, 2], ... shape=[4, 2]) >>> part.cell_boundary_vecs (array([ 0. , 0.25, 0.5 , 0.75, 1. ]), array([ 0., 1., 2.])) >>> part = odl.uniform_partition(min_pt=[0, 0], max_pt=[1, 2], ... shape=[None, 2], cell_sides=[0.25, None]) >>> part.cell_boundary_vecs (array([ 0. , 0.25, 0.5 , 0.75, 1. ]), array([ 0., 1., 2.])) >>> part = odl.uniform_partition(min_pt=[0, None], max_pt=[None, 2], ... shape=[4, 2], cell_sides=[0.25, 1]) >>> part.cell_boundary_vecs (array([ 0. , 0.25, 0.5 , 0.75, 1. ]), array([ 0., 1., 2.])) By default, no grid points are placed on the boundary: >>> part = odl.uniform_partition(0, 1, 4) >>> part.nodes_on_bdry False >>> part.cell_boundary_vecs (array([ 0. , 0.25, 0.5 , 0.75, 1. ]),) >>> part.grid.coord_vectors (array([ 0.125, 0.375, 0.625, 0.875]),) This can be changed with the nodes_on_bdry parameter: >>> part = odl.uniform_partition(0, 1, 3, nodes_on_bdry=True) >>> part.nodes_on_bdry True >>> part.cell_boundary_vecs (array([ 0. , 0.25, 0.75, 1. ]),) >>> part.grid.coord_vectors (array([ 0. , 0.5, 1. ]),) We can specify this per axis, too. In this case we choose both in the first axis and only the rightmost in the second: >>> part = odl.uniform_partition([0, 0], [1, 1], (3, 3), ... nodes_on_bdry=(True, (False, True))) ... >>> part.cell_boundary_vecs[0] # first axis, as above array([ 0. , 0.25, 0.75, 1. ]) >>> part.grid.coord_vectors[0] array([ 0. , 0.5, 1. ]) >>> part.cell_boundary_vecs[1] # second, asymmetric axis array([ 0. , 0.4, 0.8, 1. ]) >>> part.grid.coord_vectors[1] array([ 0.2, 0.6, 1. ]) """ # Normalize partition parameters # np.size(None) == 1, so that would screw it for sizes 0 of the rest sizes = [ np.size(p) for p in (min_pt, max_pt, shape, cell_sides) if p is not None ] ndim = int(np.max(sizes)) min_pt = normalized_scalar_param_list(min_pt, ndim, param_conv=float, keep_none=True) max_pt = normalized_scalar_param_list(max_pt, ndim, param_conv=float, keep_none=True) shape = normalized_scalar_param_list(shape, ndim, param_conv=safe_int_conv, keep_none=True) cell_sides = normalized_scalar_param_list(cell_sides, ndim, param_conv=float, keep_none=True) nodes_on_bdry = normalized_nodes_on_bdry(nodes_on_bdry, ndim) # Calculate the missing parameters in min_pt, max_pt, shape for i, (xmin, xmax, n, dx, on_bdry) in enumerate( zip(min_pt, max_pt, shape, cell_sides, nodes_on_bdry)): num_params = sum(p is not None for p in (xmin, xmax, n, dx)) if num_params < 3: raise ValueError('in axis {}: expected at least 3 of the ' 'parameters `min_pt`, `max_pt`, `shape`, ' '`cell_sides`, got {}' ''.format(i, num_params)) # Unpack the tuple if possible, else use bool globally for this axis try: bdry_l, bdry_r = on_bdry except TypeError: bdry_l = bdry_r = on_bdry # For each node on the boundary, we subtract 1/2 from the number of # full cells between min_pt and max_pt. if xmin is None: min_pt[i] = xmax - (n - sum([bdry_l, bdry_r]) / 2.0) * dx elif xmax is None: max_pt[i] = xmin + (n - sum([bdry_l, bdry_r]) / 2.0) * dx elif n is None: # Here we add to n since (e-b)/s gives the reduced number of cells. n_calc = (xmax - xmin) / dx + sum([bdry_l, bdry_r]) / 2.0 n_round = int(round(n_calc)) if abs(n_calc - n_round) > 1e-5: raise ValueError('in axis {}: calculated number of nodes ' '{} = ({} - {}) / {} too far from integer' ''.format(i, n_calc, xmax, xmin, dx)) shape[i] = n_round elif dx is None: pass else: xmax_calc = xmin + (n - sum([bdry_l, bdry_r]) / 2.0) * dx if not np.isclose(xmax, xmax_calc): raise ValueError('in axis {}: calculated endpoint ' '{} = {} + {} * {} too far from given ' 'endpoint {}.' ''.format(i, xmax_calc, xmin, n, dx, xmax)) return uniform_partition_fromintv(IntervalProd(min_pt, max_pt), shape, nodes_on_bdry)
def uniform_grid_fromintv(intv_prod, shape, nodes_on_bdry=True): """Return a grid from sampling an interval product uniformly. The resulting grid will by default include ``intv_prod.min_pt`` and ``intv_prod.max_pt`` as grid points. If you want a subdivision into equally sized cells with grid points in the middle, use `uniform_partition` instead. Parameters ---------- intv_prod : `IntervalProd` Set to be sampled. shape : int or sequence of ints Number of nodes per axis. Entries corresponding to degenerate axes must be equal to 1. nodes_on_bdry : bool or sequence, optional If a sequence is provided, it determines per axis whether to place the last grid point on the boundary (``True``) or shift it by half a cell size into the interior (``False``). In each axis, an entry may consist in a single bool or a 2-tuple of bool. In the latter case, the first tuple entry decides for the left, the second for the right boundary. The length of the sequence must be ``array.ndim``. A single boolean is interpreted as a global choice for all boundaries. Returns ------- sampling : `RectGrid` Uniform sampling grid for the interval product. Examples -------- >>> rbox = odl.IntervalProd([-1.5, 2], [-0.5, 3]) >>> grid = uniform_grid_fromintv(rbox, (3, 3)) >>> grid.coord_vectors (array([-1.5, -1. , -0.5]), array([ 2. , 2.5, 3. ])) To have the nodes in the "middle", use ``nodes_on_bdry=False``: >>> grid = uniform_grid_fromintv(rbox, (2, 2), nodes_on_bdry=False) >>> grid.coord_vectors (array([-1.25, -0.75]), array([ 2.25, 2.75])) See Also -------- uniform_grid : Create a uniform grid directly. odl.discr.partition.uniform_partition_fromintv : divide interval product into equally sized subsets """ if not isinstance(intv_prod, IntervalProd): raise TypeError('{!r} is not an `IntervalProd` instance' ''.format(intv_prod)) if (np.any(np.isinf(intv_prod.min_pt)) or np.any(np.isinf(intv_prod.max_pt))): raise ValueError('`intv_prod` must be finite, got {!r}' ''.format('intv_prod')) shape = normalized_scalar_param_list(shape, intv_prod.ndim, safe_int_conv) if np.shape(nodes_on_bdry) == (): nodes_on_bdry = ([(bool(nodes_on_bdry), bool(nodes_on_bdry))] * intv_prod.ndim) elif intv_prod.ndim == 1 and len(nodes_on_bdry) == 2: nodes_on_bdry = [nodes_on_bdry] elif len(nodes_on_bdry) != intv_prod.ndim: raise ValueError('`nodes_on_bdry` has length {}, expected {}' ''.format(len(nodes_on_bdry), intv_prod.ndim)) else: shape = tuple(int(n) for n in shape) # We need to determine the placement of the grid minimum and maximum # points based on the choices in nodes_on_bdry. If in a given axis, # and for a given side (left or right), the entry is True, the node lies # on the boundary, so this coordinate can simply be taken as-is. # # Otherwise, the following conditions must be met: # # 1. The node should be half a stride s away from the boundary # 2. Adding or subtracting (n-1)*s should give the other extremal node. # # If both nodes are to be shifted half a stride inside, # the second condition yields # a + s/2 + (n-1)*s = b - s/2 => s = (b - a) / n, # hence the extremal grid points are # gmin = a + s/2 = a + (b - a) / (2 * n), # gmax = b - s/2 = b - (b - a) / (2 * n). # # In the case where one node, say the rightmost, lies on the boundary, # the condition 2. reads as # a + s/2 + (n-1)*s = b => s = (b - a) / (n - 1/2), # thus # gmin = a + (b - a) / (2 * n - 1). gmin, gmax = [], [] for n, xmin, xmax, on_bdry in zip(shape, intv_prod.min_pt, intv_prod.max_pt, nodes_on_bdry): # Unpack the tuple if possible, else use bool globally for this axis try: bdry_l, bdry_r = on_bdry except TypeError: bdry_l = bdry_r = on_bdry if bdry_l and bdry_r: gmin.append(xmin) gmax.append(xmax) elif bdry_l and not bdry_r: gmin.append(xmin) gmax.append(xmax - (xmax - xmin) / (2 * n - 1)) elif not bdry_l and bdry_r: gmin.append(xmin + (xmax - xmin) / (2 * n - 1)) gmax.append(xmax) else: gmin.append(xmin + (xmax - xmin) / (2 * n)) gmax.append(xmax - (xmax - xmin) / (2 * n)) # Create the grid coord_vecs = [np.linspace(mi, ma, num, endpoint=True, dtype=np.float64) for mi, ma, num in zip(gmin, gmax, shape)] return RectGrid(*coord_vecs)
def uniform_partition(min_pt=None, max_pt=None, shape=None, cell_sides=None, nodes_on_bdry=False): """Return a partition with equally sized cells. Parameters ---------- min_pt, max_pt : float or sequence of float, optional Vectors defining the lower/upper limits of the intervals in an `IntervalProd` (a rectangular box). ``None`` entries mean "compute the value". shape : int or sequence of ints, optional Number of nodes per axis. ``None`` entries mean "compute the value". cell_sides : float or sequence of floats, optional Side length of the partition cells per axis. ``None`` entries mean "compute the value". nodes_on_bdry : bool or sequence, optional If a sequence is provided, it determines per axis whether to place the last grid point on the boundary (``True``) or shift it by half a cell size into the interior (``False``). In each axis, an entry may consist in a single bool or a 2-tuple of bool. In the latter case, the first tuple entry decides for the left, the second for the right boundary. The length of the sequence must be ``array.ndim``. A single boolean is interpreted as a global choice for all boundaries. Notes ----- In each axis, 3 of the 4 possible parameters ``min_pt``, ``max_pt``, ``shape`` and ``cell_sides`` must be given. If all four are provided, they are checked for consistency. See Also -------- uniform_partition_fromintv : partition an existing set uniform_partition_fromgrid : use an existing grid as basis Examples -------- Any combination of three of the four parameters can be used for creation of a partition: >>> part = odl.uniform_partition(min_pt=0, max_pt=2, shape=4) >>> part.cell_boundary_vecs (array([ 0. , 0.5, 1. , 1.5, 2. ]),) >>> part = odl.uniform_partition(min_pt=0, shape=4, cell_sides=0.5) >>> part.cell_boundary_vecs (array([ 0. , 0.5, 1. , 1.5, 2. ]),) >>> part = odl.uniform_partition(max_pt=2, shape=4, cell_sides=0.5) >>> part.cell_boundary_vecs (array([ 0. , 0.5, 1. , 1.5, 2. ]),) >>> part = odl.uniform_partition(min_pt=0, max_pt=2, cell_sides=0.5) >>> part.cell_boundary_vecs (array([ 0. , 0.5, 1. , 1.5, 2. ]),) In higher dimensions, the parameters can be given differently in each axis. Where ``None`` is given, the value will be computed: >>> part = odl.uniform_partition(min_pt=[0, 0], max_pt=[1, 2], ... shape=[4, 2]) >>> part.cell_boundary_vecs (array([ 0. , 0.25, 0.5 , 0.75, 1. ]), array([ 0., 1., 2.])) >>> part = odl.uniform_partition(min_pt=[0, 0], max_pt=[1, 2], ... shape=[None, 2], cell_sides=[0.25, None]) >>> part.cell_boundary_vecs (array([ 0. , 0.25, 0.5 , 0.75, 1. ]), array([ 0., 1., 2.])) >>> part = odl.uniform_partition(min_pt=[0, None], max_pt=[None, 2], ... shape=[4, 2], cell_sides=[0.25, 1]) >>> part.cell_boundary_vecs (array([ 0. , 0.25, 0.5 , 0.75, 1. ]), array([ 0., 1., 2.])) By default, no grid points are placed on the boundary: >>> part = odl.uniform_partition(0, 1, 4) >>> part.nodes_on_bdry False >>> part.cell_boundary_vecs (array([ 0. , 0.25, 0.5 , 0.75, 1. ]),) >>> part.grid.coord_vectors (array([ 0.125, 0.375, 0.625, 0.875]),) This can be changed with the nodes_on_bdry parameter: >>> part = odl.uniform_partition(0, 1, 3, nodes_on_bdry=True) >>> part.nodes_on_bdry True >>> part.cell_boundary_vecs (array([ 0. , 0.25, 0.75, 1. ]),) >>> part.grid.coord_vectors (array([ 0. , 0.5, 1. ]),) We can specify this per axis, too. In this case we choose both in the first axis and only the rightmost in the second: >>> part = odl.uniform_partition([0, 0], [1, 1], (3, 3), ... nodes_on_bdry=(True, (False, True))) ... >>> part.cell_boundary_vecs[0] # first axis, as above array([ 0. , 0.25, 0.75, 1. ]) >>> part.grid.coord_vectors[0] array([ 0. , 0.5, 1. ]) >>> part.cell_boundary_vecs[1] # second, asymmetric axis array([ 0. , 0.4, 0.8, 1. ]) >>> part.grid.coord_vectors[1] array([ 0.2, 0.6, 1. ]) """ # Normalize partition parameters # np.size(None) == 1 sizes = [np.size(p) for p in (min_pt, max_pt, shape, cell_sides)] ndim = int(np.max(sizes)) min_pt = normalized_scalar_param_list(min_pt, ndim, param_conv=float, keep_none=True) max_pt = normalized_scalar_param_list(max_pt, ndim, param_conv=float, keep_none=True) shape = normalized_scalar_param_list(shape, ndim, param_conv=safe_int_conv, keep_none=True) cell_sides = normalized_scalar_param_list(cell_sides, ndim, param_conv=float, keep_none=True) nodes_on_bdry = normalized_nodes_on_bdry(nodes_on_bdry, ndim) # Calculate the missing parameters in min_pt, max_pt, shape for i, (xmin, xmax, n, dx, on_bdry) in enumerate( zip(min_pt, max_pt, shape, cell_sides, nodes_on_bdry)): num_params = sum(p is not None for p in (xmin, xmax, n, dx)) if num_params < 3: raise ValueError('in axis {}: expected at least 3 of the ' 'parameters `min_pt`, `max_pt`, `shape`, ' '`cell_sides`, got {}' ''.format(i, num_params)) # Unpack the tuple if possible, else use bool globally for this axis try: bdry_l, bdry_r = on_bdry except TypeError: bdry_l = bdry_r = on_bdry # For each node on the boundary, we subtract 1/2 from the number of # full cells between min_pt and max_pt. if xmin is None: min_pt[i] = xmax - (n - sum([bdry_l, bdry_r]) / 2.0) * dx elif xmax is None: max_pt[i] = xmin + (n - sum([bdry_l, bdry_r]) / 2.0) * dx elif n is None: # Here we add to n since (e-b)/s gives the reduced number of cells. n_calc = (xmax - xmin) / dx + sum([bdry_l, bdry_r]) / 2.0 n_round = int(round(n_calc)) if abs(n_calc - n_round) > 1e-5: raise ValueError('in axis {}: calculated number of nodes ' '{} = ({} - {}) / {} too far from integer' ''.format(i, n_calc, xmax, xmin, dx)) shape[i] = n_round elif dx is None: pass else: xmax_calc = xmin + (n - sum([bdry_l, bdry_r]) / 2.0) * dx if not np.isclose(xmax, xmax_calc): raise ValueError('in axis {}: calculated endpoint ' '{} = {} + {} * {} too far from given ' 'endpoint {}.' ''.format(i, xmax_calc, xmin, n, dx, xmax)) return uniform_partition_fromintv( IntervalProd(min_pt, max_pt), shape, nodes_on_bdry)
def reciprocal_grid(grid, shift=True, axes=None, halfcomplex=False): """Return the reciprocal of the given regular grid. This function calculates the reciprocal (Fourier/frequency space) grid for a given regular grid defined by the nodes:: x[k] = x[0] + k * s, where ``k = (k[0], ..., k[d-1])`` is a ``d``-dimensional index in the range ``0 <= k < N`` (component-wise). The multi-index ``N`` is the shape of the input grid. This grid's reciprocal is then given by the nodes:: xi[j] = xi[0] + j * sigma, with the reciprocal grid stride ``sigma = 2*pi / (s * N)``. The minimum frequency ``xi[0]`` can in principle be chosen freely, but usually it is chosen in a such a way that the reciprocal grid is centered around zero. For this, there are two possibilities: 1. Make the grid point-symmetric around 0. 2. Make the grid "almost" point-symmetric around zero by shifting it to the left by half a reciprocal stride. In the first case, the minimum frequency (per axis) is given as:: xi_1[0] = -pi/s + pi/(s*n) = -pi/s + sigma/2. For the second case, it is:: xi_1[0] = -pi / s. Note that the zero frequency is contained in case 1 for an odd number of points, while for an even size, the second option guarantees that 0 is contained. If a real-to-complex (half-complex) transform is to be computed, the reciprocal grid has the shape ``M[i] = floor(N[i]/2) + 1`` in the last transform axis ``i``. Parameters ---------- grid : uniform `RectGrid` Original sampling grid,. shift : bool or sequence of bools, optional If ``True``, the grid is shifted by half a stride in the negative direction. With a sequence, this option is applied separately on each axis. axes : int or sequence of ints, optional Dimensions in which to calculate the reciprocal. The sequence must have the same length as ``shift`` if the latter is given as a sequence. ``None`` means all axes in ``grid``. halfcomplex : bool, optional If ``True``, return the half of the grid with last coordinate less than zero. This is related to the fact that for real-valued functions, the other half is the mirrored complex conjugate of the given half and therefore needs not be stored. Returns ------- reciprocal_grid : uniform `RectGrid` The reciprocal grid. """ if axes is None: axes = list(range(grid.ndim)) else: try: axes = [int(axes)] except TypeError: axes = list(axes) # List indicating shift or not per "active" axis, same length as axes shift_list = normalized_scalar_param_list(shift, length=len(axes), param_conv=bool) # Full-length vectors stride = grid.stride shape = np.array(grid.shape) rmin = grid.min_pt.copy() rmax = grid.max_pt.copy() rshape = list(shape) # Shifted axes (full length to avoid ugly double indexing) shifted = np.zeros(grid.ndim, dtype=bool) shifted[axes] = shift_list rmin[shifted] = -np.pi / stride[shifted] # Length min->max increases by double the shift, so we # have to compensate by a full stride rmax[shifted] = (-rmin[shifted] - 2 * np.pi / (stride[shifted] * shape[shifted])) # Non-shifted axes not_shifted = np.zeros(grid.ndim, dtype=bool) not_shifted[axes] = np.logical_not(shift_list) rmin[not_shifted] = ((-1.0 + 1.0 / shape[not_shifted]) * np.pi / stride[not_shifted]) rmax[not_shifted] = -rmin[not_shifted] # Change last axis shape and max if halfcomplex if halfcomplex: rshape[axes[-1]] = shape[axes[-1]] // 2 + 1 # - Odd and shifted: - stride / 2 # - Even and not shifted: + stride / 2 # - Otherwise: 0 last_odd = shape[axes[-1]] % 2 == 1 last_shifted = shift_list[-1] half_rstride = np.pi / (shape[axes[-1]] * stride[axes[-1]]) if last_odd and last_shifted: rmax[axes[-1]] = -half_rstride elif not last_odd and not last_shifted: rmax[axes[-1]] = half_rstride else: rmax[axes[-1]] = 0 return uniform_grid(rmin, rmax, rshape)
def dft_postprocess_data(arr, real_grid, recip_grid, shift, axes, interp, sign='-', op='multiply', out=None): """Post-process the Fourier-space data after DFT. This function multiplies the given data with the separable function:: q(xi) = exp(+- 1j * dot(x[0], xi)) * s * phi_hat(xi_bar) where ``x[0]`` and ``s`` are the minimum point and the stride of the real-space grid, respectively, and ``phi_hat(xi_bar)`` is the FT of the interpolation kernel. The sign of the exponent depends on the choice of ``sign``. Note that for ``op='divide'`` the multiplication with ``s * phi_hat(xi_bar)`` is replaced by a division with the same array. In discretized form on the reciprocal grid, the exponential part of this function becomes an array:: q[k] = exp(+- 1j * dot(x[0], xi[k])) and the arguments ``xi_bar`` to the interpolation kernel are the normalized frequencies:: for 'shift=True' : xi_bar[k] = -pi + pi * (2*k) / N for 'shift=False' : xi_bar[k] = -pi + pi * (2*k+1) / N See [Pre+2007], Section 13.9 "Computing Fourier Integrals Using the FFT" for a similar approach. Parameters ---------- arr : `array-like` Array to be pre-processed. An array with real data type is converted to its complex counterpart. real_grid : uniform `RectGrid` Real space grid in the transform. recip_grid : uniform `RectGrid` Reciprocal grid in the transform shift : bool or sequence of bools If ``True``, the grid is shifted by half a stride in the negative direction in the corresponding axes. The sequence must have the same length as ``axes``. axes : int or sequence of ints Dimensions along which to take the transform. The sequence must have the same length as ``shifts``. interp : string or sequence of strings Interpolation scheme used in the real-space. sign : {'-', '+'}, optional Sign of the complex exponent. op : {'multiply', 'divide'}, optional Operation to perform with the stride times the interpolation kernel FT out : `numpy.ndarray`, optional Array in which the result is stored. If ``out is arr``, an in-place modification is performed. Returns ------- out : `numpy.ndarray` Result of the post-processing. If ``out`` was given, the returned object is a reference to it. References ---------- [Pre+2007] Press, W H, Teukolsky, S A, Vetterling, W T, and Flannery, B P. *Numerical Recipes in C - The Art of Scientific Computing* (Volume 3). Cambridge University Press, 2007. """ arr = np.asarray(arr) if is_real_floating_dtype(arr.dtype): arr = arr.astype(complex_dtype(arr.dtype)) elif not is_complex_floating_dtype(arr.dtype): raise ValueError('array data type {} is not a complex floating point ' 'data type'.format(dtype_repr(arr.dtype))) if out is None: out = arr.copy() elif out is not arr: out[:] = arr if axes is None: axes = list(range(arr.ndim)) else: try: axes = [int(axes)] except TypeError: axes = list(axes) shift_list = normalized_scalar_param_list(shift, length=len(axes), param_conv=bool) if sign == '-': imag = -1j elif sign == '+': imag = 1j else: raise ValueError("`sign` '{}' not understood".format(sign)) op, op_in = str(op).lower(), op if op not in ('multiply', 'divide'): raise ValueError("kernel `op` '{}' not understood".format(op_in)) # Make a list from interp if that's not the case already try: # Duck-typed string check interp + '' except TypeError: pass else: interp = [str(interp).lower()] * arr.ndim onedim_arrs = [] for ax, shift, intp in zip(axes, shift_list, interp): x = real_grid.min_pt[ax] xi = recip_grid.coord_vectors[ax] # First part: exponential array onedim_arr = np.exp(imag * x * xi) # Second part: interpolation kernel len_dft = recip_grid.shape[ax] len_orig = real_grid.shape[ax] halfcomplex = (len_dft < len_orig) odd = len_orig % 2 fmin = -0.5 if shift else -0.5 + 1.0 / (2 * len_orig) if halfcomplex: # maximum lies around 0, possibly half a cell left or right of it if shift and odd: fmax = - 1.0 / (2 * len_orig) elif not shift and not odd: fmax = 1.0 / (2 * len_orig) else: fmax = 0.0 else: # not halfcomplex # maximum lies close to 0.5, half or full cell left of it if shift: # -0.5 + (N-1)/N = 0.5 - 1/N fmax = 0.5 - 1.0 / len_orig else: # -0.5 + 1/(2*N) + (N-1)/N = 0.5 - 1/(2*N) fmax = 0.5 - 1.0 / (2 * len_orig) freqs = np.linspace(fmin, fmax, num=len_dft) stride = real_grid.stride[ax] interp_kernel = _interp_kernel_ft(freqs, intp) interp_kernel *= stride if op == 'multiply': onedim_arr *= interp_kernel else: onedim_arr /= interp_kernel onedim_arrs.append(onedim_arr.astype(out.dtype, copy=False)) fast_1d_tensor_mult(out, onedim_arrs, axes=axes, out=out) return out
def kaczmarz(ops, x, rhs, niter, omega=1, projection=None, callback=None): """Optimized implementation of Kaczmarz's method. Solves the inverse problem given by the set of equations:: A_n(x) = rhs_n This is also known as the Landweber-Kaczmarz's method, since the method coincides with the Landweber method for a single operator. Parameters ---------- ops : sequence of `Operator`'s Operators in the inverse problem. ``op[i].derivative(x).adjoint`` must be well-defined for ``x`` in the operator domain and for all ``i``. x : ``op.domain`` element Element to which the result is written. Its initial value is used as starting point of the iteration, and its values are updated in each iteration step. rhs : sequence of ``ops[i].range`` elements Right-hand side of the equation defining the inverse problem. niter : int Number of iterations. omega : positive float or sequence of positive floats, optional Relaxation parameter in the iteration. If a single float is given the same step is used for all operators, otherwise separate steps are used. projection : callable, optional Function that can be used to modify the iterates in each iteration, for example enforcing positivity. The function should take one argument and modify it in-place. callback : callable, optional Object executing code per iteration, e.g. plotting each iterate. Notes ----- This method calculates an approximate least-squares solution of the inverse problem of the first kind .. math:: \mathcal{A}_i (x) = y_i \\quad 1 \\leq i \\leq n, for a given :math:`y_n \\in \mathcal{Y}_n`, i.e. an approximate solution :math:`x^*` to .. math:: \min_{x\\in \mathcal{X}} \\sum_{i=1}^n \| \mathcal{A}_i(x) - y_i \|_{\mathcal{Y}_i}^2 for a (Frechet-) differentiable operator :math:`\mathcal{A}: \mathcal{X} \\to \mathcal{Y}` between Hilbert spaces :math:`\mathcal{X}` and :math:`\mathcal{Y}`. The method starts from an initial guess :math:`x_0` and uses the iteration .. math:: x_{k+1} = x_k - \omega_{[k]} \ \partial \mathcal{A}_{[k]}(x_k)^* (\mathcal{A}_{[k]}(x_k) - y_{[k]}), where :math:`\partial \mathcal{A}_{[k]}(x_k)` is the Frechet derivative of :math:`\mathcal{A}_{[k]}` at :math:`x_k`, :math:`\omega_{[k]}` is a relaxation parameter and :math:`[k] := k \\text{ mod } n`. For linear problems, a choice :math:`0 < \omega_i < 2/\\lVert \mathcal{A}_{i}^2\\rVert` guarantees convergence, where :math:`\|\mathcal{A}_{i}\|` stands for the operator norm of :math:`\mathcal{A}_{i}`. This implementation uses a minimum amount of memory copies by applying re-usable temporaries and in-place evaluation. The method is also described in a `Wikipedia article <https://en.wikipedia.org/wiki/Kaczmarz_method>`_. and in Natterer, F. Mathematical Methods in Image Reconstruction, section 5.3.2. See Also -------- landweber """ domain = ops[0].domain if any(domain != opi.domain for opi in ops): raise ValueError('`opi[i].domain` are not all equal') if x not in domain: raise TypeError('`x` {!r} is not in the domain of `ops` {!r}' ''.format(x, domain)) if len(ops) != len(rhs): raise ValueError('`number of `ops` {} does not match number of ' '`rhs` {}'.format(len(ops), len(rhs))) omega = normalized_scalar_param_list(omega, len(ops), param_conv=float) # Reusable elements in the range, one per type of space ranges = [opi.range for opi in ops] unique_ranges = set(ranges) tmp_rans = {ran: ran.element() for ran in unique_ranges} # Single reusable element in the domain tmp_dom = domain.element() # Iteratively find solution for _ in range(niter): for i in range(len(ops)): # Find residual tmp_ran = tmp_rans[ops[i].range] ops[i](x, out=tmp_ran) tmp_ran -= rhs[i] # Update x ops[i].derivative(x).adjoint(tmp_ran, out=tmp_dom) x.lincomb(1, x, -omega[i], tmp_dom) if projection is not None: projection(x) if callback is not None: callback(x)
def dft_preprocess_data(arr, shift=True, axes=None, sign='-', out=None): """Pre-process the real-space data before DFT. This function multiplies the given data with the separable function:: p(x) = exp(+- 1j * dot(x - x[0], xi[0])) where ``x[0]`` and ``xi[0]`` are the minimum coodinates of the real-space and reciprocal grids, respectively. The sign of the exponent depends on the choice of ``sign``. In discretized form, this function becomes an array:: p[k] = exp(+- 1j * k * s * xi[0]) If the reciprocal grid is not shifted, i.e. symmetric around 0, it is ``xi[0] = pi/s * (-1 + 1/N)``, hence:: p[k] = exp(-+ 1j * pi * k * (1 - 1/N)) For a shifted grid, we have :math:``xi[0] = -pi/s``, thus the array is given by:: p[k] = (-1)**k Parameters ---------- arr : `array-like` Array to be pre-processed. If its data type is a real non-floating type, it is converted to 'float64'. shift : bool or or sequence of bools, optional If ``True``, the grid is shifted by half a stride in the negative direction. With a sequence, this option is applied separately on each axis. axes : int or sequence of ints, optional Dimensions in which to calculate the reciprocal. The sequence must have the same length as ``shift`` if the latter is given as a sequence. Default: all axes. sign : {'-', '+'}, optional Sign of the complex exponent. out : `numpy.ndarray`, optional Array in which the result is stored. If ``out is arr``, an in-place modification is performed. For real data type, this is only possible for ``shift=True`` since the factors are complex otherwise. Returns ------- out : `numpy.ndarray` Result of the pre-processing. If ``out`` was given, the returned object is a reference to it. Notes ----- If ``out`` is not specified, the data type of the returned array is the same as that of ``arr`` except when ``arr`` has real data type and ``shift`` is not ``True``. In this case, the return type is the complex counterpart of ``arr.dtype``. """ arr = np.asarray(arr) if not is_scalar_dtype(arr.dtype): raise ValueError('array has non-scalar data type {}' ''.format(dtype_repr(arr.dtype))) elif is_real_dtype(arr.dtype) and not is_real_floating_dtype(arr.dtype): arr = arr.astype('float64') if axes is None: axes = list(range(arr.ndim)) else: try: axes = [int(axes)] except TypeError: axes = list(axes) shape = arr.shape shift_list = normalized_scalar_param_list(shift, length=len(axes), param_conv=bool) # Make a copy of arr with correct data type if necessary, or copy values. if out is None: if is_real_dtype(arr.dtype) and not all(shift_list): out = np.array(arr, dtype=complex_dtype(arr.dtype), copy=True) else: out = arr.copy() else: out[:] = arr if is_real_dtype(out.dtype) and not shift: raise ValueError('cannot pre-process real input in-place without ' 'shift') if sign == '-': imag = -1j elif sign == '+': imag = 1j else: raise ValueError("`sign` '{}' not understood".format(sign)) def _onedim_arr(length, shift): if shift: # (-1)^indices factor = np.ones(length, dtype=out.dtype) factor[1::2] = -1 else: factor = np.arange(length, dtype=out.dtype) factor *= -imag * np.pi * (1 - 1.0 / length) np.exp(factor, out=factor) return factor.astype(out.dtype, copy=False) onedim_arrs = [] for axis, shift in zip(axes, shift_list): length = shape[axis] onedim_arrs.append(_onedim_arr(length, shift)) fast_1d_tensor_mult(out, onedim_arrs, axes=axes, out=out) return out
def uniform_grid_fromintv(intv_prod, shape, nodes_on_bdry=True): """Return a grid from sampling an interval product uniformly. The resulting grid will by default include ``intv_prod.min_pt`` and ``intv_prod.max_pt`` as grid points. If you want a subdivision into equally sized cells with grid points in the middle, use `uniform_partition` instead. Parameters ---------- intv_prod : `IntervalProd` Set to be sampled. shape : int or sequence of ints Number of nodes per axis. Entries corresponding to degenerate axes must be equal to 1. nodes_on_bdry : bool or sequence, optional If a sequence is provided, it determines per axis whether to place the last grid point on the boundary (``True``) or shift it by half a cell size into the interior (``False``). In each axis, an entry may consist in a single bool or a 2-tuple of bool. In the latter case, the first tuple entry decides for the left, the second for the right boundary. The length of the sequence must be ``array.ndim``. A single boolean is interpreted as a global choice for all boundaries. Returns ------- sampling : `RectGrid` Uniform sampling grid for the interval product. Examples -------- >>> rbox = odl.IntervalProd([-1.5, 2], [-0.5, 3]) >>> grid = uniform_grid_fromintv(rbox, (3, 3)) >>> grid.coord_vectors (array([-1.5, -1. , -0.5]), array([ 2. , 2.5, 3. ])) To have the nodes in the "middle", use ``nodes_on_bdry=False``: >>> grid = uniform_grid_fromintv(rbox, (2, 2), nodes_on_bdry=False) >>> grid.coord_vectors (array([-1.25, -0.75]), array([ 2.25, 2.75])) See Also -------- uniform_grid : Create a uniform grid directly. odl.discr.partition.uniform_partition_fromintv : divide interval product into equally sized subsets """ if not isinstance(intv_prod, IntervalProd): raise TypeError('{!r} is not an `IntervalProd` instance' ''.format(intv_prod)) if (np.any(np.isinf(intv_prod.min_pt)) or np.any(np.isinf(intv_prod.max_pt))): raise ValueError('`intv_prod` must be finite, got {!r}' ''.format('intv_prod')) shape = normalized_scalar_param_list(shape, intv_prod.ndim, safe_int_conv) if np.shape(nodes_on_bdry) == (): nodes_on_bdry = ([(bool(nodes_on_bdry), bool(nodes_on_bdry))] * intv_prod.ndim) elif intv_prod.ndim == 1 and len(nodes_on_bdry) == 2: nodes_on_bdry = [nodes_on_bdry] elif len(nodes_on_bdry) != intv_prod.ndim: raise ValueError('`nodes_on_bdry` has length {}, expected {}' ''.format(len(nodes_on_bdry), intv_prod.ndim)) else: shape = tuple(int(n) for n in shape) # We need to determine the placement of the grid minimum and maximum # points based on the choices in nodes_on_bdry. If in a given axis, # and for a given side (left or right), the entry is True, the node lies # on the boundary, so this coordinate can simply be taken as-is. # # Otherwise, the following conditions must be met: # # 1. The node should be half a stride s away from the boundary # 2. Adding or subtracting (n-1)*s should give the other extremal node. # # If both nodes are to be shifted half a stride inside, # the second condition yields # a + s/2 + (n-1)*s = b - s/2 => s = (b - a) / n, # hence the extremal grid points are # gmin = a + s/2 = a + (b - a) / (2 * n), # gmax = b - s/2 = b - (b - a) / (2 * n). # # In the case where one node, say the rightmost, lies on the boundary, # the condition 2. reads as # a + s/2 + (n-1)*s = b => s = (b - a) / (n - 1/2), # thus # gmin = a + (b - a) / (2 * n - 1). gmin, gmax = [], [] for n, xmin, xmax, on_bdry in zip(shape, intv_prod.min_pt, intv_prod.max_pt, nodes_on_bdry): # Unpack the tuple if possible, else use bool globally for this axis try: bdry_l, bdry_r = on_bdry except TypeError: bdry_l = bdry_r = on_bdry if bdry_l and bdry_r: gmin.append(xmin) gmax.append(xmax) elif bdry_l and not bdry_r: gmin.append(xmin) gmax.append(xmax - (xmax - xmin) / (2 * n - 1)) elif not bdry_l and bdry_r: gmin.append(xmin + (xmax - xmin) / (2 * n - 1)) gmax.append(xmax) else: gmin.append(xmin + (xmax - xmin) / (2 * n)) gmax.append(xmax - (xmax - xmin) / (2 * n)) # Create the grid coord_vecs = [np.linspace(mi, ma, num, endpoint=True, dtype=np.float64) for mi, ma, num in zip(gmin, gmax, shape)] return RectGrid(*coord_vecs)
def reciprocal_grid(grid, shift=True, axes=None, halfcomplex=False): """Return the reciprocal of the given regular grid. This function calculates the reciprocal (Fourier/frequency space) grid for a given regular grid defined by the nodes:: x[k] = x[0] + k * s, where ``k = (k[0], ..., k[d-1])`` is a ``d``-dimensional index in the range ``0 <= k < N`` (component-wise). The multi-index ``N`` is the shape of the input grid. This grid's reciprocal is then given by the nodes:: xi[j] = xi[0] + j * sigma, with the reciprocal grid stride ``sigma = 2*pi / (s * N)``. The minimum frequency ``xi[0]`` can in principle be chosen freely, but usually it is chosen in a such a way that the reciprocal grid is centered around zero. For this, there are two possibilities: 1. Make the grid point-symmetric around 0. 2. Make the grid "almost" point-symmetric around zero by shifting it to the left by half a reciprocal stride. In the first case, the minimum frequency (per axis) is given as:: xi_1[0] = -pi/s + pi/(s*n) = -pi/s + sigma/2. For the second case, it is:: xi_1[0] = -pi / s. Note that the zero frequency is contained in case 1 for an odd number of points, while for an even size, the second option guarantees that 0 is contained. If a real-to-complex (half-complex) transform is to be computed, the reciprocal grid has the shape ``M[i] = floor(N[i]/2) + 1`` in the last transform axis ``i``. Parameters ---------- grid : `RegularGrid` Original sampling grid shift : bool or sequence of bools, optional If ``True``, the grid is shifted by half a stride in the negative direction. With a sequence, this option is applied separately on each axis. axes : int or sequence of ints, optional Dimensions in which to calculate the reciprocal. The sequence must have the same length as ``shift`` if the latter is given as a sequence. ``None`` means all axes in ``grid``. halfcomplex : bool, optional If ``True``, return the half of the grid with last coordinate less than zero. This is related to the fact that for real-valued functions, the other half is the mirrored complex conjugate of the given half and therefore needs not be stored. Returns ------- reciprocal_grid : `RegularGrid` The reciprocal grid. """ if axes is None: axes = list(range(grid.ndim)) else: try: axes = [int(axes)] except TypeError: axes = list(axes) # List indicating shift or not per "active" axis, same length as axes shift_list = normalized_scalar_param_list(shift, length=len(axes), param_conv=bool) # Full-length vectors stride = grid.stride shape = np.array(grid.shape) rmin = grid.min_pt.copy() rmax = grid.max_pt.copy() rshape = list(shape) # Shifted axes (full length to avoid ugly double indexing) shifted = np.zeros(grid.ndim, dtype=bool) shifted[axes] = shift_list rmin[shifted] = -np.pi / stride[shifted] # Length min->max increases by double the shift, so we # have to compensate by a full stride rmax[shifted] = (-rmin[shifted] - 2 * np.pi / (stride[shifted] * shape[shifted])) # Non-shifted axes not_shifted = np.zeros(grid.ndim, dtype=bool) not_shifted[axes] = np.logical_not(shift_list) rmin[not_shifted] = ((-1.0 + 1.0 / shape[not_shifted]) * np.pi / stride[not_shifted]) rmax[not_shifted] = -rmin[not_shifted] # Change last axis shape and max if halfcomplex if halfcomplex: rshape[axes[-1]] = shape[axes[-1]] // 2 + 1 # - Odd and shifted: - stride / 2 # - Even and not shifted: + stride / 2 # - Otherwise: 0 last_odd = shape[axes[-1]] % 2 == 1 last_shifted = shift_list[-1] half_rstride = np.pi / (shape[axes[-1]] * stride[axes[-1]]) if last_odd and last_shifted: rmax[axes[-1]] = -half_rstride elif not last_odd and not last_shifted: rmax[axes[-1]] = half_rstride else: rmax[axes[-1]] = 0 return RegularGrid(rmin, rmax, rshape)
def dft_postprocess_data(arr, real_grid, recip_grid, shift, axes, interp, sign='-', op='multiply', out=None): """Post-process the Fourier-space data after DFT. This function multiplies the given data with the separable function:: q(xi) = exp(+- 1j * dot(x[0], xi)) * s * phi_hat(xi_bar) where ``x[0]`` and ``s`` are the minimum point and the stride of the real-space grid, respectively, and ``phi_hat(xi_bar)`` is the FT of the interpolation kernel. The sign of the exponent depends on the choice of ``sign``. Note that for ``op='divide'`` the multiplication with ``s * phi_hat(xi_bar)`` is replaced by a division with the same array. In discretized form on the reciprocal grid, the exponential part of this function becomes an array:: q[k] = exp(+- 1j * dot(x[0], xi[k])) and the arguments ``xi_bar`` to the interpolation kernel are the normalized frequencies:: for 'shift=True' : xi_bar[k] = -pi + pi * (2*k) / N for 'shift=False' : xi_bar[k] = -pi + pi * (2*k+1) / N See [Pre+2007]_, Section 13.9 "Computing Fourier Integrals Using the FFT" for a similar approach. Parameters ---------- arr : `array-like` Array to be pre-processed. An array with real data type is converted to its complex counterpart. real_grid : `RegularGrid` Real space grid in the transform recip_grid : `RegularGrid` Reciprocal grid in the transform shift : bool or sequence of bools If ``True``, the grid is shifted by half a stride in the negative direction in the corresponding axes. The sequence must have the same length as ``axes``. axes : int or sequence of ints Dimensions along which to take the transform. The sequence must have the same length as ``shifts``. interp : string or sequence of strings Interpolation scheme used in the real-space. sign : {'-', '+'}, optional Sign of the complex exponent. op : {'multiply', 'divide'} Operation to perform with the stride times the interpolation kernel FT out : `numpy.ndarray`, optional Array in which the result is stored. If ``out is arr``, an in-place modification is performed. Returns ------- out : `numpy.ndarray` Result of the post-processing. If ``out`` was given, the returned object is a reference to it. """ arr = np.asarray(arr) if is_real_floating_dtype(arr.dtype): arr = arr.astype(complex_dtype(arr.dtype)) elif not is_complex_floating_dtype(arr.dtype): raise ValueError('array data type {} is not a complex floating point ' 'data type'.format(dtype_repr(arr.dtype))) if out is None: out = arr.copy() elif out is not arr: out[:] = arr if axes is None: axes = list(range(arr.ndim)) else: try: axes = [int(axes)] except TypeError: axes = list(axes) shift_list = normalized_scalar_param_list(shift, length=len(axes), param_conv=bool) if sign == '-': imag = -1j elif sign == '+': imag = 1j else: raise ValueError("`sign` '{}' not understood".format(sign)) op, op_in = str(op).lower(), op if op not in ('multiply', 'divide'): raise ValueError("kernel `op` '{}' not understood".format(op_in)) # Make a list from interp if that's not the case already try: # Duck-typed string check interp + '' except TypeError: pass else: interp = [str(interp).lower()] * arr.ndim onedim_arrs = [] for ax, shift, intp in zip(axes, shift_list, interp): x = real_grid.min_pt[ax] xi = recip_grid.coord_vectors[ax] # First part: exponential array onedim_arr = np.exp(imag * x * xi) # Second part: interpolation kernel len_dft = recip_grid.shape[ax] len_orig = real_grid.shape[ax] halfcomplex = (len_dft < len_orig) odd = len_orig % 2 fmin = -0.5 if shift else -0.5 + 1.0 / (2 * len_orig) if halfcomplex: # maximum lies around 0, possibly half a cell left or right of it if shift and odd: fmax = - 1.0 / (2 * len_orig) elif not shift and not odd: fmax = 1.0 / (2 * len_orig) else: fmax = 0.0 else: # not halfcomplex # maximum lies close to 0.5, half or full cell left of it if shift: # -0.5 + (N-1)/N = 0.5 - 1/N fmax = 0.5 - 1.0 / len_orig else: # -0.5 + 1/(2*N) + (N-1)/N = 0.5 - 1/(2*N) fmax = 0.5 - 1.0 / (2 * len_orig) freqs = np.linspace(fmin, fmax, num=len_dft) stride = real_grid.stride[ax] if op == 'multiply': onedim_arr *= stride * _interp_kernel_ft(freqs, intp) else: onedim_arr /= stride * _interp_kernel_ft(freqs, intp) onedim_arrs.append(onedim_arr.astype(out.dtype, copy=False)) fast_1d_tensor_mult(out, onedim_arrs, axes=axes, out=out) return out
def dft_preprocess_data(arr, shift=True, axes=None, sign='-', out=None): """Pre-process the real-space data before DFT. This function multiplies the given data with the separable function:: p(x) = exp(+- 1j * dot(x - x[0], xi[0])) where ``x[0]`` and ``xi[0]`` are the minimum coodinates of the real-space and reciprocal grids, respectively. The sign of the exponent depends on the choice of ``sign``. In discretized form, this function becomes an array:: p[k] = exp(+- 1j * k * s * xi[0]) If the reciprocal grid is not shifted, i.e. symmetric around 0, it is ``xi[0] = pi/s * (-1 + 1/N)``, hence:: p[k] = exp(-+ 1j * pi * k * (1 - 1/N)) For a shifted grid, we have :math:``xi[0] = -pi/s``, thus the array is given by:: p[k] = (-1)**k Parameters ---------- arr : `array-like` Array to be pre-processed. If its data type is a real non-floating type, it is converted to 'float64'. shift : bool or or sequence of bools, optional If ``True``, the grid is shifted by half a stride in the negative direction. With a sequence, this option is applied separately on each axis. axes : int or sequence of ints, optional Dimensions in which to calculate the reciprocal. The sequence must have the same length as ``shift`` if the latter is given as a sequence. Default: all axes. sign : {'-', '+'}, optional Sign of the complex exponent. out : `numpy.ndarray`, optional Array in which the result is stored. If ``out is arr``, an in-place modification is performed. For real data type, this is only possible for ``shift=True`` since the factors are complex otherwise. Returns ------- out : `numpy.ndarray` Result of the pre-processing. If ``out`` was given, the returned object is a reference to it. Notes ----- If ``out`` is not specified, the data type of the returned array is the same as that of ``arr`` except when ``arr`` has real data type and ``shift`` is not ``True``. In this case, the return type is the complex counterpart of ``arr.dtype``. """ arr = np.asarray(arr) if not is_scalar_dtype(arr.dtype): raise ValueError('array has non-scalar data type {}' ''.format(dtype_repr(arr.dtype))) elif is_real_dtype(arr.dtype) and not is_real_floating_dtype(arr.dtype): arr = arr.astype('float64') if axes is None: axes = list(range(arr.ndim)) else: try: axes = [int(axes)] except TypeError: axes = list(axes) shape = arr.shape shift_list = normalized_scalar_param_list(shift, length=len(axes), param_conv=bool) # Make a copy of arr with correct data type if necessary, or copy values. if out is None: if is_real_dtype(arr.dtype) and not all(shift_list): out = np.array(arr, dtype=complex_dtype(arr.dtype), copy=True) else: out = arr.copy() else: out[:] = arr if is_real_dtype(out.dtype) and not shift: raise ValueError('cannot pre-process real input in-place without ' 'shift') if sign == '-': imag = -1j elif sign == '+': imag = 1j else: raise ValueError("`sign` '{}' not understood".format(sign)) def _onedim_arr(length, shift): if shift: # (-1)^indices factor = np.ones(length, dtype=out.dtype) factor[1::2] = -1 else: factor = np.arange(length, dtype=out.dtype) factor *= -imag * np.pi * (1 - 1.0 / length) np.exp(factor, out=factor) return factor.astype(out.dtype, copy=False) onedim_arrs = [] for axis, shift in zip(axes, shift_list): length = shape[axis] onedim_arrs.append(_onedim_arr(length, shift)) fast_1d_tensor_mult(out, onedim_arrs, axes=axes, out=out) return out
def __init__(self, domain, range=None, ran_shp=None, **kwargs): """Initialize a new instance. Parameters ---------- domain : uniform `DiscreteLp` Uniformly discretized space, the operator can be applied to its elements. range : uniform `DiscreteLp`, optional Uniformly discretized space in which the result of the application of this operator lies. For the default ``None``, a space with the same attributes as ``domain`` is used, except for its shape, which is set to ``ran_shp``. ran_shp : sequence of ints, optional Shape of the range of this operator. This can be provided instead of ``range`` and is mandatory if ``range`` is ``None``. offset : int or sequence of ints, optional Number of cells to add to/remove from the left of ``domain.partition``. By default, the difference is distributed evenly, with preference for left in case of ambiguity. This option is can only be used together with ``ran_shp``. pad_mode : string, optional Method to be used to fill in missing values in an enlarged array. ``'constant'``: Fill with ``pad_const``. ``'symmetric'``: Reflect at the boundaries, not doubling the outmost values. This requires left and right padding sizes to be strictly smaller than the original array shape. ``'periodic'``: Fill in values from the other side, keeping the order. This requires left and right padding sizes to be at most as large as the original array shape. ``'order0'``: Extend constantly with the outmost values (ensures continuity). ``'order1'``: Extend with constant slope (ensures continuity of the first derivative). This requires at least 2 values along each axis where padding is applied. pad_const : scalar, optional Value to be used in the ``'constant'`` padding mode. discr_kwargs: dict, optional Keyword arguments passed to the `uniform_discr` constructor. Examples -------- The simplest way of initializing a resizing operator is by providing ``ran_shp`` and, optionally, parameters for the padding variant to be used. The range is inferred from ``domain`` and the supplied parameters. If no ``offset`` is given, the difference in size is evenly distributed to both sides: >>> space = odl.uniform_discr([0, 0], [1, 1], (2, 4)) >>> resize_op = odl.ResizingOperator(space, ran_shp=(4, 4)) >>> resize_op.range uniform_discr([-0.5, 0.0], [1.5, 1.0], (4, 4)) Testing different padding methods in the first axis (zero padding is the default): >>> x = [[1, 2, 3, 4], ... [5, 6, 7, 8]] >>> resize_op = odl.ResizingOperator(space, ran_shp=(4, 4)) >>> print(resize_op(x)) [[0.0, 0.0, 0.0, 0.0], [1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0], [0.0, 0.0, 0.0, 0.0]] >>> >>> resize_op = odl.ResizingOperator(space, ran_shp=(4, 4), ... offset=(0, 0), ... pad_mode='periodic') >>> print(resize_op(x)) [[1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0], [1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0]] >>> >>> resize_op = odl.ResizingOperator(space, ran_shp=(4, 4), ... offset=(0, 0), ... pad_mode='order0') >>> print(resize_op(x)) [[1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0], [5.0, 6.0, 7.0, 8.0], [5.0, 6.0, 7.0, 8.0]] Alternatively, the range of the operator can be provided directly. This requires that the partitions match, i.e. that the cell sizes are the same and there is no shift: >>> # Same space as in the first example, see above >>> large_spc = odl.uniform_discr([-0.5, 0], [1.5, 1], (4, 4)) >>> resize_op = odl.ResizingOperator(space, large_spc, ... pad_mode='periodic') >>> print(resize_op(x)) [[5.0, 6.0, 7.0, 8.0], [1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0], [1.0, 2.0, 3.0, 4.0]] """ if not isinstance(domain, DiscreteLp): raise TypeError('`domain` must be a `DiscreteLp` instance, ' 'got {!r}'.format(domain)) if not domain.is_uniform: raise ValueError('`domain` is not uniformly discretized') offset = kwargs.pop('offset', None) discr_kwargs = kwargs.pop('discr_kwargs', {}) if range is None: if ran_shp is None: raise ValueError('either `range` or `ran_shp` must be ' 'given') offset = normalized_scalar_param_list( offset, domain.ndim, param_conv=safe_int_conv, keep_none=True) range = _resize_discr(domain, ran_shp, offset, discr_kwargs) self.__offset = tuple(_offset_from_spaces(domain, range)) elif ran_shp is None: if offset is not None: raise ValueError('`offset` can only be combined with ' '`ran_shp`') if not np.allclose(range.cell_sides, domain.cell_sides): raise ValueError( 'cell sides of domain and range differ significantly ' '(difference {})' ''.format(range.cell_sides - domain.cell_sides)) self.__offset = _offset_from_spaces(domain, range) else: raise ValueError('cannot combine `range` with `ran_shape`') pad_mode = kwargs.pop('pad_mode', 'constant') pad_mode, pad_mode_in = str(pad_mode).lower(), pad_mode if pad_mode not in _SUPPORTED_RESIZE_PAD_MODES: raise ValueError("`pad_mode` '{}' not understood" "".format(pad_mode_in)) self.__pad_mode = pad_mode # Store constant in a way that ensures safe casting (one-element array) self.__pad_const = np.array(kwargs.pop('pad_const', 0), dtype=range.dtype) # padding mode 'constant' with `pad_const != 0` is not linear linear = (self.pad_mode != 'constant' or self.pad_const == 0.0) super().__init__(domain, range, linear=linear)