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

1# Copyright 2026 CNRS 

2# This software is distributed under the terms of the CeCILL-C free software license. 

3 

4"""Convolution utilities for hexagonal lattices.""" 

5 

6import numpy as np 

7 

8 

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. 

12 

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. 

15 

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`. 

23 

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) 

31 

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]]) 

46 

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 ) 

61 

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 

69 

70 return np.lib.stride_tricks.as_strided( 

71 a, shape=convolve_view_shape, strides=convolve_view_strides, writeable=False 

72 )