Coverage for src/pyhiperta/utils/convolve.py: 100%
9 statements
« prev ^ index » next coverage.py v7.15.3, created at 2026-08-07 14:41 +0000
« prev ^ index » next coverage.py v7.15.3, created at 2026-08-07 14:41 +0000
1# Copyright 2026 CNRS
2# This software is distributed under the terms of the CeCILL-C free software license.
4"""Convolution utilities for hexagonal lattices."""
6import numpy as np
9def convolve_view(a: np.ndarray, stencil_shape: tuple[int, ...]) -> np.ndarray:
10 """Read-only view into `a` that has `stencil_shape` extra dimensions for each part of `a` that a stencil
11 of shape `stencil_shape` would operate on.
13 Directly taken from https://stackoverflow.com/questions/43086557/convolve2d-just-by-using-numpy
14 It allows to view the sub-parts of `a` of shape `stencil_shape` without copying or duplicating `a`'s data.
16 Parameters
17 ----------
18 a : np.ndarray
19 The array to get the view in.
20 stencil_shape : Tuple[int, ...]
21 The shape of the stencil that we would want to operate on `a`. It must have the same number of dimension
22 than `a`.
24 Returns
25 -------
26 np.ndarray
27 Read-only view in `a` with `stencil_shape` extra dimension for each stencil sub-parts.
28 Shape: [*(a.shape - (stencil_shape - 1)), *stencil_shape]. The shape first dimensions are the same than `a`'s,
29 subtracting the stencil elements that would fall outside of `a` in the boundaries. The shape last dimensions are
30 the same as `stencil_shape`. Example: a.shape=(55, 55), stencil_shape=(3, 3): result's shape: (53, 53, 3, 3)
32 Examples
33 --------
34 >>> a = np.arange(55 * 55).reshape((55, 55))
35 >>> stencil = np.array([[0, 1], [1, 0]])
36 >>> convolve_view(a, stencil.shape).shape
37 (54, 54, 2, 2)
38 >>> convolve_view(a, stencil.shape)[0, 0, :, :] # left/high-most 2x2 part of `a`.
39 ... # doctest: +NORMALIZE_WHITESPACE
40 array([[ 0, 1],
41 [55, 56]])
42 >>> convolve_view(a, stencil.shape)[1, 1, :, :]
43 ... # doctest: +NORMALIZE_WHITESPACE
44 array([[ 56, 57],
45 [111, 112]])
47 Raises
48 ------
49 ValueError
50 If `stencil_shape` and the array's shape are not compatible, or if the stencil shape is invalid.
51 """
52 if len(a.shape) != len(stencil_shape):
53 raise ValueError(
54 f"Stencil shape {stencil_shape} and array shape {a.shape} must have the same number of dimensions"
55 )
56 if not all([0 < s <= a.shape[i] for i, s in enumerate(stencil_shape)]):
57 raise ValueError(
58 "Stencil shape must be strictly positive and smaller or equal than a.shape in all dimensions. "
59 f"Got stencil shape {stencil_shape} and a's shape: {a.shape}"
60 )
62 # The output shape is a.shape - (stencil_shape - 1)
63 # The minus 1 is because the stencil center element is applied on each pixel of a, so it
64 # doesn't reduce the shape.
65 # Stencil of shape [3, 3] reduces each axis shape by 2: 1 element on each end of each axis for instance
66 convolve_view_shape = tuple(np.subtract(a.shape, stencil_shape) + 1) + stencil_shape
67 # strides of the view's extra dimension are the same than of the input array: we index subparts of it!
68 convolve_view_strides = a.strides + a.strides
70 return np.lib.stride_tricks.as_strided(
71 a, shape=convolve_view_shape, strides=convolve_view_strides, writeable=False
72 )