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.
- systemictau_cancellation-0.1.0/LICENSE +21 -0
- systemictau_cancellation-0.1.0/PKG-INFO +91 -0
- systemictau_cancellation-0.1.0/README.md +62 -0
- systemictau_cancellation-0.1.0/pyproject.toml +43 -0
- systemictau_cancellation-0.1.0/setup.cfg +4 -0
- systemictau_cancellation-0.1.0/src/systemictau_cancellation/__init__.py +30 -0
- systemictau_cancellation-0.1.0/src/systemictau_cancellation/channel.py +257 -0
- systemictau_cancellation-0.1.0/src/systemictau_cancellation/decomposition.py +71 -0
- systemictau_cancellation-0.1.0/src/systemictau_cancellation/synthetic.py +56 -0
- systemictau_cancellation-0.1.0/src/systemictau_cancellation/tau.py +47 -0
- systemictau_cancellation-0.1.0/src/systemictau_cancellation.egg-info/PKG-INFO +91 -0
- systemictau_cancellation-0.1.0/src/systemictau_cancellation.egg-info/SOURCES.txt +14 -0
- systemictau_cancellation-0.1.0/src/systemictau_cancellation.egg-info/dependency_links.txt +1 -0
- systemictau_cancellation-0.1.0/src/systemictau_cancellation.egg-info/requires.txt +5 -0
- systemictau_cancellation-0.1.0/src/systemictau_cancellation.egg-info/top_level.txt +1 -0
- systemictau_cancellation-0.1.0/tests/test_channel.py +120 -0
|
@@ -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,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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
systemictau_cancellation
|
|
@@ -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))
|