Source code for pyxu.operator.linop.diff

   1import collections
   2import collections.abc as cabc
   3import itertools
   4import math
   5import types
   6import typing as typ
   7
   8import numpy as np
   9
  10import pyxu.info.deps as pxd
  11import pyxu.info.ptype as pxt
  12import pyxu.operator.blocks as pxb
  13import pyxu.operator.linop.base as pxlb
  14import pyxu.operator.linop.pad as pxlp
  15import pyxu.operator.linop.reduce as pxlr
  16import pyxu.operator.linop.stencil.stencil as pxls
  17import pyxu.operator.misc as pxm
  18import pyxu.runtime as pxrt
  19import pyxu.util as pxu
  20
  21try:
  22    import scipy.ndimage._filters as scif
  23except ImportError:
  24    import scipy.ndimage.filters as scif
  25
  26__all__ = [
  27    "PartialDerivative",
  28    "Gradient",
  29    "Jacobian",
  30    "Divergence",
  31    "Hessian",
  32    "Laplacian",
  33    "DirectionalDerivative",
  34    "DirectionalGradient",
  35    "DirectionalLaplacian",
  36    "DirectionalHessian",
  37]
  38
  39ModeSpec = pxlp.Pad.ModeSpec
  40KernelSpec = pxls.Stencil.KernelSpec
  41PDMetaFD = collections.namedtuple("FiniteDifferenceMeta", "sampling scheme accuracy")
  42PDMetaGD = collections.namedtuple("GaussianDerivativeMeta", "sampling sigma truncate")
  43
  44
  45def _sanitize_init_kwargs(
  46    order: typ.Union[pxt.Integer, cabc.Sequence[pxt.Integer, ...]],
  47    dim_shape: pxt.NDArrayShape,
  48    sampling: typ.Union[pxt.Real, cabc.Sequence[pxt.Real, ...]],
  49    diff_method: str,
  50    diff_kwargs: dict,
  51    axes: pxt.NDArrayAxis = None,
  52) -> tuple[
  53    cabc.Sequence[pxt.Integer, ...],
  54    cabc.Sequence[pxt.Real, ...],
  55    typ.Union[cabc.Sequence[pxt.Real, ...], cabc.Sequence[str, ...]],
  56    cabc.Sequence[pxt.Integer, ...],
  57    pxt.NDArrayAxis,
  58]:
  59    r"""
  60    Ensures that inputs have the appropriate shape and values.
  61    """
  62
  63    dim_shape = pxu.as_canonical_shape(dim_shape)
  64
  65    def _ensure_tuple(param, param_name: str) -> typ.Union[tuple[pxt.Integer, ...], tuple[str, ...]]:
  66        r"""
  67        Enforces the input parameters to be tuples of the same size as `dim_shape`.
  68        """
  69        if not isinstance(param, cabc.Sequence) or isinstance(param, str):
  70            param = (param,)
  71        assert (len(param) == 1) | (len(param) <= len(dim_shape)), (
  72            f"The length of {param_name} cannot be larger than the"
  73            f"number of dimensions ({len(dim_shape)}) defined by `dim_shape`"
  74        )
  75        return param
  76
  77    order = _ensure_tuple(order, param_name="order")
  78    sampling = _ensure_tuple(sampling, param_name="sampling")
  79    if len(sampling) == 1:
  80        sampling = sampling * len(dim_shape)
  81    assert all([_ >= 0 for _ in order]), "Order must be positive"
  82    assert all([_ > 0 for _ in sampling]), "Sampling must be strictly positive"
  83
  84    if diff_method == "fd":
  85        param1_name = "scheme"
  86        param2_name = "accuracy"
  87        param1 = diff_kwargs.get("scheme", "forward")
  88        param2 = diff_kwargs.get("accuracy", 1)
  89    elif diff_method == "gd":
  90        param1_name = "sigma"
  91        param2_name = "truncate"
  92        param1 = diff_kwargs.get("sigma", 1.0)
  93        param2 = diff_kwargs.get("truncate", 3.0)
  94    else:
  95        raise NotImplementedError
  96
  97    _param1 = _ensure_tuple(param1, param_name=param1_name)
  98    _param2 = _ensure_tuple(param2, param_name=param2_name)
  99
 100    if param1_name == "sigma":
 101        assert all([p >= 0 for p in _param1]), "Sigma must be strictly positive"
 102    if param2_name == "accuracy":
 103        assert all([p >= 0 for p in _param2]), "Accuracy must be positive"
 104    elif param2_name == "truncate":
 105        assert all([p > 0 for p in _param2]), "Truncate must be strictly positive"
 106
 107    if len(order) != len(dim_shape):
 108        assert axes is not None, (
 109            "If `order` is not a tuple with size of dim_shape, then `axes` must be" " specified. Got `axes=None`"
 110        )
 111        axes = _ensure_tuple(axes, param_name="axes")
 112        assert len(axes) == len(order), "`axes` must have the same number of elements as `order`"
 113    else:
 114        if axes is not None:
 115            axes = _ensure_tuple(axes, param_name="axes")
 116            assert len(axes) == len(order), "`axes` must have the same number of elements as `order`"
 117        else:
 118            axes = tuple([i for i in range(len(dim_shape))])
 119
 120    if not (len(_param1) == len(order)):
 121        assert len(_param1) == 1, (
 122            f"Parameter `{param1_name}` inconsistent with the number of elements in " "parameter `order`."
 123        )
 124        _param1 = _param1 * len(dim_shape)
 125
 126    if not (len(_param2) == len(order)):
 127        assert len(_param2) == 1, (
 128            f"Parameter `{param2_name}` inconsistent with the number of elements in " "parameter `order`."
 129        )
 130        _param2 = _param2 * len(dim_shape)
 131
 132    return (
 133        order,
 134        sampling,
 135        _param1,
 136        _param2,
 137        axes,
 138    )
 139
 140
 141def _create_kernel(
 142    dim_shape: pxt.NDArrayShape,
 143    axes: pxt.NDArrayAxis,
 144    _fill_coefs: typ.Callable,
 145) -> tuple[cabc.Sequence[pxt.NDArray], pxt.NDArray]:
 146    r"""
 147    Creates kernel for stencil.
 148    """
 149    stencil_ids = [np.array([0])] * len(dim_shape)
 150    stencil_coefs = [np.array([1.0])] * len(dim_shape)
 151    center = np.zeros(len(dim_shape), dtype=int)
 152
 153    # Create finite difference coefficients for each dimension
 154    for i, ax in enumerate(axes):
 155        stencil_ids[ax], stencil_coefs[ax], center[ax] = _fill_coefs(i)
 156
 157    return stencil_coefs, center
 158
 159
 160def _FiniteDifference(
 161    order: typ.Union[pxt.Integer, cabc.Sequence[pxt.Integer, ...]],
 162    dim_shape: pxt.NDArrayShape,
 163    scheme: typ.Union[str, cabc.Sequence[str, ...]] = "forward",
 164    axes: pxt.NDArrayAxis = None,
 165    accuracy: typ.Union[pxt.Integer, cabc.Sequence[pxt.Integer, ...]] = 1,
 166    sampling: typ.Union[pxt.Real, cabc.Sequence[pxt.Real, ...]] = 1,
 167) -> tuple[cabc.Sequence[pxt.NDArray], pxt.NDArray]:
 168    r"""
 169    Finite difference base operator along a single dimension.
 170
 171    This class is used by :py:class:`~pyxu.operator.PartialDerivative`,
 172    :py:class:`~pyxu.operator.Gradient` and :py:class:`~pyxu.operator.Hessian`.
 173    See :py:class:`~pyxu.operator.PartialDerivative.finite_difference` for documentation.
 174
 175    See Also
 176    --------
 177    :py:class:`~pyxu.operator._GaussianDerivative`,
 178    :py:class:`~pyxu.operator.PartialDerivative`,
 179    :py:class:`~pyxu.operator.Gradient`,
 180    :py:class:`~pyxu.operator.Hessian`.
 181
 182    Parameters
 183    ----------
 184    order: Integer, list[Integer]
 185        Derivative order. If a single integer value is provided, then `axes` should be provided to
 186        indicate which dimension should be differentiated.
 187        If a tuple is provided, it should contain as many elements as `dim_shape`.
 188    dim_shape: pxt.NDArrayShape
 189        Shape of the input array.
 190    scheme: str, list[str]
 191        Type of finite differences: ["forward", "backward", "central"].
 192        Defaults to "forward".
 193    axes: NDArrayAxis
 194        Axes to which apply the derivative.
 195        It maps the argument `order` to the specified dimensions of the input array.
 196        Defaults to None, assuming that the `order` argument has as many elements as dimensions of
 197        the input.
 198    accuracy: Integer, list[Integer]
 199        Determines the number of points used to approximate the derivative with finite differences
 200        (see `Notes`).
 201        Defaults to 1.
 202        If an int is provided, the same `accuracy` is assumed for all dimensions.
 203        If a tuple is provided, it should have as many elements as `dim_shape`.
 204    sampling: pxt.Real, list[Real]
 205        Sampling step (i.e. distance between two consecutive elements of an array).
 206        Defaults to 1.
 207    """
 208    diff_kwargs = {"scheme": scheme, "accuracy": accuracy}
 209    order, sampling, scheme, accuracy, axes = _sanitize_init_kwargs(
 210        order=order,
 211        diff_method="fd",
 212        diff_kwargs=diff_kwargs,
 213        dim_shape=dim_shape,
 214        axes=axes,
 215        sampling=sampling,
 216    )
 217
 218    def _compute_ids(order: pxt.Integer, scheme: str, accuracy: pxt.Integer) -> list:
 219        """
 220        Computes the Finite difference indices according to the order, type and accuracy.
 221        """
 222        if scheme == "central":
 223            n_coefs = 2 * ((order + 1) // 2) - 1 + accuracy
 224            ids = np.arange(-(n_coefs // 2), n_coefs // 2 + 1, dtype=int)
 225        else:
 226            n_coefs = order + accuracy
 227            if scheme == "forward":
 228                ids = np.arange(0, n_coefs, dtype=int)
 229            elif scheme == "backward":
 230                ids = np.arange(-n_coefs + 1, 1, dtype=int)
 231            else:
 232                raise ValueError(
 233                    f"Incorrect value for variable 'type'. 'type' should be ['forward', 'backward', "
 234                    f"'central'], but got {scheme}."
 235                )
 236        return ids.tolist()
 237
 238    def _compute_coefficients(stencil_ids: cabc.Sequence, order: pxt.Integer, sampling: pxt.Real) -> pxt.NDArray:
 239        """
 240        Computes the finite difference coefficients based on the order and indices.
 241        """
 242        # vander doesn't allow precision specification
 243        stencil_mat = np.vander(
 244            np.array(stencil_ids),
 245            increasing=True,
 246        ).T
 247        vec = np.zeros(len(stencil_ids))
 248        vec[order] = math.factorial(order)
 249        coefs = np.linalg.solve(stencil_mat, vec)
 250        coefs /= sampling**order
 251        return coefs
 252
 253    # FILL COEFFICIENTS
 254    def _fill_coefs(i: pxt.Integer) -> tuple[list[pxt.NDArray], pxt.Integer]:
 255        r"""
 256        Defines kernel elements.
 257        """
 258        stencil_ids = _compute_ids(order=order[i], scheme=scheme[i], accuracy=accuracy[i])
 259        stencil_coefs = _compute_coefficients(stencil_ids=stencil_ids, order=order[i], sampling=sampling[i])
 260        center = stencil_ids.index(0)
 261        return stencil_ids, stencil_coefs, center
 262
 263    kernel, center = _create_kernel(dim_shape, axes, _fill_coefs)
 264    return kernel, center
 265
 266
 267def _GaussianDerivative(
 268    order: typ.Union[pxt.Integer, cabc.Sequence[pxt.Integer, ...]],
 269    dim_shape: pxt.NDArrayShape,
 270    sigma: typ.Union[pxt.Real, cabc.Sequence[pxt.Real, ...]],
 271    axes: pxt.NDArrayAxis = None,
 272    truncate: typ.Union[pxt.Real, cabc.Sequence[pxt.Real, ...]] = 3.0,
 273    sampling: typ.Union[pxt.Real, cabc.Sequence[pxt.Real, ...]] = 1,
 274):
 275    r"""
 276    Gaussian derivative base operator along a single dimension.
 277
 278    This class is used by :py:class:`~pyxu.operator.PartialDerivative`,
 279    :py:class:`~pyxu.operator.Gradient` and :py:class:`~pyxu.operator.Hessian`.
 280    See :py:class:`~pyxu.operator.PartialDerivative.gaussian_derivative` for documentation.
 281
 282    See Also
 283    --------
 284    :py:class:`~pyxu.operator._BaseDifferential`,
 285    :py:class:`~pyxu.operator._FiniteDifference`,
 286    :py:class:`~pyxu.operator.PartialDerivative`,
 287    :py:class:`~pyxu.operator.Gradient`,
 288    :py:class:`~pyxu.operator.Hessian`.
 289
 290    Parameters
 291    ----------
 292    order: Integer, list[Integer]
 293        Derivative order.
 294        If a single integer value is provided, then `axes` should be provided to indicate which
 295        dimension should be used for differentiation.
 296        If a tuple is provided, it should contain as many elements as number of dimensions in
 297        `axes`.
 298    dim_shape: pxt.NDArrayShape
 299        Shape of the input array.
 300    sigma: Real, list[Real]
 301        Standard deviation of the Gaussian kernel.
 302    axes: pxt.NDArrayAxis
 303        Axes to which apply the derivative.
 304        It maps the argument `order` to the specified dimensions of the input array.
 305        Defaults to None, assuming that the `order` argument has as many elements as dimensions of
 306        the input.
 307    truncate: Real, list[Real]
 308        Truncate the filter at this many standard deviations (at each side from the origin).
 309        Defaults to 3.0.
 310    sampling: Real, list[Real]
 311        Sampling step (i.e., the distance between two consecutive elements of an array).
 312        Defaults to 1.
 313    """
 314    diff_kwargs = {"sigma": sigma, "truncate": truncate}
 315    order, sampling, sigma, truncate, axes = _sanitize_init_kwargs(
 316        order=order,
 317        diff_method="gd",
 318        diff_kwargs=diff_kwargs,
 319        dim_shape=dim_shape,
 320        axes=axes,
 321        sampling=sampling,
 322    )
 323
 324    def _fill_coefs(i: pxt.Integer) -> tuple[cabc.Sequence[pxt.Integer], pxt.NDArray, pxt.Integer]:
 325        r"""
 326        Defines kernel elements.
 327        """
 328        # make the radius of the filter equal to `truncate` standard deviations
 329        sigma_pix = sigma[i] / sampling[i]  # Sigma rescaled to pixel units
 330        radius = int(truncate[i] * float(sigma_pix) + 0.5)
 331        stencil_coefs = _gaussian_kernel1d(sigma=sigma_pix, order=order[i], sampling=sampling[i], radius=radius)
 332        stencil_ids = [i for i in range(-radius, radius + 1)]
 333        return stencil_ids, stencil_coefs, radius
 334
 335    def _gaussian_kernel1d(
 336        sigma: pxt.Real,
 337        order: pxt.Integer,
 338        sampling: pxt.Real,
 339        radius: pxt.Integer,
 340    ) -> pxt.NDArray:
 341        """
 342        Computes a 1-D Gaussian convolution kernel.
 343        Wraps scipy.ndimage.filters._gaussian_kernel1d
 344        It flips the output because the original kernel is meant for convolution instead of correlation.
 345        """
 346        coefs = np.flip(scif._gaussian_kernel1d(sigma, order, radius))
 347        coefs /= sampling**order
 348        return coefs
 349
 350    kernel, center = _create_kernel(dim_shape, axes, _fill_coefs)
 351    return kernel, center
 352
 353
 354def _PartialDerivative(
 355    kernel: KernelSpec,
 356    center: KernelSpec,
 357    dim_shape: pxt.NDArrayShape,
 358    mode: ModeSpec = "constant",
 359    gpu: bool = False,
 360    dtype: typ.Optional[pxt.DType] = None,
 361    meta: typ.Optional[typ.NamedTuple] = None,
 362) -> pxt.OpT:
 363    r"""
 364    Helper base class for partial derivative operator based on Numba stencils (see
 365    https://numba.pydata.org/numba-doc/latest/user/stencil.html).
 366
 367    See Also
 368    --------
 369    :py:class:`~pyxu.operator.PartialDerivative`,
 370    :py:class:`~pyxu.operator.Stencil`,
 371    :py:class:`~pyxu.operator._FiniteDifference`,
 372    :py:class:`~pyxu.operator._GaussianDerivative`.
 373
 374    Parameters
 375    ----------
 376    kernel: KernelSpec
 377        Stencil coefficients.
 378        Two forms are accepted:
 379
 380            * NDArray of rank-:math:`D`: denotes a non-seperable stencil.
 381            * tuple[NDArray_1, ..., NDArray_D]: a sequence of 1D stencils such that is filtered by
 382              the stencil `kernel[d]` along the :math:`d`-th dimension.
 383    center: IndexSpec
 384        (i_1, ..., i_D) index of the stencil's center.
 385
 386        `center` defines how a kernel is overlaid on inputs to produce outputs.
 387
 388        .. math::
 389
 390           y[i_{1},\ldots,i_{D}]
 391           =
 392           \sum_{q_{1},\ldots,q_{D}=0}^{k_{1},\ldots,k_{D}}
 393           x[i_{1} - c_{1} + q_{1},\ldots,i_{D} - c_{D} + q_{D}]
 394           \,\cdot\,
 395           k[q_{1},\ldots,q_{D}]
 396    dim_shape: pxt.NDArrayShape
 397        Shape of the input array.
 398    mode: str, list[str]
 399        Boundary conditions.
 400        Multiple forms are accepted:
 401
 402        * str: unique mode shared amongst dimensions.
 403          Must be one of:
 404
 405          * 'constant' (default): zero-padding
 406          * 'wrap'
 407          * 'reflect'
 408          * 'symmetric'
 409          * 'edge'
 410        * tuple[str, ...]: the `d`-th dimension uses ``mode[d]`` as boundary condition.
 411
 412        (See :py:func:`numpy.pad` for details.)
 413    gpu: bool
 414        Input NDArray type (`True` for GPU, `False` for CPU). Defaults to `False`.
 415    dtype: DType
 416        Working precision of the linear operator.
 417    meta: typ.NamedTuple
 418        Partial derivative metadata describing:
 419
 420        * Diff_method
 421        * Sampling
 422        * Scheme (for finite differences)
 423        * Accuracy (for finite differences)
 424        * Sigma (for Gaussian derivative)
 425        * Truncate (for Gaussian derivative)
 426    """
 427    if dtype is None:
 428        dtype = pxrt.Width.DOUBLE.value
 429
 430    if gpu:
 431        assert pxd.CUPY_ENABLED
 432        import cupy as xp
 433    else:
 434        import numpy as xp
 435
 436    if isinstance(kernel, cabc.MutableSequence):
 437        for i in range(len(kernel)):
 438            kernel[i] = xp.array(kernel[i], dtype=dtype)
 439    else:
 440        kernel = xp.array(kernel, dtype=dtype)
 441
 442    op = pxls.Stencil(dim_shape=dim_shape, kernel=kernel, center=center, mode=mode)
 443    setattr(op, "meta", meta)
 444
 445    return op
 446
 447
[docs] 448class PartialDerivative: 449 r""" 450 Partial derivative operator based on `Numba stencils 451 <https://numba.pydata.org/numba-doc/latest/user/stencil.html>`_. 452 453 Notes 454 ----- 455 * This operator approximates the partial derivative of a :math:`D`-dimensional signal :math:`\mathbf{f} \in 456 \mathbb{R}^{N_0 \times \cdots \times N_{D-1}}` 457 458 .. math:: 459 460 \frac{\partial^{n} \mathbf{f}}{\partial x_0^{n_0} \cdots \partial x_{D-1}^{n_{D-1}}} \in 461 \mathbb{R}^{N_0 \times \cdots \times N_{D-1}} 462 463 where :math:`\frac{\partial^{n_i}}{\partial x_i^{n_i}}` is the :math:`n_i`-th order partial derivative along 464 dimension :math:`i` and :math:`n = \prod_{i=0}^{D-1} n_{i}` is the total derivative order. 465 466 Partial derivatives can be implemented with `finite differences 467 <https://en.wikipedia.org/wiki/Finite_difference>`_ via the 468 :py:meth:`~pyxu.operator.PartialDerivative.finite_difference` constructor, or with the `Gaussian 469 derivative <https://www.crisluengo.net/archives/22/>`_ via the 470 :py:meth:`~pyxu.operator.PartialDerivative.gaussian_derivative` constructor. 471 472 * When using the :py:meth:`~pyxu.operator.PartialDerivative.finite_difference` constructor, the adjoint 473 of the resulting linear operator will vary depending on the type of finite differences: 474 475 * For ``forward`` type, the adjoint corresponds to: 476 477 :math:`(\frac{\partial^{\text{fwd}}}{\partial x})^{\ast} = -\frac{\partial^{\text{bwd}}}{\partial x}` 478 479 * For ``backward`` type, the adjoint corresponds to: 480 481 :math:`(\frac{\partial^{\text{bwd}}}{\partial x})^{\ast} = -\frac{\partial^{\text{fwd}}}{\partial x}` 482 483 * For ``central`` type, and for the :py:meth:`~pyxu.operator.PartialDerivative.gaussian_derivative` 484 constructor, the adjoint corresponds to: 485 486 :math:`(\frac{\partial}{\partial x})^{\ast} = -\frac{\partial}{\partial x}` 487 488 489 .. warning:: 490 491 When dealing with high-order partial derivatives, the stencils required to compute them can become large, 492 resulting in computationally expensive evaluations. 493 In such scenarios, it can be more efficient to construct the partial derivative through a composition of 494 lower-order partial derivatives. 495 496 See Also 497 -------- 498 :py:class:`~pyxu.operator.Gradient`, 499 :py:class:`~pyxu.operator.Laplacian`, 500 :py:class:`~pyxu.operator.Hessian`. 501 """ 502
[docs] 503 @staticmethod 504 def finite_difference( 505 dim_shape: pxt.NDArrayShape, 506 order: cabc.Sequence[pxt.Integer, ...], 507 scheme: typ.Union[str, cabc.Sequence[str, ...]] = "forward", 508 accuracy: typ.Union[pxt.Integer, cabc.Sequence[pxt.Integer, ...]] = 1, 509 mode: ModeSpec = "constant", 510 gpu: bool = False, 511 dtype: typ.Optional[pxt.DType] = None, 512 sampling: typ.Union[pxt.Real, cabc.Sequence[pxt.Real, ...]] = 1, 513 ) -> pxt.OpT: 514 r""" 515 Compute partial derivatives for multi-dimensional signals using finite differences. 516 517 Parameters 518 ---------- 519 dim_shape: NDArrayShape 520 (N_1,...,N_D) input dimensions. 521 order: list[Integer] 522 Derivative order for each dimension. 523 The total order of the partial derivative is the sum of the elements of the tuple. 524 Use zeros to indicate dimensions in which the derivative should not be computed. 525 scheme: str, list[str] 526 Type of finite differences: ['forward, 'backward, 'central']. 527 Defaults to 'forward'. 528 If a string is provided, the same `scheme` is assumed for all dimensions. 529 If a tuple is provided, it should have as many elements as `order`. 530 accuracy: Integer, list[Integer] 531 Determines the number of points used to approximate the derivative with finite differences (see `Notes`). 532 Defaults to 1. 533 If an int is provided, the same `accuracy` is assumed for all dimensions. 534 If a tuple is provided, it should have as many elements as `dim_shape`. 535 mode: str, list[str] 536 Boundary conditions. 537 Multiple forms are accepted: 538 539 * str: unique mode shared amongst dimensions. 540 Must be one of: 541 542 * 'constant' (default): zero-padding 543 * 'wrap' 544 * 'reflect' 545 * 'symmetric' 546 * 'edge' 547 * tuple[str, ...]: the `d`-th dimension uses ``mode[d]`` as boundary condition. 548 549 (See :py:func:`numpy.pad` for details.) 550 gpu: bool 551 Input NDArray type (`True` for GPU, `False` for CPU). 552 Defaults to `False`. 553 dtype: DType 554 Working precision of the linear operator. 555 sampling: Real, list[Real] 556 Sampling step (i.e. distance between two consecutive elements of an array). 557 Defaults to 1. 558 559 Returns 560 ------- 561 op: OpT 562 Partial derivative 563 564 Notes 565 ----- 566 We explain here finite differences for one-dimensional signals; this operator performs finite differences for 567 multi-dimensional signals along dimensions specified by `order`. 568 569 This operator approximates derivatives with `finite differences 570 <https://en.wikipedia.org/wiki/Finite_difference>`_. 571 It is inspired by the `Finite Difference Coefficients Calculator 572 <https://web.media.mit.edu/~crtaylor/calculator.html>`_ to construct finite-difference approximations for the 573 desired *(i)* derivative order, *(ii)* approximation accuracy, and *(iii)* finite difference type. 574 Three basic types of finite differences are supported, which lead to the following first-order (``order = 1``) 575 operators with ``accuracy = 1`` and sampling step ``sampling = h`` for one-dimensional signals: 576 577 - **Forward difference**: 578 Approximates the continuous operator :math:`D_{F}f(x) = \frac{f(x+h) - f(x)}{h}` with the discrete operator 579 580 .. math:: 581 582 \mathbf{D}_{F} f [n] = \frac{f[n+1] - f[n]}{h}, 583 584 whose kernel is :math:`d = \frac{1}{h}[-1, 1]` and center is (0, ). 585 586 - **Backward difference**: 587 Approximates the continuous operator :math:`D_{B}f(x) = \frac{f(x) - f(x-h)}{h}` with the discrete operator 588 589 .. math:: 590 591 \mathbf{D}_{B} f [n] = \frac{f[n] - f[n-1]}{h}, 592 593 whose kernel is :math:`d = \frac{1}{h}[-1, 1]` and center is (1, ). 594 595 - **Central difference**: 596 Approximates the continuous operator :math:`D_{C}f(x) = \frac{f(x+h) - f(x-h)}{2h}` with the discrete operator 597 598 .. math:: 599 600 \mathbf{D}_{C} f [n] = \frac{f[n+1] - f[n-1]}{2h}, 601 602 whose kernel is :math:`d = \frac{1}{h}[-\frac12, 0, \frac12]` and center is (1, ). 603 604 .. warning:: 605 606 For forward and backward differences, higher-order operators correspond to the composition of first-order 607 operators. 608 This is not the case for central differences: the second-order continuous operator is given by 609 :math:`D^2_{C}f(x) = \frac{f(x+h) - 2 f(x) + f(x-h)}{h}`, hence :math:`D^2_{C} \neq D_{C} \circ D_{C}`. 610 The corresponding discrete operator is given by :math:`\mathbf{D}^2_{C} f [n] = \frac{f[n+1] - 2 f[n] + 611 f[n-1]}{h}`, whose kernel is :math:`d = \frac{1}{h}[1, -2, 1]` and center is (1, ). 612 We refer to `this paper 613 <https://www.ams.org/journals/mcom/1988-51-184/S0025-5718-1988-0935077-0/S0025-5718-1988-0935077-0.pdf>`_ for 614 more details. 615 616 For a given derivative order :math:`N\in\mathbb{Z}^{+}` and accuracy :math:`a\in\mathbb{Z}^{+}`, the size 617 :math:`N_s` of the stencil kernel :math:`d` used for finite differences is given by: 618 619 - For central differences: :math:`N_s = 2 \lfloor\frac{N + 1}{2}\rfloor - 1 + a` 620 621 - For forward and backward differences: :math:`N_s = N + a` 622 623 For :math:`N_s` given support indices :math:`\{s_1, \ldots , s_{N_s} \} \subset \mathbb{Z}` and a derivative 624 order :math:`N<N_s`, the stencil kernel :math:`d = [d_1, \ldots, d_{N_s}]` of the finite-difference 625 approximation of the derivative is obtained by solving the following system of linear equations (see the `Finite 626 Difference Coefficients Calculator <https://web.media.mit.edu/~crtaylor/calculator.html>`_ documentation): 627 628 .. admonition:: Remark 629 630 The number of coefficients of the finite-difference kernel is chosen to guarantee the requested accuracy, but 631 might be larger than requested accuracy. 632 For example, if choosing `scheme='central'` with `accuracy=1`, it will create a kernel corresponding to 633 `accuracy=2`, as it is the minimum accuracy possible for such scheme (see the `finite difference coefficient 634 table <https://en.wikipedia.org/wiki/Finite_difference_coefficient>`_). 635 636 .. math:: 637 638 \left(\begin{array}{ccc} 639 s_{1}^{0} & \cdots & s_{N_s}^{0} \\ 640 \vdots & \ddots & \vdots \\ 641 s_{1}^{N_s-1} & \cdots & s_{N_s}^{N_s-1} 642 \end{array}\right)\left(\begin{array}{c} 643 d_{1} \\ 644 \vdots \\ 645 d_{N_s} 646 \end{array}\right)= \frac{1}{h^{N}}\left(\begin{array}{c} 647 \delta_{0, N} \\ 648 \vdots \\ 649 \delta_{i, N} \\ 650 \vdots \\ 651 \delta_{N_s-1, N} 652 \end{array}\right), 653 654 where :math:`\delta_{i, j}` is the Kronecker delta. 655 656 This class inherits its methods from :py:class:`~pyxu.operator.Stencil`. 657 658 Example 659 ------- 660 661 .. plot:: 662 663 import numpy as np 664 import matplotlib.pyplot as plt 665 from pyxu.operator import PartialDerivative 666 from pyxu.util.misc import peaks 667 668 x = np.linspace(-2.5, 2.5, 25) 669 xx, yy = np.meshgrid(x, x) 670 image = peaks(xx, yy) 671 dim_shape = image.shape # Shape of our image 672 # Specify derivative order at each direction 673 df_dx = (1, 0) # Compute derivative of order 1 in first dimension 674 d2f_dy2 = (0, 2) # Compute derivative of order 2 in second dimension 675 d3f_dxdy2 = (1, 2) # Compute derivative of order 1 in first dimension and der. of order 2 in second dimension 676 # Instantiate derivative operators 677 sigma = 2.0 678 diff1 = PartialDerivative.gaussian_derivative(order=df_dx, dim_shape=dim_shape, sigma=sigma / np.sqrt(2)) 679 diff2 = PartialDerivative.gaussian_derivative(order=d2f_dy2, dim_shape=dim_shape, sigma=sigma / np.sqrt(2)) 680 diff = PartialDerivative.gaussian_derivative(order=d3f_dxdy2, dim_shape=dim_shape, sigma=sigma) 681 # Compute derivatives 682 out1 = (diff1 * diff2)(image) 683 out2 = diff(image) 684 # Plot derivatives 685 fig, axs = plt.subplots(1, 3, figsize=(15, 4)) 686 im = axs[0].imshow(image) 687 axs[0].axis("off") 688 axs[0].set_title("f(x,y)") 689 plt.colorbar(im, ax=axs[0]) 690 axs[1].imshow(out1) 691 axs[1].axis("off") 692 axs[1].set_title(r"$\frac{\partial^{3} f(x,y)}{\partial x\partial y^{2}}$") 693 plt.colorbar(im, ax=axs[1]) 694 695 axs[2].imshow(out2) 696 axs[2].axis("off") 697 axs[2].set_title(r"$\frac{\partial^{3} f(x,y)}{\partial x\partial y^{2}}$") 698 plt.colorbar(im, ax=axs[2]) 699 700 # Check approximation error 701 plt.figure() 702 plt.imshow(abs(out1 - out2)), plt.colorbar() 703 704 """ 705 assert isinstance(order, cabc.Sequence), "`order` should be a tuple / list" 706 dim_shape = pxu.as_canonical_shape(dim_shape) 707 assert len(order) == len(dim_shape) 708 diff_kwargs = {"scheme": scheme, "accuracy": accuracy} 709 order, sampling, scheme, accuracy, _ = _sanitize_init_kwargs( 710 order=order, 711 diff_method="fd", 712 diff_kwargs=diff_kwargs, 713 dim_shape=dim_shape, 714 sampling=sampling, 715 ) 716 717 # Compute a kernel for each axis 718 kernel = [np.array(1)] * len(dim_shape) 719 center = np.zeros(len(dim_shape), dtype=int) 720 for ax in range(len(dim_shape)): 721 if order[ax] > 0: 722 k, c = _FiniteDifference( 723 order=order[ax], 724 dim_shape=dim_shape, 725 scheme=scheme[ax], 726 axes=ax, 727 accuracy=accuracy[ax], 728 sampling=sampling[ax], 729 ) 730 kernel[ax] = k[ax] 731 center[ax] = c[ax] 732 733 meta = PDMetaFD(sampling=sampling, scheme=scheme, accuracy=accuracy) 734 735 return _PartialDerivative( 736 kernel=kernel, 737 center=center, 738 dim_shape=dim_shape, 739 mode=mode, 740 gpu=gpu, 741 dtype=dtype, 742 meta=meta, 743 )
744
[docs] 745 @staticmethod 746 def gaussian_derivative( 747 dim_shape: pxt.NDArrayShape, 748 order: cabc.Sequence[pxt.Integer, ...], 749 sigma: typ.Union[pxt.Real, cabc.Sequence[pxt.Real, ...]] = 1.0, 750 truncate: typ.Union[pxt.Real, cabc.Sequence[pxt.Real, ...]] = 3.0, 751 mode: ModeSpec = "constant", 752 gpu: bool = False, 753 dtype: typ.Optional[pxt.DType] = None, 754 sampling: typ.Union[pxt.Real, cabc.Sequence[pxt.Real, ...]] = 1, 755 ) -> pxt.OpT: 756 r""" 757 Compute partial derivatives for multi-dimensional signals using gaussian derivatives. 758 759 Parameters 760 ---------- 761 dim_shape: NDArrayShape 762 (N_1,...,N_D) input dimensions. 763 order: list[Integer] 764 Derivative order for each dimension. 765 The total order of the partial derivative is the sum of the elements of the tuple. 766 Use zeros to indicate dimensions in which the derivative should not be computed. 767 sigma: Real, list[Real] 768 Standard deviation for the Gaussian kernel. 769 Defaults to 1.0. 770 If a float is provided, the same `sigma` is assumed for all dimensions. 771 If a tuple is provided, it should have as many elements as `order`. 772 truncate: Real, list[Real] 773 Truncate the filter at this many standard deviations (at each side from the origin). 774 Defaults to 3.0. 775 If a float is provided, the same `truncate` is assumed for all dimensions. 776 If a tuple is provided, it should have as many elements as `order`. 777 mode: str, list[str] 778 Boundary conditions. 779 Multiple forms are accepted: 780 781 * str: unique mode shared amongst dimensions. 782 Must be one of: 783 784 * 'constant' (default): zero-padding 785 * 'wrap' 786 * 'reflect' 787 * 'symmetric' 788 * 'edge' 789 * tuple[str, ...]: the `d`-th dimension uses ``mode[d]`` as boundary condition. 790 791 (See :py:func:`numpy.pad` for details.) 792 gpu: bool 793 Input NDArray type (`True` for GPU, `False` for CPU). 794 Defaults to `False`. 795 dtype: DType 796 Working precision of the linear operator. 797 sampling: Real, list[Real] 798 Sampling step (i.e., the distance between two consecutive elements of an array). 799 Defaults to 1. 800 801 Returns 802 ------- 803 op: OpT 804 Partial derivative. 805 806 Notes 807 ----- 808 We explain here Gaussian derivatives for one-dimensional signals; this operator performs partial Gaussian 809 derivatives for multi-dimensional signals along dimensions specified by ``order``. 810 811 A Gaussian derivative is an approximation of a derivative that consists in convolving the input function with a 812 Gaussian function :math:`g` before applying a derivative. 813 In the continuous domain, the :math:`N`-th order Gaussian derivative :math:`D^N_G` amounts to a convolution with 814 the :math:`N`-th order derivative of :math:`g`: 815 816 .. math:: 817 818 D^N_G f (x) 819 = 820 \frac{\mathrm{d}^N (f * g) }{\mathrm{d} x^N} (x) 821 = 822 f(x) * \frac{\mathrm{d}^N g}{\mathrm{d} x^N} (x). 823 824 For discrete signals :math:`f[n]`, this operator is approximated by 825 826 .. math:: 827 828 \mathbf{D}^N_G f [n] 829 = 830 f[n] *\frac{\mathrm{d}^N g}{\mathrm{d} x^N} \left(\frac{n}{h}\right), 831 832 where :math:`h` is the spacing between samples and the operator :math:`*` is now a discrete convolution. 833 834 .. warning:: 835 836 The operator :math:`\mathbf{D}_{G} \circ \mathbf{D}_{G}` is not directly related to 837 :math:`\mathbf{D}_{G}^{2}`: Gaussian smoothing is performed twice in the case of the former, whereas it is 838 performed only once in the case of the latter. 839 840 Note that in contrast with finite differences (see 841 :py:meth:`~pyxu.operator.PartialDerivative.finite_difference`), Gaussian derivatives compute exact 842 derivatives in the continuous domain, since Gaussians can be differentiated analytically. 843 This derivative is then sampled in order to perform a discrete convolution. 844 845 This class inherits its methods from :py:class:`~pyxu.operator.Stencil`. 846 847 Example 848 ------- 849 850 .. plot:: 851 852 import numpy as np 853 import matplotlib.pyplot as plt 854 from pyxu.operator import PartialDerivative 855 from pyxu.util.misc import peaks 856 857 x = np.linspace(-2.5, 2.5, 25) 858 xx, yy = np.meshgrid(x, x) 859 image = peaks(xx, yy) 860 dim_shape = image.shape # Shape of our image 861 # Specify derivative order at each direction 862 df_dx = (1, 0) # Compute derivative of order 1 in first dimension 863 d2f_dy2 = (0, 2) # Compute derivative of order 2 in second dimension 864 d3f_dxdy2 = (1, 2) # Compute derivative of order 1 in first dimension and der. of order 2 in second dimension 865 # Instantiate derivative operators 866 diff1 = PartialDerivative.gaussian_derivative(order=df_dx, dim_shape=dim_shape, sigma=2.0) 867 diff2 = PartialDerivative.gaussian_derivative(order=d2f_dy2, dim_shape=dim_shape, sigma=2.0) 868 diff = PartialDerivative.gaussian_derivative(order=d3f_dxdy2, dim_shape=dim_shape, sigma=2.0) 869 # Compute derivatives 870 out1 = (diff1 * diff2)(image) 871 out2 = diff(image) 872 plt.figure() 873 plt.imshow(image), 874 plt.axis('off') 875 plt.colorbar() 876 plt.title('f(x,y)') 877 plt.figure() 878 plt.imshow(out1.T) 879 plt.axis('off') 880 plt.title(r'$\frac{\partial^{3} f(x,y)}{\partial x\partial y^{2}}$') 881 plt.figure() 882 plt.imshow(out2.T) 883 plt.axis('off') 884 plt.title(r'$\frac{\partial^{3} f(x,y)}{\partial x\partial y^{2}}$') 885 886 """ 887 assert isinstance(order, cabc.Sequence), "`order` should be a tuple / list" 888 dim_shape = pxu.as_canonical_shape(dim_shape) 889 assert len(order) == len(dim_shape) 890 891 diff_kwargs = {"sigma": sigma, "truncate": truncate} 892 order, sampling, sigma, truncate, _ = _sanitize_init_kwargs( 893 order=order, 894 diff_method="gd", 895 diff_kwargs=diff_kwargs, 896 dim_shape=dim_shape, 897 sampling=sampling, 898 ) 899 900 # Compute a kernel for each axes 901 kernel = [np.array(1)] * len(dim_shape) 902 center = np.zeros(len(dim_shape), dtype=int) 903 for ax in range(len(dim_shape)): 904 k, c = _GaussianDerivative( 905 order=order[ax], 906 dim_shape=dim_shape, 907 sigma=sigma[ax], 908 axes=ax, 909 truncate=truncate[ax], 910 sampling=sampling[ax], 911 ) 912 kernel[ax] = k[ax] 913 center[ax] = c[ax] 914 915 meta = PDMetaGD(sampling=sampling, sigma=sigma, truncate=truncate) 916 917 return _PartialDerivative( 918 kernel=kernel, 919 center=center, 920 dim_shape=dim_shape, 921 mode=mode, 922 gpu=gpu, 923 dtype=dtype, 924 meta=meta, 925 )
926 927 928class _StackDiffHelper: 929 r""" 930 Helper class for Gradient and Hessian. 931 932 Defines a method for computing and stacking partial derivatives. 933 934 See Also 935 -------- 936 :py:class:`~pyxu.operator.Gradient`, 937 :py:class:`~pyxu.operator.Laplacian`, 938 :py:class:`~pyxu.operator.Hessian`. 939 """ 940 941 @staticmethod 942 def _stack_diff_ops( 943 dim_shape: pxt.NDArrayShape, 944 directions: pxt.NDArrayAxis, 945 diff_method: str, 946 order: typ.Union[pxt.Integer, cabc.Sequence[pxt.Integer, ...]], 947 param1: typ.Union[str, cabc.Sequence[str, ...], pxt.Real, cabc.Sequence[pxt.Real, ...]], 948 param2: typ.Union[pxt.Integer, cabc.Sequence[pxt.Integer, ...], pxt.Real, cabc.Sequence[pxt.Real, ...]], 949 mode: ModeSpec = "constant", 950 gpu: bool = False, 951 dtype: typ.Optional[pxt.DType] = None, 952 sampling: typ.Union[pxt.Real, cabc.Sequence[pxt.Real, ...]] = 1, 953 ) -> pxt.OpT: 954 if isinstance(mode, str): 955 mode = (mode,) 956 if isinstance(mode, cabc.Sequence): 957 if len(mode) != len(dim_shape): 958 assert len(mode) == 1 959 mode = mode * len(dim_shape) 960 961 else: 962 raise ValueError("mode has to be a string or a tuple") 963 dif_op = [] 964 for i in range(0, len(directions)): 965 _order = np.zeros_like(dim_shape) 966 _order[directions[i]] = order[i] 967 _param1 = param1 if not isinstance(param1[0], (list, tuple)) else param1[i] 968 _param2 = param2 if not isinstance(param2[0], (list, tuple)) else param2[i] 969 if diff_method == "fd": 970 dif_op.append( 971 PartialDerivative.finite_difference( 972 dim_shape=dim_shape, 973 order=tuple(_order), 974 scheme=_param1, 975 accuracy=_param2, 976 mode=mode, 977 gpu=gpu, 978 dtype=dtype, 979 sampling=sampling, 980 ) 981 ) 982 983 elif diff_method == "gd": 984 dif_op.append( 985 PartialDerivative.gaussian_derivative( 986 dim_shape=dim_shape, 987 order=tuple(_order), 988 sigma=param1, 989 truncate=param2, 990 mode=mode, 991 gpu=gpu, 992 dtype=dtype, 993 sampling=sampling, 994 ) 995 ) 996 997 def visualize(_) -> str: 998 r""" 999 Show the :math:`D`-dimensional stacked partial derivative kernels. 1000 1001 The kernel's center is identified by surrounding parentheses. 1002 1003 Example 1004 ------- 1005 .. code-block:: python3 1006 1007 from pyxu.operator import Hessian 1008 1009 H = Hessian( 1010 dim_shape=(5, 6), 1011 diff_method="fd", 1012 ) 1013 print(H.visualize()) # Direction 0 1014 # [[1.0] 1015 # [(-2.0)] 1016 # [1.0]] 1017 # 1018 # Direction 1 1019 # [[(1.0) -1.0] 1020 # [-1.0 1.0]] 1021 # 1022 # Direction 2 1023 # [[1.0 (-2.0) 1.0]] 1024 """ 1025 kernels = [] 1026 for direction, stencil in enumerate(_._ops): 1027 kernels.append(f"\nDirection {direction} \n" + stencil.visualize()) 1028 return "\n".join(kernels) 1029 1030 if diff_method == "fd": 1031 meta = PDMetaFD( 1032 sampling=[op.meta.sampling for op in dif_op], 1033 scheme=[op.meta.scheme for op in dif_op], 1034 accuracy=[op.meta.accuracy for op in dif_op], 1035 ) 1036 else: # diff_method == "gd" 1037 meta = PDMetaGD( 1038 sampling=[op.meta.sampling for op in dif_op], 1039 sigma=[op.meta.sigma for op in dif_op], 1040 truncate=[op.meta.truncate for op in dif_op], 1041 ) 1042 op = pxb.stack(dif_op) 1043 setattr(op, "visualize", types.MethodType(visualize, op)) 1044 setattr(op, "meta", meta) 1045 return op 1046 1047 @staticmethod 1048 def _check_directions_and_order( 1049 dim_shape: pxt.NDArrayShape, 1050 directions: typ.Union[ 1051 str, 1052 pxt.NDArrayAxis, 1053 cabc.Sequence[pxt.NDArrayAxis, ...], 1054 ], 1055 ) -> cabc.Sequence[cabc.Sequence[pxt.NDArrayAxis, ...], pxt.NDArrayAxis]: 1056 # Convert directions to canonical form 1057 def _check_directions(_directions): 1058 assert all(0 <= _ <= (len(dim_shape) - 1) for _ in _directions), ( 1059 "Direction values must be between 0 and " "the number of dimensions in `dim_shape`." 1060 ) 1061 1062 if not isinstance(directions, cabc.Sequence): 1063 # This corresponds to [mode 0] in `Notes` 1064 directions = [directions, directions] 1065 _check_directions(directions) 1066 directions = (directions,) 1067 else: 1068 if isinstance(directions, str): 1069 # This corresponds to [mode 3] in Hessian `Notes` 1070 assert directions == "all", ( 1071 "Value for `directions` not implemented. The accepted directions types are" 1072 "int, tuple or a str with the value `all`." 1073 ) 1074 directions = tuple( 1075 list(_) for _ in itertools.combinations_with_replacement(np.arange(len(dim_shape)).astype(int), 2) 1076 ) 1077 elif not isinstance(directions[0], cabc.Sequence): 1078 # This corresponds to [mode 1] in Hessian `Notes` 1079 assert len(directions) == 2, ( 1080 "If `directions` is a tuple, it should contain two elements, corresponding " 1081 "to the i-th an j-th elements (dx_i and dx_j)" 1082 ) 1083 directions = list(directions) 1084 _check_directions(directions) 1085 directions = (directions,) 1086 else: 1087 # This corresponds to [mode 2] in Hessian `Notes` 1088 for direction in directions: 1089 _check_directions(direction) 1090 1091 # Convert to canonical form for PartialDerivative (direction + order) 1092 _directions = [ 1093 list(direction) if (len(np.unique(direction)) == len(direction)) else np.unique(direction).tolist() 1094 for direction in directions 1095 ] 1096 1097 _order = [3 - len(np.unique(direction)) for direction in directions] 1098 1099 return _directions, _order 1100 1101
[docs] 1102def Gradient( 1103 dim_shape: pxt.NDArrayShape, 1104 directions: typ.Optional[pxt.NDArrayAxis] = None, 1105 diff_method: str = "fd", 1106 mode: ModeSpec = "constant", 1107 gpu: bool = False, 1108 dtype: typ.Optional[pxt.DType] = None, 1109 **diff_kwargs, 1110) -> pxt.OpT: 1111 r""" 1112 Gradient operator. 1113 1114 Notes 1115 ----- 1116 1117 This operator stacks the first-order partial derivatives of a :math:`D`-dimensional signal :math:`\mathbf{f} \in 1118 \mathbb{R}^{N_{0} \times \cdots \times N_{D-1}}` along each dimension: 1119 1120 .. math:: 1121 1122 \boldsymbol{\nabla} \mathbf{f} = \begin{bmatrix} 1123 \frac{\partial \mathbf{f}}{\partial x_0} \\ 1124 \vdots \\ 1125 \frac{\partial \mathbf{f}}{\partial x_{D-1}} 1126 \end{bmatrix} \in \mathbb{R}^{D \times N_{0} \times \cdots \times N_{D-1}} 1127 1128 The partial derivatives can be approximated by `finite differences 1129 <https://en.wikipedia.org/wiki/Finite_difference>`_ via the 1130 :py:meth:`~pyxu.operator.PartialDerivative.finite_difference` constructor or by the `Gaussian derivative 1131 <https://www.crisluengo.net/archives/22/>`_ via 1132 :py:meth:`~pyxu.operator.PartialDerivative.gaussian_derivative` constructor. 1133 The parametrization of the partial derivatives can be done via the keyword arguments `\*\*diff_kwargs`, which will 1134 default to the same values as the :py:class:`~pyxu.operator.PartialDerivative` constructor. 1135 1136 Parameters 1137 ---------- 1138 dim_shape: NDArrayShape 1139 (N_1,...,N_D) input dimensions. 1140 directions: Integer, list[Integer], None 1141 Gradient directions. 1142 Defaults to `None`, which computes the gradient for all directions. 1143 diff_method: 'gd', 'fd' 1144 Method used to approximate the derivative. 1145 Must be one of: 1146 1147 * 'fd' (default): finite differences 1148 * 'gd': Gaussian derivative 1149 mode: str, list[str] 1150 Boundary conditions. 1151 Multiple forms are accepted: 1152 1153 * str: unique mode shared amongst dimensions. 1154 Must be one of: 1155 1156 * 'constant' (default): zero-padding 1157 * 'wrap' 1158 * 'reflect' 1159 * 'symmetric' 1160 * 'edge' 1161 * tuple[str, ...]: the `d`-th dimension uses ``mode[d]`` as boundary condition. 1162 1163 (See :py:func:`numpy.pad` for details.) 1164 gpu: bool 1165 Input NDArray type (`True` for GPU, `False` for CPU). 1166 Defaults to `False`. 1167 dtype: DType 1168 Working precision of the linear operator. 1169 diff_kwargs: dict 1170 Keyword arguments to parametrize partial derivatives (see 1171 :py:meth:`~pyxu.operator.PartialDerivative.finite_difference` and 1172 :py:meth:`~pyxu.operator.PartialDerivative.gaussian_derivative`) 1173 1174 Returns 1175 ------- 1176 op: OpT 1177 Gradient 1178 1179 Example 1180 ------- 1181 1182 .. plot:: 1183 1184 import numpy as np 1185 import matplotlib.pyplot as plt 1186 from pyxu.operator import Gradient 1187 from pyxu.util.misc import peaks 1188 1189 # Define input image 1190 n = 100 1191 x = np.linspace(-3, 3, n) 1192 xx, yy = np.meshgrid(x, x) 1193 image = peaks(xx, yy) 1194 dim_shape = image.shape # (1000, 1000) 1195 # Instantiate gradient operator 1196 grad = Gradient(dim_shape=dim_shape) 1197 1198 # Compute gradients 1199 df_dx, df_dy = grad(image) # shape = (2, 1000, 1000) 1200 1201 # Plot image 1202 fig, axs = plt.subplots(1, 3, figsize=(15, 4)) 1203 im = axs[0].imshow(image) 1204 axs[0].set_title("Image") 1205 axs[0].axis("off") 1206 plt.colorbar(im, ax=axs[0]) 1207 1208 # Plot gradient 1209 im = axs[1].imshow(df_dx) 1210 axs[1].set_title(r"$\partial f/ \partial x$") 1211 axs[1].axis("off") 1212 plt.colorbar(im, ax=axs[1]) 1213 im = axs[2].imshow(df_dy) 1214 axs[2].set_title(r"$\partial f/ \partial y$") 1215 axs[2].axis("off") 1216 plt.colorbar(im, ax=axs[2]) 1217 1218 See Also 1219 -------- 1220 :py:func:`~pyxu.operator.PartialDerivative`, 1221 :py:func:`~pyxu.operator.Jacobian`. 1222 """ 1223 dim_shape = pxu.as_canonical_shape(dim_shape) 1224 directions = tuple([i for i in range(len(dim_shape))]) if directions is None else directions 1225 axes = tuple([i for i in range(len(dim_shape)) if i in directions]) 1226 order, sampling, param1, param2, _ = _sanitize_init_kwargs( 1227 order=(1,) * len(directions), 1228 axes=axes, 1229 dim_shape=dim_shape, 1230 sampling=diff_kwargs.get("sampling", 1.0), 1231 diff_method=diff_method, 1232 diff_kwargs=diff_kwargs, 1233 ) 1234 op = _StackDiffHelper._stack_diff_ops( 1235 dim_shape=dim_shape, 1236 directions=directions, 1237 diff_method=diff_method, 1238 order=order, 1239 param1=param1, 1240 param2=param2, 1241 mode=mode, 1242 gpu=gpu, 1243 dtype=dtype, 1244 sampling=sampling, 1245 ) 1246 op._name = "Gradient" 1247 return op
1248 1249
[docs] 1250def Jacobian( 1251 dim_shape: pxt.NDArrayShape, 1252 directions: typ.Optional[pxt.NDArrayAxis] = None, 1253 diff_method: str = "fd", 1254 mode: ModeSpec = "constant", 1255 gpu: bool = False, 1256 dtype: typ.Optional[pxt.DType] = None, 1257 **diff_kwargs, 1258) -> pxt.OpT: 1259 r""" 1260 Jacobian operator. 1261 1262 Notes 1263 ----- 1264 1265 This operator computes the first-order partial derivatives of a :math:`D`-dimensional vector-valued signal of 1266 :math:`C` variables or channels :math:`\mathbf{f} = [\mathbf{f}_{0}, \ldots, \mathbf{f}_{C-1}]` with 1267 :math:`\mathbf{f}_{c} \in \mathbb{R}^{N_{0} \times \cdots \times N_{D-1}}`. 1268 1269 The Jacobian of :math:`\mathbf{f}` is computed via the gradient as follows: 1270 1271 .. math:: 1272 1273 \mathbf{J} \mathbf{f} = \begin{bmatrix} 1274 (\boldsymbol{\nabla} \mathbf{f}_{0})^{\top} \\ 1275 \vdots \\ 1276 (\boldsymbol{\nabla} \mathbf{f}_{C-1})^{\top} \\ 1277 \end{bmatrix} \in \mathbb{R}^{C \times D \times N_0 \times \cdots \times N_{D-1}} 1278 1279 The partial derivatives can be approximated by `finite differences 1280 <https://en.wikipedia.org/wiki/Finite_difference>`_ via the 1281 :py:meth:`~pyxu.operator.PartialDerivative.finite_difference` constructor or by the `Gaussian derivative 1282 <https://www.crisluengo.net/archives/22/>`_ via 1283 :py:meth:`~pyxu.operator.PartialDerivative.gaussian_derivative` constructor. 1284 The parametrization of the partial derivatives can be done via the keyword arguments `\*\*diff_kwargs`, which will 1285 default to the same values as the :py:class:`~pyxu.operator.PartialDerivative` constructor. 1286 1287 **Remark** 1288 1289 Pyxu's convention when it comes to field-vectors, is to work with vectorized arrays. However, the memory order of 1290 these arrays should be `[S_0, ..., S_B, C, N_1, ..., N_D]` shape, with `S_0, ..., S_B` being stacking or batching 1291 dimensions, `C` being the number of variables or channels, and `N_i` being the size of the `i`-th axis of the domain. 1292 1293 Parameters 1294 ---------- 1295 dim_shape: NDArrayShape 1296 (C, N_1,...,N_D) input dimensions. 1297 directions: Integer, list[Integer], None 1298 Gradient directions. 1299 Defaults to `None`, which computes the gradient for all directions. 1300 diff_method: "gd", "fd" 1301 Method used to approximate the derivative. 1302 Must be one of: 1303 1304 * 'fd' (default): finite differences 1305 * 'gd': Gaussian derivative 1306 mode: str, list[str] 1307 Boundary conditions. 1308 Multiple forms are accepted: 1309 1310 * str: unique mode shared amongst dimensions. 1311 Must be one of: 1312 1313 * 'constant' (default): zero-padding 1314 * 'wrap' 1315 * 'reflect' 1316 * 'symmetric' 1317 * 'edge' 1318 * tuple[str, ...]: the `d`-th dimension uses ``mode[d]`` as boundary condition. 1319 1320 (See :py:func:`numpy.pad` for details.) 1321 gpu: bool 1322 Input NDArray type (`True` for GPU, `False` for CPU). 1323 Defaults to `False`. 1324 dtype: DType 1325 Working precision of the linear operator. 1326 diff_kwargs: dict 1327 Keyword arguments to parametrize partial derivatives (see 1328 :py:meth:`~pyxu.operator.PartialDerivative.finite_difference` and 1329 :py:meth:`~pyxu.operator.PartialDerivative.gaussian_derivative`) 1330 1331 Returns 1332 ------- 1333 op: OpT 1334 Jacobian 1335 1336 Example 1337 ------- 1338 1339 .. plot:: 1340 1341 import numpy as np 1342 import matplotlib.pyplot as plt 1343 from pyxu.operator import Jacobian 1344 from pyxu.util.misc import peaks 1345 1346 x = np.linspace(-2.5, 2.5, 25) 1347 xx, yy = np.meshgrid(x, x) 1348 image = np.tile(peaks(xx, yy), (3, 1, 1)) 1349 jac = Jacobian(dim_shape=image.shape) 1350 out = jac(image) 1351 fig, axes = plt.subplots(3, 2, figsize=(10, 15)) 1352 for i in range(3): 1353 for j in range(2): 1354 axes[i, j].imshow(out[i, j].T, cmap=["Reds", "Greens", "Blues"][i]) 1355 axes[i, j].set_title(f"$\partial I_{{{['R', 'G', 'B'][j]}}}/\partial{{{['x', 'y'][j]}}}$") 1356 plt.suptitle("Jacobian") 1357 1358 1359 See Also 1360 -------- 1361 :py:func:`~pyxu.operator.Gradient`, 1362 :py:func:`~pyxu.operator.PartialDerivative`. 1363 """ 1364 1365 from collections.abc import Iterable 1366 1367 if directions is not None: 1368 if not isinstance(directions, Iterable): 1369 directions = [ 1370 directions, 1371 ] 1372 else: 1373 if isinstance(directions, tuple): 1374 directions = list(directions) 1375 directions = tuple([d - 1 for d in directions]) 1376 1377 dim_shape = pxu.as_canonical_shape(dim_shape) 1378 init_kwargs = dict( 1379 dim_shape=dim_shape[1:], 1380 directions=directions, 1381 diff_method=diff_method, 1382 mode=mode, 1383 gpu=gpu, 1384 dtype=dtype, 1385 **diff_kwargs, 1386 ) 1387 1388 grad = Gradient(**init_kwargs) 1389 n_channels = dim_shape[0] 1390 if n_channels > 1: 1391 op = pxb.block_diag( 1392 [ 1393 grad, 1394 ] 1395 * n_channels 1396 ) 1397 else: 1398 op = grad 1399 op._name = "Jacobian" 1400 return op
1401 1402
[docs] 1403def Divergence( 1404 dim_shape: pxt.NDArrayShape, 1405 directions: typ.Optional[pxt.NDArrayAxis] = None, 1406 diff_method: str = "fd", 1407 mode: ModeSpec = "constant", 1408 gpu: bool = False, 1409 dtype: typ.Optional[pxt.DType] = None, 1410 **diff_kwargs, 1411) -> pxt.OpT: 1412 r""" 1413 Divergence operator. 1414 1415 Notes 1416 ----- 1417 1418 This operator computes the expansion or outgoingness of a :math:`D`-dimensional vector-valued signal of :math:`C` 1419 variables :math:`\mathbf{f} = [\mathbf{f}_{0}, \ldots, \mathbf{f}_{C-1}]` with :math:`\mathbf{f}_{c} \in 1420 \mathbb{R}^{N_{0} \times \cdots \times N_{D-1}}`. 1421 1422 The Divergence of :math:`\mathbf{f}` is computed via the adjoint of the gradient as follows: 1423 1424 .. math:: 1425 1426 \operatorname{Div} \mathbf{f} = \boldsymbol{\nabla}^{\ast} \mathbf{f} 1427 = \begin{bmatrix} 1428 \frac{\partial \mathbf{f}}{\partial x_0} + \cdots + \frac{\partial \mathbf{f}}{\partial x_{D-1}} 1429 \end{bmatrix} \in \mathbb{R}^{N_{0} \times \cdots \times N_{D-1}} 1430 1431 The partial derivatives can be approximated by `finite differences 1432 <https://en.wikipedia.org/wiki/Finite_difference>`_ via the 1433 :py:meth:`~pyxu.operator.PartialDerivative.finite_difference` constructor or by the `Gaussian derivative 1434 <https://www.crisluengo.net/archives/22/>`_ via 1435 :py:meth:`~pyxu.operator.PartialDerivative.gaussian_derivative` constructor. 1436 The parametrization of the partial derivatives can be done via the keyword arguments `\*\*diff_kwargs`, which will 1437 default to the same values as the :py:class:`~pyxu.operator.PartialDerivative` constructor. 1438 1439 1440 When using finite differences to compute the Divergence (i.e., ``diff_method = "fd"``), the divergence returns the 1441 adjoint of the gradient in reversed order: 1442 1443 * For ``forward`` type divergence, the adjoint of the gradient of "backward" type is used. 1444 * For ``backward`` type divergence, the adjoint of the gradient of "forward" type is used. 1445 1446 For ``central`` type divergence, and for the Gaussian derivative method (i.e., ``diff_method = "gd"``), the adjoint 1447 of the gradient of "central" type is used (no reversed order). 1448 1449 1450 Parameters 1451 ---------- 1452 dim_shape: NDArrayShape 1453 (C, N_1,...,N_D) input dimensions. 1454 directions: Integer, list[Integer], None 1455 Divergence directions. 1456 Defaults to `None`, which computes the divergence for all directions. 1457 diff_method: "gd", "fd" 1458 Method used to approximate the derivative. 1459 Must be one of: 1460 1461 * 'fd' (default): finite differences 1462 * 'gd': Gaussian derivative 1463 mode: str, list[str] 1464 Boundary conditions. 1465 Multiple forms are accepted: 1466 1467 * str: unique mode shared amongst dimensions. 1468 Must be one of: 1469 1470 * 'constant' (default): zero-padding 1471 * 'wrap' 1472 * 'reflect' 1473 * 'symmetric' 1474 * 'edge' 1475 * tuple[str, ...]: the `d`-th dimension uses ``mode[d]`` as boundary condition. 1476 1477 (See :py:func:`numpy.pad` for details.) 1478 gpu: bool 1479 Input NDArray type (`True` for GPU, `False` for CPU). 1480 Defaults to `False`. 1481 dtype: DType 1482 Working precision of the linear operator. 1483 diff_kwargs: dict 1484 Keyword arguments to parametrize partial derivatives (see 1485 :py:meth:`~pyxu.operator.PartialDerivative.finite_difference` and 1486 :py:meth:`~pyxu.operator.PartialDerivative.gaussian_derivative`) 1487 1488 Returns 1489 ------- 1490 op: OpT 1491 Divergence 1492 1493 Example 1494 ------- 1495 1496 .. plot:: 1497 1498 import numpy as np 1499 import matplotlib.pyplot as plt 1500 from pyxu.operator import Gradient, Divergence, Laplacian 1501 from pyxu.util.misc import peaks 1502 1503 n = 100 1504 x = np.linspace(-3, 3, n) 1505 xx, yy = np.meshgrid(x, x) 1506 image = peaks(xx, yy) 1507 dim_shape = image.shape # (1000, 1000) 1508 grad = Gradient(dim_shape=dim_shape) 1509 div = Divergence(dim_shape=dim_shape) 1510 # Construct Laplacian via composition 1511 laplacian1 = div * grad 1512 # Compare to default Laplacian 1513 laplacian2 = Laplacian(dim_shape=dim_shape) 1514 output1 = laplacian1(image) 1515 output2 = laplacian2(image) 1516 fig, axes = plt.subplots(1, 2, figsize=(10, 5)) 1517 im = axes[0].imshow(np.log(abs(output1)).reshape(*dim_shape)) 1518 axes[0].set_title("Laplacian via composition") 1519 plt.colorbar(im, ax=axes[0]) 1520 im = axes[1].imshow(np.log(abs(output1)).reshape(*dim_shape)) 1521 axes[1].set_title("Default Laplacian") 1522 plt.colorbar(im, ax=axes[1]) 1523 1524 1525 See Also 1526 -------- 1527 :py:func:`~pyxu.operator.Gradient`, 1528 :py:func:`~pyxu.operator.PartialDerivative`. 1529 """ 1530 if diff_method == "fd": 1531 change = {"central": "central", "forward": "backward", "backward": "forward"} 1532 scheme = diff_kwargs.get("scheme", "central") 1533 if isinstance(scheme, str): 1534 diff_kwargs.update({"scheme": change[scheme]}) 1535 elif isinstance(scheme, cabc.Sequence): 1536 new_scheme = list(scheme) 1537 for i in range(len(new_scheme)): 1538 new_scheme[i] = change[scheme[i]] 1539 diff_kwargs.update(dict(scheme=new_scheme)) 1540 1541 init_kwargs = dict( 1542 diff_method=diff_method, 1543 mode=mode, 1544 gpu=gpu, 1545 dtype=dtype, 1546 **diff_kwargs, 1547 ) 1548 1549 # Add dummy parameters for artificial dimension 1550 for key in init_kwargs.keys(): 1551 param = init_kwargs[key] 1552 if isinstance(param, cabc.Sequence): 1553 if not isinstance(param, str): 1554 param = list(param) 1555 param.insert(0, "dummy") 1556 init_kwargs.update({key: param}) 1557 1558 dim_shape = pxu.as_canonical_shape(dim_shape) 1559 directions = tuple([i for i in range(1, len(dim_shape))]) if directions is None else directions 1560 assert all( 1561 [direction > 0 for direction in directions] 1562 ), "The first direction corresponds to the vector values, see Documentation." 1563 n_dir = len(directions) 1564 1565 pds = pxb.block_diag( 1566 [Gradient(dim_shape=dim_shape[1:], directions=(direction - 1,), **init_kwargs) for direction in directions], 1567 ) 1568 op = pxlr.Sum(dim_shape=(n_dir,) + dim_shape[1:], axis=0) * pds.reshape((n_dir,) + dim_shape[1:]) 1569 op._name = "Divergence" 1570 return op.reshape(dim_shape[1:])
1571 1572
[docs] 1573def Hessian( 1574 dim_shape: pxt.NDArrayShape, 1575 directions: typ.Union[ 1576 str, 1577 cabc.Sequence[pxt.Integer, pxt.Integer], 1578 cabc.Sequence[cabc.Sequence[pxt.Integer, pxt.Integer], ...], 1579 ] = "all", 1580 diff_method: str = "fd", 1581 mode: ModeSpec = "constant", 1582 gpu: bool = False, 1583 dtype: typ.Optional[pxt.DType] = None, 1584 **diff_kwargs, 1585) -> pxt.OpT: 1586 r""" 1587 Hessian operator. 1588 1589 Notes 1590 ----- 1591 1592 The Hessian matrix or Hessian of a :math:`D`-dimensional signal :math:`\mathbf{f} \in \mathbb{R}^{N_0 \times \cdots 1593 \times N_{D-1}}` is the square matrix of second-order partial derivatives: 1594 1595 .. math:: 1596 1597 \mathbf{H} \mathbf{f} = \begin{bmatrix} 1598 \dfrac{ \partial^{2}\mathbf{f} }{ \partial x_{0}^{2} } & \dfrac{ \partial^{2}\mathbf{f} }{ \partial x_{0}\,\partial x_{1} } & \cdots & \dfrac{ \partial^{2}\mathbf{f} }{ \partial x_{0} \, \partial x_{D-1} } \\ 1599 \dfrac{ \partial^{2}\mathbf{f} }{ \partial x_{1} \, \partial x_{0} } & \dfrac{ \partial^{2}\mathbf{f} }{ \partial x_{1}^{2} } & \cdots & \dfrac{ \partial^{2}\mathbf{f} }{\partial x_{1} \,\partial x_{D-1}} \\ 1600 \vdots & \vdots & \ddots & \vdots \\ 1601 \dfrac{ \partial^{2}\mathbf{f} }{ \partial x_{D-1} \, \partial x_{0} } & \dfrac{ \partial^{2}\mathbf{f} }{ \partial x_{D-1} \, \partial x_{1} } & \cdots & \dfrac{ \partial^{2}\mathbf{f} }{ \partial x_{D-1}^{2}} 1602 \end{bmatrix} 1603 1604 The partial derivatives can be approximated by `finite differences 1605 <https://en.wikipedia.org/wiki/Finite_difference>`_ via the 1606 :py:meth:`~pyxu.operator.PartialDerivative.finite_difference` constructor or by the `Gaussian derivative 1607 <https://www.crisluengo.net/archives/22/>`_ via 1608 :py:meth:`~pyxu.operator.PartialDerivative.gaussian_derivative` constructor. 1609 The parametrization of the partial derivatives can be done via the keyword arguments `\*\*diff_kwargs`, which will 1610 default to the same values as the :py:class:`~pyxu.operator.PartialDerivative` constructor. 1611 1612 The Hessian being symmetric, only the upper triangular part at most needs to be computed. 1613 Due to the (possibly) large size of the full Hessian, 4 different options are handled: 1614 1615 * [mode 0] ``directions`` is an integer, e.g.: ``directions=0`` 1616 1617 .. math:: 1618 1619 \partial^{2}\mathbf{f}/\partial x_{0}^{2}. 1620 1621 * [mode 1] ``directions`` is a tuple of length 2, e.g.: ``directions=(0,1)`` 1622 1623 .. math:: 1624 1625 \partial^{2}\mathbf{f}/\partial x_{0}\partial x_{1}. 1626 1627 * [mode 2] ``directions`` is a tuple of tuples, e.g.: ``directions=((0,0), (0,1))`` 1628 1629 .. math:: 1630 1631 \left(\frac{ \partial^{2}\mathbf{f} }{ \partial x_{0}^{2} }, \frac{ \partial^{2}\mathbf{f} }{ \partial x_{0}\partial x_{1} }\right). 1632 1633 * [mode 3] ``directions = 'all'`` (default), computes the Hessian for all directions (only the 1634 upper triangular part of the Hessian matrix), in row order, i.e.: 1635 1636 .. math:: 1637 1638 \left(\frac{ \partial^{2}\mathbf{f} }{ \partial x_{0}^{2} }, \frac{ \partial^{2}\mathbf{f} } 1639 { \partial x_{0}\partial x_{1} }, \, \ldots , \, \frac{ \partial^{2}\mathbf{f} }{ \partial x_{D-1}^{2} }\right). 1640 1641 The shape of the output :py:class:`~pyxu.abc.LinOp` depends on the number of computed directions; by 1642 default (all directions), we have :math:`\mathbf{H} \mathbf{f} \in \mathbb{R}^{\frac{D(D-1)}{2} \times N_0 \times 1643 \cdots \times N_{D-1}}`. 1644 1645 1646 Parameters 1647 ---------- 1648 dim_shape: NDArrayShape 1649 (N_1,...,N_D) input dimensions. 1650 directions: Integer, (Integer, Integer), ((Integer, Integer), ..., (Integer, Integer)), 'all' 1651 Hessian directions. 1652 Defaults to `all`, which computes the Hessian for all directions. (See ``Notes``.) 1653 diff_method: "gd", "fd" 1654 Method used to approximate the derivative. 1655 Must be one of: 1656 1657 * 'fd' (default): finite differences 1658 * 'gd': Gaussian derivative 1659 mode: str, list[str] 1660 Boundary conditions. 1661 Multiple forms are accepted: 1662 1663 * str: unique mode shared amongst dimensions. 1664 Must be one of: 1665 1666 * 'constant' (default): zero-padding 1667 * 'wrap' 1668 * 'reflect' 1669 * 'symmetric' 1670 * 'edge' 1671 * tuple[str, ...]: the `d`-th dimension uses ``mode[d]`` as boundary condition. 1672 1673 (See :py:func:`numpy.pad` for details.) 1674 gpu: bool 1675 Input NDArray type (`True` for GPU, `False` for CPU). 1676 Defaults to `False`. 1677 dtype: DType 1678 Working precision of the linear operator. 1679 diff_kwargs: dict 1680 Keyword arguments to parametrize partial derivatives (see 1681 :py:meth:`~pyxu.operator.PartialDerivative.finite_difference` and 1682 :py:meth:`~pyxu.operator.PartialDerivative.gaussian_derivative`) 1683 1684 Returns 1685 ------- 1686 op: OpT 1687 Hessian 1688 1689 Example 1690 ------- 1691 1692 .. plot:: 1693 1694 import numpy as np 1695 import matplotlib.pyplot as plt 1696 from pyxu.operator import Hessian, PartialDerivative 1697 from pyxu.util.misc import peaks 1698 1699 n = 100 1700 x = np.linspace(-3, 3, n) 1701 xx, yy = np.meshgrid(x, x) 1702 image = peaks(xx, yy) 1703 dim_shape = image.shape # (1000, 1000) 1704 1705 # Instantiate Hessian operator 1706 hessian = Hessian(dim_shape=dim_shape, directions="all") 1707 # Compute Hessian 1708 d2f_dx2, d2f_dxdy, d2f_dy2 = hessian(image) 1709 1710 # Plot 1711 fig, axs = plt.subplots(1, 4, figsize=(20, 4)) 1712 im = axs[0].imshow(image) 1713 plt.colorbar(im, ax=axs[0]) 1714 axs[0].set_title("Image") 1715 axs[0].axis("off") 1716 1717 im = axs[1].imshow(d2f_dx2) 1718 plt.colorbar(im, ax=axs[1]) 1719 axs[1].set_title(r"$\partial^{2} f/ \partial x^{2}$") 1720 axs[1].axis("off") 1721 1722 im = axs[2].imshow(d2f_dxdy) 1723 plt.colorbar(im, ax=axs[2]) 1724 axs[2].set_title(r"$\partial^{2} f/ \partial x\partial y$") 1725 axs[2].axis("off") 1726 1727 im = axs[3].imshow(d2f_dy2) 1728 plt.colorbar(im, ax=axs[3]) 1729 axs[3].set_title(r"$\partial^{2} f/ \partial y^{2}$") 1730 axs[3].axis("off") 1731 1732 See Also 1733 -------- 1734 :py:class:`~pyxu.operator.PartialDerivative`, 1735 :py:class:`~pyxu.operator.Gradient`, 1736 :py:class:`~pyxu.operator.Laplacian`. 1737 """ 1738 # We assume Schwarz's theorem holds and thus the symmetry of second derivatives. 1739 # For this reason, when directions == `all`, only the upper triangular part of the Hessian is 1740 # returned. 1741 # However, this might not hold for non-trivial padding conditions, and the user can demand all 1742 # Hessian components via the `directions` 2nd mode (see ``Notes``). 1743 1744 dim_shape = pxu.as_canonical_shape(dim_shape) 1745 order, sampling, param1, param2, _ = _sanitize_init_kwargs( 1746 order=(1,) * len(dim_shape), 1747 diff_method=diff_method, 1748 diff_kwargs=diff_kwargs, 1749 dim_shape=dim_shape, 1750 sampling=diff_kwargs.get("sampling", 1.0), 1751 ) 1752 1753 directions, order = _StackDiffHelper._check_directions_and_order(dim_shape, directions) 1754 1755 # If diff_method is "fd" default to "central" for diag components and "forward" for off-diag. 1756 if (diff_method == "fd") and (diff_kwargs.get("scheme", None) is None): 1757 param1 = [("central", "central") if o == 2 else param1 for o in order] 1758 1759 op = _StackDiffHelper._stack_diff_ops( 1760 dim_shape=dim_shape, 1761 directions=directions, 1762 diff_method=diff_method, 1763 order=order, 1764 param1=param1, 1765 param2=param2, 1766 mode=mode, 1767 gpu=gpu, 1768 dtype=dtype, 1769 sampling=sampling, 1770 ) 1771 op._name = "Hessian" 1772 return op
1773 1774
[docs] 1775def Laplacian( 1776 dim_shape: pxt.NDArrayShape, 1777 directions: typ.Optional[pxt.NDArrayAxis] = None, 1778 diff_method: str = "fd", 1779 mode: ModeSpec = "constant", 1780 gpu: bool = False, 1781 dtype: typ.Optional[pxt.DType] = None, 1782 **diff_kwargs, 1783) -> pxt.OpT: 1784 r""" 1785 Laplacian operator. 1786 1787 Notes 1788 ----- 1789 1790 The Laplacian of a :math:`D`-dimensional signal :math:`\mathbf{f} \in \mathbb{R}^{N_0 \times \cdots \times N_{D-1}}` 1791 is the sum of second-order partial derivatives across all input directions: 1792 1793 .. math:: 1794 1795 \sum_{d = 0}^{D-1} \dfrac{ \partial^{2}\mathbf{f} }{ \partial x_{d}^{2} } 1796 1797 The partial derivatives can be approximated by `finite differences 1798 <https://en.wikipedia.org/wiki/Finite_difference>`_ via the 1799 :py:meth:`~pyxu.operator.PartialDerivative.finite_difference` constructor or by the `Gaussian derivative 1800 <https://www.crisluengo.net/archives/22/>`_ via 1801 :py:meth:`~pyxu.operator.PartialDerivative.gaussian_derivative` constructor. 1802 The parametrization of the partial derivatives can be done via the keyword arguments `\*\*diff_kwargs`, which will 1803 default to `scheme='central'` and `accuracy=2` for `diff_method='fd'` (finite difference), and the same values as 1804 the :py:class:`~pyxu.operator.PartialDerivative` constructor for `diff_method='gd'` (gaussian 1805 derivative). 1806 1807 Parameters 1808 ---------- 1809 dim_shape: NDArrayShape 1810 (N_1,...,N_D) input dimensions. 1811 directions: Integer, list[Integer], None 1812 Laplacian directions. Defaults to `None`, which computes the Laplacian with all directions. 1813 diff_method: "gd", "fd" 1814 Method used to approximate the derivative. 1815 Must be one of: 1816 1817 * 'fd' (default): finite differences 1818 * 'gd': Gaussian derivative 1819 mode: str, list[str] 1820 Boundary conditions. 1821 Multiple forms are accepted: 1822 1823 * str: unique mode shared amongst dimensions. 1824 Must be one of: 1825 1826 * 'constant' (default): zero-padding 1827 * 'wrap' 1828 * 'reflect' 1829 * 'symmetric' 1830 * 'edge' 1831 * tuple[str, ...]: the `d`-th dimension uses ``mode[d]`` as boundary condition. 1832 1833 (See :py:func:`numpy.pad` for details.) 1834 gpu: bool 1835 Input NDArray type (`True` for GPU, `False` for CPU). 1836 Defaults to `False`. 1837 dtype: DType 1838 Working precision of the linear operator. 1839 diff_kwargs: dict 1840 Keyword arguments to parametrize partial derivatives (see 1841 :py:meth:`~pyxu.operator.PartialDerivative.finite_difference` and 1842 :py:meth:`~pyxu.operator.PartialDerivative.gaussian_derivative`) 1843 1844 Returns 1845 ------- 1846 op: OpT 1847 Laplacian 1848 1849 Example 1850 ------- 1851 1852 .. plot:: 1853 1854 import numpy as np 1855 import matplotlib.pyplot as plt 1856 from pyxu.operator import Laplacian 1857 from pyxu.util.misc import peaks 1858 1859 # Define input image 1860 n = 100 1861 x = np.linspace(-3, 3, n) 1862 xx, yy = np.meshgrid(x, x) 1863 image = peaks(xx, yy) 1864 1865 dim_shape = image.shape # (1000, 1000) 1866 # Compute Laplacian 1867 laplacian = Laplacian(dim_shape=dim_shape) 1868 output = laplacian(image) # shape = (1, 1000, 1000) 1869 1870 # Plot 1871 fig, axs = plt.subplots(1, 2, figsize=(10, 4)) 1872 im = axs[0].imshow(image) 1873 plt.colorbar(im, ax=axs[0]) 1874 axs[0].set_title("Image") 1875 axs[0].axis("off") 1876 1877 im = axs[1].imshow(output.squeeze()) 1878 plt.colorbar(im, ax=axs[1]) 1879 axs[1].set_title(r"$\partial^{2} f/ \partial x^{2}+\partial^{2} f/ \partial y^{2}$") 1880 axs[1].axis("off") 1881 1882 fig.show() 1883 1884 See Also 1885 -------- 1886 :py:class:`~pyxu.operator.PartialDerivative`, 1887 :py:class:`~pyxu.operator.Gradient`, 1888 :py:class:`~pyxu.operator.Hessian`. 1889 """ 1890 dim_shape = pxu.as_canonical_shape(dim_shape) 1891 ndims = len(dim_shape) 1892 directions = tuple([i for i in range(len(dim_shape))]) if directions is None else directions 1893 directions = [(i, i) for i in range(ndims) if i in directions] 1894 pds = Hessian( 1895 dim_shape=dim_shape, 1896 directions=directions, 1897 diff_method=diff_method, 1898 mode=mode, 1899 gpu=gpu, 1900 dtype=dtype, 1901 **diff_kwargs, 1902 ) 1903 op = pxlr.Sum(dim_shape=(ndims,) + dim_shape, axis=0) * pds 1904 op._name = "Laplacian" 1905 return op.reshape(op.dim_shape)
1906 1907
[docs] 1908def DirectionalDerivative( 1909 dim_shape: pxt.NDArrayShape, 1910 order: pxt.Integer, 1911 directions: typ.Union[pxt.NDArray, typ.Sequence[pxt.NDArray]], 1912 diff_method: str = "fd", 1913 mode: ModeSpec = "constant", 1914 **diff_kwargs, 1915) -> pxt.OpT: 1916 r""" 1917 Directional derivative operator. 1918 1919 Notes 1920 ----- 1921 The **first-order** ``DirectionalDerivative`` of a :math:`D`-dimensional signal :math:`\mathbf{f} \in 1922 \mathbb{R}^{N_0 \times \cdots \times N_{D-1}}` applies a derivative along the direction specified by a constant 1923 unitary vector :math:`\mathbf{v} \in \mathbb{R}^D`: 1924 1925 .. math:: 1926 1927 \boldsymbol{\nabla}_\mathbf{v} \mathbf{f} = \sum_{i=0}^{D-1} v_i \frac{\partial \mathbf{f}}{\partial x_i} \in 1928 \mathbb{R}^{N_0 \times \cdots \times N_{D-1}} 1929 1930 or along spatially-varying directions :math:`\mathbf{v} = [\mathbf{v}_0, \ldots , \mathbf{v}_{D-1}]^\top \in 1931 \mathbb{R}^{D \times N_0 \times \cdots \times N_{D-1} }` where each direction :math:`\mathbf{v}_{\cdot, i_0, \ldots 1932 , i_{D-1}} \in \mathbb{R}^D` for any :math:`0 \leq i_d \leq N_d-1` with :math:`0 \leq d \leq D-1` is a unitary 1933 vector: 1934 1935 .. math:: 1936 1937 \boldsymbol{\nabla}_\mathbf{v} \mathbf{f} = \sum_{i=0}^{D-1} \mathbf{v}_i \odot 1938 \frac{\partial \mathbf{f}}{\partial x_i} \in \mathbb{R}^{N_0 \times \cdots \times N_{D-1}}, 1939 1940 where :math:`\odot` denotes the Hadamard (elementwise) product. 1941 1942 Note that choosing :math:`\mathbf{v}= \mathbf{e}_d \in \mathbb{R}^D` (the :math:`d`-th canonical basis vector) 1943 amounts to the first-order :py:func:`~pyxu.operator.PartialDerivative` operator applied along axis 1944 :math:`d`. 1945 1946 The **second-order** ``DirectionalDerivative`` :math:`\boldsymbol{\nabla}^2_{\mathbf{v}_{1, 2}}` is obtained by 1947 composing the first-order directional derivatives :math:`\boldsymbol{\nabla}_{\mathbf{v}_{1}}` and 1948 :math:`\boldsymbol{\nabla}_{\mathbf{v}_{2}}`: 1949 1950 .. math:: 1951 1952 \boldsymbol{\nabla}_{\mathbf{v}_{1}} (\boldsymbol{\nabla}_{\mathbf{v}_{2}} \mathbf{f}) = 1953 \boldsymbol{\nabla}_{\mathbf{v}_{1}} (\sum_{i=0}^{D-1} {v_{2}}_{i} \frac{\partial \mathbf{f}}{\partial x_i}) = 1954 \sum_{j=0}^{D-1} {v_{1}}_{j} \frac{\partial}{\partial x_j} (\sum_{i=0}^{D-1} {v_{2}}_{i} \frac{\partial \mathbf{f}}{\partial x_i}) = 1955 \sum_{i, j=0}^{D-1} {v_{1}}_{j} {v_{2}}_{i} \frac{\partial}{\partial x_j} \frac{\partial}{\partial x_i}\mathbf{f} = 1956 \mathbf{v}_{1}^{\top}\mathbf{H}\mathbf{f}\mathbf{v}_{2}, 1957 1958 where :math:`\mathbf{H}` is the discrete Hessian operator, implemented via 1959 :py:class:`~pyxu.operator.Hessian`. 1960 1961 Higher-order ``DirectionalDerivative`` :math:`\boldsymbol{\nabla}^{N}_\mathbf{v}` can be obtained by composing the 1962 first-order directional derivative :math:`\boldsymbol{\nabla}_\mathbf{v}` :math:`N` times. 1963 1964 1965 .. warning:: 1966 1967 - :py:func:`~pyxu.operator.DirectionalDerivative` instances are **not array module-agnostic**: they 1968 will only work with NDArrays belonging to the same array module as ``directions``. Inner 1969 computations may recast input arrays when the precision of ``directions`` does not match the user-requested 1970 precision. 1971 - ``directions`` are always normalized to be unit vectors. 1972 1973 Parameters 1974 ---------- 1975 dim_shape: NDArrayShape 1976 Shape of the input array. 1977 order: Integer 1978 Which directional derivative (restricted to 1: First or 2: Second, see ``Notes``). 1979 directions: NDArray, list 1980 For ``order=1``, it can be a single direction (array of size :math:`(D,)`, where :math:`D` is the number of 1981 axes) or spatially-varying directions: 1982 1983 * array of size :math:`(D, N_0 \times \ldots \times N_{D-1})` for ``order=1``, i.e., one direction per element 1984 in the gradient, and, 1985 1986 * array of size :math:`(D * (D + 1) / 2, N_0 \times \ldots \times N_{D-1})` for ``order=2``, i.e., one direction 1987 per element in the Hessian. 1988 1989 For ``order=2``, it can be a tuple/list of two single directions or spatially-varying dimensions of the same 1990 shape, which will compute :math:`\mathbf{v}_{1}^{\top}\mathbf{H}\mathbf{f}\mathbf{v}_{2}` or a single direction, 1991 as for ``order=1``, which will compute :math:`\mathbf{v}_{1}^{\top}\mathbf{H}\mathbf{f}\mathbf{v}_{1}`. Note 1992 that for ``order=2``, even though directions are spatially-varying, no differentiation is performed for this 1993 parameter. 1994 diff_method: "gd", "fd" 1995 Method used to approximate the derivative. Must be one of: 1996 1997 * 'fd' (default): finite differences 1998 * 'gd': Gaussian derivative 1999 mode: str, list[str] 2000 Boundary conditions. 2001 Multiple forms are accepted: 2002 2003 * str: unique mode shared amongst dimensions. 2004 Must be one of: 2005 2006 * 'constant' (default): zero-padding 2007 * 'wrap' 2008 * 'reflect' 2009 * 'symmetric' 2010 * 'edge' 2011 * tuple[str, ...]: the `d`-th dimension uses ``mode[d]`` as boundary condition. 2012 2013 (See :py:func:`numpy.pad` for details.) 2014 diff_kwargs: dict 2015 Keyword arguments to parametrize partial derivatives (see 2016 :py:meth:`~pyxu.operator.PartialDerivative.finite_difference` and 2017 :py:meth:`~pyxu.operator.PartialDerivative.gaussian_derivative`) 2018 2019 Returns 2020 ------- 2021 op: OpT 2022 Directional derivative 2023 2024 Example 2025 ------- 2026 2027 .. plot:: 2028 2029 import numpy as np 2030 import matplotlib.pyplot as plt 2031 from pyxu.operator import DirectionalDerivative 2032 from pyxu.util.misc import peaks 2033 2034 x = np.linspace(-2.5, 2.5, 25) 2035 xx, yy = np.meshgrid(x, x) 2036 z = peaks(xx, yy) 2037 directions = np.zeros(shape=(2, z.size)) 2038 directions[0, : z.size // 2] = 1 2039 directions[1, z.size // 2:] = 1 2040 dop = DirectionalDerivative(dim_shape=z.shape, order=1, directions=directions) 2041 out = dop(z) 2042 dop2 = DirectionalDerivative(dim_shape=z.shape, order=2, directions=directions) 2043 out2 = dop2(z) 2044 fig, axs = plt.subplots(1, 3, figsize=(15, 5)) 2045 axs = np.ravel(axs) 2046 h = axs[0].pcolormesh(xx, yy, z, shading="auto") 2047 axs[0].quiver(x, x, directions[1].reshape(xx.shape), directions[0].reshape(xx.shape)) 2048 plt.colorbar(h, ax=axs[0]) 2049 axs[0].set_title("Signal and directions of first derivatives") 2050 2051 h = axs[1].pcolormesh(xx, yy, out.squeeze(), shading="auto") 2052 plt.colorbar(h, ax=axs[1]) 2053 axs[1].set_title("First-order directional derivatives") 2054 2055 h = axs[2].pcolormesh(xx, yy, out2.squeeze(), shading="auto") 2056 plt.colorbar(h, ax=axs[2]) 2057 axs[2].set_title("Second-order directional derivative") 2058 2059 See Also 2060 -------- 2061 :py:func:`~pyxu.operator.Gradient`, 2062 :py:func:`~pyxu.operator.DirectionalGradient` 2063 """ 2064 dim_shape = pxu.as_canonical_shape(dim_shape) 2065 ndim = len(dim_shape) 2066 # For first directional derivative, ndim_diff == number of elements in gradient 2067 # For second directional derivative, ndim_diff == number of unique elements in Hessian 2068 ndim_diff = ndim if order == 1 else ndim * (ndim + 1) // 2 2069 2070 assert order in [1, 2], "`order` should be either 1 or 2" 2071 2072 # Ensure correct format of `directions` 2073 if order == 1: 2074 assert not isinstance(directions, cabc.Sequence), ( 2075 "`directions` for first directional derivative should be an " "NDArray" 2076 ) 2077 diff_op = Gradient 2078 else: 2079 if isinstance(directions, cabc.Sequence): 2080 error_msg = ( 2081 "`directions` for second directional derivative should be an NDArray or a tuple/list with two " 2082 "NDArrays of the same shape" 2083 ) 2084 assert len(directions) == 2, error_msg 2085 directions, directions2 = directions 2086 assert directions.shape == directions2.shape, error_msg 2087 else: 2088 directions2 = directions 2089 diff_op = Hessian 2090 2091 assert directions.shape[0] == ndim, "The length of `directions` should match `len(dim_shape)`" 2092 2093 xp = pxu.get_array_module(directions) 2094 gpu = xp == pxd.NDArrayInfo.CUPY.module() 2095 2096 dtype = directions.dtype 2097 2098 diff = diff_op( 2099 dim_shape=dim_shape, 2100 diff_method=diff_method, 2101 mode=mode, 2102 gpu=gpu, 2103 dtype=dtype, 2104 **diff_kwargs, 2105 ) 2106 # normalize directions to unit norm 2107 norm_dirs = (directions / xp.linalg.norm(directions, axis=0, keepdims=True)).astype(dtype) 2108 2109 if order == 1: 2110 op_name = "FirstDirectionalDerivative" 2111 else: # order == 2 2112 op_name = "SecondDirectionalDerivative" 2113 # Compute directions' outer product (see Notes) 2114 2115 norm_dirs2 = (directions2 / xp.linalg.norm(directions2, axis=0, keepdims=True)).astype(dtype) 2116 norm_dirs = norm_dirs[:, None, ...] * norm_dirs2[None, ...] 2117 # Multiply off-diag components x2 and use only triangular upper part of the outer product 2118 if ndim > 1: 2119 # (fancy nd indexing not supported in Dask) 2120 norm_dirs = norm_dirs.reshape(ndim**2, *norm_dirs.shape[2:]) 2121 dummy_mat = np.arange(ndim**2).reshape(ndim, ndim) 2122 off_diag_inds = dummy_mat[np.triu_indices(ndim, k=1)].ravel() 2123 norm_dirs[off_diag_inds] *= 2 2124 inds = dummy_mat[np.triu_indices(ndim, k=0)].ravel() 2125 norm_dirs = norm_dirs[inds] 2126 else: 2127 norm_dirs = norm_dirs.ravel() 2128 2129 if directions.ndim == 1: 2130 diag = xp.tile(norm_dirs, dim_shape + (1,)).transpose().reshape(-1, *dim_shape) 2131 else: 2132 diag = norm_dirs.reshape(-1, *dim_shape) 2133 2134 dop = pxlb.DiagonalOp(diag) 2135 sop = pxlr.Sum(dim_shape=(ndim_diff,) + dim_shape, axis=0) 2136 sqop = pxm.SqueezeAxes(dim_shape=sop.codim_shape, axes=0) 2137 op = sqop * sop * dop * diff 2138 dop_compute = pxlb.DiagonalOp(pxu.compute(diag)) 2139 op_compute = sqop * sop * dop_compute * diff 2140 2141 def op_svdvals(_, **kwargs) -> pxt.NDArray: 2142 return op_compute.svdvals(**kwargs) 2143 2144 setattr(op, "svdvals", types.MethodType(op_svdvals, op)) 2145 op._name = op_name 2146 return op
2147 2148
[docs] 2149def DirectionalGradient( 2150 dim_shape: pxt.NDArrayShape, 2151 directions: cabc.Sequence[pxt.NDArray], 2152 diff_method: str = "fd", 2153 mode: ModeSpec = "constant", 2154 **diff_kwargs, 2155) -> pxt.OpT: 2156 r""" 2157 Directional gradient operator. 2158 2159 Notes 2160 ----- 2161 The ``DirectionalGradient`` of a :math:`D`-dimensional signal :math:`\mathbf{f} \in \mathbb{R}^{N_0 \times \cdots 2162 \times N_{D-1}}` stacks the directional derivatives of :math:`\mathbf{f}` along a list of :math:`m` directions 2163 :math:`\mathbf{v}_i` for :math:`1 \leq i \leq m`: 2164 2165 .. math:: 2166 2167 \boldsymbol{\nabla}_{\mathbf{v}_1, \ldots ,\mathbf{v}_m} \mathbf{f} = \begin{bmatrix} 2168 \boldsymbol{\nabla}_{\mathbf{v}_1} \\ 2169 \vdots\\ 2170 \boldsymbol{\nabla}_{\mathbf{v}_m}\\ 2171 \end{bmatrix} \mathbf{f} \in \mathbb{R}^{m \times N_0 \times \cdots \times N_{D-1}}, 2172 2173 where :math:`\boldsymbol{\nabla}_{\mathbf{v}_i}` is the first-order directional derivative along 2174 :math:`\mathbf{v}_i` implemented with :py:func:`~pyxu.operator.DirectionalDerivative`, with 2175 :math:`\mathbf{v}_i \in \mathbb{R}^D` or :math:`\mathbf{v}_i \in \mathbb{R}^{D \times N_0 \times \cdots \times 2176 N_{D-1}}`. 2177 2178 Note that choosing :math:`m=D` and :math:`\mathbf{v}_i = \mathbf{e}_i \in \mathbb{R}^D` (the :math:`i`-th canonical 2179 basis vector) amounts to the :py:func:`~pyxu.operator.Gradient` operator. 2180 2181 Parameters 2182 ---------- 2183 dim_shape: NDArrayShape 2184 Shape of the input array. 2185 directions: list[NDArray] 2186 List of directions, either constant (array of size :math:`(D,)`) or spatially-varying (array of size :math:`(D, 2187 N_0, \ldots, N_{D-1})`) 2188 diff_method: "gd", "fd" 2189 Method used to approximate the derivative. Must be one of: 2190 2191 * 'fd' (default): finite differences 2192 * 'gd': Gaussian derivative 2193 mode: str, list[str] 2194 Boundary conditions. 2195 Multiple forms are accepted: 2196 2197 * str: unique mode shared amongst dimensions. 2198 Must be one of: 2199 2200 * 'constant' (default): zero-padding 2201 * 'wrap' 2202 * 'reflect' 2203 * 'symmetric' 2204 * 'edge' 2205 * tuple[str, ...]: the `d`-th dimension uses ``mode[d]`` as boundary condition. 2206 2207 (See :py:func:`numpy.pad` for details.) 2208 diff_kwargs: dict 2209 Keyword arguments to parametrize partial derivatives (see 2210 :py:meth:`~pyxu.operator.PartialDerivative.finite_difference` and 2211 :py:meth:`~pyxu.operator.PartialDerivative.gaussian_derivative`) 2212 2213 Returns 2214 ------- 2215 op: OpT 2216 Directional gradient 2217 2218 2219 Example 2220 ------- 2221 2222 .. plot:: 2223 2224 import numpy as np 2225 import matplotlib.pyplot as plt 2226 from pyxu.operator import DirectionalGradient 2227 from pyxu.util.misc import peaks 2228 2229 x = np.linspace(-2.5, 2.5, 25) 2230 xx, yy = np.meshgrid(x, x) 2231 z = peaks(xx, yy) 2232 directions1 = np.zeros(shape=(2, z.size)) 2233 directions1[0, :z.size // 2] = 1 2234 directions1[1, z.size // 2:] = 1 2235 directions2 = np.zeros(shape=(2, z.size)) 2236 directions2[1, :z.size // 2] = -1 2237 directions2[0, z.size // 2:] = -1 2238 dim_shape = z.shape 2239 dop = DirectionalGradient(dim_shape=dim_shape, directions=[directions1, directions2]) 2240 out = dop(z) 2241 plt.figure() 2242 h = plt.pcolormesh(xx, yy, z, shading='auto') 2243 plt.quiver(x, x, directions1[1].reshape(dim_shape), directions1[0].reshape(xx.shape)) 2244 plt.quiver(x, x, directions2[1].reshape(dim_shape), directions2[0].reshape(xx.shape), color='red') 2245 plt.colorbar(h) 2246 plt.title(r'Signal $\mathbf{f}$ and directions of derivatives') 2247 plt.figure() 2248 h = plt.pcolormesh(xx, yy, out[0], shading='auto') 2249 plt.colorbar(h) 2250 plt.title(r'$\nabla_{\mathbf{v}_0} \mathbf{f}$') 2251 plt.figure() 2252 h = plt.pcolormesh(xx, yy, out[1], shading='auto') 2253 plt.colorbar(h) 2254 plt.title(r'$\nabla_{\mathbf{v}_1} \mathbf{f}$') 2255 2256 See Also 2257 -------- 2258 :py:func:`~pyxu.operator.Gradient`, 2259 :py:func:`~pyxu.operator.DirectionalDerivative` 2260 """ 2261 2262 diag_ops = [] 2263 diag_ops_compute = [] 2264 dim_shape = pxu.as_canonical_shape(dim_shape) 2265 ndim = len(dim_shape) 2266 assert isinstance(directions, cabc.Sequence) 2267 2268 xp = pxu.get_array_module(directions[0]) 2269 gpu = xp == pxd.NDArrayInfo.CUPY.module() 2270 dtype = directions[0].dtype 2271 2272 grad = Gradient( 2273 dim_shape=dim_shape, 2274 diff_method=diff_method, 2275 mode=mode, 2276 gpu=gpu, 2277 dtype=dtype, 2278 **diff_kwargs, 2279 ) 2280 sop = pxlr.Sum( 2281 dim_shape=( 2282 len(directions), 2283 ndim, 2284 ) 2285 + dim_shape, 2286 axis=1, 2287 ) 2288 for direction in directions: 2289 # normalize directions to unit norm 2290 norm_dirs = (direction / xp.linalg.norm(direction, axis=0, keepdims=True)).astype(dtype) 2291 if direction.ndim == 1: 2292 diag = xp.tile(norm_dirs, dim_shape + (1,)).transpose().reshape(-1, *dim_shape) 2293 else: 2294 diag = norm_dirs.reshape(-1, *dim_shape) 2295 2296 diag_ops.append(pxlb.DiagonalOp(diag)) 2297 diag_ops_compute.append(pxlb.DiagonalOp(pxu.compute(diag))) 2298 2299 dop = pxb.stack(diag_ops) 2300 dop_compute = pxb.stack(diag_ops_compute) 2301 2302 sqop = pxm.SqueezeAxes(dim_shape=sop.codim_shape, axes=1) 2303 2304 op = sqop * sop * dop * grad 2305 op_compute = sqop * sop * dop_compute * grad 2306 2307 def op_svdvals(_, **kwargs) -> pxt.NDArray: 2308 return op_compute.svdvals(**kwargs) 2309 2310 setattr(op, "svdvals", types.MethodType(op_svdvals, op)) 2311 2312 op._name = "DirectionalGradient" 2313 return op
2314 2315
[docs] 2316def DirectionalLaplacian( 2317 dim_shape: pxt.NDArrayShape, 2318 directions: cabc.Sequence[pxt.NDArray], 2319 weights: typ.Iterable = None, 2320 diff_method: str = "fd", 2321 mode: ModeSpec = "constant", 2322 **diff_kwargs, 2323) -> pxt.OpT: 2324 r""" 2325 Directional Laplacian operator. 2326 2327 Sum of the second directional derivatives of a multi-dimensional array (at least two dimensions are required) along 2328 multiple ``directions`` for each entry of the array. 2329 2330 Notes 2331 ----- 2332 2333 The ``DirectionalLaplacian`` of a :math:`D`-dimensional signal :math:`\mathbf{f} \in \mathbb{R}^{N_0 \times \cdots 2334 \times N_{D-1}}` sums the second-order directional derivatives of :math:`\mathbf{f}` along a list of :math:`m` 2335 directions :math:`\mathbf{v}_i` for :math:`1 \leq i \leq m`: 2336 2337 .. math:: 2338 2339 \boldsymbol{\Delta}_{\mathbf{v}_1, \ldots ,\mathbf{v}_m} \mathbf{f} = \sum_{i=1}^m 2340 \boldsymbol{\nabla}^2_{\mathbf{v}_i} \mathbf{f} \in \mathbb{R}^{N_0 \times \cdots \times N_{D-1}}, 2341 2342 where :math:`\boldsymbol{\nabla}^2_{\mathbf{v}_i}` is the second-order directional derivative along 2343 :math:`\mathbf{v}_i` implemented with :py:func:`~pyxu.operator.DirectionalDerivative`. 2344 2345 Note that choosing :math:`m=D` and :math:`\mathbf{v}_i = \mathbf{e}_i \in \mathbb{R}^D` (the :math:`i`-th canonical 2346 basis vector) amounts to the :py:func:`~pyxu.operator.Laplacian` operator. 2347 2348 Parameters 2349 ---------- 2350 dim_shape: NDArrayShape 2351 Shape of the input array. 2352 directions: list[NDArray] 2353 List of directions, either constant (array of size :math:`(D,)`) or spatially-varying (array of size 2354 :math:`(D, N_0, \ldots, N_{D-1})`) 2355 weights: list[Real], None 2356 List of optional positive weights with which each second directional derivative operator is multiplied. 2357 diff_method: "gd", "fd" 2358 Method used to approximate the derivative. Must be one of: 2359 2360 * 'fd' (default): finite differences 2361 * 'gd': Gaussian derivative 2362 mode: str, list[str] 2363 Boundary conditions. 2364 Multiple forms are accepted: 2365 2366 * str: unique mode shared amongst dimensions. 2367 Must be one of: 2368 2369 * 'constant' (default): zero-padding 2370 * 'wrap' 2371 * 'reflect' 2372 * 'symmetric' 2373 * 'edge' 2374 * tuple[str, ...]: the `d`-th dimension uses ``mode[d]`` as boundary condition. 2375 2376 (See :py:func:`numpy.pad` for details.) 2377 diff_kwargs: dict 2378 Keyword arguments to parametrize partial derivatives (see 2379 :py:meth:`~pyxu.operator.PartialDerivative.finite_difference` and 2380 :py:meth:`~pyxu.operator.PartialDerivative.gaussian_derivative`) 2381 2382 Returns 2383 ------- 2384 op: OpT 2385 Directional Laplacian 2386 2387 Example 2388 ------- 2389 2390 .. plot:: 2391 2392 import numpy as np 2393 import matplotlib.pyplot as plt 2394 from pyxu.operator import DirectionalLaplacian 2395 from pyxu.util.misc import peaks 2396 2397 x = np.linspace(-2.5, 2.5, 25) 2398 xx, yy = np.meshgrid(x, x) 2399 z = peaks(xx, yy) 2400 directions1 = np.zeros(shape=(2, z.size)) 2401 directions1[0, :z.size // 2] = 1 2402 directions1[1, z.size // 2:] = 1 2403 directions2 = np.zeros(shape=(2, z.size)) 2404 directions2[1, :z.size // 2] = -1 2405 directions2[0, z.size // 2:] = -1 2406 dim_shape = z.shape 2407 dop = DirectionalLaplacian(dim_shape=dim_shape, directions=[directions1, directions2]) 2408 out = dop(z) 2409 plt.figure() 2410 h = plt.pcolormesh(xx, yy, z, shading='auto') 2411 plt.quiver(x, x, directions1[1].reshape(dim_shape), directions1[0].reshape(xx.shape)) 2412 plt.quiver(x, x, directions2[1].reshape(dim_shape), directions2[0].reshape(xx.shape), color='red') 2413 plt.colorbar(h) 2414 plt.title('Signal and directions of derivatives') 2415 plt.figure() 2416 h = plt.pcolormesh(xx, yy, out.squeeze(), shading='auto') 2417 plt.colorbar(h) 2418 plt.title('Directional Laplacian') 2419 2420 See Also 2421 -------- 2422 :py:func:`~pyxu.operator.Laplacian`, 2423 :py:func:`~pyxu.operator.DirectionalDerivative` 2424 """ 2425 assert isinstance(directions, cabc.Sequence) 2426 2427 if weights is None: 2428 weights = [1.0] * len(directions) 2429 else: 2430 if len(weights) != len(directions): 2431 raise ValueError("The number of weights and directions provided differ.") 2432 2433 xp = pxu.get_array_module(directions[0]) 2434 gpu = xp == pxd.NDArrayInfo.CUPY.module() 2435 dtype = directions[0].dtype 2436 dim_shape = pxu.as_canonical_shape(dim_shape) 2437 hess = Hessian( 2438 dim_shape=dim_shape, 2439 diff_method=diff_method, 2440 mode=mode, 2441 gpu=gpu, 2442 dtype=dtype, 2443 **diff_kwargs, 2444 ) 2445 2446 ndim = len(dim_shape) 2447 ndim_diff = ndim * (ndim + 1) // 2 2448 dop = [] 2449 dop_compute = [] 2450 for i, (weight, direction) in enumerate(zip(weights, directions)): 2451 # normalize directions to unit norm 2452 norm_dirs = (direction / xp.linalg.norm(direction, axis=0, keepdims=True)).astype(dtype) 2453 norm_dirs = norm_dirs[:, None, ...] * norm_dirs[None, ...] 2454 # Multiply off-diag components x2 and use only triangular upper part of the outer product 2455 if ndim > 1: 2456 # (fancy nd indexing not supported in Dask) 2457 norm_dirs = norm_dirs.reshape(ndim**2, *norm_dirs.shape[2:]) 2458 dummy_mat = np.arange(ndim**2).reshape(ndim, ndim) 2459 off_diag_inds = dummy_mat[np.triu_indices(ndim, k=1)].ravel() 2460 norm_dirs[off_diag_inds] *= 2 2461 inds = dummy_mat[np.triu_indices(ndim, k=0)].ravel() 2462 norm_dirs = norm_dirs[inds] 2463 else: 2464 norm_dirs = norm_dirs.reshape(-1, *dim_shape) 2465 2466 if direction.ndim == 1: 2467 dop.append( 2468 pxlb.DiagonalOp(weight * xp.tile(norm_dirs, dim_shape + (1,)).transpose().reshape(-1, *dim_shape)) 2469 ) 2470 dop_compute.append( 2471 pxlb.DiagonalOp( 2472 pxu.compute(weight * xp.tile(norm_dirs, dim_shape + (1,)).transpose().reshape(-1, *dim_shape)) 2473 ) 2474 ) 2475 else: 2476 dop.append(pxlb.DiagonalOp(weight * norm_dirs.reshape(-1, *dim_shape))) 2477 dop_compute.append(pxlb.DiagonalOp(pxu.compute(weight * norm_dirs.reshape(-1, *dim_shape)))) 2478 2479 dop = pxb.stack(dop) 2480 dop_compute = pxb.stack(dop_compute) 2481 sop = pxlr.Sum( 2482 dim_shape=( 2483 len(directions), 2484 ndim_diff, 2485 ) 2486 + dim_shape, 2487 axis=(0, 1), 2488 ) 2489 sqop = pxm.SqueezeAxes(dim_shape=sop.codim_shape, axes=(0, 1)) 2490 op = sqop * sop * dop * hess 2491 op_compute = sqop * sop * dop_compute * hess 2492 2493 def op_svdvals(_, **kwargs) -> pxt.NDArray: 2494 return op_compute.svdvals(**kwargs) 2495 2496 setattr(op, "svdvals", types.MethodType(op_svdvals, op)) 2497 2498 op._name = "DirectionalLaplacian" 2499 return op
2500 2501
[docs] 2502def DirectionalHessian( 2503 dim_shape: pxt.NDArrayShape, 2504 directions: cabc.Sequence[pxt.NDArray], 2505 diff_method="gd", 2506 mode: ModeSpec = "constant", 2507 **diff_kwargs, 2508) -> pxt.OpT: 2509 r""" 2510 Directional Hessian operator. 2511 2512 Notes 2513 ----- 2514 2515 The ``DirectionalHessian`` of a :math:`D`-dimensional signal :math:`\mathbf{f} \in \mathbb{R}^{N_0 \times \cdots 2516 \times N_{D-1}}` stacks the second-order directional derivatives of :math:`\mathbf{f}` along a list of :math:`m` 2517 directions :math:`\mathbf{v}_i` for :math:`1 \leq i \leq m`: 2518 2519 .. math:: 2520 2521 \mathbf{H}_{\mathbf{v}_1, \ldots ,\mathbf{v}_m} \mathbf{f} = \begin{bmatrix} 2522 \boldsymbol{\nabla}^2_{\mathbf{v}_0} & \cdots & \boldsymbol{\nabla}_{\mathbf{v}_0} \boldsymbol{\nabla}_{\mathbf{v}_{m-1}} \\ 2523 \vdots & \ddots & \vdots \\ 2524 \boldsymbol{\nabla}_{\mathbf{v}_{m-1}} \boldsymbol{\nabla}_{\mathbf{v}_0} & \cdots & \boldsymbol{\nabla}^2_{\mathbf{v}_{m-1}} 2525 \end{bmatrix} \mathbf{f}, 2526 2527 where :math:`\boldsymbol{\nabla}_{\mathbf{v}_i}` is the first-order directional derivative along 2528 :math:`\mathbf{v}_i` implemented with :py:func:`~pyxu.operator.DirectionalDerivative`. 2529 2530 However, due to the symmetry of the Hessian, only the upper triangular part is computed in practice: 2531 2532 .. math:: 2533 2534 \mathbf{H}_{\mathbf{v}_1, \ldots ,\mathbf{v}_m} \mathbf{f} = \begin{bmatrix} 2535 \boldsymbol{\nabla}^2_{\mathbf{v}_0}\\ 2536 \boldsymbol{\nabla}_{\mathbf{v}_0} \boldsymbol{\nabla}_{\mathbf{v}_{1}} \\ 2537 \vdots \\ 2538 \boldsymbol{\nabla}^2_{\mathbf{v}_{m-1}} 2539 \end{bmatrix} \mathbf{f} \in \mathbb{R}^{\frac{m (m-1)}{2} \times N_0 \times \cdots \times N_{D-1}} 2540 2541 Note that choosing :math:`m=D` and :math:`\mathbf{v}_i = \mathbf{e}_i \in \mathbb{R}^D` (the :math:`i`-th canonical 2542 basis vector) amounts to the :py:func:`~pyxu.operator.Hessian` operator. 2543 2544 Parameters 2545 ---------- 2546 dim_shape: NDArrayShape 2547 Shape of the input array. 2548 directions: list[NDArray] 2549 List of directions, either constant (array of size :math:`(D,)`) or spatially-varying (array of size :math:`(D, 2550 N_0, \ldots, N_{D-1})`) 2551 diff_method: "gd", "fd" 2552 Method used to approximate the derivative. Must be one of: 2553 2554 * 'fd' (default): finite differences 2555 * 'gd': Gaussian derivative 2556 mode: str, list[str] 2557 Boundary conditions. 2558 Multiple forms are accepted: 2559 2560 * str: unique mode shared amongst dimensions. 2561 Must be one of: 2562 2563 * 'constant' (default): zero-padding 2564 * 'wrap' 2565 * 'reflect' 2566 * 'symmetric' 2567 * 'edge' 2568 * tuple[str, ...]: the `d`-th dimension uses ``mode[d]`` as boundary condition. 2569 2570 (See :py:func:`numpy.pad` for details.) 2571 diff_kwargs: dict 2572 Keyword arguments to parametrize partial derivatives (see 2573 :py:meth:`~pyxu.operator.PartialDerivative.finite_difference` and 2574 :py:meth:`~pyxu.operator.PartialDerivative.gaussian_derivative`) 2575 2576 Returns 2577 ------- 2578 op: OpT 2579 Directional Hessian 2580 2581 Example 2582 ------- 2583 2584 .. plot:: 2585 2586 import numpy as np 2587 import matplotlib.pyplot as plt 2588 from pyxu.operator import DirectionalHessian 2589 from pyxu.util.misc import peaks 2590 2591 x = np.linspace(-2.5, 2.5, 25) 2592 xx, yy = np.meshgrid(x, x) 2593 z = peaks(xx, yy) 2594 directions1 = np.zeros(shape=(2, z.size)) 2595 directions1[0, :z.size // 2] = 1 2596 directions1[1, z.size // 2:] = 1 2597 directions2 = np.zeros(shape=(2, z.size)) 2598 directions2[1, :z.size // 2] = -1 2599 directions2[0, z.size // 2:] = -1 2600 dim_shape = z.shape 2601 d_hess = DirectionalHessian(dim_shape=dim_shape, directions=[directions1, directions2]) 2602 out = d_hess(z) 2603 plt.figure() 2604 h = plt.pcolormesh(xx, yy, z, shading='auto') 2605 plt.quiver(x, x, directions1[1].reshape(dim_shape), directions1[0].reshape(xx.shape)) 2606 plt.quiver(x, x, directions2[1].reshape(dim_shape), directions2[0].reshape(xx.shape), color='red') 2607 plt.colorbar(h) 2608 plt.title(r'Signal $\mathbf{f}$ and directions of derivatives') 2609 plt.figure() 2610 h = plt.pcolormesh(xx, yy, out[0], shading='auto') 2611 plt.colorbar(h) 2612 plt.title(r'$\nabla^2_{\mathbf{v}_0} \mathbf{f}$') 2613 plt.figure() 2614 h = plt.pcolormesh(xx, yy, out[1], shading='auto') 2615 plt.colorbar(h) 2616 plt.title(r'$\nabla_{\mathbf{v}_0} \nabla_{\mathbf{v}_{1}} \mathbf{f}$') 2617 plt.figure() 2618 h = plt.pcolormesh(xx, yy, out[2], shading='auto') 2619 plt.colorbar(h) 2620 plt.title(r'$\nabla^2_{\mathbf{v}_1} \mathbf{f}$') 2621 2622 See Also 2623 -------- 2624 :py:func:`~pyxu.operator.Hessian`, 2625 :py:func:`~pyxu.operator.DirectionalDerivative` 2626 """ 2627 2628 assert isinstance(directions, cabc.Sequence) 2629 2630 xp = pxu.get_array_module(directions[0]) 2631 gpu = xp == pxd.NDArrayInfo.CUPY.module() 2632 dtype = directions[0].dtype 2633 dim_shape = pxu.as_canonical_shape(dim_shape) 2634 hess = Hessian( 2635 dim_shape=dim_shape, 2636 diff_method=diff_method, 2637 mode=mode, 2638 gpu=gpu, 2639 dtype=dtype, 2640 **diff_kwargs, 2641 ) 2642 2643 ndim = len(dim_shape) 2644 ndim_diff = ndim * (ndim + 1) // 2 2645 dop = [] 2646 dop_compute = [] 2647 for i1, direction1 in enumerate(directions): 2648 norm_dirs1 = (direction1 / xp.linalg.norm(direction1, axis=0, keepdims=True)).astype(dtype) 2649 for i2, direction2 in enumerate(directions[i1:]): 2650 norm_dirs2 = (direction2 / xp.linalg.norm(direction2, axis=0, keepdims=True)).astype(dtype) 2651 norm_dirs = norm_dirs1[:, None, ...] * norm_dirs2[None, ...] 2652 2653 # Multiply off-diag components x2 and use only triangular upper part of the outer product 2654 if ndim > 1: 2655 # (fancy nd indexing not supported in Dask) 2656 norm_dirs = norm_dirs.reshape(ndim**2, *norm_dirs.shape[2:]) 2657 dummy_mat = np.arange(ndim**2).reshape(ndim, ndim) 2658 off_diag_inds = dummy_mat[np.triu_indices(ndim, k=1)].ravel() 2659 norm_dirs[off_diag_inds] *= 2 2660 inds = dummy_mat[np.triu_indices(ndim, k=0)].ravel() 2661 norm_dirs = norm_dirs[inds] 2662 else: 2663 norm_dirs = norm_dirs.reshape(-1, *dim_shape) 2664 2665 if norm_dirs.ndim == 1: 2666 dop.append(pxlb.DiagonalOp(xp.tile(norm_dirs, dim_shape + (1,)).transpose().reshape(-1, *dim_shape))) 2667 dop_compute.append( 2668 pxlb.DiagonalOp( 2669 pxu.compute(xp.tile(norm_dirs, dim_shape + (1,)).transpose().reshape(-1, *dim_shape)) 2670 ) 2671 ) 2672 else: 2673 dop.append(pxlb.DiagonalOp(norm_dirs.reshape(-1, *dim_shape))) 2674 dop_compute.append(pxlb.DiagonalOp(pxu.compute(norm_dirs.reshape(-1, *dim_shape)))) 2675 2676 dop = pxb.stack(dop) 2677 dop_compute = pxb.stack(dop_compute) 2678 ndim_hess = len(directions) * (len(directions) + 1) // 2 2679 sop = pxlr.Sum( 2680 dim_shape=( 2681 ndim_hess, 2682 ndim_diff, 2683 ) 2684 + dim_shape, 2685 axis=1, 2686 ) 2687 sqop = pxm.SqueezeAxes(dim_shape=sop.codim_shape, axes=1) 2688 op = sqop * sop * dop * hess 2689 op_compute = sqop * sop * dop_compute * hess 2690 2691 def op_svdvals(_, **kwargs) -> pxt.NDArray: 2692 return op_compute.svdvals(**kwargs) 2693 2694 setattr(op, "svdvals", types.MethodType(op_svdvals, op)) 2695 2696 op._name = "DirectionalHessian" 2697 return op