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
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-17 19:57 +0000
1import numpy as np
2from numpy import ndarray
4from foapy.core._binding import binding as binding_cls
5from foapy.core._tuple_mode import tuple_mode as tuple_mode_cls
8def intervals_tuple(chain, binding: int, tuple_mode: int) -> ndarray:
9 """
10 Apply a boundary handling strategy to a plain 1-D intervals chain.
12 Takes a chain produced by ``intervals_chain`` and transforms it according
13 to the requested ``tuple_mode``:
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.
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`:
36 ``tuple_mode.normal`` = 2 – return the chain as-is.
38 ``tuple_mode.lossy`` = 1 – drop boundary intervals.
40 ``tuple_mode.redundant`` = 3 – include both boundary intervals for
41 every element.
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.
51 Raises
52 ------
53 ValueError
54 When ``tuple_mode`` is not a recognised value.
56 Examples
57 --------
59 ``` py linenums="1"
60 import foapy
61 from foapy.core import intervals_chain, intervals_tuple
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]
67 print(intervals_tuple(chain, foapy.binding.start, foapy.tuple_mode.normal))
68 # [1 2 2 4 2]
70 print(intervals_tuple(chain, foapy.binding.start, foapy.tuple_mode.lossy))
71 # [2 2]
73 print(intervals_tuple(chain, foapy.binding.start, foapy.tuple_mode.redundant))
74 # [1 2 2 4 2 4 2 1]
75 ```
76 """
78 def normal(ar):
79 return ar.copy()
81 def lossy(ar):
82 # Infer binding direction to choose correct boundary detection formula.
83 if binding == binding_cls.end:
84 ar = ar[::-1]
86 positions = np.arange(ar.size, dtype=np.intp)
88 # First entrance interval at position i iff ar[i] > i
89 first = ar > positions
91 return ar[~first]
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]
98 n = ar.size
99 positions = np.arange(n, dtype=np.intp)
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]
110 # Concatenate chain with its trailing intervals.
111 return np.concatenate((ar, trailing))
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 )
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 )
130 ar = np.asanyarray(chain, dtype=np.intp)
132 if ar.size == 0:
133 return np.array([], dtype=np.intp)
135 if tuple_mode == tuple_mode_cls.normal:
136 return normal(ar)
138 if tuple_mode == tuple_mode_cls.lossy:
139 return lossy(ar)
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)