Coverage for src/foapy/core/_intervals_tuple.py: 94%

37 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-17 19:57 +0000

1import numpy as np 

2from numpy import ndarray 

3 

4from foapy.core._binding import binding as binding_cls 

5from foapy.core._tuple_mode import tuple_mode as tuple_mode_cls 

6 

7 

8def intervals_tuple(chain, binding: int, tuple_mode: int) -> ndarray: 

9 """ 

10 Apply a boundary handling strategy to a plain 1-D intervals chain. 

11 

12 Takes a chain produced by ``intervals_chain`` and transforms it according 

13 to the requested ``tuple_mode``: 

14 

15 - ``tuple_mode.normal``: return the chain unchanged. 

16 - ``tuple_mode.lossy``: remove boundary (first-/last-occurrence) intervals, 

17 keeping only interior intervals between repeated occurrences. 

18 - ``tuple_mode.redundant``: append the complementary boundary intervals 

19 (trailing for ``binding.start`` chains, leading for ``binding.end`` 

20 chains), so every element contributes both its start-side and end-side 

21 boundary interval. 

22 

23 Parameters 

24 ---------- 

25 chain : array_like 

26 A 1-D intervals chain produced by ``intervals_chain``. 

27 Must be a 1-D array of positive integers. 

28 binding : int 

29 ``binding.start`` (1) — chain was produced left-to-right. 

30 ``binding.end`` (2) — chain was produced right-to-left. 

31 Must match the binding used in the ``intervals_chain`` call. 

32 tuple_mode : int 

33 Boundary handling strategy. Use one of the class attributes on 

34 :class:`foapy.core.tuple_mode`: 

35 

36 ``tuple_mode.normal`` = 2 – return the chain as-is. 

37 

38 ``tuple_mode.lossy`` = 1 – drop boundary intervals. 

39 

40 ``tuple_mode.redundant`` = 3 – include both boundary intervals for 

41 every element. 

42 

43 Returns 

44 ------- 

45 ndarray 

46 1-D integer array of intervals. Length equals ``n`` for *normal*, 

47 ``n - k`` for *lossy*, and ``n + k`` for *redundant*, where ``n`` is 

48 the chain length and ``k`` is the number of unique elements inferred 

49 from the chain. 

50 

51 Raises 

52 ------ 

53 ValueError 

54 When ``tuple_mode`` is not a recognised value. 

55 

56 Examples 

57 -------- 

58 

59 ``` py linenums="1" 

60 import foapy 

61 from foapy.core import intervals_chain, intervals_tuple 

62 

63 X = ['b', 'a', 'b', 'c', 'b'] 

64 chain = intervals_chain(X, foapy.binding.start, foapy.chain_mode.boundary) 

65 # chain = [1, 2, 2, 4, 2] 

66 

67 print(intervals_tuple(chain, foapy.binding.start, foapy.tuple_mode.normal)) 

68 # [1 2 2 4 2] 

69 

70 print(intervals_tuple(chain, foapy.binding.start, foapy.tuple_mode.lossy)) 

71 # [2 2] 

72 

73 print(intervals_tuple(chain, foapy.binding.start, foapy.tuple_mode.redundant)) 

74 # [1 2 2 4 2 4 2 1] 

75 ``` 

76 """ 

77 

78 def normal(ar): 

79 return ar.copy() 

80 

81 def lossy(ar): 

82 # Infer binding direction to choose correct boundary detection formula. 

83 if binding == binding_cls.end: 

84 ar = ar[::-1] 

85 

86 positions = np.arange(ar.size, dtype=np.intp) 

87 

88 # First entrance interval at position i iff ar[i] > i 

89 first = ar > positions 

90 

91 return ar[~first] 

92 

93 def redundant(ar): 

94 # If the chain was created using binding.end, reverse it for correct handling. 

95 if binding == binding_cls.end: 

96 ar = ar[::-1] 

97 

98 n = ar.size 

99 positions = np.arange(n, dtype=np.intp) 

100 

101 # For each position, compute where its "previous occurrence" is. 

102 prev_pos = positions - ar 

103 # Build a mask to detect "last occurrences" 

104 # (not referred to as previous by any other element). 

105 last_mask = np.ones_like(positions, dtype=bool) 

106 last_mask[prev_pos[prev_pos >= 0]] = False 

107 # Trailing intervals are n - position for each detected "last occurrence". 

108 trailing = n - positions[last_mask] 

109 

110 # Concatenate chain with its trailing intervals. 

111 return np.concatenate((ar, trailing)) 

112 

113 if binding not in {binding_cls.start, binding_cls.end}: 113 ↛ 114line 113 didn't jump to line 114 because the condition on line 113 was never true

114 raise ValueError( 

115 {"message": "Invalid binding value. Use binding.start or binding.end."} 

116 ) 

117 

118 valid_modes = { 

119 tuple_mode_cls.lossy, 

120 tuple_mode_cls.normal, 

121 tuple_mode_cls.redundant, 

122 } 

123 if tuple_mode not in valid_modes: 

124 raise ValueError( 

125 { 

126 "message": "Invalid tuple_mode value. Use tuple_mode.lossy, normal, or redundant." # noqa: E501 

127 } 

128 ) 

129 

130 ar = np.asanyarray(chain, dtype=np.intp) 

131 

132 if ar.size == 0: 

133 return np.array([], dtype=np.intp) 

134 

135 if tuple_mode == tuple_mode_cls.normal: 

136 return normal(ar) 

137 

138 if tuple_mode == tuple_mode_cls.lossy: 

139 return lossy(ar) 

140 

141 if tuple_mode == tuple_mode_cls.redundant: 141 ↛ exitline 141 didn't return from function 'intervals_tuple' because the condition on line 141 was always true

142 return redundant(ar)