Coverage for src/foapy/core/_intervals_chain.py: 100%
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._chain_mode import chain_mode as chain_mode_cls
6from foapy.exceptions import Not1DArrayException
9def intervals_chain(X, binding: int, chain_mode: int) -> ndarray:
10 """
11 Compute the raw intervals chain from a sequence.
13 The intervals chain is the n-tuple of distances between consecutive
14 occurrences of the same element, with the first occurrence's distance
15 measured from the sequence boundary (or cyclically). The chain values
16 alone are sufficient to determine both binding direction and chain mode
17 via structural mathematical properties.
19 Parameters
20 ----------
21 X : array_like
22 Input sequence (strings, integers, or any comparable elements).
23 Must be 1-dimensional. No pre-ordering via ``order()`` is required.
24 binding : int
25 ``binding.start`` (1) — intervals extracted left-to-right.
26 ``binding.end`` (2) — intervals extracted right-to-left.
27 chain_mode : int
28 ``chain_mode.boundary`` (1) — sequence treated as finite; boundary
29 intervals are distances from sequence edges to first/last occurrence.
30 ``chain_mode.cycle`` (2) — sequence treated as circular; leading and
31 trailing boundary distances are combined into a single cyclic interval.
33 Returns
34 -------
35 ndarray
36 Plain 1-D ndarray of dtype ``intp`` containing the raw interval chain
37 in original sequence order. All values are positive integers ≥ 1 and
38 ≤ ``len(X)``.
40 Raises
41 ------
42 Not1DArrayException
43 When ``X`` is not a 1-dimensional array.
44 ValueError
45 When ``binding`` is not ``binding.start`` or ``binding.end``.
46 When ``chain_mode`` is not ``chain_mode.boundary`` or ``chain_mode.cycle``.
48 Examples
49 --------
51 ``` py linenums="1"
52 import foapy
54 X = ['b', 'a', 'b', 'c', 'b']
55 chain = foapy.intervals_chain(X, foapy.binding.start, foapy.chain_mode.boundary)
56 print(chain) # [1 2 2 4 2]
57 ```
59 ``` py linenums="1"
60 import foapy
62 X = ['b', 'a', 'b', 'c', 'b']
63 chain = foapy.intervals_chain(X, foapy.binding.start, foapy.chain_mode.cycle)
64 print(chain) # [1 5 2 5 2]
65 ```
67 ``` py linenums="1"
68 import foapy
70 source = []
71 chain = foapy.intervals_chain(source, foapy.binding.start, foapy.chain_mode.boundary) # noqa: E501
72 print(chain) # []
73 ```
74 """
75 if binding not in {binding_cls.start, binding_cls.end}:
76 raise ValueError(
77 {"message": "Invalid binding value. Use binding.start or binding.end."}
78 )
80 if chain_mode not in {chain_mode_cls.boundary, chain_mode_cls.cycle}:
81 raise ValueError(
82 {
83 "message": (
84 "Invalid chain_mode value. "
85 "Use chain_mode.boundary or chain_mode.cycle."
86 )
87 }
88 )
90 ar = np.asanyarray(X)
92 if ar.ndim != 1:
93 raise Not1DArrayException(
94 {"message": (f"Incorrect array form. Expected d1 array, exists {ar.ndim}")}
95 )
97 if ar.shape[0] == 0:
98 return np.array([], dtype=np.intp)
100 if binding == binding_cls.end:
101 ar = ar[::-1]
103 perm = ar.argsort(kind="mergesort")
105 n = ar.shape[0]
106 mask = np.empty(n + 1, dtype=bool)
107 mask[:1] = True
108 mask[1:-1] = ar[perm[1:]] != ar[perm[:-1]]
109 mask[-1:] = True
111 first_mask = mask[:-1]
112 last_mask = mask[1:]
114 chain = np.empty(ar.shape, dtype=np.intp)
115 chain[1:] = perm[1:] - perm[:-1]
117 if chain_mode == chain_mode_cls.cycle:
118 delta = n - perm[last_mask]
119 else:
120 delta = 1
122 chain[first_mask] = perm[first_mask] + delta
124 inverse_perm = np.empty(ar.shape, dtype=np.intp)
125 inverse_perm[perm] = np.arange(n)
127 result = chain[inverse_perm]
129 if binding == binding_cls.end:
130 result = result[::-1]
132 return result