systemictau-cancellation 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Johel Padilla-Villanueva
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,91 @@
1
+ Metadata-Version: 2.4
2
+ Name: systemictau-cancellation
3
+ Version: 0.1.0
4
+ Summary: Cancellation channel beside signed Systemic Tau: D = |mean change| + C
5
+ Author-email: Johel Padilla-Villanueva <joel.padilla2@upr.edu>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/johelpadilla/systemictau-cancellation
8
+ Project-URL: Source, https://github.com/johelpadilla/systemictau-cancellation
9
+ Project-URL: Note, https://doi.org/10.5281/zenodo.23073069
10
+ Keywords: kendall-tau,systemic-tau,concordance,cancellation,time-series
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Scientific/Engineering
20
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
21
+ Requires-Python: >=3.9
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: numpy>=1.22
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=7.0; extra == "dev"
27
+ Requires-Dist: scipy>=1.10; extra == "dev"
28
+ Dynamic: license-file
29
+
30
+ # systemictau-cancellation 0.1.0
31
+
32
+ Canal de cancelación al lado de la media con signo de Tau Sistémico.
33
+
34
+ La nota [10.5281/zenodo.23073069](https://doi.org/10.5281/zenodo.23073069) parte el cambio absoluto de las concordancias por pares:
35
+
36
+ ```text
37
+ D = |Δ̄| + C, κ = C / D
38
+ ```
39
+
40
+ `Δ̄` es el cambio de la media con signo: el τ_s que ya devuelve `systemictau` 4.6.1. `C` es la parte que se cancela cuando unos pares ganan concordancia y otros la pierden. Este paquete calcula las dos piezas. No importa `systemictau` y no modifica la compuerta, el reloj `T_n`, `Protocol.frozen_v1` ni `nested-recd`.
41
+
42
+ Instalación: `pip install systemictau-cancellation`.
43
+
44
+ ## Uso
45
+
46
+ `X` tiene la misma forma que en `systemictau`: `(tiempo, módulos)`.
47
+
48
+ ```python
49
+ from systemictau_cancellation import cancellation_channel, format_report
50
+
51
+ out = cancellation_channel(X, window=13, n_cal=19, alpha=0.05)
52
+ print(format_report(out))
53
+ print(out.mode) # calibration, quiet, coherent, cancellation, mixed
54
+ print(out.tau_s) # media con signo, tau-b
55
+ print(out.D, out.C, out.kappa)
56
+ ```
57
+
58
+ Las primeras `n_cal` ventanas son la línea base. El paso por defecto es igual a la ventana, así que los bloques no se solapan: esa es la disposición en la que la nota calibra el valor p. Con `alpha=0.05` hacen falta al menos 19 ventanas de calibración; si no, el valor p mínimo no alcanza 0.05.
59
+
60
+ | Modo | Qué pasó en esa ventana |
61
+ |---|---|
62
+ | `coherent` | Se movió la media con signo |
63
+ | `cancellation` | Se movió el cambio absoluto y la media con signo se quedó |
64
+ | `mixed` | Las dos alarmas se encienden |
65
+ | `quiet` | Ninguna alarma |
66
+ | `calibration` | Ventana usada como línea base |
67
+
68
+ Un desplazamiento grande y puro enciende las dos alarmas, porque ahí `D = |Δ̄|`. El modo queda `mixed`. La cancelación es el caso en el que `D` se enciende y la media no.
69
+
70
+ ## Cómo leer el modo
71
+
72
+ `cancellation` dice que `D` salió de la calibración y la media con signo no. En la nota, ese patrón es el recableado balanceado. En un ciclo corto cuyos estados vecinos están más juntos que el ruido, el mismo patrón puede ser el ruido volteando rangos mientras el orden medio se sostiene. El modo no nombra la causa.
73
+
74
+ El valor p es la calibración de la nota cuando los bloques son intercambiables y no se solapan. En una órbita continua las ventanas siguen siendo dependientes aunque el paso sea igual a la ventana, y el valor p queda descriptivo. Si el paso es menor que la ventana, `windows_overlap` es verdadero y el informe lo dice.
75
+
76
+ ## Qué τ se usa
77
+
78
+ Cada ventana usa tau-b, el coseno de los vectores de signos, como la nota. El camino Numba de `systemictau` 4.6.1 usa tau-a. Sin empates los dos coinciden. `signed_tau_series(X, window=13)` devuelve la media con signo en el mismo índice temporal que `compute_taus`.
79
+
80
+ ## Reproducir las ilustraciones
81
+
82
+ ```bash
83
+ python -m pytest -q
84
+ python examples/demo_modes.py
85
+ ```
86
+
87
+ La figura queda en `examples/modes_demo.png`. Esas series son un ejemplo corto. La Tabla 1 de la nota (semilla 20260930, 1000 ventanas) sigue viviendo en `cancellation-aware-tau` y no se regenera aquí.
88
+
89
+ ## Licencia
90
+
91
+ MIT. Johel Padilla-Villanueva, ORCID 0000-0002-5797-6931.
@@ -0,0 +1,62 @@
1
+ # systemictau-cancellation 0.1.0
2
+
3
+ Canal de cancelación al lado de la media con signo de Tau Sistémico.
4
+
5
+ La nota [10.5281/zenodo.23073069](https://doi.org/10.5281/zenodo.23073069) parte el cambio absoluto de las concordancias por pares:
6
+
7
+ ```text
8
+ D = |Δ̄| + C, κ = C / D
9
+ ```
10
+
11
+ `Δ̄` es el cambio de la media con signo: el τ_s que ya devuelve `systemictau` 4.6.1. `C` es la parte que se cancela cuando unos pares ganan concordancia y otros la pierden. Este paquete calcula las dos piezas. No importa `systemictau` y no modifica la compuerta, el reloj `T_n`, `Protocol.frozen_v1` ni `nested-recd`.
12
+
13
+ Instalación: `pip install systemictau-cancellation`.
14
+
15
+ ## Uso
16
+
17
+ `X` tiene la misma forma que en `systemictau`: `(tiempo, módulos)`.
18
+
19
+ ```python
20
+ from systemictau_cancellation import cancellation_channel, format_report
21
+
22
+ out = cancellation_channel(X, window=13, n_cal=19, alpha=0.05)
23
+ print(format_report(out))
24
+ print(out.mode) # calibration, quiet, coherent, cancellation, mixed
25
+ print(out.tau_s) # media con signo, tau-b
26
+ print(out.D, out.C, out.kappa)
27
+ ```
28
+
29
+ Las primeras `n_cal` ventanas son la línea base. El paso por defecto es igual a la ventana, así que los bloques no se solapan: esa es la disposición en la que la nota calibra el valor p. Con `alpha=0.05` hacen falta al menos 19 ventanas de calibración; si no, el valor p mínimo no alcanza 0.05.
30
+
31
+ | Modo | Qué pasó en esa ventana |
32
+ |---|---|
33
+ | `coherent` | Se movió la media con signo |
34
+ | `cancellation` | Se movió el cambio absoluto y la media con signo se quedó |
35
+ | `mixed` | Las dos alarmas se encienden |
36
+ | `quiet` | Ninguna alarma |
37
+ | `calibration` | Ventana usada como línea base |
38
+
39
+ Un desplazamiento grande y puro enciende las dos alarmas, porque ahí `D = |Δ̄|`. El modo queda `mixed`. La cancelación es el caso en el que `D` se enciende y la media no.
40
+
41
+ ## Cómo leer el modo
42
+
43
+ `cancellation` dice que `D` salió de la calibración y la media con signo no. En la nota, ese patrón es el recableado balanceado. En un ciclo corto cuyos estados vecinos están más juntos que el ruido, el mismo patrón puede ser el ruido volteando rangos mientras el orden medio se sostiene. El modo no nombra la causa.
44
+
45
+ El valor p es la calibración de la nota cuando los bloques son intercambiables y no se solapan. En una órbita continua las ventanas siguen siendo dependientes aunque el paso sea igual a la ventana, y el valor p queda descriptivo. Si el paso es menor que la ventana, `windows_overlap` es verdadero y el informe lo dice.
46
+
47
+ ## Qué τ se usa
48
+
49
+ Cada ventana usa tau-b, el coseno de los vectores de signos, como la nota. El camino Numba de `systemictau` 4.6.1 usa tau-a. Sin empates los dos coinciden. `signed_tau_series(X, window=13)` devuelve la media con signo en el mismo índice temporal que `compute_taus`.
50
+
51
+ ## Reproducir las ilustraciones
52
+
53
+ ```bash
54
+ python -m pytest -q
55
+ python examples/demo_modes.py
56
+ ```
57
+
58
+ La figura queda en `examples/modes_demo.png`. Esas series son un ejemplo corto. La Tabla 1 de la nota (semilla 20260930, 1000 ventanas) sigue viviendo en `cancellation-aware-tau` y no se regenera aquí.
59
+
60
+ ## Licencia
61
+
62
+ MIT. Johel Padilla-Villanueva, ORCID 0000-0002-5797-6931.
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "systemictau-cancellation"
7
+ version = "0.1.0"
8
+ description = "Cancellation channel beside signed Systemic Tau: D = |mean change| + C"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [
14
+ {name = "Johel Padilla-Villanueva", email = "joel.padilla2@upr.edu"},
15
+ ]
16
+ keywords = ["kendall-tau", "systemic-tau", "concordance", "cancellation", "time-series"]
17
+ classifiers = [
18
+ "Development Status :: 3 - Alpha",
19
+ "Intended Audience :: Science/Research",
20
+ "Operating System :: OS Independent",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.9",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Topic :: Scientific/Engineering",
27
+ "Topic :: Scientific/Engineering :: Information Analysis",
28
+ ]
29
+ dependencies = ["numpy>=1.22"]
30
+
31
+ [project.optional-dependencies]
32
+ dev = ["pytest>=7.0", "scipy>=1.10"]
33
+
34
+ [project.urls]
35
+ Homepage = "https://github.com/johelpadilla/systemictau-cancellation"
36
+ Source = "https://github.com/johelpadilla/systemictau-cancellation"
37
+ Note = "https://doi.org/10.5281/zenodo.23073069"
38
+
39
+ [tool.setuptools.packages.find]
40
+ where = ["src"]
41
+
42
+ [tool.pytest.ini_options]
43
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,30 @@
1
+ """Cancellation channel beside signed Systemic Tau.
2
+
3
+ The published motor (systemictau 4.6.1) returns the signed mean. This
4
+ package keeps that mean and adds the residual of the 2026-09-30 note:
5
+ D = |mean change| + C.
6
+ """
7
+
8
+ from .channel import (
9
+ NOTE_DOI,
10
+ ChannelResult,
11
+ cancellation_channel,
12
+ format_report,
13
+ minimum_calibration_windows,
14
+ signed_tau_series,
15
+ )
16
+ from .decomposition import decompose
17
+ from .tau import tau_b_matrix
18
+
19
+ __version__ = "0.1.0"
20
+ __all__ = [
21
+ "NOTE_DOI",
22
+ "ChannelResult",
23
+ "cancellation_channel",
24
+ "decompose",
25
+ "format_report",
26
+ "minimum_calibration_windows",
27
+ "signed_tau_series",
28
+ "tau_b_matrix",
29
+ "__version__",
30
+ ]
@@ -0,0 +1,257 @@
1
+ """Cancellation channel for a multivariate series.
2
+
3
+ Input layout matches systemictau: X has shape (n_time, n_modules).
4
+ The signed mean on each window is tau-b. D, C and kappa are measured
5
+ against the mean of a calibration block of non-overlapping windows.
6
+
7
+ Modes on a monitoring window:
8
+
9
+ - coherent: the signed mean leaves the calibration range
10
+ - cancellation: the absolute change leaves that range and the signed mean does not
11
+ - mixed: both scores leave the range
12
+ - quiet: neither score leaves the range
13
+
14
+ The gate g(tau_s), the accumulated clock T_n and Protocol.frozen_v1 are
15
+ left untouched. This module does not import them.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import math
20
+ from dataclasses import dataclass
21
+
22
+ import numpy as np
23
+
24
+ from .decomposition import conformal_pvalue, conformal_scores, decompose, stat_D, stat_abs_mean
25
+ from .tau import all_pairs, tau_b_matrix
26
+
27
+ NOTE_DOI = "10.5281/zenodo.23073069"
28
+ NOTE_RECORD = "https://doi.org/10.5281/zenodo.23073069"
29
+
30
+
31
+ def minimum_calibration_windows(alpha: float) -> int:
32
+ """Smallest n_cal whose conformal p-value can fall at or under alpha."""
33
+ if not 0.0 < float(alpha) < 1.0:
34
+ raise ValueError("alpha must be strictly between 0 and 1")
35
+ return int(math.ceil(1.0 / float(alpha) - 1.0))
36
+
37
+
38
+ @dataclass
39
+ class ChannelResult:
40
+ """One row per window. Calibration rows leave the scores empty."""
41
+
42
+ window_end: np.ndarray
43
+ tau_s: np.ndarray
44
+ dbar: np.ndarray
45
+ D: np.ndarray
46
+ C: np.ndarray
47
+ kappa: np.ndarray
48
+ p_D: np.ndarray
49
+ p_signed: np.ndarray
50
+ mode: np.ndarray
51
+ undefined_fraction: np.ndarray
52
+ baseline: np.ndarray
53
+ pairs: np.ndarray
54
+ alpha: float
55
+ n_cal: int
56
+ window: int
57
+ stride: int
58
+ windows_overlap: bool
59
+ note_doi: str = NOTE_DOI
60
+
61
+ def monitoring_mask(self) -> np.ndarray:
62
+ return self.mode != "calibration"
63
+
64
+ def summary(self) -> dict:
65
+ mon = self.monitoring_mask()
66
+ modes, counts = np.unique(self.mode[mon], return_counts=True)
67
+ return {
68
+ "n_monitoring": int(mon.sum()),
69
+ "mean_abs_dbar": float(np.mean(np.abs(self.dbar[mon]))) if mon.any() else float("nan"),
70
+ "mean_D": float(np.mean(self.D[mon])) if mon.any() else float("nan"),
71
+ "mean_kappa": float(np.nanmean(self.kappa[mon])) if mon.any() else float("nan"),
72
+ "modes": {str(k): int(v) for k, v in zip(modes, counts)},
73
+ }
74
+
75
+
76
+ def format_report(result: ChannelResult) -> str:
77
+ """Spanish one-screen summary of a channel result."""
78
+ s = result.summary()
79
+ modes = ", ".join(f"{k}: {v}" for k, v in sorted(s["modes"].items()))
80
+ overlap = ""
81
+ if result.windows_overlap:
82
+ overlap = (
83
+ "\nLas ventanas se solapan. El valor p describe el puntaje; "
84
+ "la nota calibra ventanas no solapadas."
85
+ )
86
+ return (
87
+ f"Canal de cancelación (nota {result.note_doi})\n"
88
+ f"ventana={result.window} paso={result.stride} "
89
+ f"calibración={result.n_cal} alpha={result.alpha}\n"
90
+ f"ventanas de monitoreo: {s['n_monitoring']}\n"
91
+ f"media |Δ̄| = {s['mean_abs_dbar']:.4f}\n"
92
+ f"media D = {s['mean_D']:.4f}\n"
93
+ f"media κ = {s['mean_kappa']:.4f}\n"
94
+ f"modos: {modes}"
95
+ f"{overlap}"
96
+ )
97
+
98
+
99
+ def _as_series(X: np.ndarray) -> np.ndarray:
100
+ X = np.asarray(X, dtype=float)
101
+ if X.ndim != 2:
102
+ raise ValueError("X must have shape (n_time, n_modules)")
103
+ if X.shape[1] < 2:
104
+ raise ValueError("at least two modules are required")
105
+ if not np.isfinite(X).all():
106
+ raise ValueError("X contains a non-finite value")
107
+ return X
108
+
109
+
110
+ def _pairs_of(n_modules: int, pairs) -> np.ndarray:
111
+ if pairs is None:
112
+ return all_pairs(n_modules)
113
+ out = np.asarray(pairs, dtype=int)
114
+ if out.ndim != 2 or out.shape[1] != 2 or out.shape[0] == 0:
115
+ raise ValueError("pairs must have shape (n_pairs, 2)")
116
+ if out.min() < 0 or out.max() >= n_modules:
117
+ raise ValueError("a pair index is outside the module range")
118
+ if np.any(out[:, 0] == out[:, 1]):
119
+ raise ValueError("a pair repeats the same module")
120
+ return out
121
+
122
+
123
+ def _window_ends(n_time: int, window: int, stride: int) -> np.ndarray:
124
+ if window < 2:
125
+ raise ValueError("window must be at least 2")
126
+ if stride < 1:
127
+ raise ValueError("stride must be at least 1")
128
+ if n_time < window:
129
+ raise ValueError("the series is shorter than one window")
130
+ return np.arange(window - 1, n_time, stride, dtype=int)
131
+
132
+
133
+ def _pair_rows(X: np.ndarray, window: int, stride: int, pairs: np.ndarray):
134
+ ends = _window_ends(X.shape[0], window, stride)
135
+ n = len(ends)
136
+ m = pairs.shape[0]
137
+ values = np.empty((n, m), dtype=float)
138
+ undefined = np.empty(n, dtype=float)
139
+ left = pairs[:, 0]
140
+ right = pairs[:, 1]
141
+ for k, end in enumerate(ends):
142
+ block = X[end - window + 1:end + 1].T
143
+ tau, defined = tau_b_matrix(block)
144
+ values[k] = tau[left, right]
145
+ undefined[k] = 1.0 - float(defined[left, right].mean())
146
+ return ends, values, undefined
147
+
148
+
149
+ def signed_tau_series(X: np.ndarray, window: int = 13, stride: int = 1, pairs=None) -> np.ndarray:
150
+ """Signed mean of pairwise tau-b, indexed like systemictau.compute_taus.
151
+
152
+ The value sits at the last time index of each window. Earlier positions,
153
+ and positions skipped by the stride, are nan.
154
+ """
155
+ X = _as_series(X)
156
+ chosen = _pairs_of(X.shape[1], pairs)
157
+ ends, values, _ = _pair_rows(X, int(window), int(stride), chosen)
158
+ taus = np.full(X.shape[0], np.nan)
159
+ taus[ends] = values.mean(axis=1)
160
+ return taus
161
+
162
+
163
+ def _mode(p_D: float, p_signed: float, alpha: float) -> str:
164
+ hit_D = p_D <= alpha
165
+ hit_signed = p_signed <= alpha
166
+ if hit_signed and hit_D:
167
+ return "mixed"
168
+ if hit_signed:
169
+ return "coherent"
170
+ if hit_D:
171
+ return "cancellation"
172
+ return "quiet"
173
+
174
+
175
+ def cancellation_channel(
176
+ X: np.ndarray,
177
+ window: int = 13,
178
+ stride: int | None = None,
179
+ n_cal: int = 19,
180
+ alpha: float = 0.05,
181
+ pairs=None,
182
+ ) -> ChannelResult:
183
+ """Baseline the first n_cal windows and label every later window.
184
+
185
+ The default stride equals the window, so the blocks do not overlap.
186
+ That is the layout in which the note calibrates the conformal p-value,
187
+ and the calibration applies when those blocks are exchangeable.
188
+ On a continuous orbit the windows stay serially dependent even when they
189
+ do not overlap, so the p-value is descriptive. A smaller stride is
190
+ accepted; ChannelResult.windows_overlap is then true and the report says so.
191
+
192
+ A mode of ``cancellation`` means D left the calibration range and the
193
+ signed mean did not. On a short cycle this can be noise flipping ranks.
194
+ """
195
+ X = _as_series(X)
196
+ window = int(window)
197
+ if stride is None:
198
+ stride = window
199
+ stride = int(stride)
200
+ n_cal = int(n_cal)
201
+ alpha = float(alpha)
202
+ need = minimum_calibration_windows(alpha)
203
+ if n_cal < need:
204
+ raise ValueError(
205
+ f"n_cal={n_cal} cannot reach alpha={alpha}. "
206
+ f"Use at least {need} calibration windows."
207
+ )
208
+ chosen = _pairs_of(X.shape[1], pairs)
209
+ ends, values, undefined = _pair_rows(X, window, stride, chosen)
210
+ if len(ends) <= n_cal:
211
+ raise ValueError(
212
+ f"the series yields {len(ends)} windows and n_cal={n_cal}. "
213
+ "One monitoring window is required."
214
+ )
215
+
216
+ n = len(ends)
217
+ dbar = np.full(n, np.nan)
218
+ D = np.full(n, np.nan)
219
+ C = np.full(n, np.nan)
220
+ kappa = np.full(n, np.nan)
221
+ p_D = np.full(n, np.nan)
222
+ p_signed = np.full(n, np.nan)
223
+ mode = np.full(n, "calibration", dtype="<U12")
224
+ cal = values[:n_cal]
225
+ baseline = cal.mean(axis=0)
226
+
227
+ for k in range(n_cal, n):
228
+ part = decompose(values[k] - baseline)
229
+ scores_D, new_D = conformal_scores(cal, values[k], stat_D)
230
+ scores_s, new_s = conformal_scores(cal, values[k], stat_abs_mean)
231
+ dbar[k] = part["dbar"]
232
+ D[k] = part["D"]
233
+ C[k] = part["C"]
234
+ kappa[k] = part["kappa"]
235
+ p_D[k] = conformal_pvalue(scores_D, new_D)
236
+ p_signed[k] = conformal_pvalue(scores_s, new_s)
237
+ mode[k] = _mode(p_D[k], p_signed[k], alpha)
238
+
239
+ return ChannelResult(
240
+ window_end=ends,
241
+ tau_s=values.mean(axis=1),
242
+ dbar=dbar,
243
+ D=D,
244
+ C=C,
245
+ kappa=kappa,
246
+ p_D=p_D,
247
+ p_signed=p_signed,
248
+ mode=mode,
249
+ undefined_fraction=undefined,
250
+ baseline=baseline,
251
+ pairs=chosen,
252
+ alpha=alpha,
253
+ n_cal=n_cal,
254
+ window=window,
255
+ stride=stride,
256
+ windows_overlap=stride < window,
257
+ )
@@ -0,0 +1,71 @@
1
+ """Exact split of a vector of pairwise changes.
2
+
3
+ D = |mean(delta)| + C
4
+ kappa = C / D
5
+
6
+ C is the absolute change cancelled by opposite signs. The identity is the
7
+ one proved and checked in Padilla-Villanueva (2026),
8
+ https://doi.org/10.5281/zenodo.23073069.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import numpy as np
13
+
14
+
15
+ def decompose(delta: np.ndarray) -> dict:
16
+ """Split one vector of edge changes into signed mean, D, C and kappa."""
17
+ d = np.asarray(delta, dtype=float).ravel()
18
+ m = int(d.size)
19
+ if m == 0:
20
+ raise ValueError("empty edge set")
21
+ s_plus = float(d[d > 0].sum())
22
+ s_minus = float((-d[d < 0]).sum())
23
+ D = float(np.abs(d).mean())
24
+ dbar = float(d.mean())
25
+ C = D - abs(dbar)
26
+ if dbar > 0:
27
+ minority = s_minus
28
+ elif dbar < 0:
29
+ minority = s_plus
30
+ else:
31
+ minority = s_plus
32
+ C_min = 2.0 * minority / m
33
+ kappa = C / D if D > 0 else float("nan")
34
+ return {
35
+ "D": D,
36
+ "dbar": dbar,
37
+ "abs_dbar": abs(dbar),
38
+ "C": C,
39
+ "C_minority": C_min,
40
+ "kappa": kappa,
41
+ "S_plus": s_plus,
42
+ "S_minus": s_minus,
43
+ "argmax": int(np.argmax(np.abs(d))),
44
+ }
45
+
46
+
47
+ def stat_D(delta: np.ndarray) -> float:
48
+ return float(np.mean(np.abs(np.asarray(delta, dtype=float))))
49
+
50
+
51
+ def stat_abs_mean(delta: np.ndarray) -> float:
52
+ return float(abs(np.mean(np.asarray(delta, dtype=float))))
53
+
54
+
55
+ def conformal_scores(cal: np.ndarray, new: np.ndarray, stat) -> tuple[np.ndarray, float]:
56
+ """Full-conformal scores. Calibration scores depend on the new window.
57
+
58
+ For the monitored window the baseline is the mean of the calibration
59
+ windows. Same construction as the technical note.
60
+ """
61
+ allw = np.vstack([np.asarray(cal, dtype=float), np.asarray(new, dtype=float)[None, :]])
62
+ n1 = allw.shape[0]
63
+ total = allw.sum(axis=0)
64
+ base = (total[None, :] - allw) / (n1 - 1)
65
+ scores = np.array([stat(allw[k] - base[k]) for k in range(n1)], dtype=float)
66
+ return scores[:-1], float(scores[-1])
67
+
68
+
69
+ def conformal_pvalue(cal_scores: np.ndarray, new_score: float) -> float:
70
+ cal_scores = np.asarray(cal_scores, dtype=float)
71
+ return float((1.0 + np.sum(cal_scores >= new_score)) / (len(cal_scores) + 1.0))
@@ -0,0 +1,56 @@
1
+ """Small series with a known order pattern.
2
+
3
+ These are illustrations for the channel. They are not the Monte Carlo of
4
+ the technical note (seed 20260930, 1000 windows).
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import numpy as np
9
+
10
+
11
+ def _independent(n_windows: int, window: int, n_modules: int, rng) -> np.ndarray:
12
+ return rng.normal(size=(n_windows, window, n_modules))
13
+
14
+
15
+ def _ordered(window: int, n_modules: int, reversed_modules: tuple[int, ...]) -> np.ndarray:
16
+ ticks = np.arange(window, dtype=float)
17
+ block = np.tile(ticks[:, None], (1, n_modules))
18
+ for j in reversed_modules:
19
+ block[:, j] = ticks[::-1]
20
+ return block
21
+
22
+
23
+ def stack_windows(blocks: np.ndarray) -> np.ndarray:
24
+ """(n_windows, window, n_modules) -> (n_time, n_modules)."""
25
+ n_windows, window, n_modules = blocks.shape
26
+ return np.ascontiguousarray(blocks.reshape(n_windows * window, n_modules))
27
+
28
+
29
+ def quiet_series(n_cal: int, n_mon: int, window: int, n_modules: int, seed: int) -> np.ndarray:
30
+ rng = np.random.default_rng(seed)
31
+ blocks = _independent(n_cal + n_mon, window, n_modules, rng)
32
+ return stack_windows(blocks)
33
+
34
+
35
+ def coherent_series(n_cal: int, n_mon: int, window: int, n_modules: int, seed: int) -> np.ndarray:
36
+ """Calibration is independent noise. Monitoring windows share one order."""
37
+ rng = np.random.default_rng(seed)
38
+ cal = _independent(n_cal, window, n_modules, rng)
39
+ mono = _ordered(window, n_modules, ())
40
+ mon = np.broadcast_to(mono, (n_mon, window, n_modules)).copy()
41
+ return stack_windows(np.concatenate([cal, mon], axis=0))
42
+
43
+
44
+ def cancellation_series(n_cal: int, n_mon: int, window: int, seed: int) -> np.ndarray:
45
+ """Four modules. Monitoring taus are +1,+1,+1,-1,-1,-1.
46
+
47
+ Modules 0, 1 and 3 rise together. Module 2 is the reverse order.
48
+ The signed mean of the six pairs is 0. Every pair is at ±1, so the
49
+ absolute change is large when the calibration baseline is near 0.
50
+ """
51
+ rng = np.random.default_rng(seed)
52
+ n_modules = 4
53
+ cal = _independent(n_cal, window, n_modules, rng)
54
+ mono = _ordered(window, n_modules, (2,))
55
+ mon = np.broadcast_to(mono, (n_mon, window, n_modules)).copy()
56
+ return stack_windows(np.concatenate([cal, mon], axis=0))
@@ -0,0 +1,47 @@
1
+ """Kendall tau-b on one window.
2
+
3
+ Same definition as the technical note
4
+ *Cancellation-aware order coherence* (Padilla-Villanueva, 2026;
5
+ https://doi.org/10.5281/zenodo.23073069): tau-b is the cosine of the
6
+ pairwise sign vectors. A constant stream has an undefined coefficient;
7
+ it is stored as 0 and marked in the definedness mask.
8
+
9
+ This is tau-b. The Numba path of systemictau 4.6.1 divides by n(n-1)/2
10
+ and is tau-a. The two agree when the window has no ties.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ from functools import lru_cache
15
+
16
+ import numpy as np
17
+
18
+
19
+ @lru_cache(maxsize=16)
20
+ def _pair_index(n_bins: int):
21
+ return np.triu_indices(int(n_bins), 1)
22
+
23
+
24
+ def sign_vectors(Z: np.ndarray) -> np.ndarray:
25
+ """Sign of every within-stream comparison. Z has shape (n_modules, n_bins)."""
26
+ Z = np.asarray(Z, dtype=float)
27
+ k, l = _pair_index(Z.shape[1])
28
+ return np.sign(Z[:, l] - Z[:, k])
29
+
30
+
31
+ def tau_b_matrix(Z: np.ndarray):
32
+ """Return (tau, defined) for one window. Z has shape (n_modules, n_bins)."""
33
+ S = sign_vectors(Z)
34
+ G = S @ S.T
35
+ nrm = np.sqrt(np.clip(np.diag(G), 0.0, None))
36
+ den = np.outer(nrm, nrm)
37
+ defined = den > 0
38
+ T = np.zeros_like(G, dtype=float)
39
+ np.divide(G, den, out=T, where=defined)
40
+ np.fill_diagonal(T, 1.0)
41
+ return T, defined
42
+
43
+
44
+ def all_pairs(n_modules: int) -> np.ndarray:
45
+ """Upper-triangle pairs as an array of shape (n_pairs, 2)."""
46
+ i, j = np.triu_indices(int(n_modules), 1)
47
+ return np.column_stack((i, j))
@@ -0,0 +1,91 @@
1
+ Metadata-Version: 2.4
2
+ Name: systemictau-cancellation
3
+ Version: 0.1.0
4
+ Summary: Cancellation channel beside signed Systemic Tau: D = |mean change| + C
5
+ Author-email: Johel Padilla-Villanueva <joel.padilla2@upr.edu>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/johelpadilla/systemictau-cancellation
8
+ Project-URL: Source, https://github.com/johelpadilla/systemictau-cancellation
9
+ Project-URL: Note, https://doi.org/10.5281/zenodo.23073069
10
+ Keywords: kendall-tau,systemic-tau,concordance,cancellation,time-series
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Scientific/Engineering
20
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
21
+ Requires-Python: >=3.9
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: numpy>=1.22
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=7.0; extra == "dev"
27
+ Requires-Dist: scipy>=1.10; extra == "dev"
28
+ Dynamic: license-file
29
+
30
+ # systemictau-cancellation 0.1.0
31
+
32
+ Canal de cancelación al lado de la media con signo de Tau Sistémico.
33
+
34
+ La nota [10.5281/zenodo.23073069](https://doi.org/10.5281/zenodo.23073069) parte el cambio absoluto de las concordancias por pares:
35
+
36
+ ```text
37
+ D = |Δ̄| + C, κ = C / D
38
+ ```
39
+
40
+ `Δ̄` es el cambio de la media con signo: el τ_s que ya devuelve `systemictau` 4.6.1. `C` es la parte que se cancela cuando unos pares ganan concordancia y otros la pierden. Este paquete calcula las dos piezas. No importa `systemictau` y no modifica la compuerta, el reloj `T_n`, `Protocol.frozen_v1` ni `nested-recd`.
41
+
42
+ Instalación: `pip install systemictau-cancellation`.
43
+
44
+ ## Uso
45
+
46
+ `X` tiene la misma forma que en `systemictau`: `(tiempo, módulos)`.
47
+
48
+ ```python
49
+ from systemictau_cancellation import cancellation_channel, format_report
50
+
51
+ out = cancellation_channel(X, window=13, n_cal=19, alpha=0.05)
52
+ print(format_report(out))
53
+ print(out.mode) # calibration, quiet, coherent, cancellation, mixed
54
+ print(out.tau_s) # media con signo, tau-b
55
+ print(out.D, out.C, out.kappa)
56
+ ```
57
+
58
+ Las primeras `n_cal` ventanas son la línea base. El paso por defecto es igual a la ventana, así que los bloques no se solapan: esa es la disposición en la que la nota calibra el valor p. Con `alpha=0.05` hacen falta al menos 19 ventanas de calibración; si no, el valor p mínimo no alcanza 0.05.
59
+
60
+ | Modo | Qué pasó en esa ventana |
61
+ |---|---|
62
+ | `coherent` | Se movió la media con signo |
63
+ | `cancellation` | Se movió el cambio absoluto y la media con signo se quedó |
64
+ | `mixed` | Las dos alarmas se encienden |
65
+ | `quiet` | Ninguna alarma |
66
+ | `calibration` | Ventana usada como línea base |
67
+
68
+ Un desplazamiento grande y puro enciende las dos alarmas, porque ahí `D = |Δ̄|`. El modo queda `mixed`. La cancelación es el caso en el que `D` se enciende y la media no.
69
+
70
+ ## Cómo leer el modo
71
+
72
+ `cancellation` dice que `D` salió de la calibración y la media con signo no. En la nota, ese patrón es el recableado balanceado. En un ciclo corto cuyos estados vecinos están más juntos que el ruido, el mismo patrón puede ser el ruido volteando rangos mientras el orden medio se sostiene. El modo no nombra la causa.
73
+
74
+ El valor p es la calibración de la nota cuando los bloques son intercambiables y no se solapan. En una órbita continua las ventanas siguen siendo dependientes aunque el paso sea igual a la ventana, y el valor p queda descriptivo. Si el paso es menor que la ventana, `windows_overlap` es verdadero y el informe lo dice.
75
+
76
+ ## Qué τ se usa
77
+
78
+ Cada ventana usa tau-b, el coseno de los vectores de signos, como la nota. El camino Numba de `systemictau` 4.6.1 usa tau-a. Sin empates los dos coinciden. `signed_tau_series(X, window=13)` devuelve la media con signo en el mismo índice temporal que `compute_taus`.
79
+
80
+ ## Reproducir las ilustraciones
81
+
82
+ ```bash
83
+ python -m pytest -q
84
+ python examples/demo_modes.py
85
+ ```
86
+
87
+ La figura queda en `examples/modes_demo.png`. Esas series son un ejemplo corto. La Tabla 1 de la nota (semilla 20260930, 1000 ventanas) sigue viviendo en `cancellation-aware-tau` y no se regenera aquí.
88
+
89
+ ## Licencia
90
+
91
+ MIT. Johel Padilla-Villanueva, ORCID 0000-0002-5797-6931.
@@ -0,0 +1,14 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/systemictau_cancellation/__init__.py
5
+ src/systemictau_cancellation/channel.py
6
+ src/systemictau_cancellation/decomposition.py
7
+ src/systemictau_cancellation/synthetic.py
8
+ src/systemictau_cancellation/tau.py
9
+ src/systemictau_cancellation.egg-info/PKG-INFO
10
+ src/systemictau_cancellation.egg-info/SOURCES.txt
11
+ src/systemictau_cancellation.egg-info/dependency_links.txt
12
+ src/systemictau_cancellation.egg-info/requires.txt
13
+ src/systemictau_cancellation.egg-info/top_level.txt
14
+ tests/test_channel.py
@@ -0,0 +1,5 @@
1
+ numpy>=1.22
2
+
3
+ [dev]
4
+ pytest>=7.0
5
+ scipy>=1.10
@@ -0,0 +1,120 @@
1
+ import sys
2
+ from pathlib import Path
3
+
4
+ import numpy as np
5
+ import pytest
6
+
7
+ from systemictau_cancellation import (
8
+ cancellation_channel,
9
+ decompose,
10
+ signed_tau_series,
11
+ tau_b_matrix,
12
+ )
13
+ from systemictau_cancellation.synthetic import (
14
+ cancellation_series,
15
+ coherent_series,
16
+ quiet_series,
17
+ )
18
+
19
+
20
+ def test_identity_on_random_changes():
21
+ rng = np.random.default_rng(1)
22
+ for _ in range(2000):
23
+ d = rng.normal(size=int(rng.integers(2, 40)))
24
+ r = decompose(d)
25
+ assert r["D"] == pytest.approx(r["abs_dbar"] + r["C"], abs=1e-12)
26
+ assert r["C"] == pytest.approx(r["C_minority"], abs=1e-12)
27
+ if r["D"] > 0:
28
+ assert r["kappa"] == pytest.approx(1.0 - r["abs_dbar"] / r["D"], abs=1e-12)
29
+
30
+
31
+ def test_kappa_extremes():
32
+ assert decompose(np.array([0.1, 0.3, 0.0, 0.2]))["kappa"] == pytest.approx(0.0)
33
+ assert decompose(np.array([0.1, -0.1, 0.4, -0.4]))["kappa"] == pytest.approx(1.0)
34
+
35
+
36
+ def test_channel_identity_and_index():
37
+ X = quiet_series(n_cal=19, n_mon=6, window=12, n_modules=4, seed=2)
38
+ out = cancellation_channel(X, window=12, n_cal=19)
39
+ mon = out.monitoring_mask()
40
+ assert np.allclose(out.D[mon], np.abs(out.dbar[mon]) + out.C[mon])
41
+ assert out.mode[:19].tolist() == ["calibration"] * 19
42
+ assert out.window_end[0] == 11
43
+ assert out.windows_overlap is False
44
+ series = signed_tau_series(X, window=12, stride=12)
45
+ assert series[out.window_end] == pytest.approx(out.tau_s)
46
+
47
+
48
+ def test_coherent_series_moves_the_signed_mean():
49
+ X = coherent_series(n_cal=19, n_mon=4, window=12, n_modules=4, seed=3)
50
+ out = cancellation_channel(X, window=12, n_cal=19)
51
+ mon = out.monitoring_mask()
52
+ assert set(out.mode[mon]).issubset({"coherent", "mixed"})
53
+ assert float(np.mean(out.dbar[mon])) > 0.7
54
+ assert float(np.nanmean(out.kappa[mon])) < 0.15
55
+
56
+
57
+ def test_balanced_reversal_is_cancellation():
58
+ X = cancellation_series(n_cal=19, n_mon=4, window=12, seed=4)
59
+ out = cancellation_channel(X, window=12, n_cal=19)
60
+ mon = out.monitoring_mask()
61
+ assert set(out.mode[mon]) == {"cancellation"}
62
+ assert abs(float(np.mean(out.dbar[mon]))) < 0.15
63
+ assert float(np.nanmean(out.kappa[mon])) > 0.85
64
+ assert float(np.mean(out.D[mon])) > 0.7
65
+
66
+
67
+ def test_quiet_series_rarely_alarms():
68
+ X = quiet_series(n_cal=19, n_mon=40, window=10, n_modules=3, seed=5)
69
+ out = cancellation_channel(X, window=10, n_cal=19)
70
+ mon = out.mode[out.monitoring_mask()]
71
+ alarmed = np.isin(mon, ["coherent", "cancellation", "mixed"]).mean()
72
+ assert alarmed < 0.25
73
+
74
+
75
+ def test_short_calibration_is_refused():
76
+ X = quiet_series(n_cal=5, n_mon=2, window=8, n_modules=3, seed=6)
77
+ with pytest.raises(ValueError, match="n_cal"):
78
+ cancellation_channel(X, window=8, n_cal=5, alpha=0.05)
79
+
80
+
81
+ def test_tau_b_matches_scipy_when_present():
82
+ scipy_stats = pytest.importorskip("scipy.stats")
83
+ rng = np.random.default_rng(7)
84
+ Z = rng.poisson(0.8, size=(4, 30)).astype(float)
85
+ T, defined = tau_b_matrix(Z)
86
+ for i, j in [(0, 1), (0, 2), (1, 3)]:
87
+ if defined[i, j]:
88
+ ref = scipy_stats.kendalltau(Z[i], Z[j], variant="b").statistic
89
+ assert T[i, j] == pytest.approx(ref, abs=1e-12)
90
+
91
+
92
+ def test_matches_published_note_code():
93
+ note = Path("/Users/johelpadilla/grok-safe/cancellation-aware-tau/code")
94
+ if not (note / "cancellation_tau" / "decomposition.py").is_file():
95
+ pytest.skip("local note tree is absent")
96
+ sys.path.insert(0, str(note))
97
+ from cancellation_tau import decompose as note_decompose
98
+ from cancellation_tau import tau_b_matrix as note_tau
99
+ from cancellation_tau.calibration import conformal_pvalue as note_p
100
+ from cancellation_tau.calibration import conformal_scores as note_scores
101
+ from cancellation_tau.calibration import stat_D as note_stat_D
102
+ from systemictau_cancellation.decomposition import conformal_pvalue, conformal_scores, stat_D
103
+
104
+ rng = np.random.default_rng(20260930)
105
+ Z = rng.poisson(0.8, size=(5, 30)).astype(float)
106
+ got, got_defined = tau_b_matrix(Z)
107
+ ref, ref_defined = note_tau(Z)
108
+ assert np.allclose(got, ref)
109
+ assert np.array_equal(got_defined, ref_defined)
110
+ delta = rng.normal(size=15)
111
+ a, b = decompose(delta), note_decompose(delta)
112
+ for key in ("D", "dbar", "C", "kappa", "C_minority"):
113
+ assert a[key] == pytest.approx(b[key], abs=1e-12)
114
+ cal = rng.normal(size=(19, 6))
115
+ new = rng.normal(size=6)
116
+ got_scores, got_new = conformal_scores(cal, new, stat_D)
117
+ ref_scores, ref_new = note_scores(cal, new, note_stat_D)
118
+ assert got_scores == pytest.approx(ref_scores, abs=1e-12)
119
+ assert got_new == pytest.approx(ref_new, abs=1e-12)
120
+ assert conformal_pvalue(got_scores, got_new) == pytest.approx(note_p(ref_scores, ref_new))