tnfr 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,1305 @@
1
+ from dataclasses import dataclass
2
+ from collections import defaultdict
3
+ from typing import List, Dict, Optional
4
+ import networkx as nx
5
+ import numpy as np
6
+ import random
7
+
8
+ # GESTIÓN TEMPORAL TOPOLÓGICA TNFR #
9
+
10
+ class TemporalCoordinatorTNFR:
11
+ """
12
+ Coordinador temporal canónico que gestiona tiempo topológico variable
13
+ según frecuencias estructurales νf de cada NFR y principios de entrainment.
14
+ """
15
+
16
+ def __init__(self, sincronizacion_global=True, pulsos_reorganizacion=50):
17
+ # Configuración temporal canónica
18
+ self.sincronizacion_global = sincronizacion_global
19
+ self.frecuencia_pulsos = pulsos_reorganizacion
20
+ self.tiempo_topologico = 0.0
21
+
22
+ # Estados temporales de nodos
23
+ self.cronometros_nodales = {} # tiempo local de cada nodo
24
+ self.fases_sincronizacion = {} # fase temporal de cada nodo
25
+ self.ultimas_activaciones = {} # última activación de cada nodo
26
+
27
+ # Historial de sincronización
28
+ self.historial_entrainment = []
29
+ self.historial_coherencia_temporal = []
30
+
31
+ # Cola de eventos temporales
32
+ self.cola_eventos = [] # [(tiempo_activacion, nodo_id, tipo_evento)]
33
+
34
+ # Parámetros de resonancia temporal
35
+ self.umbral_resonancia = 0.15 # diferencia máxima en νf para resonancia
36
+ self.factor_aceleracion = 1.8 # aceleración temporal por coherencia alta
37
+
38
+ def calcular_paso_temporal_nodal(self, nodo, paso_global):
39
+ # Validación preventiva de parámetros
40
+ vf_nodo = nodo.get("νf", 1.0)
41
+ if not np.isfinite(vf_nodo) or vf_nodo <= 0:
42
+ vf_nodo = 1.0
43
+ nodo["νf"] = 1.0 # Corrección in-situ
44
+
45
+ Si_nodo = nodo.get("Si", 0.5)
46
+ if not np.isfinite(Si_nodo):
47
+ Si_nodo = 0.5
48
+ nodo["Si"] = 0.5
49
+
50
+ vf_nodo = nodo.get("νf", 1.0)
51
+ Si_nodo = nodo.get("Si", 0.5)
52
+ theta_nodo = nodo.get("θ", 0.5)
53
+ estado = nodo.get("estado", "latente")
54
+
55
+ # Paso base según frecuencia estructural (inversa de νf)
56
+ # Alta frecuencia = pasos más pequeños (más actividad)
57
+ paso_base = 1.0 / max(0.1, vf_nodo)
58
+
59
+ # Factor de coherencia: mayor Si permite pasos más largos (estabilidad)
60
+ factor_coherencia = 0.5 + 0.5 * Si_nodo
61
+
62
+ # Factor de activación: nodos activos necesitan pasos más pequeños
63
+ factor_activacion = {
64
+ "activo": 0.7, # pasos más pequeños para actividad
65
+ "latente": 1.0, # pasos normales
66
+ "silencio": 1.5, # pasos más grandes en silencio
67
+ "inactivo": 2.0 # pasos muy grandes si inactivo
68
+ }.get(estado, 1.0)
69
+
70
+ # Factor de umbral: cerca de bifurcación = pasos pequeños
71
+ factor_umbral = 1.0 - 0.3 * min(1.0, theta_nodo)
72
+
73
+ # Combinar todos los factores
74
+ paso_temporal = paso_base * factor_coherencia * factor_activacion * factor_umbral
75
+
76
+ # Limitar al rango [0.1, 5.0] para evitar extremos
77
+ paso_temporal = max(0.1, min(5.0, paso_temporal))
78
+
79
+ return paso_temporal
80
+
81
+ def detectar_nodos_resonantes(self, G):
82
+ """
83
+ Detecta grupos de nodos con frecuencias νf compatibles para entrainment.
84
+ """
85
+ nodos_por_frecuencia = defaultdict(list)
86
+
87
+ # Agrupar nodos por bandas de frecuencia
88
+ for nodo_id, nodo_data in G.nodes(data=True):
89
+ vf = nodo_data.get("νf", 1.0)
90
+ if np.isfinite(vf) and vf > 0 and np.isfinite(self.umbral_resonancia):
91
+ try:
92
+ ratio = vf / self.umbral_resonancia
93
+ if np.isfinite(ratio) and abs(ratio) < 1e6: # Límite de seguridad
94
+ banda_freq = round(ratio) * self.umbral_resonancia
95
+ else:
96
+ banda_freq = self.umbral_resonancia # Valor por defecto seguro
97
+ except (ValueError, OverflowError):
98
+ banda_freq = self.umbral_resonancia
99
+ else:
100
+ banda_freq = self.umbral_resonancia # Manejo de casos inválidos
101
+ nodos_por_frecuencia[banda_freq].append(nodo_id)
102
+
103
+ # Identificar grupos resonantes (2+ nodos en misma banda)
104
+ grupos_resonantes = []
105
+ for banda, nodos in nodos_por_frecuencia.items():
106
+ if len(nodos) >= 2:
107
+ # Verificar coherencia estructural dentro del grupo
108
+ coherencias = [G.nodes[n].get("Si", 0) for n in nodos]
109
+ if np.mean(coherencias) > 0.4: # grupo coherente
110
+ grupos_resonantes.append({
111
+ 'banda_frecuencia': banda,
112
+ 'nodos': nodos,
113
+ 'coherencia_grupal': np.mean(coherencias),
114
+ 'tamaño': len(nodos)
115
+ })
116
+
117
+ return grupos_resonantes
118
+
119
+ def sincronizar_grupo_resonante(self, G, grupo):
120
+ """
121
+ Sincroniza temporalmente un grupo de nodos resonantes mediante entrainment.
122
+ """
123
+ nodos = grupo['nodos']
124
+ banda_freq = grupo['banda_frecuencia']
125
+
126
+ # Calcular fase de sincronización grupal
127
+ fases_actuales = [self.fases_sincronizacion.get(n, 0.0) for n in nodos]
128
+ fase_promedio = np.mean(fases_actuales)
129
+
130
+ # Factor de atracción hacia sincronización
131
+ for nodo_id in nodos:
132
+ nodo = G.nodes[nodo_id]
133
+ fase_actual = self.fases_sincronizacion.get(nodo_id, 0.0)
134
+
135
+ # Calcular corrección de fase hacia el promedio grupal
136
+ diferencia_fase = fase_promedio - fase_actual
137
+ factor_correccion = 0.1 * nodo.get("Si", 0.5) # más Si = más atraído
138
+
139
+ # Aplicar corrección suave
140
+ nueva_fase = fase_actual + factor_correccion * diferencia_fase
141
+ self.fases_sincronizacion[nodo_id] = nueva_fase % (2 * np.pi)
142
+
143
+ # Ajustar cronómetro nodal para sincronización
144
+ ajuste_temporal = np.sin(diferencia_fase) * 0.05
145
+ cronometro_actual = self.cronometros_nodales.get(nodo_id, 0.0)
146
+ self.cronometros_nodales[nodo_id] = cronometro_actual + ajuste_temporal
147
+
148
+ return len(nodos) # cantidad de nodos sincronizados
149
+
150
+ def generar_pulso_reorganizacion_global(self, G, paso_global):
151
+ """
152
+ Genera pulso de reorganización global que sincroniza toda la red.
153
+ """
154
+ if paso_global % self.frecuencia_pulsos != 0:
155
+ return False
156
+
157
+ # Calcular coherencia global actual
158
+ EPIs = [G.nodes[n].get("EPI", 1.0) for n in G.nodes]
159
+ coherencia_global = np.mean(EPIs)
160
+
161
+ # Intensidad del pulso basada en necesidad de reorganización
162
+ variabilidad_EPI = np.std(EPIs)
163
+ intensidad_pulso = min(1.0, variabilidad_EPI / coherencia_global)
164
+
165
+ # Aplicar pulso a todos los nodos
166
+ nodos_afectados = 0
167
+ for nodo_id, nodo_data in G.nodes(data=True):
168
+ # Reset parcial del cronómetro según intensidad
169
+ cronometro_actual = self.cronometros_nodales.get(nodo_id, 0.0)
170
+ ajuste = intensidad_pulso * 0.2 * random.uniform(-1, 1)
171
+ self.cronometros_nodales[nodo_id] = cronometro_actual + ajuste
172
+
173
+ # Sincronizar fases hacia coherencia global
174
+ fase_actual = self.fases_sincronizacion.get(nodo_id, 0.0)
175
+ # Fase objetivo basada en frecuencia del nodo
176
+ vf = nodo_data.get("νf", 1.0)
177
+ tiempo_normalizado = self.tiempo_topologico % (4 * np.pi) # Ciclo de normalización
178
+ if np.isfinite(vf) and np.isfinite(tiempo_normalizado):
179
+ fase_objetivo = (vf * tiempo_normalizado) % (2 * np.pi)
180
+ else:
181
+ fase_objetivo = 0.0 # Valor seguro por defecto
182
+ diferencia = fase_objetivo - fase_actual
183
+ self.fases_sincronizacion[nodo_id] = fase_actual + 0.3 * diferencia
184
+
185
+ nodos_afectados += 1
186
+
187
+ return True
188
+
189
+ def calcular_simultaneidad_eventos(self, G, eventos_candidatos):
190
+ """
191
+ Determina qué eventos pueden ocurrir simultáneamente basado en coherencia.
192
+ """
193
+ if len(eventos_candidatos) <= 1:
194
+ return eventos_candidatos
195
+
196
+ eventos_simultaneos = []
197
+ eventos_procesados = set()
198
+
199
+ for i, (tiempo_i, nodo_i, evento_i) in enumerate(eventos_candidatos):
200
+ if i in eventos_procesados:
201
+ continue
202
+
203
+ grupo_simultaneo = [(tiempo_i, nodo_i, evento_i)]
204
+ eventos_procesados.add(i)
205
+
206
+ # Buscar eventos compatibles para simultaneidad
207
+ for j, (tiempo_j, nodo_j, evento_j) in enumerate(eventos_candidatos[i+1:], i+1):
208
+ if j in eventos_procesados:
209
+ continue
210
+
211
+ # Verificar criterios de simultaneidad
212
+ diferencia_temporal = abs(tiempo_i - tiempo_j)
213
+ if diferencia_temporal > 0.1: # demasiado separados en tiempo
214
+ continue
215
+
216
+ # Verificar coherencia estructural entre nodos
217
+ nodo_data_i = G.nodes[nodo_i]
218
+ nodo_data_j = G.nodes[nodo_j]
219
+
220
+ diferencia_vf = abs(nodo_data_i.get("νf", 1) - nodo_data_j.get("νf", 1))
221
+ diferencia_Si = abs(nodo_data_i.get("Si", 0) - nodo_data_j.get("Si", 0))
222
+
223
+ # Criterios de compatibilidad para simultaneidad
224
+ if (diferencia_vf < self.umbral_resonancia and
225
+ diferencia_Si < 0.3 and
226
+ len(grupo_simultaneo) < 5): # máximo 5 eventos simultáneos
227
+
228
+ grupo_simultaneo.append((tiempo_j, nodo_j, evento_j))
229
+ eventos_procesados.add(j)
230
+
231
+ eventos_simultaneos.append(grupo_simultaneo)
232
+
233
+ return eventos_simultaneos
234
+
235
+ def avanzar_tiempo_topologico(self, G, paso_global):
236
+ """
237
+ Función principal que avanza el tiempo topológico de la red.
238
+ """
239
+ eventos_este_paso = []
240
+ grupos_resonantes = self.detectar_nodos_resonantes(G)
241
+
242
+ if self.tiempo_topologico > 1e6 or not np.isfinite(self.tiempo_topologico):
243
+ self.tiempo_topologico = self.tiempo_topologico % (8 * np.pi) # Normalizar
244
+ if not np.isfinite(self.tiempo_topologico):
245
+ self.tiempo_topologico = 0.0 # Reset completo si persiste NaN
246
+
247
+ # Procesar cada nodo con su tiempo topológico individual
248
+ for nodo_id, nodo_data in G.nodes(data=True):
249
+ # Inicializar cronómetro si es necesario
250
+ if nodo_id not in self.cronometros_nodales:
251
+ self.cronometros_nodales[nodo_id] = 0.0
252
+ self.fases_sincronizacion[nodo_id] = random.uniform(0, 2*np.pi)
253
+
254
+ # Calcular paso temporal para este nodo
255
+ paso_nodal = self.calcular_paso_temporal_nodal(nodo_data, paso_global)
256
+
257
+ # Avanzar cronómetro nodal
258
+ self.cronometros_nodales[nodo_id] += paso_nodal
259
+
260
+ # Actualizar fase de sincronización
261
+ vf = nodo_data.get("νf", 1.0)
262
+ incremento_fase = 2 * np.pi * paso_nodal * vf
263
+ self.fases_sincronizacion[nodo_id] = (self.fases_sincronizacion[nodo_id] + incremento_fase) % (2 * np.pi)
264
+
265
+ # Verificar si el nodo debe activarse en este paso
266
+ tiempo_desde_activacion = self.cronometros_nodales[nodo_id] - self.ultimas_activaciones.get(nodo_id, 0)
267
+
268
+ # Umbral de activación basado en frecuencia y fase
269
+ umbral_activacion = 1.0 / max(0.1, vf) # período de activación
270
+
271
+ if tiempo_desde_activacion >= umbral_activacion:
272
+ eventos_este_paso.append((self.cronometros_nodales[nodo_id], nodo_id, "activacion_temporal"))
273
+ self.ultimas_activaciones[nodo_id] = self.cronometros_nodales[nodo_id]
274
+
275
+ # Control de desbordamiento de cronómetros
276
+ for nodo_id in self.cronometros_nodales:
277
+ if self.cronometros_nodales[nodo_id] > 1e4:
278
+ self.cronometros_nodales[nodo_id] = self.cronometros_nodales[nodo_id] % 100.0
279
+
280
+ # Sincronizar grupos resonantes
281
+ nodos_sincronizados = 0
282
+ for grupo in grupos_resonantes:
283
+ nodos_sincronizados += self.sincronizar_grupo_resonante(G, grupo)
284
+
285
+ # Generar pulso de reorganización global si corresponde
286
+ pulso_global = self.generar_pulso_reorganizacion_global(G, paso_global)
287
+
288
+ # Calcular eventos simultáneos
289
+ grupos_simultaneos = self.calcular_simultaneidad_eventos(G, eventos_este_paso)
290
+
291
+ # Avanzar tiempo topológico global
292
+ incremento_global = np.mean([self.calcular_paso_temporal_nodal(G.nodes[n], paso_global) for n in G.nodes])
293
+ self.tiempo_topologico += incremento_global
294
+
295
+ # Registrar estadísticas temporales
296
+ coherencia_temporal = self.calcular_coherencia_temporal(G)
297
+ self.historial_coherencia_temporal.append((paso_global, coherencia_temporal))
298
+
299
+ # Registrar información de entrainment
300
+ self.historial_entrainment.append({
301
+ 'paso': paso_global,
302
+ 'grupos_resonantes': len(grupos_resonantes),
303
+ 'nodos_sincronizados': nodos_sincronizados,
304
+ 'eventos_simultaneos': len([g for g in grupos_simultaneos if len(g) > 1]),
305
+ 'pulso_global': pulso_global,
306
+ 'coherencia_temporal': coherencia_temporal
307
+ })
308
+
309
+ return {
310
+ 'tiempo_topologico': self.tiempo_topologico,
311
+ 'grupos_resonantes': grupos_resonantes,
312
+ 'eventos_simultaneos': grupos_simultaneos,
313
+ 'estadisticas': self.historial_entrainment[-1]
314
+ }
315
+
316
+ def calcular_coherencia_temporal(self, G):
317
+ """
318
+ Calcula la coherencia temporal global de la red.
319
+ """
320
+ if len(G.nodes) == 0:
321
+ return 0.0
322
+
323
+ # Coherencia basada en sincronización de fases
324
+ fases = [self.fases_sincronizacion.get(n, 0) for n in G.nodes]
325
+
326
+ # Calcular parámetro de orden de Kuramoto
327
+ suma_compleja = sum(np.exp(1j * fase) for fase in fases)
328
+ parametro_orden = abs(suma_compleja) / len(fases)
329
+
330
+ # Coherencia basada en distribución de cronómetros
331
+ cronometros = [self.cronometros_nodales.get(n, 0) for n in G.nodes]
332
+ variabilidad_cronometros = np.std(cronometros) / (np.mean(cronometros) + 0.1)
333
+ coherencia_cronometros = 1.0 / (1.0 + variabilidad_cronometros)
334
+
335
+ # Combinar ambas métricas
336
+ coherencia_temporal = 0.6 * parametro_orden + 0.4 * coherencia_cronometros
337
+
338
+ return coherencia_temporal
339
+
340
+ def inicializar_coordinador_temporal_canonico():
341
+ """
342
+ Inicializa el coordinador temporal canónico para OntoSim.
343
+ """
344
+ return TemporalCoordinatorTNFR(
345
+ sincronizacion_global=True,
346
+ pulsos_reorganizacion=75 # pulso cada 75 pasos
347
+ )
348
+
349
+ def integrar_tiempo_topologico_en_simulacion(G, paso, coordinador_temporal):
350
+ """
351
+ Función de integración que debe llamarse en cada paso de simular_emergencia().
352
+ Reemplaza la gestión temporal lineal por tiempo topológico canónico.
353
+ """
354
+ resultado_temporal = coordinador_temporal.avanzar_tiempo_topologico(G, paso)
355
+
356
+ # Aplicar efectos temporales a los nodos
357
+ for nodo_id, nodo_data in G.nodes(data=True):
358
+ # Obtener información temporal del nodo
359
+ cronometro = coordinador_temporal.cronometros_nodales.get(nodo_id, 0)
360
+ fase = coordinador_temporal.fases_sincronizacion.get(nodo_id, 0)
361
+
362
+ # Modular parámetros TNFR según tiempo topológico
363
+ modulacion_temporal = 1.0 + 0.1 * np.sin(fase) # modulación suave
364
+
365
+ # Aplicar modulación a νf (retroalimentación temporal)
366
+ vf_actual = nodo_data.get("νf", 1.0)
367
+ nodo_data["νf"] = vf_actual * modulacion_temporal
368
+
369
+ # Registrar información temporal en el nodo
370
+ nodo_data["cronometro_topologico"] = cronometro
371
+ nodo_data["fase_temporal"] = fase
372
+ nodo_data["ultima_sincronizacion"] = paso
373
+
374
+ return resultado_temporal
375
+
376
+ # Sistema de Bifurcaciones Estructurales Múltiples
377
+
378
+ # Clase para representar una trayectoria de bifurcación
379
+ @dataclass
380
+ class TrayectoriaBifurcacion:
381
+ """Representa una trayectoria específica en una bifurcación estructural"""
382
+ id: str
383
+ tipo: str
384
+ secuencia_glifica: List[str]
385
+ parametros_iniciales: Dict[str, float]
386
+ viabilidad: float = 1.0
387
+ pasos_completados: int = 0
388
+ activa: bool = True
389
+ convergencia_objetivo: Optional[str] = None
390
+
391
+ # Clase para gestionar espacios de bifurcación
392
+ @dataclass
393
+ class EspacioBifurcacion:
394
+ """Representa el espacio completo de una bifurcación con múltiples trayectorias"""
395
+ nodo_origen_id: str
396
+ tipo_bifurcacion: str
397
+ trayectorias: List[TrayectoriaBifurcacion]
398
+ paso_inicio: int
399
+ pasos_exploracion: int = 10
400
+ convergencias_detectadas: List[Dict] = None
401
+
402
+ def __post_init__(self):
403
+ if self.convergencias_detectadas is None:
404
+ self.convergencias_detectadas = []
405
+
406
+ # Gestor principal de bifurcaciones TNFR
407
+ class BifurcationManagerTNFR:
408
+ """Gestor canónico de bifurcaciones estructurales múltiples según principios TNFR"""
409
+
410
+ def __init__(self):
411
+ self.bifurcaciones_activas = {} # {nodo_id: EspacioBifurcacion}
412
+ self.trayectorias_exploradas = []
413
+ self.convergencias_detectadas = []
414
+ self.estadisticas_bifurcacion = {
415
+ 'total_bifurcaciones': 0,
416
+ 'bifurcaciones_simetricas': 0,
417
+ 'bifurcaciones_disonantes': 0,
418
+ 'bifurcaciones_fractales': 0,
419
+ 'convergencias_exitosas': 0,
420
+ 'trayectorias_colapsadas': 0
421
+ }
422
+
423
+ def detectar_bifurcacion_canonica(self, nodo, nodo_id, umbral_aceleracion=0.15):
424
+ """Detecta si un nodo está en condiciones de bifurcación canónica TNFR"""
425
+ try:
426
+ # Métricas de aceleración estructural
427
+ aceleracion = abs(nodo.get("d2EPI_dt2", 0))
428
+ gradiente = abs(nodo.get("ΔNFR", 0))
429
+ coherencia = nodo.get("Si", 0)
430
+ energia = nodo.get("EPI", 0)
431
+ frecuencia = nodo.get("νf", 1.0)
432
+
433
+ # Validación de valores numéricos
434
+ if not all(np.isfinite([aceleracion, gradiente, coherencia, energia, frecuencia])):
435
+ return False, "valores_no_finitos"
436
+
437
+ # Condiciones múltiples para bifurcación canónica
438
+ condiciones = {
439
+ 'aceleracion_critica': aceleracion > umbral_aceleracion,
440
+ 'gradiente_alto': gradiente > 0.8,
441
+ 'coherencia_suficiente': coherencia > 0.4,
442
+ 'energia_minima': energia > 1.2,
443
+ 'frecuencia_activa': frecuencia > 0.8
444
+ }
445
+
446
+ # Evaluación de umbral de bifurcación
447
+ condiciones_cumplidas = sum(condiciones.values())
448
+ umbral_minimo = 3 # Al menos 3 de 5 condiciones
449
+
450
+ # Determinación del tipo de bifurcación según las condiciones
451
+ tipo_bifurcacion = self._determinar_tipo_bifurcacion(nodo, condiciones)
452
+
453
+ es_bifurcacion = condiciones_cumplidas >= umbral_minimo
454
+ return es_bifurcacion, tipo_bifurcacion
455
+
456
+ except Exception as e:
457
+ return False, "error_deteccion"
458
+
459
+ def _determinar_tipo_bifurcacion(self, nodo, condiciones):
460
+ """Determina el tipo de bifurcación según las condiciones estructurales"""
461
+ aceleracion = abs(nodo.get("d2EPI_dt2", 0))
462
+ coherencia = nodo.get("Si", 0)
463
+ energia = nodo.get("EPI", 0)
464
+
465
+ # Bifurcación simétrica: alta coherencia, aceleración moderada
466
+ if coherencia > 0.7 and 0.15 < aceleracion < 0.3:
467
+ return "simetrica"
468
+
469
+ # Bifurcación disonante: baja coherencia, alta aceleración
470
+ elif coherencia < 0.5 and aceleracion > 0.3:
471
+ return "disonante"
472
+
473
+ # Bifurcación fractal-expansiva: alta energía, alta aceleración
474
+ elif energia > 2.0 and aceleracion > 0.25:
475
+ return "fractal_expansiva"
476
+
477
+ # Por defecto: simétrica
478
+ else:
479
+ return "simetrica"
480
+
481
+ def generar_espacio_bifurcacion(self, nodo_id, nodo_data, tipo_bifurcacion, paso_actual):
482
+ """Genera el espacio completo de bifurcación con múltiples trayectorias"""
483
+ try:
484
+ if tipo_bifurcacion == "simetrica":
485
+ trayectorias = self._generar_bifurcacion_simetrica(nodo_id, nodo_data)
486
+ elif tipo_bifurcacion == "disonante":
487
+ trayectorias = self._generar_bifurcacion_disonante(nodo_id, nodo_data)
488
+ elif tipo_bifurcacion == "fractal_expansiva":
489
+ trayectorias = self._generar_bifurcacion_fractal_expansiva(nodo_id, nodo_data)
490
+ else:
491
+ trayectorias = self._generar_bifurcacion_simetrica(nodo_id, nodo_data)
492
+
493
+ # Crear espacio de bifurcación
494
+ espacio = EspacioBifurcacion(
495
+ nodo_origen_id=nodo_id,
496
+ tipo_bifurcacion=tipo_bifurcacion,
497
+ trayectorias=trayectorias,
498
+ paso_inicio=paso_actual,
499
+ pasos_exploracion=random.randint(8, 15) # Exploración variable
500
+ )
501
+
502
+ # Registrar estadísticas
503
+ self.estadisticas_bifurcacion['total_bifurcaciones'] += 1
504
+ self.estadisticas_bifurcacion[f'bifurcaciones_{tipo_bifurcacion}s'] += 1
505
+
506
+ return espacio
507
+
508
+ except Exception as e:
509
+ return None
510
+
511
+ def _generar_bifurcacion_simetrica(self, nodo_id, nodo_data):
512
+ """Genera bifurcación simétrica con dos trayectorias complementarias"""
513
+ trayectorias = []
514
+
515
+ # Trayectoria A: Expansión coherente
516
+ trayectoria_a = TrayectoriaBifurcacion(
517
+ id=f"{nodo_id}_sym_A",
518
+ tipo="expansion_coherente",
519
+ secuencia_glifica=["VA'L", "R'A", "I'L"],
520
+ parametros_iniciales={
521
+ "EPI": nodo_data.get("EPI", 1.0) * 1.2,
522
+ "νf": nodo_data.get("νf", 1.0) * 1.1,
523
+ "Si": nodo_data.get("Si", 0.5) * 1.15
524
+ },
525
+ convergencia_objetivo="coherencia_expandida"
526
+ )
527
+
528
+ # Trayectoria B: Contracción resonante
529
+ trayectoria_b = TrayectoriaBifurcacion(
530
+ id=f"{nodo_id}_sym_B",
531
+ tipo="contraccion_resonante",
532
+ secuencia_glifica=["NUL", "UM", "IL"],
533
+ parametros_iniciales={
534
+ "EPI": nodo_data.get("EPI", 1.0) * 0.8,
535
+ "νf": nodo_data.get("νf", 1.0) * 0.9,
536
+ "Si": nodo_data.get("Si", 0.5) * 1.2
537
+ },
538
+ convergencia_objetivo="coherencia_concentrada"
539
+ )
540
+
541
+ trayectorias.extend([trayectoria_a, trayectoria_b])
542
+ return trayectorias
543
+
544
+ def _generar_bifurcacion_disonante(self, nodo_id, nodo_data):
545
+ """Genera bifurcación disonante con múltiples resoluciones"""
546
+ trayectorias = []
547
+
548
+ # Trayectoria A: Mutación directa
549
+ trayectoria_a = TrayectoriaBifurcacion(
550
+ id=f"{nodo_id}_dis_A",
551
+ tipo="mutacion_directa",
552
+ secuencia_glifica=["THOL"],
553
+ parametros_iniciales={
554
+ "EPI": nodo_data.get("EPI", 1.0) * 1.5,
555
+ "νf": nodo_data.get("νf", 1.0) * 1.3,
556
+ "ΔNFR": nodo_data.get("ΔNFR", 0) * 1.4
557
+ },
558
+ convergencia_objetivo="mutacion_estable"
559
+ )
560
+
561
+ # Trayectoria B: Reorganización recursiva
562
+ trayectoria_b = TrayectoriaBifurcacion(
563
+ id=f"{nodo_id}_dis_B",
564
+ tipo="reorganizacion_recursiva",
565
+ secuencia_glifica=["RE'MESH", "NA'V"],
566
+ parametros_iniciales={
567
+ "EPI": nodo_data.get("EPI", 1.0) * 0.9,
568
+ "νf": nodo_data.get("νf", 1.0),
569
+ "Si": nodo_data.get("Si", 0.5) * 1.3
570
+ },
571
+ convergencia_objetivo="reorganizacion_estable"
572
+ )
573
+
574
+ # Trayectoria C: Silencio regenerativo
575
+ trayectoria_c = TrayectoriaBifurcacion(
576
+ id=f"{nodo_id}_dis_C",
577
+ tipo="silencio_regenerativo",
578
+ secuencia_glifica=["SH'A", "A'L"],
579
+ parametros_iniciales={
580
+ "EPI": nodo_data.get("EPI", 1.0) * 0.7,
581
+ "νf": nodo_data.get("νf", 1.0) * 0.8,
582
+ "Si": nodo_data.get("Si", 0.5) * 0.9
583
+ },
584
+ convergencia_objetivo="regeneracion_silenciosa"
585
+ )
586
+
587
+ trayectorias.extend([trayectoria_a, trayectoria_b, trayectoria_c])
588
+ return trayectorias
589
+
590
+ def _generar_bifurcacion_fractal_expansiva(self, nodo_id, nodo_data):
591
+ """Genera bifurcación fractal-expansiva con sub-nodos derivados"""
592
+ trayectorias = []
593
+
594
+ # Trayectoria principal: Nodo padre con T'HOL
595
+ trayectoria_padre = TrayectoriaBifurcacion(
596
+ id=f"{nodo_id}_frac_padre",
597
+ tipo="autoorganizacion_padre",
598
+ secuencia_glifica=["THOL"],
599
+ parametros_iniciales=nodo_data.copy(),
600
+ convergencia_objetivo="autoorganizacion_estable"
601
+ )
602
+
603
+ # Sub-nodos derivados con variaciones
604
+ for i in range(3): # 3 sub-nodos derivados
605
+ variacion = 0.8 + 0.4 * random.random() # Variación 0.8-1.2
606
+
607
+ trayectoria_derivado = TrayectoriaBifurcacion(
608
+ id=f"{nodo_id}_frac_der_{i}",
609
+ tipo="derivado_fractal",
610
+ secuencia_glifica=["A'L", "E'N"],
611
+ parametros_iniciales={
612
+ "EPI": nodo_data.get("EPI", 1.0) * variacion,
613
+ "νf": nodo_data.get("νf", 1.0) * variacion,
614
+ "Si": nodo_data.get("Si", 0.5) * (0.5 + 0.5 * variacion),
615
+ "derivado_de": nodo_id,
616
+ "factor_variacion": variacion
617
+ },
618
+ convergencia_objetivo="derivacion_coherente"
619
+ )
620
+
621
+ trayectorias.append(trayectoria_derivado)
622
+
623
+ trayectorias.insert(0, trayectoria_padre) # Padre al inicio
624
+ return trayectorias
625
+
626
+ def procesar_bifurcaciones_activas(self, G, paso_actual):
627
+ """Procesa todas las bifurcaciones activas en el paso actual"""
628
+ resultados = {
629
+ 'trayectorias_procesadas': 0,
630
+ 'convergencias_detectadas': 0,
631
+ 'bifurcaciones_completadas': [],
632
+ 'nuevos_nodos_generados': []
633
+ }
634
+
635
+ bifurcaciones_completadas = []
636
+
637
+ for nodo_id, espacio_bifurcacion in list(self.bifurcaciones_activas.items()):
638
+ try:
639
+ # Verificar si la bifurcación ha completado su exploración
640
+ pasos_transcurridos = paso_actual - espacio_bifurcacion.paso_inicio
641
+
642
+ if pasos_transcurridos < espacio_bifurcacion.pasos_exploracion:
643
+ # Evolucionar trayectorias activas
644
+ resultado_evolucion = self._evolucionar_trayectorias(
645
+ espacio_bifurcacion, pasos_transcurridos, G
646
+ )
647
+ resultados['trayectorias_procesadas'] += resultado_evolucion['procesadas']
648
+
649
+ else:
650
+ # Convergencia final de trayectorias
651
+ resultado_convergencia = self._converger_bifurcacion(
652
+ espacio_bifurcacion, G, nodo_id
653
+ )
654
+
655
+ if resultado_convergencia['exitosa']:
656
+ resultados['convergencias_detectadas'] += 1
657
+ resultados['bifurcaciones_completadas'].append(nodo_id)
658
+ resultados['nuevos_nodos_generados'].extend(
659
+ resultado_convergencia.get('nodos_generados', [])
660
+ )
661
+ bifurcaciones_completadas.append(nodo_id)
662
+
663
+ # Actualizar estadísticas
664
+ self.estadisticas_bifurcacion['convergencias_exitosas'] += 1
665
+
666
+ except Exception as e:
667
+ bifurcaciones_completadas.append(nodo_id) # Eliminar bifurcación problemática
668
+
669
+ # Limpiar bifurcaciones completadas
670
+ for nodo_id in bifurcaciones_completadas:
671
+ if nodo_id in self.bifurcaciones_activas:
672
+ del self.bifurcaciones_activas[nodo_id]
673
+
674
+ return resultados
675
+
676
+ def _evolucionar_trayectorias(self, espacio_bifurcacion, paso_relativo, G):
677
+ """Evoluciona las trayectorias de una bifurcación en el paso actual"""
678
+ resultado = {'procesadas': 0, 'colapsadas': 0}
679
+
680
+ for trayectoria in espacio_bifurcacion.trayectorias:
681
+ if not trayectoria.activa:
682
+ continue
683
+
684
+ try:
685
+ # Aplicar transformación glífica según el paso relativo
686
+ if paso_relativo < len(trayectoria.secuencia_glifica):
687
+ glifo_actual = trayectoria.secuencia_glifica[paso_relativo]
688
+
689
+ # Aplicar transformación específica de la trayectoria
690
+ self._aplicar_transformacion_trayectoria(
691
+ trayectoria, glifo_actual, G, espacio_bifurcacion.nodo_origen_id
692
+ )
693
+
694
+ trayectoria.pasos_completados += 1
695
+ resultado['procesadas'] += 1
696
+
697
+ # Evaluar viabilidad de la trayectoria
698
+ viabilidad = self._evaluar_viabilidad_trayectoria(trayectoria)
699
+ trayectoria.viabilidad = viabilidad
700
+
701
+ # Marcar como inactiva si la viabilidad es muy baja
702
+ if viabilidad < 0.2:
703
+ trayectoria.activa = False
704
+ resultado['colapsadas'] += 1
705
+ self.estadisticas_bifurcacion['trayectorias_colapsadas'] += 1
706
+
707
+ except Exception as e:
708
+ trayectoria.activa = False
709
+ resultado['colapsadas'] += 1
710
+
711
+ return resultado
712
+
713
+ def _aplicar_transformacion_trayectoria(self, trayectoria, glifo, G, nodo_origen_id):
714
+ """Aplica una transformación glífica específica a una trayectoria"""
715
+ try:
716
+ # Obtener nodo origen desde el grafo
717
+ if nodo_origen_id not in G.nodes():
718
+ return
719
+
720
+ nodo_data = G.nodes[nodo_origen_id]
721
+
722
+ # Aplicar transformación según el glifo y tipo de trayectoria
723
+ if glifo == "VAL": # Expansión
724
+ factor = 1.15 if trayectoria.tipo == "expansion_coherente" else 1.05
725
+ trayectoria.parametros_iniciales["EPI"] *= factor
726
+
727
+ elif glifo == "NUL": # Contracción
728
+ factor = 0.85 if trayectoria.tipo == "contraccion_resonante" else 0.95
729
+ trayectoria.parametros_iniciales["EPI"] *= factor
730
+
731
+ elif glifo == "ZHIR": # Mutación
732
+ trayectoria.parametros_iniciales["EPI"] += 0.5
733
+ trayectoria.parametros_iniciales["νf"] *= 1.2
734
+
735
+ elif glifo == "RA": # Propagación
736
+ trayectoria.parametros_iniciales["Si"] *= 1.1
737
+
738
+ elif glifo == "IL": # Estabilización
739
+ # Convergencia hacia valores estables
740
+ epi_objetivo = 1.5
741
+ trayectoria.parametros_iniciales["EPI"] = (
742
+ trayectoria.parametros_iniciales["EPI"] * 0.8 + epi_objetivo * 0.2
743
+ )
744
+
745
+ elif glifo == "THOL": # Autoorganización
746
+ # Equilibrar todos los parámetros
747
+ for param in ["EPI", "νf", "Si"]:
748
+ if param in trayectoria.parametros_iniciales:
749
+ valor_actual = trayectoria.parametros_iniciales[param]
750
+ valor_equilibrado = max(0.8, min(2.0, valor_actual))
751
+ trayectoria.parametros_iniciales[param] = valor_equilibrado
752
+
753
+ except Exception as e:
754
+ pass
755
+
756
+ def _evaluar_viabilidad_trayectoria(self, trayectoria):
757
+ """Evalúa la viabilidad estructural de una trayectoria"""
758
+ try:
759
+ # Obtener parámetros actuales
760
+ epi = trayectoria.parametros_iniciales.get("EPI", 1.0)
761
+ vf = trayectoria.parametros_iniciales.get("νf", 1.0)
762
+ si = trayectoria.parametros_iniciales.get("Si", 0.5)
763
+
764
+ # Validación numérica
765
+ if not all(np.isfinite([epi, vf, si])):
766
+ return 0.0
767
+
768
+ # Criterios de viabilidad TNFR
769
+ criterios = []
770
+
771
+ # 1. Rango estructural válido
772
+ criterios.append(1.0 if 0.3 <= epi <= 3.5 else 0.0)
773
+
774
+ # 2. Frecuencia resonante
775
+ criterios.append(1.0 if 0.2 <= vf <= 2.5 else 0.0)
776
+
777
+ # 3. Coherencia mínima
778
+ criterios.append(1.0 if si >= 0.1 else 0.0)
779
+
780
+ # 4. Equilibrio energético
781
+ ratio_equilibrio = min(epi/vf, vf/epi) if vf > 0 else 0
782
+ criterios.append(ratio_equilibrio)
783
+
784
+ # 5. Progreso en secuencia
785
+ progreso = trayectoria.pasos_completados / max(len(trayectoria.secuencia_glifica), 1)
786
+ criterios.append(min(progreso, 1.0))
787
+
788
+ # Viabilidad como promedio ponderado
789
+ viabilidad = np.mean(criterios)
790
+ return max(0.0, min(1.0, viabilidad))
791
+
792
+ except Exception as e:
793
+ return 0.0
794
+
795
+ def _converger_bifurcacion(self, espacio_bifurcacion, G, nodo_origen_id):
796
+ """Convierte el espacio de bifurcación en configuración final estable"""
797
+ resultado = {
798
+ 'exitosa': False,
799
+ 'nodos_generados': [],
800
+ 'configuracion_final': None
801
+ }
802
+
803
+ try:
804
+ # Filtrar trayectorias viables
805
+ trayectorias_viables = [
806
+ t for t in espacio_bifurcacion.trayectorias
807
+ if t.activa and t.viabilidad > 0.3
808
+ ]
809
+
810
+ if not trayectorias_viables:
811
+ return resultado
812
+
813
+ # Ordenar por viabilidad
814
+ trayectorias_viables.sort(key=lambda t: t.viabilidad, reverse=True)
815
+
816
+ # Seleccionar trayectoria ganadora o fusionar múltiples
817
+ if len(trayectorias_viables) == 1:
818
+ # Una sola trayectoria viable
819
+ resultado = self._aplicar_trayectoria_ganadora(
820
+ trayectorias_viables[0], G, nodo_origen_id
821
+ )
822
+ else:
823
+ # Múltiples trayectorias: fusionar las más compatibles
824
+ resultado = self._fusionar_trayectorias_compatibles(
825
+ trayectorias_viables[:3], G, nodo_origen_id # Máximo 3 trayectorias
826
+ )
827
+
828
+ if resultado['exitosa']:
829
+ # Registrar convergencia
830
+ convergencia_info = {
831
+ 'nodo_origen': nodo_origen_id,
832
+ 'tipo_bifurcacion': espacio_bifurcacion.tipo_bifurcacion,
833
+ 'trayectorias_fusionadas': len(trayectorias_viables),
834
+ 'configuracion_final': resultado['configuracion_final']
835
+ }
836
+ self.convergencias_detectadas.append(convergencia_info)
837
+
838
+ return resultado
839
+
840
+ except Exception as e:
841
+ return resultado
842
+
843
+ def _aplicar_trayectoria_ganadora(self, trayectoria, G, nodo_origen_id):
844
+ """Aplica la configuración de una trayectoria ganadora al nodo origen"""
845
+ try:
846
+ if nodo_origen_id not in G.nodes():
847
+ return {'exitosa': False}
848
+
849
+ nodo_data = G.nodes[nodo_origen_id]
850
+
851
+ # Aplicar parámetros finales de la trayectoria
852
+ for param, valor in trayectoria.parametros_iniciales.items():
853
+ if param in ["EPI", "νf", "Si", "ΔNFR"] and np.isfinite(valor):
854
+ nodo_data[param] = max(0.1, min(3.0, valor)) # Límites de seguridad
855
+
856
+ # Marcar convergencia exitosa
857
+ nodo_data["ultima_bifurcacion"] = {
858
+ 'tipo': trayectoria.tipo,
859
+ 'convergencia': trayectoria.convergencia_objetivo,
860
+ 'viabilidad_final': trayectoria.viabilidad
861
+ }
862
+
863
+ return {
864
+ 'exitosa': True,
865
+ 'configuracion_final': trayectoria.parametros_iniciales.copy(),
866
+ 'nodos_generados': [nodo_origen_id]
867
+ }
868
+
869
+ except Exception as e:
870
+ return {'exitosa': False}
871
+
872
+ def _fusionar_trayectorias_compatibles(self, trayectorias, G, nodo_origen_id):
873
+ """Fusiona múltiples trayectorias compatibles en una configuración híbrida"""
874
+ try:
875
+ if nodo_origen_id not in G.nodes():
876
+ return {'exitosa': False}
877
+
878
+ # Calcular promedios ponderados por viabilidad
879
+ total_viabilidad = sum(t.viabilidad for t in trayectorias)
880
+ if total_viabilidad == 0:
881
+ return {'exitosa': False}
882
+
883
+ configuracion_fusionada = {}
884
+
885
+ for param in ["EPI", "νf", "Si", "ΔNFR"]:
886
+ suma_ponderada = sum(
887
+ t.parametros_iniciales.get(param, 1.0) * t.viabilidad
888
+ for t in trayectorias
889
+ )
890
+ valor_fusionado = suma_ponderada / total_viabilidad
891
+
892
+ if np.isfinite(valor_fusionado):
893
+ configuracion_fusionada[param] = max(0.1, min(3.0, valor_fusionado))
894
+
895
+ # Aplicar configuración fusionada
896
+ nodo_data = G.nodes[nodo_origen_id]
897
+ for param, valor in configuracion_fusionada.items():
898
+ nodo_data[param] = valor
899
+
900
+ # Marcar fusión exitosa
901
+ nodo_data["ultima_bifurcacion"] = {
902
+ 'tipo': 'fusion_multiple',
903
+ 'trayectorias_fusionadas': len(trayectorias),
904
+ 'viabilidad_promedio': total_viabilidad / len(trayectorias)
905
+ }
906
+
907
+ return {
908
+ 'exitosa': True,
909
+ 'configuracion_final': configuracion_fusionada,
910
+ 'nodos_generados': [nodo_origen_id]
911
+ }
912
+
913
+ except Exception as e:
914
+ return {'exitosa': False}
915
+
916
+ def obtener_estadisticas_bifurcacion(self):
917
+ """Retorna estadísticas completas del sistema de bifurcaciones"""
918
+ stats = self.estadisticas_bifurcacion.copy()
919
+ stats.update({
920
+ 'bifurcaciones_activas': len(self.bifurcaciones_activas),
921
+ 'tasa_convergencia': (
922
+ stats['convergencias_exitosas'] / max(stats['total_bifurcaciones'], 1)
923
+ ),
924
+ 'tasa_colapso': (
925
+ stats['trayectorias_colapsadas'] / max(stats['total_bifurcaciones'] * 2.5, 1)
926
+ )
927
+ })
928
+ return stats
929
+
930
+ def integrar_bifurcaciones_canonicas_en_simulacion(G, paso, coordinador_temporal, bifurcation_manager):
931
+ """
932
+ Función principal de integración de bifurcaciones canónicas en OntoSim
933
+ Reemplaza la lógica simple de aplicar T'HOL automáticamente
934
+ """
935
+ resultados = {
936
+ 'nuevas_bifurcaciones': 0,
937
+ 'trayectorias_procesadas': 0,
938
+ 'convergencias_completadas': 0,
939
+ 'nodos_modificados': []
940
+ }
941
+
942
+ try:
943
+ # Procesar bifurcaciones existentes primero
944
+ if hasattr(bifurcation_manager, 'bifurcaciones_activas'):
945
+ resultado_procesamiento = bifurcation_manager.procesar_bifurcaciones_activas(G, paso)
946
+ resultados['trayectorias_procesadas'] = resultado_procesamiento['trayectorias_procesadas']
947
+ resultados['convergencias_completadas'] = resultado_procesamiento['convergencias_detectadas']
948
+ resultados['nodos_modificados'].extend(resultado_procesamiento['nuevos_nodos_generados'])
949
+
950
+ # Detectar nuevas bifurcaciones
951
+ for nodo_id, nodo_data in G.nodes(data=True):
952
+ if nodo_data.get("estado") != "activo":
953
+ continue
954
+
955
+ # Verificar si el nodo ya está en bifurcación
956
+ if nodo_id in bifurcation_manager.bifurcaciones_activas:
957
+ continue
958
+
959
+ # Detectar condición de bifurcación canónica
960
+ es_bifurcacion, tipo_bifurcacion = bifurcation_manager.detectar_bifurcacion_canonica(
961
+ nodo_data, nodo_id
962
+ )
963
+
964
+ if es_bifurcacion and tipo_bifurcacion != "error_deteccion":
965
+ # Generar espacio de bifurcación múltiple
966
+ espacio_bifurcacion = bifurcation_manager.generar_espacio_bifurcacion(
967
+ nodo_id, nodo_data, tipo_bifurcacion, paso
968
+ )
969
+
970
+ if espacio_bifurcacion:
971
+ # Registrar bifurcación activa
972
+ bifurcation_manager.bifurcaciones_activas[nodo_id] = espacio_bifurcacion
973
+ resultados['nuevas_bifurcaciones'] += 1
974
+ resultados['nodos_modificados'].append(nodo_id)
975
+
976
+ return resultados
977
+
978
+ except Exception as e:
979
+ return resultados
980
+
981
+ def reemplazar_deteccion_bifurcacion_simple(G, paso, umbrales, bifurcation_manager):
982
+ """
983
+ Función que reemplaza la detección simple de bifurcaciones en OntoSim
984
+ Debe llamarse en lugar del bloque original de detección de bifurcaciones
985
+ """
986
+ nodos_bifurcados = []
987
+
988
+ try:
989
+ # Parámetros dinámicos para detección
990
+ umbral_aceleracion = umbrales.get('bifurcacion_umbral', 0.15)
991
+
992
+ for nodo_id, nodo_data in G.nodes(data=True):
993
+ if nodo_data.get("estado") != "activo":
994
+ continue
995
+
996
+ # Skip nodos ya en bifurcación
997
+ if nodo_id in bifurcation_manager.bifurcaciones_activas:
998
+ continue
999
+
1000
+ # Usar detección canónica en lugar de simple
1001
+ es_bifurcacion, tipo_bifurcacion = bifurcation_manager.detectar_bifurcacion_canonica(
1002
+ nodo_data, nodo_id, umbral_aceleracion
1003
+ )
1004
+
1005
+ if es_bifurcacion:
1006
+ nodos_bifurcados.append((nodo_id, tipo_bifurcacion))
1007
+
1008
+ return nodos_bifurcados
1009
+
1010
+ except Exception as e:
1011
+ return []
1012
+
1013
+ def mostrar_trayectorias_activas(bifurcation_manager):
1014
+ """Muestra detalles de las trayectorias activas"""
1015
+ if not bifurcation_manager.bifurcaciones_activas:
1016
+ return "No hay bifurcaciones activas"
1017
+
1018
+ detalles = []
1019
+ for nodo_id, espacio in bifurcation_manager.bifurcaciones_activas.items():
1020
+ trayectorias_activas = [t for t in espacio.trayectorias if t.activa]
1021
+ viabilidades = [f"{t.viabilidad:.2f}" for t in trayectorias_activas]
1022
+
1023
+ detalles.append(
1024
+ f" {nodo_id}: {espacio.tipo_bifurcacion} "
1025
+ f"({len(trayectorias_activas)} activas, viabilidades: {viabilidades})"
1026
+ )
1027
+
1028
+ return "Trayectorias activas:\n" + "\n".join(detalles)
1029
+
1030
+ def limpiar_bifurcaciones_obsoletas(bifurcation_manager, paso_actual, limite_pasos=50):
1031
+ """Limpia bifurcaciones que han excedido el tiempo máximo de exploración"""
1032
+ bifurcaciones_obsoletas = []
1033
+
1034
+ for nodo_id, espacio in list(bifurcation_manager.bifurcaciones_activas.items()):
1035
+ pasos_transcurridos = paso_actual - espacio.paso_inicio
1036
+
1037
+ if pasos_transcurridos > limite_pasos:
1038
+ bifurcaciones_obsoletas.append(nodo_id)
1039
+
1040
+ for nodo_id in bifurcaciones_obsoletas:
1041
+ del bifurcation_manager.bifurcaciones_activas[nodo_id]
1042
+
1043
+ return len(bifurcaciones_obsoletas)
1044
+
1045
+ # =========================================================================================
1046
+ # SISTEMA DE UMBRALES DINÁMICOS
1047
+ # =========================================================================================
1048
+
1049
+ def calcular_umbrales_dinamicos(C_t, densidad_nodal, fase_simulacion="emergencia"):
1050
+
1051
+ # Factor de sensibilidad basado en desviación de C(t) del punto de equilibrio
1052
+ equilibrio_base = 1.0
1053
+ desviacion_C_t = abs(C_t - equilibrio_base)
1054
+
1055
+ # Sensibilidad adaptativa: más restrictivo cuando C(t) está lejos del equilibrio
1056
+ sensibilidad = max(0.4, min(2.0, 1.0 + 0.8 * desviacion_C_t))
1057
+
1058
+ # Factor de densidad: redes densas requieren umbrales más estrictos
1059
+ factor_densidad = max(0.7, min(1.5, 1.0 - 0.1 * (densidad_nodal - 3.0)))
1060
+
1061
+ # Ajuste por fase de simulación
1062
+ multiplicadores_fase = {
1063
+ "emergencia": 1.2, # más tolerante para permitir emergencia inicial
1064
+ "estabilizacion": 0.8, # más restrictivo para consolidar estructuras
1065
+ "bifurcacion": 1.5 # muy tolerante para permitir reorganización
1066
+ }
1067
+
1068
+ factor_fase = multiplicadores_fase.get(fase_simulacion, 1.0)
1069
+
1070
+ # Cálculo de umbrales fundamentales
1071
+ sensibilidad_final = sensibilidad * factor_densidad * factor_fase
1072
+
1073
+ return {
1074
+ # Umbrales de conexión (para crear/eliminar aristas)
1075
+ 'θ_conexion': 0.12 * sensibilidad_final,
1076
+ 'EPI_conexion': 1.8 * sensibilidad_final,
1077
+ 'νf_conexion': 0.2 * sensibilidad_final,
1078
+ 'Si_conexion': 0.25 * sensibilidad_final,
1079
+
1080
+ # Umbrales críticos nodales
1081
+ 'θ_mutacion': 0.25 * sensibilidad_final, # para activar Z'HIR
1082
+ 'θ_colapso': 0.45 * sensibilidad_final, # para activar SH'A
1083
+ 'θ_autoorganizacion': 0.35 * sensibilidad_final, # para activar T'HOL
1084
+
1085
+ # Límites de estabilidad estructural
1086
+ 'EPI_max_dinamico': max(2.5, C_t * 2.8), # límite superior adaptativo
1087
+ 'EPI_min_coherencia': max(0.4, C_t * 0.3), # límite inferior para coherencia
1088
+
1089
+ # Umbrales de bifurcación estructural
1090
+ 'bifurcacion_aceleracion': 0.15 * sensibilidad_final,
1091
+ 'bifurcacion_gradiente': 0.8 * sensibilidad_final,
1092
+
1093
+ # Metadatos de cálculo
1094
+ 'C_t_usado': C_t,
1095
+ 'sensibilidad_calculada': sensibilidad_final,
1096
+ 'factor_densidad': factor_densidad,
1097
+ 'fase': fase_simulacion
1098
+ }
1099
+
1100
+ def evaluar_condiciones_emergencia_dinamica(nodo, umbrales, campo_coherencia):
1101
+ """
1102
+ Evalúa si un nodo cumple condiciones de emergencia con umbrales dinámicos.
1103
+
1104
+ Args:
1105
+ nodo: Diccionario con parámetros del nodo
1106
+ umbrales: Umbrales calculados dinámicamente
1107
+ campo_coherencia: C(t) actual de la red
1108
+
1109
+ Returns:
1110
+ tuple: (puede_emerger, razon_rechazo)
1111
+ """
1112
+ # Verificación de coherencia estructural mínima
1113
+ if nodo.get("EPI", 0) < umbrales['EPI_min_coherencia']:
1114
+ return False, f"EPI insuficiente: {nodo.get('EPI', 0):.3f} < {umbrales['EPI_min_coherencia']:.3f}"
1115
+
1116
+ # Verificación de frecuencia resonante
1117
+ if nodo.get("νf", 0) < 0.3: # mínimo absoluto para vibración
1118
+ return False, f"Frecuencia demasiado baja: {nodo.get('νf', 0):.3f}"
1119
+
1120
+ # Verificación de compatibilidad con campo de coherencia
1121
+ fase_nodo = nodo.get("fase", 0.5)
1122
+ if abs(fase_nodo - 0.0) > 0.7 and campo_coherencia > 1.2:
1123
+ return False, f"Disonancia con campo: fase={fase_nodo:.3f}, C(t)={campo_coherencia:.3f}"
1124
+
1125
+ # Verificación de gradiente nodal dentro de límites
1126
+ ΔNFR = abs(nodo.get("ΔNFR", 0))
1127
+ if ΔNFR > umbrales['bifurcacion_gradiente']:
1128
+ return False, f"Gradiente excesivo: {ΔNFR:.3f} > {umbrales['bifurcacion_gradiente']:.3f}"
1129
+
1130
+ return True, "Condiciones de emergencia cumplidas"
1131
+
1132
+ def detectar_fase_simulacion(G, paso_actual, historial_C_t, ventana=50):
1133
+ """
1134
+ Detecta la fase actual de la simulación para ajustar umbrales.
1135
+
1136
+ Args:
1137
+ G: Grafo actual
1138
+ paso_actual: Paso de simulación actual
1139
+ historial_C_t: Historia de coherencia total [(paso, C_t), ...]
1140
+ ventana: Ventana de pasos para análisis de tendencias
1141
+
1142
+ Returns:
1143
+ str: "emergencia", "estabilizacion", "bifurcacion"
1144
+ """
1145
+ if len(historial_C_t) < ventana:
1146
+ return "emergencia"
1147
+
1148
+ # Analizar últimos valores de C(t)
1149
+ valores_recientes = [c_t for _, c_t in historial_C_t[-ventana:]]
1150
+
1151
+ # Calcular variabilidad
1152
+ variabilidad = np.std(valores_recientes)
1153
+ tendencia = np.mean(valores_recientes[-10:]) - np.mean(valores_recientes[:10])
1154
+
1155
+ # Contar nodos activos
1156
+ nodos_activos = sum(1 for n in G.nodes if G.nodes[n].get("estado") == "activo")
1157
+ fraccion_activa = nodos_activos / len(G.nodes) if G.nodes else 0
1158
+
1159
+ # Lógica de clasificación
1160
+ if variabilidad > 0.3 and abs(tendencia) > 0.2:
1161
+ return "bifurcacion" # alta variabilidad y cambio direccional
1162
+ elif variabilidad < 0.05 and fraccion_activa > 0.6:
1163
+ return "estabilizacion" # baja variabilidad, muchos nodos activos
1164
+ else:
1165
+ return "emergencia" # estado por defecto
1166
+
1167
+ def aplicar_umbrales_dinamicos_conexiones(G, umbrales):
1168
+ """
1169
+ Aplica umbrales dinámicos para gestión de conexiones de red.
1170
+
1171
+ Args:
1172
+ G: Grafo de red
1173
+ umbrales: Umbrales calculados dinámicamente
1174
+
1175
+ Returns:
1176
+ dict: Estadísticas de conexiones creadas/eliminadas
1177
+ """
1178
+ conexiones_creadas = 0
1179
+ conexiones_eliminadas = 0
1180
+ nodos_lista = list(G.nodes)
1181
+
1182
+ for i in range(len(nodos_lista)):
1183
+ for j in range(i + 1, len(nodos_lista)):
1184
+ n1, n2 = nodos_lista[i], nodos_lista[j]
1185
+ nodo1, nodo2 = G.nodes[n1], G.nodes[n2]
1186
+
1187
+ # Evaluar condiciones de resonancia con umbrales dinámicos
1188
+ condiciones_resonancia = [
1189
+ abs(nodo1.get("θ", 0) - nodo2.get("θ", 0)) < umbrales['θ_conexion'],
1190
+ abs(nodo1.get("EPI", 0) - nodo2.get("EPI", 0)) < umbrales['EPI_conexion'],
1191
+ abs(nodo1.get("νf", 1) - nodo2.get("νf", 1)) < umbrales['νf_conexion'],
1192
+ abs(nodo1.get("Si", 0) - nodo2.get("Si", 0)) < umbrales['Si_conexion']
1193
+ ]
1194
+
1195
+ # Criterio: al menos 3 de 4 condiciones cumplidas
1196
+ resonancia_suficiente = sum(condiciones_resonancia) >= 3
1197
+
1198
+ # Verificar saturación de conexiones
1199
+ vecinos_n1 = len(list(G.neighbors(n1)))
1200
+ vecinos_n2 = len(list(G.neighbors(n2)))
1201
+ max_conexiones = int(8 * umbrales['sensibilidad_calculada'])
1202
+
1203
+ saturacion = vecinos_n1 >= max_conexiones and vecinos_n2 >= max_conexiones
1204
+
1205
+ # Lógica de conexión/desconexión
1206
+ existe_conexion = G.has_edge(n1, n2)
1207
+
1208
+ if resonancia_suficiente and not saturacion and not existe_conexion:
1209
+ G.add_edge(n1, n2)
1210
+ conexiones_creadas += 1
1211
+ elif not resonancia_suficiente and existe_conexion:
1212
+ G.remove_edge(n1, n2)
1213
+ conexiones_eliminadas += 1
1214
+
1215
+ return {
1216
+ 'conexiones_creadas': conexiones_creadas,
1217
+ 'conexiones_eliminadas': conexiones_eliminadas,
1218
+ 'umbrales_usados': umbrales
1219
+ }
1220
+
1221
+ def evaluar_activacion_glifica_dinamica(nodo, umbrales, vecinos_data=None):
1222
+ """
1223
+ Evalúa qué glifo debería activarse basado en umbrales dinámicos.
1224
+
1225
+ Args:
1226
+ nodo: Datos del nodo
1227
+ umbrales: Umbrales dinámicos calculados
1228
+ vecinos_data: Lista de datos de nodos vecinos
1229
+
1230
+ Returns:
1231
+ str or None: Glifo a activar o None si no hay activación
1232
+ """
1233
+ # Z'HIR - Mutación por umbral de cambio estructural
1234
+ θ_actual = nodo.get("θ", 0)
1235
+ θ_prev = nodo.get("θ_prev", θ_actual)
1236
+
1237
+ if abs(θ_actual - θ_prev) > umbrales['θ_mutacion']:
1238
+ return "ZHIR"
1239
+
1240
+ # SH'A - Colapso por pérdida de coherencia
1241
+ if (nodo.get("EPI", 0) < umbrales['EPI_min_coherencia'] and
1242
+ abs(nodo.get("ΔNFR", 0)) > umbrales['θ_colapso']):
1243
+ return "SHA"
1244
+
1245
+ # T'HOL - Autoorganización por aceleración estructural
1246
+ aceleracion = abs(nodo.get("d2EPI_dt2", 0))
1247
+ if aceleracion > umbrales.get('bifurcacion_aceleracion', 0.15):
1248
+ return "THOL"
1249
+
1250
+ # R'A - Resonancia con vecinos (requiere datos de vecinos)
1251
+ if vecinos_data and len(vecinos_data) > 0:
1252
+ θ_vecinos = [v.get("θ", 0) for v in vecinos_data]
1253
+ resonancia_promedio = sum(abs(θ_actual - θ_v) for θ_v in θ_vecinos) / len(θ_vecinos)
1254
+
1255
+ if resonancia_promedio < umbrales['θ_conexion'] * 0.5: # muy sincronizado
1256
+ return "RA"
1257
+
1258
+ if (nodo.get("EPI", 0) < umbrales.get('EPI_min_coherencia', 0.4) and
1259
+ abs(nodo.get("ΔNFR", 0)) > umbrales.get('θ_colapso', 0.45)):
1260
+ return "SHA"
1261
+
1262
+ return None
1263
+
1264
+ def gestionar_conexiones_canonico(G, paso, historia_Ct):
1265
+ """
1266
+ Reemplaza la gestión manual de conexiones por sistema canónico TNFR.
1267
+ Esta función debe reemplazar el bloque de gestión de aristas en simular_emergencia().
1268
+ """
1269
+ # Calcular coherencia total actual
1270
+ if len(G.nodes) == 0:
1271
+ C_t = 0
1272
+ else:
1273
+ C_t = sum(G.nodes[n]["EPI"] for n in G.nodes) / len(G)
1274
+
1275
+ # Calcular densidad nodal promedio
1276
+ densidad_promedio = sum(len(list(G.neighbors(n))) for n in G.nodes) / len(G.nodes) if G.nodes else 0
1277
+
1278
+ # Detectar fase actual de simulación
1279
+ fase_actual = detectar_fase_simulacion(G, paso, historia_Ct)
1280
+
1281
+ # Calcular umbrales dinámicos
1282
+ umbrales = calcular_umbrales_dinamicos(C_t, densidad_promedio, fase_actual)
1283
+
1284
+ # Aplicar gestión de conexiones canónica
1285
+ estadisticas = aplicar_umbrales_dinamicos_conexiones(G, umbrales)
1286
+
1287
+ return umbrales, estadisticas
1288
+
1289
+ __all__ = [
1290
+ 'TemporalCoordinatorTNFR',
1291
+ 'EspacioBifurcacion',
1292
+ 'BifurcationManagerTNFR',
1293
+ 'calcular_umbrales_dinamicos',
1294
+ 'aplicar_umbrales_dinamicos_conexiones',
1295
+ 'integrar_tiempo_topologico_en_simulacion',
1296
+ 'integrar_bifurcaciones_canonicas_en_simulacion',
1297
+ 'reemplazar_deteccion_bifurcacion_simple',
1298
+ 'mostrar_trayectorias_activas',
1299
+ 'limpiar_bifurcaciones_obsoletas',
1300
+ 'detectar_fase_simulacion',
1301
+ 'inicializar_coordinador_temporal_canonico',
1302
+ 'evaluar_condiciones_emergencia_dinamica',
1303
+ 'evaluar_activacion_glifica_dinamica',
1304
+ 'gestionar_conexiones_canonico'
1305
+ ]