nested-recd 0.2.0__tar.gz → 0.2.3__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.
@@ -1,14 +1,14 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nested-recd
3
- Version: 0.2.0
4
- Summary: Nested ordinal RECD: Φ1–Φ3, continuous excess³ (primary Level-3), and λ-weighted Discrete Extramental Clock
3
+ Version: 0.2.3
4
+ Summary: Nested ordinal RECD: Φ1–Φ3, continuous excess³ (exportable Level-3 proxy), Res_pair (strong residual), and λ-weighted Discrete Extramental Clock
5
5
  Author-email: Johel Padilla-Villanueva <joel.padilla2@upr.edu>
6
6
  License-Expression: MIT
7
7
  Project-URL: Homepage, https://github.com/johelpadilla/nested-recd
8
8
  Project-URL: Repository, https://github.com/johelpadilla/nested-recd
9
9
  Project-URL: Issues, https://github.com/johelpadilla/nested-recd/issues
10
10
  Project-URL: Documentation, https://github.com/johelpadilla/nested-recd#readme
11
- Project-URL: DOI, https://doi.org/10.5281/zenodo.21270699
11
+ Project-URL: DOI, https://doi.org/10.5281/zenodo.22970079
12
12
  Keywords: RECD,ordinal patterns,nested time,Systemic Tau,complexity,early-warning,network physiology,Bandt-Pompe
13
13
  Classifier: Development Status :: 4 - Beta
14
14
  Classifier: Intended Audience :: Science/Research
@@ -35,13 +35,26 @@ Dynamic: license-file
35
35
 
36
36
  # nested-recd
37
37
 
38
+ > **Correction (0.2.3).** Versions ≤ 0.2.2 computed `H_joint` and the pairwise
39
+ > entropies inside `Syn` with `np.unique` on a list of tuples and no `axis=0`.
40
+ > NumPy flattens that input, so `Syn`, `excess3` and binary `phi3` were not the
41
+ > published definition when `N ≥ 3`. `Surp` and `Res_pair` were not affected.
42
+ > For `N = 2`, `Syn` is 0 either way. 0.2.3 counts joint rows.
43
+ > `legacy_pooled_counting=True` reproduces the old numbers and warns.
44
+ > See `CHANGELOG.md`. This version:
45
+ > [10.5281/zenodo.22970079](https://doi.org/10.5281/zenodo.22970079).
46
+ > The 0.2.2 deposit remains
47
+ > [10.5281/zenodo.21937204](https://doi.org/10.5281/zenodo.21937204).
48
+
38
49
  [![PyPI version](https://img.shields.io/pypi/v/nested-recd.svg)](https://pypi.org/project/nested-recd/)
39
50
  [![Python](https://img.shields.io/pypi/pyversions/nested-recd.svg)](https://pypi.org/project/nested-recd/)
40
51
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
41
52
 
42
- **Nested ordinal RECD** — pure-NumPy implementation of nested ordinal conjunction levels (Φ₁, Φ₂, Φ₃), the continuous **excess³** Level-3 proxy, and λ-weighted Discrete Extramental Clock (RECD) accumulation.
53
+ **Nested ordinal RECD** — pure-NumPy implementation of nested ordinal conjunction levels (Φ₁, Φ₂, Φ₃), the continuous **excess³** Level-3 *proxy* (T-IV; not the strong residual), optional **Res_pair** \(=\mathrm{KL}(P\|P^{(2)})\), Gibbs-α weights, and λ-weighted Discrete Extramental Clock (RECD) accumulation.
54
+
55
+ This is the **canonical nested-clock software** for the Systemic Tau / RECD stack (foundations paper, excess³ methods preprint, cardiac CCTP/SDDB pilot, Academy Learning Tau).
43
56
 
44
- This is the **canonical Level-3 software** for the Systemic Tau / RECD stack (cardiac CCTP/SDDB pilot, excess³ methods preprint, Academy Learning Tau).
57
+ **Léxico (no negociable):** *strong Level-3* := `Res_pair`. *excess3* := exportable proxy. Report `w` with any Res_pair figure. Abundance (excess3) ≠ aggregation share \(f_3\).
45
58
 
46
59
  ## Install
47
60
 
@@ -88,6 +101,15 @@ print("weights:", out["params"]["alpha_syn"], out["params"]["alpha_surp"])
88
101
  print("final T_recd:", float(out["T_recd"][-1]))
89
102
  ```
90
103
 
104
+ ### Strong residual Res_pair (optional)
105
+
106
+ ```python
107
+ out = compute_recd_from_conjunctions(X, compute_res=True)
108
+ # or, on a symbol window:
109
+ from nested_recd import compute_res_pair_window
110
+ # Always report window length with Res_pair (Nota 4: w=13 can bias chaos).
111
+ ```
112
+
91
113
  ### Continuous excess³ only
92
114
 
93
115
  ```python
@@ -114,7 +136,7 @@ out = compute_recd_from_conjunctions(X, tau_s=tau_s)
114
136
  out = compute_recd_from_conjunctions(X, lam_override=0.5)
115
137
  ```
116
138
 
117
- **Note:** `delta_recd` / `T_recd` use **binary Φ₃** in the α-weighted clock (legacy Discrete Extramental Clock). For scientific Level-3 claims, report **continuous excess³** and nulls on `|Δ excess3|`.
139
+ **Note:** `delta_recd` / `T_recd` use **binary Φ₃** in the α-weighted clock (legacy Discrete Extramental Clock). For **exportable / scalable** Level-3 reporting, use continuous **excess³** and nulls on `|Δ excess3|`. For **strong** Level-3 claims, report **`res_pair`** (enable `compute_res=True`) **and the window `w`**. Do not identify the two.
118
140
 
119
141
  ## What it computes
120
142
 
@@ -122,10 +144,11 @@ out = compute_recd_from_conjunctions(X, lam_override=0.5)
122
144
  |--------|---------|
123
145
  | **Φ₁** | Co-occurrence of identical ordinal symbols across variable pairs |
124
146
  | **Φ₂** | Persistence of pairwise ordinal relations over lag `d` |
125
- | **excess³** | Continuous hybrid: `0.6·Syn + 0.4·Surp` (**primary** Level-3) |
147
+ | **excess³** | Continuous hybrid: `0.6·Syn + 0.4·Surp` (**exportable Level-3 proxy**; T-IV) |
126
148
  | **Φ₃** | Binary ticks of excess³ above `theta3` (**secondary**) |
149
+ | **Res_pair** | Strong Level-3 residual `KL(P ‖ P^(2))` (optional; always report `w`) |
127
150
  | **λ** | Chaos / reorganization intensity (from `τ_s` or `lam_override`) |
128
- | **α(λ)** | Level weights: α₁ decays with λ; α₂, α₃ grow with λ |
151
+ | **α(λ)** | Design-engine weights (template or Gibbs-α); **not** a joint observable |
129
152
  | **ΔRECD** | `α₁Φ₁ + α₂Φ₂ + α₃Φ₃` (legacy clock; binary Φ₃) |
130
153
  | **T_recd** | Cumulative sum of ΔRECD |
131
154
 
@@ -150,7 +173,8 @@ from nested_recd import (
150
173
  compute_phi1, compute_phi2, compute_phi3,
151
174
  compute_phi3_excess, compute_excess3_window,
152
175
  mean_excess_pre_post, surrogate_pvalue_delta_excess3,
153
- compute_lambda, alpha_weights, regime_lambda_proxy,
176
+ compute_lambda, alpha_weights, alpha_weights_gibbs, regime_lambda_proxy,
177
+ compute_res_pair, compute_res_pair_window,
154
178
  compute_weighted_contributions, high_level3_rate,
155
179
  generate_multivariate_symbols,
156
180
  phase_shuffle_independent, generate_surrogate_ensemble,
@@ -173,6 +197,12 @@ If you use excess³ / Level-3 from this package, cite the **methods** preprint (
173
197
 
174
198
  > Padilla-Villanueva, J. (2026). *excess³: A Pre-Specified Continuous Proxy for Order-3 Synergistic Surplus* (methods). Zenodo. https://doi.org/10.5281/zenodo.21385937
175
199
 
200
+ This software (0.2.3):
201
+
202
+ > Padilla-Villanueva, J. (2026). *nested-recd 0.2.3: Nested ordinal RECD, excess³ proxy, and Res_pair*. Zenodo. https://doi.org/10.5281/zenodo.22970079
203
+
204
+ The previous release (0.2.2), which pooled symbols inside Syn, remains citable as https://doi.org/10.5281/zenodo.21937204.
205
+
176
206
  Software / related stack:
177
207
 
178
208
  > Padilla-Villanueva, J. (2026). Systemic Tau software archive. Zenodo. https://doi.org/10.5281/zenodo.20576241
@@ -187,7 +217,8 @@ CCTP/SDDB pilot:
187
217
  title = {nested-recd: Nested ordinal RECD levels and continuous excess³},
188
218
  year = {2026},
189
219
  url = {https://github.com/johelpadilla/nested-recd},
190
- version = {0.2.0}
220
+ version = {0.2.3},
221
+ doi = {10.5281/zenodo.22970079}
191
222
  }
192
223
  ```
193
224
 
@@ -1,12 +1,25 @@
1
1
  # nested-recd
2
2
 
3
+ > **Correction (0.2.3).** Versions ≤ 0.2.2 computed `H_joint` and the pairwise
4
+ > entropies inside `Syn` with `np.unique` on a list of tuples and no `axis=0`.
5
+ > NumPy flattens that input, so `Syn`, `excess3` and binary `phi3` were not the
6
+ > published definition when `N ≥ 3`. `Surp` and `Res_pair` were not affected.
7
+ > For `N = 2`, `Syn` is 0 either way. 0.2.3 counts joint rows.
8
+ > `legacy_pooled_counting=True` reproduces the old numbers and warns.
9
+ > See `CHANGELOG.md`. This version:
10
+ > [10.5281/zenodo.22970079](https://doi.org/10.5281/zenodo.22970079).
11
+ > The 0.2.2 deposit remains
12
+ > [10.5281/zenodo.21937204](https://doi.org/10.5281/zenodo.21937204).
13
+
3
14
  [![PyPI version](https://img.shields.io/pypi/v/nested-recd.svg)](https://pypi.org/project/nested-recd/)
4
15
  [![Python](https://img.shields.io/pypi/pyversions/nested-recd.svg)](https://pypi.org/project/nested-recd/)
5
16
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
6
17
 
7
- **Nested ordinal RECD** — pure-NumPy implementation of nested ordinal conjunction levels (Φ₁, Φ₂, Φ₃), the continuous **excess³** Level-3 proxy, and λ-weighted Discrete Extramental Clock (RECD) accumulation.
18
+ **Nested ordinal RECD** — pure-NumPy implementation of nested ordinal conjunction levels (Φ₁, Φ₂, Φ₃), the continuous **excess³** Level-3 *proxy* (T-IV; not the strong residual), optional **Res_pair** \(=\mathrm{KL}(P\|P^{(2)})\), Gibbs-α weights, and λ-weighted Discrete Extramental Clock (RECD) accumulation.
19
+
20
+ This is the **canonical nested-clock software** for the Systemic Tau / RECD stack (foundations paper, excess³ methods preprint, cardiac CCTP/SDDB pilot, Academy Learning Tau).
8
21
 
9
- This is the **canonical Level-3 software** for the Systemic Tau / RECD stack (cardiac CCTP/SDDB pilot, excess³ methods preprint, Academy Learning Tau).
22
+ **Léxico (no negociable):** *strong Level-3* := `Res_pair`. *excess3* := exportable proxy. Report `w` with any Res_pair figure. Abundance (excess3) ≠ aggregation share \(f_3\).
10
23
 
11
24
  ## Install
12
25
 
@@ -53,6 +66,15 @@ print("weights:", out["params"]["alpha_syn"], out["params"]["alpha_surp"])
53
66
  print("final T_recd:", float(out["T_recd"][-1]))
54
67
  ```
55
68
 
69
+ ### Strong residual Res_pair (optional)
70
+
71
+ ```python
72
+ out = compute_recd_from_conjunctions(X, compute_res=True)
73
+ # or, on a symbol window:
74
+ from nested_recd import compute_res_pair_window
75
+ # Always report window length with Res_pair (Nota 4: w=13 can bias chaos).
76
+ ```
77
+
56
78
  ### Continuous excess³ only
57
79
 
58
80
  ```python
@@ -79,7 +101,7 @@ out = compute_recd_from_conjunctions(X, tau_s=tau_s)
79
101
  out = compute_recd_from_conjunctions(X, lam_override=0.5)
80
102
  ```
81
103
 
82
- **Note:** `delta_recd` / `T_recd` use **binary Φ₃** in the α-weighted clock (legacy Discrete Extramental Clock). For scientific Level-3 claims, report **continuous excess³** and nulls on `|Δ excess3|`.
104
+ **Note:** `delta_recd` / `T_recd` use **binary Φ₃** in the α-weighted clock (legacy Discrete Extramental Clock). For **exportable / scalable** Level-3 reporting, use continuous **excess³** and nulls on `|Δ excess3|`. For **strong** Level-3 claims, report **`res_pair`** (enable `compute_res=True`) **and the window `w`**. Do not identify the two.
83
105
 
84
106
  ## What it computes
85
107
 
@@ -87,10 +109,11 @@ out = compute_recd_from_conjunctions(X, lam_override=0.5)
87
109
  |--------|---------|
88
110
  | **Φ₁** | Co-occurrence of identical ordinal symbols across variable pairs |
89
111
  | **Φ₂** | Persistence of pairwise ordinal relations over lag `d` |
90
- | **excess³** | Continuous hybrid: `0.6·Syn + 0.4·Surp` (**primary** Level-3) |
112
+ | **excess³** | Continuous hybrid: `0.6·Syn + 0.4·Surp` (**exportable Level-3 proxy**; T-IV) |
91
113
  | **Φ₃** | Binary ticks of excess³ above `theta3` (**secondary**) |
114
+ | **Res_pair** | Strong Level-3 residual `KL(P ‖ P^(2))` (optional; always report `w`) |
92
115
  | **λ** | Chaos / reorganization intensity (from `τ_s` or `lam_override`) |
93
- | **α(λ)** | Level weights: α₁ decays with λ; α₂, α₃ grow with λ |
116
+ | **α(λ)** | Design-engine weights (template or Gibbs-α); **not** a joint observable |
94
117
  | **ΔRECD** | `α₁Φ₁ + α₂Φ₂ + α₃Φ₃` (legacy clock; binary Φ₃) |
95
118
  | **T_recd** | Cumulative sum of ΔRECD |
96
119
 
@@ -115,7 +138,8 @@ from nested_recd import (
115
138
  compute_phi1, compute_phi2, compute_phi3,
116
139
  compute_phi3_excess, compute_excess3_window,
117
140
  mean_excess_pre_post, surrogate_pvalue_delta_excess3,
118
- compute_lambda, alpha_weights, regime_lambda_proxy,
141
+ compute_lambda, alpha_weights, alpha_weights_gibbs, regime_lambda_proxy,
142
+ compute_res_pair, compute_res_pair_window,
119
143
  compute_weighted_contributions, high_level3_rate,
120
144
  generate_multivariate_symbols,
121
145
  phase_shuffle_independent, generate_surrogate_ensemble,
@@ -138,6 +162,12 @@ If you use excess³ / Level-3 from this package, cite the **methods** preprint (
138
162
 
139
163
  > Padilla-Villanueva, J. (2026). *excess³: A Pre-Specified Continuous Proxy for Order-3 Synergistic Surplus* (methods). Zenodo. https://doi.org/10.5281/zenodo.21385937
140
164
 
165
+ This software (0.2.3):
166
+
167
+ > Padilla-Villanueva, J. (2026). *nested-recd 0.2.3: Nested ordinal RECD, excess³ proxy, and Res_pair*. Zenodo. https://doi.org/10.5281/zenodo.22970079
168
+
169
+ The previous release (0.2.2), which pooled symbols inside Syn, remains citable as https://doi.org/10.5281/zenodo.21937204.
170
+
141
171
  Software / related stack:
142
172
 
143
173
  > Padilla-Villanueva, J. (2026). Systemic Tau software archive. Zenodo. https://doi.org/10.5281/zenodo.20576241
@@ -152,7 +182,8 @@ CCTP/SDDB pilot:
152
182
  title = {nested-recd: Nested ordinal RECD levels and continuous excess³},
153
183
  year = {2026},
154
184
  url = {https://github.com/johelpadilla/nested-recd},
155
- version = {0.2.0}
185
+ version = {0.2.3},
186
+ doi = {10.5281/zenodo.22970079}
156
187
  }
157
188
  ```
158
189
 
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "nested-recd"
7
- version = "0.2.0"
8
- description = "Nested ordinal RECD: Φ1–Φ3, continuous excess³ (primary Level-3), and λ-weighted Discrete Extramental Clock"
7
+ version = "0.2.3"
8
+ description = "Nested ordinal RECD: Φ1–Φ3, continuous excess³ (exportable Level-3 proxy), Res_pair (strong residual), and λ-weighted Discrete Extramental Clock"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
11
11
  license = "MIT"
@@ -49,7 +49,7 @@ Homepage = "https://github.com/johelpadilla/nested-recd"
49
49
  Repository = "https://github.com/johelpadilla/nested-recd"
50
50
  Issues = "https://github.com/johelpadilla/nested-recd/issues"
51
51
  Documentation = "https://github.com/johelpadilla/nested-recd#readme"
52
- DOI = "https://doi.org/10.5281/zenodo.21270699"
52
+ DOI = "https://doi.org/10.5281/zenodo.22970079"
53
53
 
54
54
  [tool.setuptools.packages.find]
55
55
  where = ["src"]
@@ -3,7 +3,8 @@ nested-recd
3
3
  ===========
4
4
 
5
5
  Nested ordinal RECD: Φ₁ / Φ₂ / Φ₃ conjunction levels, continuous excess³
6
- (primary Level-3 readout), and λ-weighted Discrete Extramental Clock.
6
+ (exportable Level-3 proxy; not the strong residual), optional Res_pair,
7
+ and λ-weighted Discrete Extramental Clock.
7
8
 
8
9
  Quick start
9
10
  -----------
@@ -11,7 +12,7 @@ Quick start
11
12
  >>> from nested_recd import compute_recd_from_conjunctions
12
13
  >>> X = np.random.randn(500, 3).cumsum(axis=0)
13
14
  >>> out = compute_recd_from_conjunctions(X)
14
- >>> float(np.nanmean(out["excess3"])) # continuous primary Level-3 score
15
+ >>> float(np.nanmean(out["excess3"])) # continuous Level-3 *proxy* (T-IV)
15
16
  """
16
17
 
17
18
  from nested_recd.ordinal_levels import (
@@ -32,10 +33,16 @@ from nested_recd.ordinal_levels import (
32
33
  compute_phi3,
33
34
  compute_phi3_excess,
34
35
  compute_excess3_window,
36
+ compute_res_pair_window,
37
+ compute_res_pair,
38
+ pairwise_maxent_ipf,
39
+ kl_divergence,
35
40
  mean_excess_pre_post,
36
41
  surrogate_pvalue_delta_excess3,
37
42
  compute_lambda,
38
43
  alpha_weights,
44
+ alpha_weights_gibbs,
45
+ alpha_compare_template_gibbs,
39
46
  regime_lambda_proxy,
40
47
  compute_recd_from_conjunctions,
41
48
  simple_level_classification,
@@ -51,7 +58,7 @@ from nested_recd.surrogates import (
51
58
  compute_null_distribution,
52
59
  )
53
60
 
54
- __version__ = "0.2.0"
61
+ __version__ = "0.2.3"
55
62
 
56
63
  __all__ = [
57
64
  "__version__",
@@ -72,10 +79,16 @@ __all__ = [
72
79
  "compute_phi3",
73
80
  "compute_phi3_excess",
74
81
  "compute_excess3_window",
82
+ "compute_res_pair_window",
83
+ "compute_res_pair",
84
+ "pairwise_maxent_ipf",
85
+ "kl_divergence",
75
86
  "mean_excess_pre_post",
76
87
  "surrogate_pvalue_delta_excess3",
77
88
  "compute_lambda",
78
89
  "alpha_weights",
90
+ "alpha_weights_gibbs",
91
+ "alpha_compare_template_gibbs",
79
92
  "regime_lambda_proxy",
80
93
  "compute_recd_from_conjunctions",
81
94
  "simple_level_classification",
@@ -189,62 +189,76 @@ def compute_phi2(
189
189
  # NIVEL 3: Emergencia / Sinergia (proxy)
190
190
  # ============================================================
191
191
 
192
- def _joint_entropy_and_marginals(S_window: np.ndarray) -> Tuple[float, float, float]:
192
+ def _entropy_from_counts(counts: np.ndarray) -> float:
193
+ p = counts / counts.sum()
194
+ return float(-np.sum(p * np.log2(p + 1e-12)))
195
+
196
+
197
+ def _joint_entropy_and_marginals(
198
+ S_window: np.ndarray,
199
+ legacy_pooled_counting: bool = False,
200
+ ) -> Tuple[float, float, float]:
193
201
  """
194
- Calcula H(joint), promedio H(marginal), y proxy de info mutua pairwise promedio
195
- sobre una ventana de símbolos.
196
- Muy quirúrgico: conteos exactos (N pequeño, m=3 → 6^N factible).
202
+ H(joint), sum of marginal entropies, and the Syn proxy on one window.
203
+
204
+ Correct joint counting (default since 0.2.3) uses
205
+ ``np.unique(..., axis=0)`` on rows. ``legacy_pooled_counting=True``
206
+ reproduces <= 0.2.2, where ``np.unique`` on a list of tuples flattened
207
+ the tuples, so H_joint and each pairwise H were entropies of pooled
208
+ symbol values. Use the legacy mode only to reproduce archived numbers.
209
+
210
+ TC = sum_i H(S_i) - H(S)
211
+ MI = H(S_i) + H(S_j) - H(S_i, S_j)
212
+ Syn = max(0, TC - (N-1) * mean MI)
197
213
  """
198
- T, N = S_window.shape
199
- # Joint tuples como tuplas hashables
200
- joint_tuples = [tuple(row) for row in S_window]
201
- unique_j, counts_j = np.unique(joint_tuples, return_counts=True)
202
- p_joint = counts_j / counts_j.sum()
203
- H_joint = -np.sum(p_joint * np.log2(p_joint + 1e-12))
214
+ S_window = np.asarray(S_window)
215
+ _, N = S_window.shape
216
+
217
+ if legacy_pooled_counting:
218
+ _, counts_j = np.unique(S_window.ravel(), return_counts=True)
219
+ else:
220
+ _, counts_j = np.unique(S_window, axis=0, return_counts=True)
221
+ H_joint = _entropy_from_counts(counts_j)
204
222
 
205
- # Marginales por variable
206
223
  H_margs = []
207
224
  for k in range(N):
208
225
  _, c = np.unique(S_window[:, k], return_counts=True)
209
- p = c / c.sum()
210
- H_margs.append(-np.sum(p * np.log2(p + 1e-12)))
211
- H_marg_mean = float(np.mean(H_margs))
226
+ H_margs.append(_entropy_from_counts(c))
212
227
  H_marg_sum = float(np.sum(H_margs))
213
228
 
214
- # Pairwise MI promedio (aprox rápida)
215
229
  pair_mi = []
216
230
  for i in range(N):
217
231
  for j in range(i + 1, N):
218
- joint2 = list(zip(S_window[:, i], S_window[:, j]))
219
- _, cj = np.unique(joint2, return_counts=True)
220
- pj = cj / cj.sum()
221
- H2 = -np.sum(pj * np.log2(pj + 1e-12))
222
-
223
- _, ci = np.unique(S_window[:, i], return_counts=True)
224
- pi = ci / ci.sum()
225
- Hi = -np.sum(pi * np.log2(pi + 1e-12))
226
-
227
- _, cj2 = np.unique(S_window[:, j], return_counts=True)
228
- pj2 = cj2 / cj2.sum()
229
- Hj = -np.sum(pj2 * np.log2(pj2 + 1e-12))
230
-
231
- mi = Hi + Hj - H2
232
- pair_mi.append(max(0.0, mi))
232
+ pair = S_window[:, [i, j]]
233
+ if legacy_pooled_counting:
234
+ _, cj = np.unique(pair.ravel(), return_counts=True)
235
+ else:
236
+ _, cj = np.unique(pair, axis=0, return_counts=True)
237
+ H2 = _entropy_from_counts(cj)
238
+ pair_mi.append(max(0.0, H_margs[i] + H_margs[j] - H2))
233
239
  mi_pair_avg = float(np.mean(pair_mi)) if pair_mi else 0.0
234
240
 
235
- # Total correlation approx = sum H - H_joint
236
241
  tc = H_marg_sum - H_joint
237
- # "Synergy beyond pairwise" rough: tc - (N-1)*mi_pair_avg (heurística; puede ser negativa)
238
242
  synergy_proxy = max(0.0, tc - (N - 1) * mi_pair_avg)
239
-
240
243
  return H_joint, H_marg_sum, synergy_proxy
241
244
 
242
245
 
246
+ def _warn_legacy() -> None:
247
+ warnings.warn(
248
+ "legacy_pooled_counting=True reproduces the joint-counting bug of "
249
+ "nested-recd <= 0.2.2 (pooled symbol values instead of joint tuples "
250
+ "in H_joint and pairwise H). Use only to reproduce archived results.",
251
+ UserWarning,
252
+ stacklevel=3,
253
+ )
254
+
255
+
243
256
  def compute_excess3_window(
244
257
  win: np.ndarray,
245
258
  use_surprise: bool = True,
246
259
  alpha_syn: float = ALPHA_SYN,
247
260
  alpha_surp: float = ALPHA_SURP,
261
+ legacy_pooled_counting: bool = False,
248
262
  ) -> float:
249
263
  """
250
264
  excess³ score on a single window of joint ordinal symbols (shape (w, N)).
@@ -259,6 +273,9 @@ def compute_excess3_window(
259
273
  Surp: observed-vs-independence joint surprise (frequency-weighted log-ratio).
260
274
 
261
275
  This is a **proxy**, not a complete PID synergy atom.
276
+
277
+ ``legacy_pooled_counting`` (default False) reproduces the <= 0.2.2
278
+ pooled-symbol bug. Surp is the same in both modes.
262
279
  """
263
280
  win = np.asarray(win)
264
281
  if win.ndim != 2 or win.shape[0] == 0:
@@ -266,7 +283,9 @@ def compute_excess3_window(
266
283
  if win.shape[1] < 2:
267
284
  return 0.0
268
285
 
269
- _, _, syn = _joint_entropy_and_marginals(win)
286
+ _, _, syn = _joint_entropy_and_marginals(
287
+ win, legacy_pooled_counting=legacy_pooled_counting
288
+ )
270
289
  if not use_surprise:
271
290
  return float(syn)
272
291
 
@@ -297,6 +316,7 @@ def compute_phi3_excess(
297
316
  alpha_syn: float = ALPHA_SYN,
298
317
  alpha_surp: float = ALPHA_SURP,
299
318
  fill: bool = False,
319
+ legacy_pooled_counting: bool = False,
300
320
  ) -> Tuple[np.ndarray, np.ndarray]:
301
321
  """
302
322
  Continuous excess³ (primary) and binary Φ₃ (secondary) on symbol matrix S.
@@ -313,7 +333,11 @@ def compute_phi3_excess(
313
333
  fill : bool
314
334
  If True, forward-fill NaNs after the first finite score (plot convenience).
315
335
  Default False preserves sparse stride semantics.
336
+ legacy_pooled_counting : bool
337
+ Default False (joint tuples). True reproduces the <= 0.2.2 bug.
316
338
  """
339
+ if legacy_pooled_counting:
340
+ _warn_legacy()
317
341
  S = np.asarray(S)
318
342
  T_eff, N = S.shape
319
343
  phi3 = np.full(T_eff, np.nan)
@@ -328,6 +352,7 @@ def compute_phi3_excess(
328
352
  use_surprise=use_surprise,
329
353
  alpha_syn=alpha_syn,
330
354
  alpha_surp=alpha_surp,
355
+ legacy_pooled_counting=legacy_pooled_counting,
331
356
  )
332
357
  excess[t] = score
333
358
  phi3[t] = 1.0 if score > theta else 0.0
@@ -352,6 +377,7 @@ def compute_phi3(
352
377
  use_surprise: bool = True,
353
378
  alpha_syn: float = ALPHA_SYN,
354
379
  alpha_surp: float = ALPHA_SURP,
380
+ legacy_pooled_counting: bool = False,
355
381
  ) -> Tuple[np.ndarray, np.ndarray]:
356
382
  """
357
383
  Φ₃(t) binary + continuous excess³ series.
@@ -370,9 +396,201 @@ def compute_phi3(
370
396
  alpha_syn=alpha_syn,
371
397
  alpha_surp=alpha_surp,
372
398
  fill=False,
399
+ legacy_pooled_counting=legacy_pooled_counting,
373
400
  )
374
401
 
375
402
 
403
+ # ============================================================
404
+ # Strong Level-3 residual: Res_pair = KL(P || P^(2))
405
+ # ============================================================
406
+
407
+ def _empirical_joint_table(win: np.ndarray) -> Tuple[np.ndarray, list]:
408
+ """
409
+ Build dense joint probability table over observed symbol alphabet.
410
+
411
+ Returns
412
+ -------
413
+ p : ndarray shape (k_0, k_1, ..., k_{N-1})
414
+ Empirical frequencies (sums to 1).
415
+ levels : list of 1d arrays
416
+ Sorted unique symbols per coordinate (axis order of ``p``).
417
+ """
418
+ win = np.asarray(win)
419
+ if win.ndim != 2 or win.shape[0] == 0:
420
+ raise ValueError("win must be (w, N) with w>0")
421
+ T, N = win.shape
422
+ levels = [np.unique(win[:, i]) for i in range(N)]
423
+ shape = tuple(len(lev) for lev in levels)
424
+ # Map symbols to indices
425
+ idx = np.zeros((T, N), dtype=int)
426
+ for i in range(N):
427
+ # searchsorted works because levels[i] is sorted unique
428
+ idx[:, i] = np.searchsorted(levels[i], win[:, i])
429
+ p = np.zeros(shape, dtype=float)
430
+ for t in range(T):
431
+ p[tuple(idx[t])] += 1.0
432
+ p /= p.sum()
433
+ return p, levels
434
+
435
+
436
+ def _pair_marginal(p: np.ndarray, i: int, j: int) -> np.ndarray:
437
+ """Marginal of joint tensor p over all axes except i and j."""
438
+ axes = tuple(a for a in range(p.ndim) if a not in (i, j))
439
+ if axes:
440
+ m = p.sum(axis=axes)
441
+ else:
442
+ m = p.copy()
443
+ # Ensure axes order (i, j): if i>j, sum order may swap
444
+ if i > j:
445
+ m = m.T
446
+ return m
447
+
448
+
449
+ def pairwise_maxent_ipf(
450
+ p_emp: np.ndarray,
451
+ max_iter: int = 200,
452
+ tol: float = 1e-10,
453
+ ) -> np.ndarray:
454
+ """
455
+ Fit discrete pairwise maxent / IPF model matching all bivariate marginals.
456
+
457
+ Starts from the product of univariate marginals and iteratively rescales
458
+ so that each pair marginal matches that of ``p_emp``. On finite alphabets
459
+ this is the standard iterative proportional fitting procedure toward the
460
+ I-projection onto the pairwise exponential family (when it exists in the
461
+ relative interior).
462
+
463
+ Parameters
464
+ ----------
465
+ p_emp : ndarray
466
+ Empirical joint (N-way table).
467
+ max_iter, tol :
468
+ Convergence controls.
469
+
470
+ Returns
471
+ -------
472
+ p2 : ndarray
473
+ Pairwise-consistent joint with same shape as ``p_emp``.
474
+ """
475
+ p_emp = np.asarray(p_emp, dtype=float)
476
+ if p_emp.ndim < 2:
477
+ return p_emp.copy()
478
+ N = p_emp.ndim
479
+ # Univariate marginals
480
+ marg1 = []
481
+ for i in range(N):
482
+ axes = tuple(a for a in range(N) if a != i)
483
+ mi = p_emp.sum(axis=axes)
484
+ marg1.append(mi)
485
+ # Initialize as product of marginals
486
+ p = marg1[0]
487
+ for i in range(1, N):
488
+ p = p[..., None] * marg1[i]
489
+ # p may need reshape to full shape
490
+ p = np.broadcast_to(p, p_emp.shape).astype(float).copy()
491
+ # Target pair marginals
492
+ targets = {}
493
+ for i in range(N):
494
+ for j in range(i + 1, N):
495
+ targets[(i, j)] = _pair_marginal(p_emp, i, j)
496
+
497
+ for _ in range(max_iter):
498
+ max_dev = 0.0
499
+ for i in range(N):
500
+ for j in range(i + 1, N):
501
+ cur = _pair_marginal(p, i, j)
502
+ tgt = targets[(i, j)]
503
+ # Avoid div by zero: only rescale cells with positive cur
504
+ ratio = np.ones_like(tgt)
505
+ mask = cur > 0
506
+ ratio[mask] = tgt[mask] / cur[mask]
507
+ # Broadcast ratio onto full joint
508
+ # Build scaling tensor
509
+ scale = ratio
510
+ # Insert singleton dims for axes other than i,j
511
+ shape = [1] * N
512
+ shape[i] = p.shape[i]
513
+ shape[j] = p.shape[j]
514
+ # ratio is (k_i, k_j); reshape
515
+ scale = scale.reshape(shape)
516
+ p = p * scale
517
+ # Renormalize lightly for numerical stability
518
+ s = p.sum()
519
+ if s > 0:
520
+ p /= s
521
+ max_dev = max(max_dev, float(np.max(np.abs(cur - tgt))))
522
+ if max_dev < tol:
523
+ break
524
+ return p
525
+
526
+
527
+ def kl_divergence(p: np.ndarray, q: np.ndarray, eps: float = 1e-12) -> float:
528
+ """KL(p||q) in bits over a common support table."""
529
+ p = np.asarray(p, dtype=float)
530
+ q = np.asarray(q, dtype=float)
531
+ mask = p > 0
532
+ q_safe = np.maximum(q, eps)
533
+ return float(np.sum(p[mask] * np.log2(p[mask] / q_safe[mask])))
534
+
535
+
536
+ def compute_res_pair_window(win: np.ndarray, max_iter: int = 200) -> float:
537
+ """
538
+ Strong Level-3 residual on one window of joint symbols.
539
+
540
+ Res_pair = KL(P_emp || P^(2))
541
+
542
+ where P^(2) is the pairwise IPF / maxent fit to all bipolar marginals
543
+ (Foundations paper / Nota formal 2).
544
+
545
+ For N<3 the residual is identically 0 (no order-3 structure possible).
546
+ """
547
+ win = np.asarray(win)
548
+ if win.ndim != 2 or win.shape[0] == 0:
549
+ return float("nan")
550
+ if win.shape[1] < 3:
551
+ return 0.0
552
+ try:
553
+ p_emp, _ = _empirical_joint_table(win)
554
+ except ValueError:
555
+ return float("nan")
556
+ # Degenerate: if support is too thin, IPF still runs on observed alphabet
557
+ p2 = pairwise_maxent_ipf(p_emp, max_iter=max_iter)
558
+ return kl_divergence(p_emp, p2)
559
+
560
+
561
+ def compute_res_pair(
562
+ S: np.ndarray,
563
+ window: int = DEFAULT_WINDOW_TAU,
564
+ stride: int = 1,
565
+ max_iter: int = 200,
566
+ fill: bool = False,
567
+ ) -> np.ndarray:
568
+ """
569
+ Windowed strong Level-3 residual Res_pair(t) on symbol matrix S (T, N).
570
+
571
+ Primary theoretical Level-3 object; excess3 remains the scalable proxy.
572
+ Computational cost grows with alphabet product; intended for small N
573
+ (e.g. m=3 Bandt–Pompe, N<=4–5) or as a validation diagnostic.
574
+ """
575
+ S = np.asarray(S)
576
+ T_eff, N = S.shape
577
+ out = np.full(T_eff, np.nan)
578
+ if T_eff < window or N < 3:
579
+ if N < 3:
580
+ out[:] = 0.0
581
+ return out
582
+ for t in range(window - 1, T_eff, stride):
583
+ out[t] = compute_res_pair_window(S[t - window + 1 : t + 1], max_iter=max_iter)
584
+ if fill:
585
+ last = np.nan
586
+ for t in range(T_eff):
587
+ if np.isfinite(out[t]):
588
+ last = out[t]
589
+ elif np.isfinite(last):
590
+ out[t] = last
591
+ return out
592
+
593
+
376
594
  def mean_excess_pre_post(
377
595
  excess3: np.ndarray,
378
596
  split: int,
@@ -404,6 +622,7 @@ def surrogate_pvalue_delta_excess3(
404
622
  delay: int = DEFAULT_DELAY,
405
623
  alpha_syn: float = ALPHA_SYN,
406
624
  alpha_surp: float = ALPHA_SURP,
625
+ legacy_pooled_counting: bool = False,
407
626
  ) -> Dict[str, float]:
408
627
  """
409
628
  Two-sided surrogate p-value for |Δ excess3| under independent phase-shuffle
@@ -444,6 +663,7 @@ def surrogate_pvalue_delta_excess3(
444
663
  stride=stride,
445
664
  alpha_syn=alpha_syn,
446
665
  alpha_surp=alpha_surp,
666
+ legacy_pooled_counting=legacy_pooled_counting,
447
667
  )
448
668
  _, _, d = mean_excess_pre_post(excess, split_s)
449
669
  return float(d)
@@ -515,7 +735,10 @@ def alpha_weights(
515
735
  delta3: float = 2.0
516
736
  ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
517
737
  """
518
- Retorna α1(λ), α2(λ), α3(λ) según la forma propuesta.
738
+ Template α family (Via A / design engine defaults).
739
+
740
+ Returns α1(λ), α2(λ), α3(λ). Not normalized; used as relative weights
741
+ in ΔRECD and f_ℓ shares. Foundations Note 3.
519
742
  """
520
743
  a1 = alpha10 * np.exp(-beta1 * lam)
521
744
  a2 = alpha20 * (1.0 + gamma2 * lam)
@@ -523,6 +746,93 @@ def alpha_weights(
523
746
  return a1, a2, a3
524
747
 
525
748
 
749
+ def alpha_weights_gibbs(
750
+ lam: np.ndarray,
751
+ u: Tuple[float, float, float] = (0.0, 1.0, 2.0),
752
+ beta0: float = 0.0,
753
+ kappa: float = 1.5,
754
+ normalize: bool = False,
755
+ ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
756
+ """
757
+ Gibbs / maxent depth weights (Via B, Foundations Note 3).
758
+
759
+ α_ℓ^G(λ) ∝ exp( β(λ) · u(ℓ) ), β(λ) = β0 + κ·λ ≥ 0
760
+
761
+ with u(1) < u(2) < u(3). Relative monotonicity α3/α1 = exp(β·Δu)
762
+ increases in λ when κ≥0. This is an *admissible design engine*, not
763
+ an observable of the joint S_t.
764
+
765
+ Parameters
766
+ ----------
767
+ lam : array-like
768
+ Regime intensity λ ≥ 0.
769
+ u : triple
770
+ Depth utilities (u1, u2, u3), strictly increasing recommended.
771
+ beta0, kappa :
772
+ β(λ) = max(0, β0 + κ·λ).
773
+ normalize :
774
+ If True, return simplex weights (sum_ℓ α_ℓ = 1). If False (default),
775
+ return unnormalized positive weights comparable in spirit to the
776
+ template family (which is also unnormalized).
777
+
778
+ Returns
779
+ -------
780
+ a1, a2, a3 : ndarrays
781
+ Gibbs depth weights.
782
+ """
783
+ lam = np.asarray(lam, dtype=float)
784
+ u1, u2, u3 = u
785
+ beta = np.maximum(0.0, beta0 + kappa * lam)
786
+ e1 = np.exp(beta * u1)
787
+ e2 = np.exp(beta * u2)
788
+ e3 = np.exp(beta * u3)
789
+ if normalize:
790
+ z = e1 + e2 + e3
791
+ return e1 / z, e2 / z, e3 / z
792
+ return e1, e2, e3
793
+
794
+
795
+ def alpha_compare_template_gibbs(
796
+ lam_grid: Optional[np.ndarray] = None,
797
+ u: Tuple[float, float, float] = (0.0, 1.0, 2.0),
798
+ beta0: float = 0.0,
799
+ kappa: float = 1.5,
800
+ **template_kwargs,
801
+ ) -> Dict[str, np.ndarray]:
802
+ """
803
+ Compare normalized template vs Gibbs α on a λ grid (Conj. bridge Note 3).
804
+
805
+ Both families are L1-normalized per λ so shapes are comparable.
806
+ Returns dict with lam, a*_template, a*_gibbs, L1 and L∞ errors.
807
+ """
808
+ if lam_grid is None:
809
+ lam_grid = np.linspace(0.0, 2.0, 41)
810
+ lam_grid = np.asarray(lam_grid, dtype=float)
811
+ t1, t2, t3 = alpha_weights(lam_grid, **template_kwargs)
812
+ g1, g2, g3 = alpha_weights_gibbs(
813
+ lam_grid, u=u, beta0=beta0, kappa=kappa, normalize=False
814
+ )
815
+ # L1 normalize each family
816
+ ts = t1 + t2 + t3
817
+ gs = g1 + g2 + g3
818
+ nt = (t1 / ts, t2 / ts, t3 / ts)
819
+ ng = (g1 / gs, g2 / gs, g3 / gs)
820
+ l1 = np.abs(nt[0] - ng[0]) + np.abs(nt[1] - ng[1]) + np.abs(nt[2] - ng[2])
821
+ linf = np.maximum.reduce(
822
+ [np.abs(nt[0] - ng[0]), np.abs(nt[1] - ng[1]), np.abs(nt[2] - ng[2])]
823
+ )
824
+ return {
825
+ "lam": lam_grid,
826
+ "t1": nt[0], "t2": nt[1], "t3": nt[2],
827
+ "g1": ng[0], "g2": ng[1], "g3": ng[2],
828
+ "L1": l1,
829
+ "Linf": linf,
830
+ "max_L1": np.array([float(np.max(l1))]),
831
+ "max_Linf": np.array([float(np.max(linf))]),
832
+ "mean_L1": np.array([float(np.mean(l1))]),
833
+ }
834
+
835
+
526
836
  def regime_lambda_proxy(
527
837
  r: float,
528
838
  r_onset: float = 3.57,
@@ -561,17 +871,22 @@ def compute_recd_from_conjunctions(
561
871
  stride: int = 1,
562
872
  alpha_syn: float = ALPHA_SYN,
563
873
  alpha_surp: float = ALPHA_SURP,
874
+ compute_res: bool = False,
875
+ legacy_pooled_counting: bool = False,
564
876
  **alpha_kwargs
565
877
  ) -> Dict[str, np.ndarray]:
566
878
  """
567
879
  Full nested ordinal RECD pipeline:
568
880
  - Bandt–Pompe symbols
569
- - Φ1, Φ2, Φ3 (binary) + continuous excess³ (primary Level-3 readout)
881
+ - Φ1, Φ2, Φ3 (binary) + continuous excess³ (exportable Level-3 proxy)
882
+ - optional ``res_pair`` strong residual (IPF / KL to pairwise maxent)
570
883
  - λ and α(λ) regime weights
571
884
  - ΔRECD and cumulative T_recd
572
885
 
573
- Level-3 contract (excess³ methods):
574
- - Continuous ``excess3`` is the primary magnitude for order-3 claims.
886
+ Level-3 contract (excess³ methods + foundations paper):
887
+ - Continuous ``excess3`` is the scalable primary *proxy* for order-3 claims.
888
+ - ``res_pair`` (if ``compute_res=True``) is the strong theoretical residual
889
+ Res_pair = KL(P || P^(2)); costly for large alphabets / N.
575
890
  - ``phi3`` is a secondary threshold discretisation of excess3.
576
891
  - ``delta_recd`` keeps the legacy λ-weighted clock using **binary** Φ₃
577
892
  (back-compat with CCTP / Discrete Extramental Clock nesting). Do not
@@ -591,6 +906,12 @@ def compute_recd_from_conjunctions(
591
906
  stride=stride,
592
907
  alpha_syn=alpha_syn,
593
908
  alpha_surp=alpha_surp,
909
+ legacy_pooled_counting=legacy_pooled_counting,
910
+ )
911
+ res_pair = (
912
+ compute_res_pair(S, window=window_tau, stride=stride)
913
+ if compute_res
914
+ else None
594
915
  )
595
916
 
596
917
  # Determinar λ: override > tau-derived > zero
@@ -619,7 +940,7 @@ def compute_recd_from_conjunctions(
619
940
  delta_recd = a1 * phi1 + a2 * phi2 + a3 * phi3_safe
620
941
  T_recd = np.nancumsum(delta_recd)
621
942
 
622
- return {
943
+ result = {
623
944
  "S": S,
624
945
  "phi1": phi1,
625
946
  "phi2": phi2,
@@ -641,9 +962,15 @@ def compute_recd_from_conjunctions(
641
962
  "alpha_surp": alpha_surp,
642
963
  "level3_primary": "excess3",
643
964
  "level3_secondary": "phi3",
965
+ "level3_strong": "res_pair",
966
+ "compute_res": compute_res,
967
+ "legacy_pooled_counting": legacy_pooled_counting,
644
968
  **alpha_kwargs,
645
969
  },
646
970
  }
971
+ if res_pair is not None:
972
+ result["res_pair"] = res_pair
973
+ return result
647
974
 
648
975
 
649
976
  # ============================================================
@@ -1,14 +1,14 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nested-recd
3
- Version: 0.2.0
4
- Summary: Nested ordinal RECD: Φ1–Φ3, continuous excess³ (primary Level-3), and λ-weighted Discrete Extramental Clock
3
+ Version: 0.2.3
4
+ Summary: Nested ordinal RECD: Φ1–Φ3, continuous excess³ (exportable Level-3 proxy), Res_pair (strong residual), and λ-weighted Discrete Extramental Clock
5
5
  Author-email: Johel Padilla-Villanueva <joel.padilla2@upr.edu>
6
6
  License-Expression: MIT
7
7
  Project-URL: Homepage, https://github.com/johelpadilla/nested-recd
8
8
  Project-URL: Repository, https://github.com/johelpadilla/nested-recd
9
9
  Project-URL: Issues, https://github.com/johelpadilla/nested-recd/issues
10
10
  Project-URL: Documentation, https://github.com/johelpadilla/nested-recd#readme
11
- Project-URL: DOI, https://doi.org/10.5281/zenodo.21270699
11
+ Project-URL: DOI, https://doi.org/10.5281/zenodo.22970079
12
12
  Keywords: RECD,ordinal patterns,nested time,Systemic Tau,complexity,early-warning,network physiology,Bandt-Pompe
13
13
  Classifier: Development Status :: 4 - Beta
14
14
  Classifier: Intended Audience :: Science/Research
@@ -35,13 +35,26 @@ Dynamic: license-file
35
35
 
36
36
  # nested-recd
37
37
 
38
+ > **Correction (0.2.3).** Versions ≤ 0.2.2 computed `H_joint` and the pairwise
39
+ > entropies inside `Syn` with `np.unique` on a list of tuples and no `axis=0`.
40
+ > NumPy flattens that input, so `Syn`, `excess3` and binary `phi3` were not the
41
+ > published definition when `N ≥ 3`. `Surp` and `Res_pair` were not affected.
42
+ > For `N = 2`, `Syn` is 0 either way. 0.2.3 counts joint rows.
43
+ > `legacy_pooled_counting=True` reproduces the old numbers and warns.
44
+ > See `CHANGELOG.md`. This version:
45
+ > [10.5281/zenodo.22970079](https://doi.org/10.5281/zenodo.22970079).
46
+ > The 0.2.2 deposit remains
47
+ > [10.5281/zenodo.21937204](https://doi.org/10.5281/zenodo.21937204).
48
+
38
49
  [![PyPI version](https://img.shields.io/pypi/v/nested-recd.svg)](https://pypi.org/project/nested-recd/)
39
50
  [![Python](https://img.shields.io/pypi/pyversions/nested-recd.svg)](https://pypi.org/project/nested-recd/)
40
51
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
41
52
 
42
- **Nested ordinal RECD** — pure-NumPy implementation of nested ordinal conjunction levels (Φ₁, Φ₂, Φ₃), the continuous **excess³** Level-3 proxy, and λ-weighted Discrete Extramental Clock (RECD) accumulation.
53
+ **Nested ordinal RECD** — pure-NumPy implementation of nested ordinal conjunction levels (Φ₁, Φ₂, Φ₃), the continuous **excess³** Level-3 *proxy* (T-IV; not the strong residual), optional **Res_pair** \(=\mathrm{KL}(P\|P^{(2)})\), Gibbs-α weights, and λ-weighted Discrete Extramental Clock (RECD) accumulation.
54
+
55
+ This is the **canonical nested-clock software** for the Systemic Tau / RECD stack (foundations paper, excess³ methods preprint, cardiac CCTP/SDDB pilot, Academy Learning Tau).
43
56
 
44
- This is the **canonical Level-3 software** for the Systemic Tau / RECD stack (cardiac CCTP/SDDB pilot, excess³ methods preprint, Academy Learning Tau).
57
+ **Léxico (no negociable):** *strong Level-3* := `Res_pair`. *excess3* := exportable proxy. Report `w` with any Res_pair figure. Abundance (excess3) ≠ aggregation share \(f_3\).
45
58
 
46
59
  ## Install
47
60
 
@@ -88,6 +101,15 @@ print("weights:", out["params"]["alpha_syn"], out["params"]["alpha_surp"])
88
101
  print("final T_recd:", float(out["T_recd"][-1]))
89
102
  ```
90
103
 
104
+ ### Strong residual Res_pair (optional)
105
+
106
+ ```python
107
+ out = compute_recd_from_conjunctions(X, compute_res=True)
108
+ # or, on a symbol window:
109
+ from nested_recd import compute_res_pair_window
110
+ # Always report window length with Res_pair (Nota 4: w=13 can bias chaos).
111
+ ```
112
+
91
113
  ### Continuous excess³ only
92
114
 
93
115
  ```python
@@ -114,7 +136,7 @@ out = compute_recd_from_conjunctions(X, tau_s=tau_s)
114
136
  out = compute_recd_from_conjunctions(X, lam_override=0.5)
115
137
  ```
116
138
 
117
- **Note:** `delta_recd` / `T_recd` use **binary Φ₃** in the α-weighted clock (legacy Discrete Extramental Clock). For scientific Level-3 claims, report **continuous excess³** and nulls on `|Δ excess3|`.
139
+ **Note:** `delta_recd` / `T_recd` use **binary Φ₃** in the α-weighted clock (legacy Discrete Extramental Clock). For **exportable / scalable** Level-3 reporting, use continuous **excess³** and nulls on `|Δ excess3|`. For **strong** Level-3 claims, report **`res_pair`** (enable `compute_res=True`) **and the window `w`**. Do not identify the two.
118
140
 
119
141
  ## What it computes
120
142
 
@@ -122,10 +144,11 @@ out = compute_recd_from_conjunctions(X, lam_override=0.5)
122
144
  |--------|---------|
123
145
  | **Φ₁** | Co-occurrence of identical ordinal symbols across variable pairs |
124
146
  | **Φ₂** | Persistence of pairwise ordinal relations over lag `d` |
125
- | **excess³** | Continuous hybrid: `0.6·Syn + 0.4·Surp` (**primary** Level-3) |
147
+ | **excess³** | Continuous hybrid: `0.6·Syn + 0.4·Surp` (**exportable Level-3 proxy**; T-IV) |
126
148
  | **Φ₃** | Binary ticks of excess³ above `theta3` (**secondary**) |
149
+ | **Res_pair** | Strong Level-3 residual `KL(P ‖ P^(2))` (optional; always report `w`) |
127
150
  | **λ** | Chaos / reorganization intensity (from `τ_s` or `lam_override`) |
128
- | **α(λ)** | Level weights: α₁ decays with λ; α₂, α₃ grow with λ |
151
+ | **α(λ)** | Design-engine weights (template or Gibbs-α); **not** a joint observable |
129
152
  | **ΔRECD** | `α₁Φ₁ + α₂Φ₂ + α₃Φ₃` (legacy clock; binary Φ₃) |
130
153
  | **T_recd** | Cumulative sum of ΔRECD |
131
154
 
@@ -150,7 +173,8 @@ from nested_recd import (
150
173
  compute_phi1, compute_phi2, compute_phi3,
151
174
  compute_phi3_excess, compute_excess3_window,
152
175
  mean_excess_pre_post, surrogate_pvalue_delta_excess3,
153
- compute_lambda, alpha_weights, regime_lambda_proxy,
176
+ compute_lambda, alpha_weights, alpha_weights_gibbs, regime_lambda_proxy,
177
+ compute_res_pair, compute_res_pair_window,
154
178
  compute_weighted_contributions, high_level3_rate,
155
179
  generate_multivariate_symbols,
156
180
  phase_shuffle_independent, generate_surrogate_ensemble,
@@ -173,6 +197,12 @@ If you use excess³ / Level-3 from this package, cite the **methods** preprint (
173
197
 
174
198
  > Padilla-Villanueva, J. (2026). *excess³: A Pre-Specified Continuous Proxy for Order-3 Synergistic Surplus* (methods). Zenodo. https://doi.org/10.5281/zenodo.21385937
175
199
 
200
+ This software (0.2.3):
201
+
202
+ > Padilla-Villanueva, J. (2026). *nested-recd 0.2.3: Nested ordinal RECD, excess³ proxy, and Res_pair*. Zenodo. https://doi.org/10.5281/zenodo.22970079
203
+
204
+ The previous release (0.2.2), which pooled symbols inside Syn, remains citable as https://doi.org/10.5281/zenodo.21937204.
205
+
176
206
  Software / related stack:
177
207
 
178
208
  > Padilla-Villanueva, J. (2026). Systemic Tau software archive. Zenodo. https://doi.org/10.5281/zenodo.20576241
@@ -187,7 +217,8 @@ CCTP/SDDB pilot:
187
217
  title = {nested-recd: Nested ordinal RECD levels and continuous excess³},
188
218
  year = {2026},
189
219
  url = {https://github.com/johelpadilla/nested-recd},
190
- version = {0.2.0}
220
+ version = {0.2.3},
221
+ doi = {10.5281/zenodo.22970079}
191
222
  }
192
223
  ```
193
224
 
@@ -10,4 +10,5 @@ src/nested_recd.egg-info/SOURCES.txt
10
10
  src/nested_recd.egg-info/dependency_links.txt
11
11
  src/nested_recd.egg-info/requires.txt
12
12
  src/nested_recd.egg-info/top_level.txt
13
+ tests/test_joint_counting.py
13
14
  tests/test_smoke.py
@@ -0,0 +1,144 @@
1
+ """Regression tests for the joint-counting fix (0.2.3).
2
+
3
+ nested-recd <= 0.2.2 computed H_joint and the pairwise H(S_i, S_j) inside
4
+ ``_joint_entropy_and_marginals`` over pooled symbol values (np.unique on a
5
+ list of tuples flattens it). These tests pin the joint-tuple values.
6
+ ``test_xor_synergy_is_one_bit`` and ``test_joint_entropy_counts_tuples``
7
+ fail on 0.2.2 and pass on 0.2.3.
8
+ """
9
+ import inspect
10
+
11
+ import numpy as np
12
+ import pytest
13
+
14
+ from nested_recd import compute_excess3_window, compute_phi3_excess
15
+ from nested_recd.ordinal_levels import _joint_entropy_and_marginals
16
+
17
+
18
+ def _xor_window(reps: int = 3) -> np.ndarray:
19
+ # columns: a, b independent fair bits; c = a XOR b; d constant
20
+ base = np.array(
21
+ [
22
+ [0, 0, 0, 0],
23
+ [0, 1, 1, 0],
24
+ [1, 0, 1, 0],
25
+ [1, 1, 0, 0],
26
+ ]
27
+ )
28
+ return np.tile(base, (reps, 1))
29
+
30
+
31
+ def test_xor_synergy_is_one_bit():
32
+ """XOR: every pair is independent (MI = 0) but the triple carries 1 bit.
33
+
34
+ H_joint = 2, sum H_i = 3, TC = 1, mean MI = 0 -> Syn = 1 bit.
35
+ 0.2.2 returns H_joint = 0.954 and Syn = 0.263.
36
+ """
37
+ H_joint, H_sum, syn = _joint_entropy_and_marginals(_xor_window())
38
+ assert H_joint == pytest.approx(2.0, abs=1e-9)
39
+ assert H_sum == pytest.approx(3.0, abs=1e-9)
40
+ assert syn == pytest.approx(1.0, abs=1e-9)
41
+
42
+
43
+ def test_joint_entropy_counts_tuples():
44
+ """13 distinct joint tuples in a window of 13 -> H_joint = log2(13)."""
45
+ rng = np.random.default_rng(0)
46
+ while True:
47
+ win = rng.integers(0, 6, size=(13, 4))
48
+ if len({tuple(r) for r in win}) == 13:
49
+ break
50
+ H_joint, _, _ = _joint_entropy_and_marginals(win)
51
+ assert H_joint == pytest.approx(np.log2(13), abs=1e-9)
52
+
53
+
54
+ def test_identical_columns_have_no_synergy():
55
+ """Four copies of one series: TC = 3 H, every MI = H -> Syn = 0."""
56
+ rng = np.random.default_rng(1)
57
+ x = rng.integers(0, 6, size=40)
58
+ win = np.stack([x, x, x, x], axis=1)
59
+ H_joint, H_sum, syn = _joint_entropy_and_marginals(win)
60
+ assert H_sum == pytest.approx(4 * H_joint, abs=1e-9)
61
+ assert syn == pytest.approx(0.0, abs=1e-9)
62
+
63
+
64
+ def test_matches_counter_reference():
65
+ """Random windows agree with an independent Counter-over-tuples version."""
66
+ from collections import Counter
67
+
68
+ def H(counter):
69
+ c = np.array(list(counter.values()), float)
70
+ p = c / c.sum()
71
+ return float(-np.sum(p * np.log2(p + 1e-12)))
72
+
73
+ rng = np.random.default_rng(2)
74
+ for _ in range(200):
75
+ win = rng.integers(0, 6, size=(13, 4))
76
+ N = win.shape[1]
77
+ rows = [tuple(r) for r in win.tolist()]
78
+ Hj = H(Counter(rows))
79
+ Hm = [H(Counter(win[:, k].tolist())) for k in range(N)]
80
+ mi = [
81
+ max(
82
+ 0.0,
83
+ Hm[i]
84
+ + Hm[j]
85
+ - H(Counter(zip(win[:, i].tolist(), win[:, j].tolist()))),
86
+ )
87
+ for i in range(N)
88
+ for j in range(i + 1, N)
89
+ ]
90
+ syn_ref = max(0.0, sum(Hm) - Hj - (N - 1) * np.mean(mi))
91
+ H_joint, H_sum, syn = _joint_entropy_and_marginals(win)
92
+ assert H_joint == pytest.approx(Hj, abs=1e-12)
93
+ assert syn == pytest.approx(syn_ref, abs=1e-12)
94
+
95
+
96
+ def test_legacy_flag_reproduces_022():
97
+ """legacy_pooled_counting=True reproduces nested-recd 0.2.2."""
98
+ assert "legacy_pooled_counting" in inspect.signature(
99
+ _joint_entropy_and_marginals
100
+ ).parameters
101
+ H_joint, _, syn = _joint_entropy_and_marginals(
102
+ _xor_window(), legacy_pooled_counting=True
103
+ )
104
+ assert H_joint == pytest.approx(LEGACY_XOR_HJOINT, abs=1e-12)
105
+ assert syn == pytest.approx(LEGACY_XOR_SYN, abs=1e-12)
106
+ rng = np.random.default_rng(3)
107
+ win = rng.integers(0, 6, size=(13, 4))
108
+ assert compute_excess3_window(
109
+ win, legacy_pooled_counting=True
110
+ ) == pytest.approx(LEGACY_RANDOM_EXCESS3, abs=1e-12)
111
+
112
+
113
+ def test_legacy_flag_warns():
114
+ S = np.random.default_rng(4).integers(0, 6, size=(40, 4))
115
+ with pytest.warns(UserWarning, match="0.2.2"):
116
+ compute_phi3_excess(S, legacy_pooled_counting=True)
117
+
118
+
119
+ def test_surp_unchanged_by_fix():
120
+ """Surp always counted tuples: with Syn switched off both modes agree."""
121
+ rng = np.random.default_rng(5)
122
+ for _ in range(50):
123
+ win = rng.integers(0, 6, size=(13, 4))
124
+ a = compute_excess3_window(win, alpha_syn=0.0, alpha_surp=1.0)
125
+ b = compute_excess3_window(
126
+ win, alpha_syn=0.0, alpha_surp=1.0, legacy_pooled_counting=True
127
+ )
128
+ assert a == pytest.approx(b, abs=1e-15)
129
+
130
+
131
+ def test_bivariate_synergy_is_zero():
132
+ """For N=2, Syn = TC - MI of that single pair, so both modes give 0."""
133
+ rng = np.random.default_rng(8)
134
+ win = rng.integers(0, 6, size=(13, 2))
135
+ _, _, syn = _joint_entropy_and_marginals(win)
136
+ _, _, legacy = _joint_entropy_and_marginals(win, legacy_pooled_counting=True)
137
+ assert syn == pytest.approx(0.0, abs=1e-12)
138
+ assert legacy == pytest.approx(0.0, abs=1e-12)
139
+
140
+
141
+ # Frozen outputs of nested-recd 0.2.2
142
+ LEGACY_XOR_HJOINT = 0.9544340029220797
143
+ LEGACY_XOR_SYN = 0.2624831837630126
144
+ LEGACY_RANDOM_EXCESS3 = 2.355186195068367
@@ -9,11 +9,15 @@ from nested_recd import (
9
9
  ALPHA_SURP,
10
10
  DEFAULT_THETA3,
11
11
  DEFAULT_THETA3_CARDIO,
12
+ alpha_weights,
13
+ alpha_weights_gibbs,
14
+ alpha_compare_template_gibbs,
12
15
  compute_recd_from_conjunctions,
13
16
  compute_phi1,
14
17
  compute_phi3,
15
18
  compute_phi3_excess,
16
19
  compute_excess3_window,
20
+ compute_res_pair_window,
17
21
  compute_weighted_contributions,
18
22
  generate_multivariate_symbols,
19
23
  mean_excess_pre_post,
@@ -24,7 +28,29 @@ from nested_recd import (
24
28
 
25
29
 
26
30
  def test_version():
27
- assert __version__ == "0.2.0"
31
+ assert __version__ == "0.2.3"
32
+
33
+
34
+ def test_gibbs_alpha_monotonicity():
35
+ """α3/α1 increases with λ when κ>0 (Note 3 Prop. Gibbs admissible)."""
36
+ lam = np.array([0.0, 0.5, 1.0, 2.0])
37
+ a1, a2, a3 = alpha_weights_gibbs(lam, kappa=1.5, normalize=False)
38
+ ratio = a3 / np.maximum(a1, 1e-15)
39
+ assert np.all(np.diff(ratio) >= -1e-12)
40
+ assert float(ratio[-1]) > float(ratio[0])
41
+ # normalized simplex
42
+ n1, n2, n3 = alpha_weights_gibbs(lam, normalize=True)
43
+ assert np.allclose(n1 + n2 + n3, 1.0)
44
+
45
+
46
+ def test_template_gibbs_bridge_bounded():
47
+ """Conj. bridge: L∞ distance of normalized families on [0,2] is finite."""
48
+ cmp = alpha_compare_template_gibbs()
49
+ assert float(cmp["max_Linf"][0]) < 1.0
50
+ assert float(cmp["max_L1"][0]) < 2.0
51
+ # both put more mass on level 3 at high λ than at λ=0
52
+ assert float(cmp["t3"][-1]) > float(cmp["t3"][0])
53
+ assert float(cmp["g3"][-1]) > float(cmp["g3"][0])
28
54
 
29
55
 
30
56
  def test_weights_a_priori():
@@ -132,6 +158,42 @@ def test_excess3_window_weights():
132
158
  assert score >= 0.0
133
159
 
134
160
 
161
+ def test_res_pair_xor_vs_common_driver():
162
+ """Strong L3: XOR residual > common-driver residual (Nota 2 geometry)."""
163
+ rng = np.random.default_rng(42)
164
+ n = 400
165
+ # XOR / parity lock
166
+ a = rng.integers(0, 2, size=n)
167
+ b = rng.integers(0, 2, size=n)
168
+ c = a ^ b
169
+ win_xor = np.column_stack([a, b, c]).astype(int)
170
+ res_xor = compute_res_pair_window(win_xor)
171
+ # Common driver: b,c ≈ a
172
+ driver = rng.integers(0, 2, size=n)
173
+ noise = rng.random(n) < 0.05
174
+ b2 = np.where(noise, 1 - driver, driver)
175
+ noise2 = rng.random(n) < 0.05
176
+ c2 = np.where(noise2, 1 - driver, driver)
177
+ win_drv = np.column_stack([driver, b2, c2]).astype(int)
178
+ res_drv = compute_res_pair_window(win_drv)
179
+ assert res_xor >= 0.0 and res_drv >= 0.0
180
+ assert res_xor > res_drv + 0.05 # clear geometric separation
181
+
182
+
183
+ def test_res_pair_d2_is_zero():
184
+ win = np.column_stack([np.zeros(30, dtype=int), np.ones(30, dtype=int)])
185
+ assert compute_res_pair_window(win) == 0.0
186
+
187
+
188
+ def test_pipeline_optional_res_pair():
189
+ rng = np.random.default_rng(9)
190
+ X = rng.normal(size=(180, 3)).cumsum(axis=0)
191
+ out = compute_recd_from_conjunctions(X, compute_res=True, window_tau=20, stride=5)
192
+ assert "res_pair" in out
193
+ assert out["res_pair"].shape == out["excess3"].shape
194
+ assert np.nanmin(out["res_pair"][np.isfinite(out["res_pair"])]) >= -1e-9
195
+
196
+
135
197
  def test_mean_excess_pre_post():
136
198
  x = np.array([0.0, 0.0, 1.0, 1.0, 1.0])
137
199
  pre, post, delta = mean_excess_pre_post(x, split=2)
File without changes
File without changes