nested-recd 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,74 @@
1
+ """
2
+ nested-recd
3
+ ===========
4
+
5
+ Nested ordinal RECD: Φ₁ / Φ₂ / Φ₃ conjunction levels and λ-weighted
6
+ Discrete Extramental Clock accumulation.
7
+
8
+ Quick start
9
+ -----------
10
+ >>> import numpy as np
11
+ >>> from nested_recd import compute_recd_from_conjunctions
12
+ >>> X = np.random.randn(500, 3).cumsum(axis=0)
13
+ >>> out = compute_recd_from_conjunctions(X)
14
+ >>> out["T_recd"][-1]
15
+ """
16
+
17
+ from nested_recd.ordinal_levels import (
18
+ DEFAULT_M,
19
+ DEFAULT_DELAY,
20
+ DEFAULT_D_PERSIST,
21
+ DEFAULT_WINDOW_TAU,
22
+ DEFAULT_THETA_CHAOS,
23
+ DEFAULT_THETA3,
24
+ DELTA_FEIGENBAUM,
25
+ generate_multivariate_symbols,
26
+ compute_phi1,
27
+ compute_phi2,
28
+ compute_phi3,
29
+ compute_lambda,
30
+ alpha_weights,
31
+ regime_lambda_proxy,
32
+ compute_recd_from_conjunctions,
33
+ simple_level_classification,
34
+ compute_weighted_contributions,
35
+ high_level3_rate,
36
+ run_logical_demonstrations,
37
+ )
38
+ from nested_recd.surrogates import (
39
+ iaaft_surrogate_1d,
40
+ phase_shuffle_independent,
41
+ random_permutation_independent,
42
+ generate_surrogate_ensemble,
43
+ compute_null_distribution,
44
+ )
45
+
46
+ __version__ = "0.1.0"
47
+
48
+ __all__ = [
49
+ "__version__",
50
+ "DEFAULT_M",
51
+ "DEFAULT_DELAY",
52
+ "DEFAULT_D_PERSIST",
53
+ "DEFAULT_WINDOW_TAU",
54
+ "DEFAULT_THETA_CHAOS",
55
+ "DEFAULT_THETA3",
56
+ "DELTA_FEIGENBAUM",
57
+ "generate_multivariate_symbols",
58
+ "compute_phi1",
59
+ "compute_phi2",
60
+ "compute_phi3",
61
+ "compute_lambda",
62
+ "alpha_weights",
63
+ "regime_lambda_proxy",
64
+ "compute_recd_from_conjunctions",
65
+ "simple_level_classification",
66
+ "compute_weighted_contributions",
67
+ "high_level3_rate",
68
+ "run_logical_demonstrations",
69
+ "iaaft_surrogate_1d",
70
+ "phase_shuffle_independent",
71
+ "random_permutation_independent",
72
+ "generate_surrogate_ensemble",
73
+ "compute_null_distribution",
74
+ ]
@@ -0,0 +1,562 @@
1
+ """
2
+ Nested ordinal RECD levels (Φ₁, Φ₂, Φ₃) and λ-weighted accumulation.
3
+
4
+ Reference implementation of nested ordinal conjunctions and the emergent
5
+ Discrete Extramental Clock (RECD) from the nested-time structure proposed in
6
+ "Conversación de la naturaleza del tiempo" (Padilla-Villanueva).
7
+
8
+ Levels
9
+ ------
10
+ - Φ₁: co-occurrence of ordinal symbols across variables
11
+ - Φ₂: persistent pairwise relations over a short lag window
12
+ - Φ₃: higher-order synergy / irreducibility proxy (excess + joint surprise)
13
+ - λ(τ), α(λ): regime-dependent weights for ΔRECD = α₁Φ₁ + α₂Φ₂ + α₃Φ₃
14
+
15
+ Pure NumPy; no hard dependency on the full ``systemictau`` package.
16
+ """
17
+
18
+ import numpy as np
19
+ from typing import Dict, Tuple, Optional
20
+ import warnings
21
+
22
+ # Defaults quirúrgicos (ver diseño)
23
+ DEFAULT_M = 3
24
+ DEFAULT_DELAY = 1
25
+ DEFAULT_D_PERSIST = 4
26
+ DEFAULT_WINDOW_TAU = 13
27
+ DEFAULT_THETA_CHAOS = 0.41
28
+ DELTA_FEIGENBAUM = 4.6692016091
29
+ DEFAULT_THETA3 = 0.10 # Ajustado a la baja tras piloto (más sensible sin perder especificidad)
30
+
31
+ def _gen_ordinal(x: np.ndarray, m: int = DEFAULT_M, delay: int = DEFAULT_DELAY) -> np.ndarray:
32
+ """Bandt–Pompe ordinal symbols (pure NumPy, no numba required)."""
33
+ x = np.asarray(x).ravel()
34
+ n = len(x) - (m - 1) * delay
35
+ if n <= 0:
36
+ return np.array([], dtype=int)
37
+ symbols = np.zeros(n, dtype=int)
38
+ fact = np.array([1])
39
+ for i in range(1, m):
40
+ fact = np.append(fact, fact[-1] * i)
41
+ for i in range(n):
42
+ word = np.array([x[i + j * delay] for j in range(m)])
43
+ perm = np.argsort(word)
44
+ symbol = 0
45
+ for j in range(m - 1):
46
+ cnt = np.sum(perm[j + 1:] < perm[j])
47
+ symbol += cnt * fact[m - 1 - j]
48
+ symbols[i] = symbol
49
+ return symbols
50
+
51
+
52
+ def generate_multivariate_symbols(
53
+ X: np.ndarray,
54
+ m: int = DEFAULT_M,
55
+ delay: int = DEFAULT_DELAY
56
+ ) -> np.ndarray:
57
+ """
58
+ Genera matriz de símbolos ordinales S[t, i] para cada variable.
59
+ S shape: (T_eff, N)
60
+ """
61
+ X = np.asarray(X)
62
+ if X.ndim != 2:
63
+ raise ValueError("X debe ser (T, N)")
64
+ T, N = X.shape
65
+ symbols_list = []
66
+ min_len = None
67
+ for i in range(N):
68
+ sym = _gen_ordinal(X[:, i], m=m, delay=delay)
69
+ symbols_list.append(sym)
70
+ min_len = len(sym) if min_len is None else min(min_len, len(sym))
71
+ # Alinear al mínimo (por si hay bordes diferentes, raro)
72
+ S = np.stack([s[:min_len] for s in symbols_list], axis=1)
73
+ return S
74
+
75
+
76
+ # ============================================================
77
+ # NIVEL 1: Coincidencia
78
+ # ============================================================
79
+
80
+ def compute_phi1(S: np.ndarray) -> np.ndarray:
81
+ """
82
+ Φ₁(t): Fracción normalizada de pares de variables que comparten símbolo idéntico en t.
83
+ Φ₁ ∈ [0, 1]
84
+ """
85
+ T_eff, N = S.shape
86
+ if N < 2:
87
+ return np.zeros(T_eff)
88
+ pairs = N * (N - 1) / 2.0
89
+ phi1 = np.zeros(T_eff)
90
+ for t in range(T_eff):
91
+ row = S[t]
92
+ matches = 0
93
+ for i in range(N):
94
+ for j in range(i + 1, N):
95
+ if row[i] == row[j]:
96
+ matches += 1
97
+ phi1[t] = matches / pairs
98
+ return phi1
99
+
100
+
101
+ # ============================================================
102
+ # NIVEL 2: Relación Persistente
103
+ # ============================================================
104
+
105
+ def _relation_code(a: int, b: int) -> int:
106
+ """Codifica relación entre dos símbolos como entero simple."""
107
+ if a == b:
108
+ return 0 # EQ
109
+ elif a > b:
110
+ return 1 # GT
111
+ else:
112
+ return 2 # LT
113
+
114
+
115
+ def compute_persistent_relations(
116
+ S: np.ndarray,
117
+ d: int = DEFAULT_D_PERSIST,
118
+ min_fraction: float = 0.75
119
+ ) -> np.ndarray:
120
+ """
121
+ Para cada par y cada t, detecta si la relación actual se mantuvo en los últimos d pasos.
122
+ Retorna máscara (T_eff, num_pairs) o score agregado.
123
+ """
124
+ T_eff, N = S.shape
125
+ if N < 2 or T_eff < d:
126
+ return np.zeros(T_eff)
127
+
128
+ num_pairs = N * (N - 1) // 2
129
+ persistence_score = np.zeros(T_eff)
130
+
131
+ pair_idx = 0
132
+ for i in range(N):
133
+ for j in range(i + 1, N):
134
+ rel_history = np.array([_relation_code(S[t, i], S[t, j]) for t in range(T_eff)])
135
+ for t in range(d - 1, T_eff):
136
+ window = rel_history[t - d + 1 : t + 1]
137
+ current = rel_history[t]
138
+ # ¿Qué fracción del window coincide con la relación actual?
139
+ match_frac = np.mean(window == current)
140
+ if match_frac >= min_fraction:
141
+ persistence_score[t] += 1.0
142
+ pair_idx += 1
143
+
144
+ # Normalizar por número de pares
145
+ persistence_score = persistence_score / num_pairs
146
+ return persistence_score
147
+
148
+
149
+ def compute_phi2(
150
+ S: np.ndarray,
151
+ d: int = DEFAULT_D_PERSIST,
152
+ min_fraction: float = 0.75,
153
+ weight_eq: float = 1.0
154
+ ) -> np.ndarray:
155
+ """
156
+ Φ₂(t): Promedio normalizado de relaciones persistentes.
157
+ Versión quirúrgica simple: cuenta pares con persistencia + peso extra para igualdad si se desea.
158
+ """
159
+ T_eff, N = S.shape
160
+ if N < 2:
161
+ return np.zeros(T_eff)
162
+
163
+ phi2 = np.zeros(T_eff)
164
+ num_pairs = float(N * (N - 1) // 2)
165
+
166
+ for i in range(N):
167
+ for j in range(i + 1, N):
168
+ rel_hist = np.array([_relation_code(S[t, i], S[t, j]) for t in range(T_eff)])
169
+ for t in range(d - 1, T_eff):
170
+ win = rel_hist[max(0, t - d + 1): t + 1]
171
+ curr = rel_hist[t]
172
+ frac = np.mean(win == curr)
173
+ if frac >= min_fraction:
174
+ w = weight_eq if curr == 0 else 0.85
175
+ phi2[t] += w
176
+
177
+ phi2 = phi2 / num_pairs
178
+ # Clip a [0,1] por si pesos >1
179
+ return np.clip(phi2, 0.0, 1.0)
180
+
181
+
182
+ # ============================================================
183
+ # NIVEL 3: Emergencia / Sinergia (proxy)
184
+ # ============================================================
185
+
186
+ def _joint_entropy_and_marginals(S_window: np.ndarray) -> Tuple[float, float, float]:
187
+ """
188
+ Calcula H(joint), promedio H(marginal), y proxy de info mutua pairwise promedio
189
+ sobre una ventana de símbolos.
190
+ Muy quirúrgico: conteos exactos (N pequeño, m=3 → 6^N factible).
191
+ """
192
+ T, N = S_window.shape
193
+ # Joint tuples como tuplas hashables
194
+ joint_tuples = [tuple(row) for row in S_window]
195
+ unique_j, counts_j = np.unique(joint_tuples, return_counts=True)
196
+ p_joint = counts_j / counts_j.sum()
197
+ H_joint = -np.sum(p_joint * np.log2(p_joint + 1e-12))
198
+
199
+ # Marginales por variable
200
+ H_margs = []
201
+ for k in range(N):
202
+ _, c = np.unique(S_window[:, k], return_counts=True)
203
+ p = c / c.sum()
204
+ H_margs.append(-np.sum(p * np.log2(p + 1e-12)))
205
+ H_marg_mean = float(np.mean(H_margs))
206
+ H_marg_sum = float(np.sum(H_margs))
207
+
208
+ # Pairwise MI promedio (aprox rápida)
209
+ pair_mi = []
210
+ for i in range(N):
211
+ for j in range(i + 1, N):
212
+ joint2 = list(zip(S_window[:, i], S_window[:, j]))
213
+ _, cj = np.unique(joint2, return_counts=True)
214
+ pj = cj / cj.sum()
215
+ H2 = -np.sum(pj * np.log2(pj + 1e-12))
216
+
217
+ _, ci = np.unique(S_window[:, i], return_counts=True)
218
+ pi = ci / ci.sum()
219
+ Hi = -np.sum(pi * np.log2(pi + 1e-12))
220
+
221
+ _, cj2 = np.unique(S_window[:, j], return_counts=True)
222
+ pj2 = cj2 / cj2.sum()
223
+ Hj = -np.sum(pj2 * np.log2(pj2 + 1e-12))
224
+
225
+ mi = Hi + Hj - H2
226
+ pair_mi.append(max(0.0, mi))
227
+ mi_pair_avg = float(np.mean(pair_mi)) if pair_mi else 0.0
228
+
229
+ # Total correlation approx = sum H - H_joint
230
+ tc = H_marg_sum - H_joint
231
+ # "Synergy beyond pairwise" rough: tc - (N-1)*mi_pair_avg (heurística; puede ser negativa)
232
+ synergy_proxy = max(0.0, tc - (N - 1) * mi_pair_avg)
233
+
234
+ return H_joint, H_marg_sum, synergy_proxy
235
+
236
+
237
+ def compute_phi3(
238
+ S: np.ndarray,
239
+ window: int = 13,
240
+ theta: float = DEFAULT_THETA3,
241
+ stride: int = 1,
242
+ use_surprise: bool = True
243
+ ) -> Tuple[np.ndarray, np.ndarray]:
244
+ """
245
+ Φ₃(t): Score de sinergia / irreductibilidad (proxy mejorado post-piloto).
246
+
247
+ Dos componentes:
248
+ - excess (total correlation - pairwise) del método anterior.
249
+ - joint_surprise: promedio de "sorpresa" de las tuplas observadas
250
+ vs modelo de independencia ( -log2(P_indep) ponderado por frecuencia observada ).
251
+
252
+ Si use_surprise=True, el score combina ambos. Esto hace el proxy
253
+ más sensible a configuraciones conjuntas "improbables bajo independencia"
254
+ que no se explican por marginales (más cerca de irreductibilidad).
255
+
256
+ Retorna (phi3_binary, excess_raw) -- excess ahora es el score combinado.
257
+ """
258
+ T_eff, N = S.shape
259
+ phi3 = np.full(T_eff, np.nan)
260
+ excess = np.full(T_eff, np.nan)
261
+
262
+ if T_eff < window:
263
+ return phi3, excess
264
+
265
+ for t in range(window - 1, T_eff, stride):
266
+ win = S[t - window + 1 : t + 1]
267
+ T_win = len(win)
268
+
269
+ # Componente 1: exceso sinérgico previo (total corr - pairwise)
270
+ _, _, syn = _joint_entropy_and_marginals(win)
271
+
272
+ # Componente 2: Joint surprise (más directo para "irreductible")
273
+ if use_surprise:
274
+ from collections import Counter
275
+ joint_tuples = [tuple(int(v) for v in row) for row in win] # asegurar python ints
276
+ counter = Counter(joint_tuples)
277
+ uniq = list(counter.keys())
278
+ counts = np.array(list(counter.values()))
279
+ T_win = len(joint_tuples)
280
+
281
+ # P_indep por tupla + "exceso de ocurrencia" (log ratio observado / independencia)
282
+ # Esto captura configuraciones que ocurren MÁS de lo esperado por marginales → más "irreducible"
283
+ surprises = []
284
+ for u, cnt in zip(uniq, counts):
285
+ p_indep = 1.0
286
+ for k, val in enumerate(u):
287
+ pk = np.mean([row[k] == val for row in joint_tuples])
288
+ p_indep *= max(pk, 1e-9)
289
+ p_obs = cnt / T_win
290
+ # log-ratio: >0 cuando ocurre más de lo esperado por independencia
291
+ ratio = p_obs / max(p_indep, 1e-9)
292
+ excess_log = max(0.0, np.log2(ratio)) if ratio > 1 else 0.0
293
+ weight = cnt / T_win
294
+ surprises.append(excess_log * weight)
295
+
296
+ joint_surprise = float(np.sum(surprises)) if surprises else 0.0
297
+ # Combinar (syn ya es en bits-ish, surprise también)
298
+ combined = 0.6 * syn + 0.4 * joint_surprise # pesos heurísticos pero transparentes
299
+ else:
300
+ combined = syn
301
+
302
+ excess[t] = combined
303
+ phi3[t] = 1.0 if combined > theta else 0.0
304
+
305
+ return phi3, excess
306
+
307
+
308
+ # ============================================================
309
+ # λ(t) y pesos α(λ)
310
+ # ============================================================
311
+
312
+ def compute_lambda(
313
+ tau_s: np.ndarray,
314
+ theta_chaos: float = DEFAULT_THETA_CHAOS,
315
+ delta_f: float = DELTA_FEIGENBAUM,
316
+ gamma_universal: float = 0.2
317
+ ) -> np.ndarray:
318
+ """
319
+ λ(t) híbrido según documento:
320
+ componente empírica (Tau) + factor de universalidad Feigenbaum.
321
+ """
322
+ tau = np.asarray(tau_s)
323
+ lam_emp = np.maximum(0.0, (np.abs(tau) - theta_chaos) / theta_chaos)
324
+ # Factor suave de universalidad
325
+ univ_factor = 1.0 + gamma_universal * np.log(delta_f)
326
+ lam = lam_emp * univ_factor
327
+ return lam
328
+
329
+
330
+ def alpha_weights(
331
+ lam: np.ndarray,
332
+ alpha10: float = 1.0,
333
+ alpha20: float = 1.0,
334
+ alpha30: float = 1.0,
335
+ beta1: float = 2.0,
336
+ gamma2: float = 1.5,
337
+ gamma3: float = 3.0,
338
+ delta3: float = 2.0
339
+ ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
340
+ """
341
+ Retorna α1(λ), α2(λ), α3(λ) según la forma propuesta.
342
+ """
343
+ a1 = alpha10 * np.exp(-beta1 * lam)
344
+ a2 = alpha20 * (1.0 + gamma2 * lam)
345
+ a3 = alpha30 * (1.0 + gamma3 * lam + delta3 * lam**2)
346
+ return a1, a2, a3
347
+
348
+
349
+ def regime_lambda_proxy(
350
+ r: float,
351
+ r_onset: float = 3.57,
352
+ r_full: float = 3.85,
353
+ lam_full: float = 1.0
354
+ ) -> float:
355
+ """
356
+ Mapea ground-truth r (parámetro del mapa logístico acoplado) a un λ efectivo.
357
+ Esto permite aislar el efecto puro del régimen sobre el Nivel 3 (Opción 1 ligera),
358
+ sin depender del |τ_s| empírico (que en proxies acoplados puede ser más alto en pre
359
+ que en caos desarrollado).
360
+
361
+ - r <= r_onset (pre-Feigenbaum): lam=0 → α3 baseline (menor peso ontológico)
362
+ - r > r_onset (caos): lam crece → α3 aumenta (más peso al Nivel 3, como postula la tesis)
363
+
364
+ Ramp simple hasta r_full. Transparente y falsable.
365
+ """
366
+ if r <= r_onset:
367
+ return 0.0
368
+ frac = min(1.0, (r - r_onset) / max(1e-9, (r_full - r_onset)))
369
+ return frac * lam_full
370
+
371
+
372
+ # ============================================================
373
+ # RECD desde conjunciones (nuevo)
374
+ # ============================================================
375
+
376
+ def compute_recd_from_conjunctions(
377
+ X: np.ndarray,
378
+ tau_s: Optional[np.ndarray] = None,
379
+ m: int = DEFAULT_M,
380
+ d: int = DEFAULT_D_PERSIST,
381
+ theta3: float = DEFAULT_THETA3,
382
+ window_tau: int = DEFAULT_WINDOW_TAU,
383
+ lam_override: Optional[np.ndarray] = None,
384
+ **alpha_kwargs
385
+ ) -> Dict[str, np.ndarray]:
386
+ """
387
+ Pipeline quirúrgico completo:
388
+ - Símbolos ordinales
389
+ - Φ1, Φ2, Φ3
390
+ - λ y α(λ)
391
+ - ΔRECD_new y T_new
392
+
393
+ Si tau_s no se provee, se espera que el caller lo calcule con la infraestructura existente.
394
+
395
+ lam_override: si se provee (escalar o array), se usa directamente para calcular α(λ)
396
+ en lugar de derivar λ de tau_s. Útil para Opción 1 (ground-truth r)
397
+ o alphas fijos, para aislar el efecto de régimen sobre Nivel 3.
398
+ """
399
+ S = generate_multivariate_symbols(X, m=m)
400
+ T_eff = S.shape[0]
401
+
402
+ phi1 = compute_phi1(S)
403
+ phi2 = compute_phi2(S, d=d)
404
+ phi3, excess3 = compute_phi3(S, window=window_tau, theta=theta3)
405
+
406
+ # Determinar λ: override > tau-derived > zero
407
+ if lam_override is not None:
408
+ if np.isscalar(lam_override):
409
+ lam = np.full(T_eff, float(lam_override))
410
+ else:
411
+ lo = np.asarray(lam_override).ravel()
412
+ lam = lo[-T_eff:] if len(lo) >= T_eff else np.pad(lo, (T_eff - len(lo), 0), constant_values=0.0)
413
+ elif tau_s is None:
414
+ # Placeholder: el caller debe proveer tau_s alineado
415
+ lam = np.zeros(T_eff)
416
+ warnings.warn("tau_s no provisto. λ=0 para todos los t. Proporcione tau_s para resultados reales.")
417
+ else:
418
+ # Recortar o alinear de forma conservadora
419
+ tau_aligned = tau_s[-T_eff:] if len(tau_s) > T_eff else np.pad(tau_s, (T_eff - len(tau_s), 0), constant_values=np.nan)
420
+ lam = compute_lambda(tau_aligned)
421
+
422
+ a1, a2, a3 = alpha_weights(lam, **alpha_kwargs)
423
+
424
+ # Rellenar NaN en phi3 con 0 para acumulación (o mantener y usar nan-safe)
425
+ phi3_safe = np.nan_to_num(phi3, nan=0.0)
426
+ excess3_safe = np.nan_to_num(excess3, nan=0.0)
427
+
428
+ delta_recd = a1 * phi1 + a2 * phi2 + a3 * phi3_safe
429
+ T_recd = np.nancumsum(delta_recd)
430
+
431
+ return {
432
+ "S": S,
433
+ "phi1": phi1,
434
+ "phi2": phi2,
435
+ "phi3": phi3,
436
+ "excess3": excess3,
437
+ "lambda": lam,
438
+ "alpha1": a1,
439
+ "alpha2": a2,
440
+ "alpha3": a3,
441
+ "delta_recd": delta_recd,
442
+ "T_recd": T_recd,
443
+ "params": {
444
+ "m": m, "d": d, "theta3": theta3,
445
+ "window_tau": window_tau, **alpha_kwargs
446
+ }
447
+ }
448
+
449
+
450
+ # ============================================================
451
+ # Utilidades quirúrgicas
452
+ # ============================================================
453
+
454
+ def simple_level_classification(phi1: np.ndarray, phi2: np.ndarray, phi3: np.ndarray) -> np.ndarray:
455
+ """
456
+ Clasificación burda del nivel dominante en cada t.
457
+ 1,2,3 o 0 si todo bajo.
458
+ """
459
+ levels = np.zeros_like(phi1, dtype=int)
460
+ for t in range(len(phi1)):
461
+ p3 = phi3[t] if not np.isnan(phi3[t]) else 0
462
+ p2 = phi2[t]
463
+ p1 = phi1[t]
464
+ if p3 > 0.1:
465
+ levels[t] = 3
466
+ elif p2 > 0.2:
467
+ levels[t] = 2
468
+ elif p1 > 0.1:
469
+ levels[t] = 1
470
+ return levels
471
+
472
+
473
+ def compute_weighted_contributions(res: Dict) -> Dict[str, np.ndarray]:
474
+ """
475
+ Retorna las contribuciones ponderadas reales de cada nivel al ΔRECD:
476
+ contrib1 = α1(t) * Φ1(t), etc.
477
+ Incluye también los totales medios para análisis rápido.
478
+ """
479
+ a1 = res["alpha1"]
480
+ a2 = res["alpha2"]
481
+ a3 = res["alpha3"]
482
+ p1 = res["phi1"]
483
+ p2 = res["phi2"]
484
+ p3 = np.nan_to_num(res["phi3"], nan=0.0)
485
+
486
+ c1 = a1 * p1
487
+ c2 = a2 * p2
488
+ c3 = a3 * p3
489
+
490
+ return {
491
+ "contrib1": c1,
492
+ "contrib2": c2,
493
+ "contrib3": c3,
494
+ "mean_contrib1": float(np.nanmean(c1)),
495
+ "mean_contrib2": float(np.nanmean(c2)),
496
+ "mean_contrib3": float(np.nanmean(c3)),
497
+ "total_mean_delta": float(np.nanmean(c1 + c2 + c3)),
498
+ "frac_contrib3": float(np.nanmean(c3) / (np.nanmean(c1 + c2 + c3) + 1e-12)),
499
+ }
500
+
501
+
502
+ def high_level3_rate(excess3: np.ndarray, thresh: float = 1.75) -> float:
503
+ """Fracción de tiempo donde el exceso de Nivel 3 excede el umbral exigente."""
504
+ excess3 = np.asarray(excess3)
505
+ return float(np.nanmean(excess3 > thresh))
506
+
507
+
508
+ def run_logical_demonstrations(n_steps: int = 800, m: int = 3, d: int = 4, theta3: float = DEFAULT_THETA3):
509
+ """
510
+ Demostraciones lógicas / casos degenerados (analíticos + numéricos mínimos).
511
+ Cumple el punto de mejora identificado: validación conceptual antes de confiar solo en numérico.
512
+
513
+ Casos:
514
+ 1. Variables completamente independientes (coupling=0, ruido independiente) → Φ2 y Φ3 ~ 0.
515
+ 2. Variables idénticas (totalmente sincronizadas) → Φ1 alto, Φ2 alto (persistencia), Φ3 bajo (sin novedad irreducible).
516
+ 3. Acoplamiento fuerte con drive común → Φ3 detectable (configuraciones conjuntas específicas).
517
+ """
518
+ print("\n=== DEMOSTRACIONES LÓGICAS (Casos Degenerados) ===")
519
+ rng = np.random.default_rng(123)
520
+
521
+ # Caso 1: Independientes
522
+ X_indep = rng.normal(size=(n_steps, 3))
523
+ res1 = compute_recd_from_conjunctions(X_indep, tau_s=np.zeros(n_steps), m=m, d=d, theta3=theta3)
524
+ print(f"1. Independientes (ruido blanco):")
525
+ print(f" Φ1={np.nanmean(res1['phi1']):.3f} Φ2={np.nanmean(res1['phi2']):.3f} Φ3_act={np.nanmean(res1['phi3']>0):.3f} excess={np.nanmean(res1['excess3']):.4f}")
526
+ print(f" (Esperado: Φ3 bajo; solo fluctuaciones)")
527
+
528
+ # Caso 2: Idénticas (sincronizadas triviales)
529
+ base = rng.normal(size=n_steps).cumsum() * 0.05
530
+ X_ident = np.stack([base + rng.normal(0, 0.01, n_steps) for _ in range(3)], axis=1)
531
+ res2 = compute_recd_from_conjunctions(X_ident, tau_s=np.zeros(n_steps), m=m, d=d, theta3=theta3)
532
+ print(f"2. Idénticas (sincronización trivial):")
533
+ print(f" Φ1={np.nanmean(res2['phi1']):.3f} Φ2={np.nanmean(res2['phi2']):.3f} Φ3_act={np.nanmean(res2['phi3']>0):.3f} excess={np.nanmean(res2['excess3']):.4f}")
534
+ print(f" (Esperado: Φ1/Φ2 altos; Φ3 no mucho más alto que ruido)")
535
+
536
+ # Caso 3: Acoplamiento fuerte (simulado con correlación ordinal fuerte + algo de drive)
537
+ X_coupled = np.zeros((n_steps, 3))
538
+ X_coupled[:, 0] = rng.normal(size=n_steps).cumsum() * 0.03
539
+ for i in range(1, 3):
540
+ X_coupled[:, i] = 0.85 * X_coupled[:, 0] + 0.15 * rng.normal(size=n_steps).cumsum() * 0.03
541
+ res3 = compute_recd_from_conjunctions(X_coupled, tau_s=np.zeros(n_steps), m=m, d=d, theta3=theta3)
542
+ print(f"3. Acoplamiento fuerte (drive común):")
543
+ print(f" Φ1={np.nanmean(res3['phi1']):.3f} Φ2={np.nanmean(res3['phi2']):.3f} Φ3_act={np.nanmean(res3['phi3']>0):.3f} excess={np.nanmean(res3['excess3']):.4f}")
544
+ print(f" (Esperado: Φ3 más alto por estructuras conjuntas específicas)")
545
+
546
+ return {"indep": res1, "identical": res2, "coupled": res3}
547
+
548
+
549
+ if __name__ == "__main__":
550
+ # Smoke test quirúrgico + demos lógicas
551
+ print("Smoke test nested_recd.ordinal_levels")
552
+ np.random.seed(42)
553
+ X_test = np.random.randn(200, 3).cumsum(axis=0)
554
+ res = compute_recd_from_conjunctions(X_test, tau_s=np.zeros(200))
555
+ print("phi1 mean:", round(np.nanmean(res["phi1"]), 4))
556
+ print("phi2 mean:", round(np.nanmean(res["phi2"]), 4))
557
+ print("phi3 active fraction:", round(np.nanmean(res["phi3"] > 0), 4))
558
+ print("T_recd final:", round(res["T_recd"][-1], 2))
559
+
560
+ # Ejecutar demostraciones lógicas
561
+ run_logical_demonstrations(n_steps=500, theta3=0.10)
562
+ print("\nOK - Incluye demostraciones lógicas de casos degenerados.")
nested_recd/py.typed ADDED
File without changes
@@ -0,0 +1,134 @@
1
+ """
2
+ Surrogate helpers for nested ordinal RECD null models.
3
+
4
+ Destroy cross-variable ordinal / temporal dependence while preserving
5
+ marginal distributions (and, for IAAFT, approximate power spectra).
6
+
7
+ Use for null baselines of Φ₂ / Φ₃ and empirical p-values.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import numpy as np
13
+ from typing import Callable, Dict
14
+
15
+
16
+ def iaaft_surrogate_1d(x: np.ndarray, n_iters: int = 50, seed: int = None) -> np.ndarray:
17
+ """
18
+ IAAFT (Iterative Amplitude Adjusted Fourier Transform) surrogate 1D.
19
+ Preserva distribución de amplitudes y espectro de potencia aproximado.
20
+ Bueno para series con estructura temporal.
21
+ """
22
+ rng = np.random.default_rng(seed)
23
+ x = np.asarray(x).ravel()
24
+ n = len(x)
25
+
26
+ # FFT original
27
+ xf = np.fft.rfft(x)
28
+ amp = np.abs(xf)
29
+
30
+ # Fase aleatoria inicial
31
+ phase = rng.uniform(0, 2*np.pi, len(xf))
32
+ phase[0] = 0.0 # DC
33
+ if len(xf) > 1:
34
+ phase[-1] = 0.0 # Nyquist si aplica
35
+
36
+ s = np.fft.irfft(amp * np.exp(1j * phase), n=n)
37
+
38
+ # Iterar para ajustar distribución
39
+ x_sorted = np.sort(x)
40
+ for _ in range(n_iters):
41
+ # Ajustar amplitudes en Fourier
42
+ sf = np.fft.rfft(s)
43
+ s = np.fft.irfft(amp * np.exp(1j * np.angle(sf)), n=n)
44
+ # Ajustar distribución (rank order)
45
+ ranks = np.argsort(np.argsort(s))
46
+ s = x_sorted[ranks]
47
+ return s
48
+
49
+
50
+ def phase_shuffle_independent(X: np.ndarray, seed: int = None) -> np.ndarray:
51
+ """
52
+ Surrogate más agresivo y quirúrgico para H0 de "sin relaciones ordinales":
53
+ - Para cada columna (variable) de forma INDEPENDIENTE:
54
+ permuta aleatoriamente los valores (o mejor: phase shuffle por columna).
55
+ Esto destruye TODAS las relaciones temporales y entre variables.
56
+ """
57
+ rng = np.random.default_rng(seed)
58
+ X = np.asarray(X).copy()
59
+ T, N = X.shape
60
+ for i in range(N):
61
+ # Phase shuffle por columna
62
+ X[:, i] = iaaft_surrogate_1d(X[:, i], n_iters=30, seed=rng.integers(0, 2**32))
63
+ return X
64
+
65
+
66
+ def random_permutation_independent(X: np.ndarray, seed: int = None) -> np.ndarray:
67
+ """
68
+ Versión aún más simple: permutación aleatoria de valores por columna.
69
+ Destruye toda estructura temporal/ordinal.
70
+ Útil como baseline extremo.
71
+ """
72
+ rng = np.random.default_rng(seed)
73
+ X = np.asarray(X).copy()
74
+ for i in range(X.shape[1]):
75
+ rng.shuffle(X[:, i])
76
+ return X
77
+
78
+
79
+ def generate_surrogate_ensemble(
80
+ X: np.ndarray,
81
+ n_surrogates: int = 50,
82
+ method: str = "iaaft",
83
+ seed: int = 1234
84
+ ) -> list:
85
+ """
86
+ Genera ensemble de surrogates.
87
+ method: "iaaft" | "phase" | "permute"
88
+ """
89
+ rng = np.random.default_rng(seed)
90
+ surrogates = []
91
+ for k in range(n_surrogates):
92
+ s = rng.integers(0, 2**32)
93
+ if method == "iaaft":
94
+ Xs = phase_shuffle_independent(X, seed=s) # usa iaaft interno
95
+ elif method == "phase":
96
+ Xs = phase_shuffle_independent(X, seed=s)
97
+ else:
98
+ Xs = random_permutation_independent(X, seed=s)
99
+ surrogates.append(Xs)
100
+ return surrogates
101
+
102
+
103
+ def compute_null_distribution(
104
+ X: np.ndarray,
105
+ compute_fn: Callable,
106
+ n_surrogates: int = 50,
107
+ method: str = "iaaft",
108
+ **compute_kwargs
109
+ ) -> Dict[str, np.ndarray]:
110
+ """
111
+ compute_fn(X, **kwargs) -> dict con métricas (ej. mean_excess3, phi3_active_frac)
112
+ Retorna distribuciones nulas.
113
+ """
114
+ surrogates = generate_surrogate_ensemble(X, n_surrogates=n_surrogates, method=method)
115
+ nulls = {"phi3_active": [], "mean_excess3": [], "corr_excess3_lam": []}
116
+
117
+ for Xs in surrogates:
118
+ # Nota: el caller debe proveer tau_s consistente o recalcular dentro de compute_fn
119
+ res = compute_fn(Xs, **compute_kwargs)
120
+ nulls["phi3_active"].append(res.get("phi3_active_frac", np.nan))
121
+ nulls["mean_excess3"].append(res.get("mean_excess3", np.nan))
122
+ nulls["corr_excess3_lam"].append(res.get("corr_excess3_lambda", np.nan))
123
+
124
+ return {k: np.array(v) for k, v in nulls.items()}
125
+
126
+
127
+ if __name__ == "__main__":
128
+ # Smoke
129
+ X = np.random.randn(500, 3).cumsum(0)
130
+ Xs = phase_shuffle_independent(X)
131
+ print("Original var col0:", X[:,0].var(), "Surrogate:", Xs[:,0].var())
132
+ print("Cross corr orig:", np.corrcoef(X.T)[0,1])
133
+ print("Cross corr surr:", np.corrcoef(Xs.T)[0,1])
134
+ print("Surrogates OK")
@@ -0,0 +1,156 @@
1
+ Metadata-Version: 2.4
2
+ Name: nested-recd
3
+ Version: 0.1.0
4
+ Summary: Nested ordinal RECD: Φ1–Φ3 conjunction levels and λ-weighted Discrete Extramental Clock
5
+ Author-email: Johel Padilla-Villanueva <joel.padilla2@upr.edu>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/johelpadilla/nested-recd
8
+ Project-URL: Repository, https://github.com/johelpadilla/nested-recd
9
+ Project-URL: Issues, https://github.com/johelpadilla/nested-recd/issues
10
+ Project-URL: Documentation, https://github.com/johelpadilla/nested-recd#readme
11
+ Project-URL: DOI, https://doi.org/10.5281/zenodo.21270699
12
+ Keywords: RECD,ordinal patterns,nested time,Systemic Tau,complexity,early-warning,network physiology,Bandt-Pompe
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Scientific/Engineering
23
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
24
+ Classifier: Topic :: Scientific/Engineering :: Physics
25
+ Requires-Python: >=3.9
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Requires-Dist: numpy>=1.22
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest>=7.0; extra == "dev"
31
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
32
+ Requires-Dist: build; extra == "dev"
33
+ Requires-Dist: twine; extra == "dev"
34
+ Dynamic: license-file
35
+
36
+ # nested-recd
37
+
38
+ [![PyPI version](https://img.shields.io/pypi/v/nested-recd.svg)](https://pypi.org/project/nested-recd/)
39
+ [![Python](https://img.shields.io/pypi/pyversions/nested-recd.svg)](https://pypi.org/project/nested-recd/)
40
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
41
+
42
+ **Nested ordinal RECD** — pure-NumPy implementation of nested ordinal conjunction levels (Φ₁, Φ₂, Φ₃) and λ-weighted Discrete Extramental Clock (RECD) accumulation.
43
+
44
+ This is the standalone library behind the nested-time structure used in the CCTP/SDDB cardiac pilot and the experimental design in *Conversación de la naturaleza del tiempo*.
45
+
46
+ ## Install
47
+
48
+ ```bash
49
+ pip install nested-recd
50
+ ```
51
+
52
+ From source:
53
+
54
+ ```bash
55
+ pip install git+https://github.com/johelpadilla/nested-recd.git
56
+ ```
57
+
58
+ ## Quick start
59
+
60
+ ```python
61
+ import numpy as np
62
+ from nested_recd import compute_recd_from_conjunctions, compute_weighted_contributions
63
+
64
+ # Multivariate series: shape (T, N)
65
+ rng = np.random.default_rng(0)
66
+ X = rng.normal(size=(800, 3)).cumsum(axis=0)
67
+
68
+ out = compute_recd_from_conjunctions(X, m=3, d=4, theta3=0.10)
69
+
70
+ print("mean Φ1:", float(np.nanmean(out["phi1"])))
71
+ print("mean Φ2:", float(np.nanmean(out["phi2"])))
72
+ print("Φ3 active fraction:", float(np.nanmean(out["phi3"] > 0)))
73
+ print("final T_recd:", float(out["T_recd"][-1]))
74
+
75
+ w = compute_weighted_contributions(out)
76
+ print("frac level-3 contribution:", w["frac_contrib3"])
77
+ ```
78
+
79
+ Optional: supply a Systemic Tau series `tau_s` (aligned in time) so that λ is derived empirically:
80
+
81
+ ```python
82
+ out = compute_recd_from_conjunctions(X, tau_s=tau_s)
83
+ ```
84
+
85
+ Or fix the regime weight with a scalar / array override:
86
+
87
+ ```python
88
+ out = compute_recd_from_conjunctions(X, lam_override=0.5)
89
+ ```
90
+
91
+ ## What it computes
92
+
93
+ | Symbol | Meaning |
94
+ |--------|---------|
95
+ | **Φ₁** | Co-occurrence of identical ordinal symbols across variable pairs |
96
+ | **Φ₂** | Persistence of pairwise ordinal relations over lag `d` |
97
+ | **Φ₃** | Higher-order synergy proxy (total-correlation excess + joint surprise) |
98
+ | **λ** | Chaos / reorganization intensity (from `τ_s` or `lam_override`) |
99
+ | **α(λ)** | Level weights: α₁ decays with λ; α₂, α₃ grow with λ |
100
+ | **ΔRECD** | `α₁Φ₁ + α₂Φ₂ + α₃Φ₃` |
101
+ | **T_recd** | Cumulative sum of ΔRECD (nested-time clock) |
102
+
103
+ Ordinal symbols use Bandt–Pompe patterns (`m=3` by default).
104
+
105
+ ## Surrogates
106
+
107
+ ```python
108
+ from nested_recd import phase_shuffle_independent, random_permutation_independent
109
+
110
+ X_null = phase_shuffle_independent(X, seed=42) # IAAFT per column
111
+ X_perm = random_permutation_independent(X, seed=42)
112
+ ```
113
+
114
+ ## API surface
115
+
116
+ ```python
117
+ from nested_recd import (
118
+ compute_recd_from_conjunctions,
119
+ compute_phi1, compute_phi2, compute_phi3,
120
+ compute_lambda, alpha_weights, regime_lambda_proxy,
121
+ compute_weighted_contributions, high_level3_rate,
122
+ generate_multivariate_symbols,
123
+ phase_shuffle_independent, generate_surrogate_ensemble,
124
+ )
125
+ ```
126
+
127
+ ## Relation to other projects
128
+
129
+ | Project | Role |
130
+ |---------|------|
131
+ | [`systemictau`](https://pypi.org/project/systemictau/) | Full Systemic Tau library (τ_s, platform, studio) |
132
+ | [`cctp-sddb-systemic-tau`](https://github.com/johelpadilla/cctp-sddb-systemic-tau) | Cardiac VF pilot using this nested RECD pipeline |
133
+ | This package | Lightweight, installable nested-time / ordinal RECD core |
134
+
135
+ ## Citation
136
+
137
+ If you use this software, please cite the CCTP/SDDB pilot archive:
138
+
139
+ > Padilla-Villanueva, J. (2026). *CCTP/SDDB: Systemic Tau and ordinal RECD before spontaneous ventricular fibrillation* (v1.0.1). Zenodo. https://doi.org/10.5281/zenodo.21270699
140
+
141
+ And the theoretical nested-time / RECD framework as appropriate for your venue.
142
+
143
+ ```bibtex
144
+ @software{padilla_nested_recd_2026,
145
+ author = {Padilla-Villanueva, Johel},
146
+ title = {nested-recd: Nested ordinal RECD levels},
147
+ year = {2026},
148
+ url = {https://github.com/johelpadilla/nested-recd},
149
+ version = {0.1.0}
150
+ }
151
+ ```
152
+
153
+ ## License
154
+
155
+ MIT © 2026 Johel Padilla-Villanueva
156
+ ORCID: [0000-0002-5797-6931](https://orcid.org/0000-0002-5797-6931)
@@ -0,0 +1,9 @@
1
+ nested_recd/__init__.py,sha256=1N4G3Z5fkJzk61m1ar9wSfgw0B_Kc9BoqZncHAz1t3g,1790
2
+ nested_recd/ordinal_levels.py,sha256=GrzCewIXNJTdisAjAVS_qmeRxJbXa-Ok5aNQmGUmjnQ,20460
3
+ nested_recd/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ nested_recd/surrogates.py,sha256=524WzcpaZeC23RVBgNRt2I0UhUwlxLktlyOhilyR5VM,4370
5
+ nested_recd-0.1.0.dist-info/licenses/LICENSE,sha256=FZskxVbIH5VkmIzOyzZfAe2KpuaRLRW0IuijY1iPDKg,1081
6
+ nested_recd-0.1.0.dist-info/METADATA,sha256=4K3OAxLz5q_fG6LP3U3W8dp4wPVf6aByjMqxJdfGiKU,5687
7
+ nested_recd-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
8
+ nested_recd-0.1.0.dist-info/top_level.txt,sha256=fXKERfoS_tkUI6jJBvtLtNMFzLT8mbvGi8QRX1kBHEQ,12
9
+ nested_recd-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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 @@
1
+ nested_recd