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.
core/ontosim.py ADDED
@@ -0,0 +1,757 @@
1
+ from typing import List
2
+ import networkx as nx
3
+
4
+ def inicializar_nfr_emergente(forma_base, campo_coherencia=None):
5
+ """
6
+ Inicializa NFR siguiendo condiciones de emergencia nodal TNFR.
7
+
8
+ Reemplaza las heurísticas ad-hoc por evaluación estructural canónica.
9
+ """
10
+ # Verificar condiciones de emergencia
11
+ if not cumple_condiciones_emergencia(forma_base, campo_coherencia):
12
+ return None
13
+
14
+ # Calcular parámetros estructurales
15
+ EPI = evaluar_coherencia_estructural(forma_base)
16
+ νf = calcular_frecuencia_resonante(forma_base)
17
+ Wi_t = generar_matriz_coherencia(forma_base)
18
+ fase = sincronizar_con_campo(campo_coherencia, νf)
19
+
20
+ # Calcular parámetros derivados
21
+ # ΔNFR: gradiente nodal basado en estabilidad interna de Wi_t
22
+ estabilidad_interna = np.trace(Wi_t) / len(Wi_t)
23
+ ΔNFR = round((1.0 - estabilidad_interna) * 0.5 - 0.1, 3) # rango típico [-0.1, 0.4]
24
+
25
+ # Si: índice de sentido basado en coherencia estructural y frecuencia
26
+ Si = round((EPI / 2.5) * (νf / 3.0) * (1.0 - fase), 3) # decrece con disonancia
27
+
28
+ # θ: umbral estructural basado en EPI y estabilidad
29
+ θ = round(min(1.0, EPI * estabilidad_interna * 0.4), 3)
30
+
31
+ # Crear NFR canónico
32
+ nfr = {
33
+ "estado": "activo",
34
+ "glifo": "ninguno",
35
+ "categoria": "ninguna",
36
+ "EPI": EPI,
37
+ "EPI_prev": EPI,
38
+ "EPI_prev2": EPI,
39
+ "EPI_prev3": EPI,
40
+ "νf": νf,
41
+ "ΔNFR": ΔNFR,
42
+ "Si": Si,
43
+ "θ": θ,
44
+ "Wi_t": Wi_t,
45
+ "fase": fase,
46
+ "simetria_interna": round(estabilidad_interna, 3)
47
+ }
48
+
49
+ return nfr
50
+
51
+ def crear_red_desde_datos(datos: List[dict]) -> nx.Graph:
52
+ """Crea red TNFR desde datos estructurados - NUEVA FUNCIÓN"""
53
+ G = nx.Graph()
54
+ campo_coherencia = {}
55
+
56
+ for nodo_data in datos:
57
+ nodo_id = nodo_data.get('id', f"nodo_{len(G)}")
58
+
59
+ # Usar inicialización canónica existente
60
+ if 'forma_base' in nodo_data:
61
+ nfr = inicializar_nfr_emergente(nodo_data['forma_base'], campo_coherencia)
62
+ if nfr:
63
+ G.add_node(nodo_id, **nfr)
64
+ campo_coherencia[nodo_id] = nfr
65
+ else:
66
+ # Datos ya procesados
67
+ G.add_node(nodo_id, **nodo_data)
68
+
69
+ # Usar conectividad canónica existente
70
+ umbrales, _ = gestionar_conexiones_canonico(G, 0, [])
71
+ return G
72
+
73
+ def _deben_conectarse_canonico(n1: dict, n2: dict) -> bool:
74
+ """Mejora la lógica existente con umbral áureo"""
75
+ phi = (1 + math.sqrt(5)) / 2 # φ ≈ 1.618
76
+
77
+ diferencia_vf = abs(n1.get('νf', 1) - n2.get('νf', 1))
78
+ diferencia_fase = abs(n1.get('fase', 0) - n2.get('fase', 0)) % (2 * math.pi)
79
+
80
+ return (diferencia_vf < 0.01 * phi and
81
+ diferencia_fase < math.pi / 2)
82
+
83
+ def simular_emergencia(G, pasos=250):
84
+
85
+ umbrales = {
86
+ 'θ_min': 0.18,
87
+ 'EPI_max_dinamico': 3.0,
88
+ 'θ_mutacion': 0.25,
89
+ 'θ_colapso': 0.45,
90
+ 'bifurcacion_aceleracion': 0.15,
91
+ 'EPI_min_coherencia': 0.4, # ← Añade este valor por defecto
92
+ 'θ_conexion': 0.12,
93
+ 'EPI_conexion': 1.8,
94
+ 'νf_conexion': 0.2,
95
+ 'Si_conexion': 0.25,
96
+ 'θ_autoorganizacion': 0.35,
97
+ 'bifurcacion_gradiente': 0.8,
98
+ 'sensibilidad_calculada': 1.0,
99
+ 'factor_densidad': 1.0,
100
+ 'fase': 'emergencia'
101
+ }
102
+
103
+
104
+ global historia_Ct
105
+ if 'historia_Ct' not in globals():
106
+ historia_Ct = []
107
+ historia_epi = []
108
+ historia_glifos = ["paso,nodo,glifo"]
109
+ historial_glifos_por_nodo = {}
110
+ G_historia = []
111
+ registro_conexiones = []
112
+ coordinador_temporal = inicializar_coordinador_temporal_canonico()
113
+ bifurcation_manager = BifurcationManagerTNFR()
114
+
115
+ historial_temporal = []
116
+
117
+ glifo_categoria = {
118
+ "AL": "activador", "EN": "receptor", "IL": "estabilizador",
119
+ "OZ": "disonante", "UM": "acoplador", "RA": "resonador",
120
+ "SHA": "latente", "VAL": "expansivo", "NUL": "contractivo",
121
+ "THOL": "autoorganizador", "ZHIR": "mutante", "NAV": "transicional",
122
+ "REMESH": "recursivo"
123
+ }
124
+
125
+ total_pasos = 250
126
+
127
+ # Activación mínima inicial si todos están inactivos o silenciosos
128
+ if all(G.nodes[n]["estado"] in ["latente", "silencio"] for n in G.nodes):
129
+ for n in G.nodes:
130
+ if G.nodes[n]["EPI"] > 0.8 and G.nodes[n]["νf"] > 0.5:
131
+ G.nodes[n]["estado"] = "activo"
132
+ G.nodes[n]["glifo"] = "AL"
133
+ break # activa solo uno, para iniciar pulso
134
+
135
+ for paso in range(total_pasos):
136
+ nodos_activos = [n for n in G.nodes if G.nodes[n]["estado"] == "activo"]
137
+ paso_data = []
138
+
139
+ acoplar_nodos(G)
140
+
141
+ # Cálculo de umbrales adaptativos para emergencia nodal
142
+ vf_values = [G.nodes[n]["νf"] for n in G.nodes if G.nodes[n]["estado"] == "activo"]
143
+ dNFR_values = [G.nodes[n]["ΔNFR"] for n in G.nodes if G.nodes[n]["estado"] == "activo"]
144
+
145
+ media_vf = np.mean(vf_values) if vf_values else 0
146
+ std_dNFR = np.std(dNFR_values) if dNFR_values else 0
147
+
148
+ for n in list(G.nodes):
149
+
150
+ nodo = G.nodes[n]
151
+ def valor_valido(x):
152
+ return x is not None and not isinstance(x, str) and not isnan(x)
153
+
154
+ for n in list(G.nodes):
155
+ nodo = G.nodes[n]
156
+
157
+ for clave in ["EPI_prev", "EPI_prev2", "EPI_prev3"]:
158
+ if not valor_valido(nodo.get(clave)):
159
+ nodo[clave] = nodo.get("EPI", 1.0)
160
+
161
+ if nodo["estado"] == "activo":
162
+ # Dinámica basal influida por νf y sentido
163
+ factor_ruido = random.uniform(0.98, 1.02) + 0.02 * random.uniform(-1, 1) * (1 - nodo["Si"])
164
+ modulador = factor_ruido * (1 + 0.02 * min(nodo.get("νf", 1.0), 5)) # cap νf por seguridad
165
+
166
+ nodo["EPI"] *= modulador
167
+
168
+ # Evitar NaN o valores extremos
169
+ if not np.isfinite(nodo["EPI"]) or nodo["EPI"] > 10:
170
+ nodo["EPI"] = 1.0 + random.uniform(-0.05, 0.05) # reset suave)
171
+ if nodo["EPI"] > 1e4:
172
+ nodo["EPI"] = 1e4
173
+ nodo["ΔNFR"] += random.uniform(-0.08, 0.08) * (1.1 - nodo["Si"])
174
+ nodo["ΔNFR"] = max(min(nodo["ΔNFR"], 1.5), -1.5)
175
+
176
+ # Condición de apagado nodal si pierde coherencia estructural
177
+ if (
178
+ nodo["EPI"] < 0.85
179
+ and abs(nodo["ΔNFR"]) > 0.4
180
+ and nodo["Si"] < 0.3
181
+ ):
182
+ nodo["estado"] = "inactivo"
183
+
184
+ evaluar_si_nodal(nodo, paso)
185
+
186
+ if (
187
+ nodo["estado"] == "silencio"
188
+ and abs(nodo["ΔNFR"] - nodo["νf"]) < 0.05
189
+ and nodo.get("Si", 0) > 0.25
190
+ and nodo.get("d2EPI_dt2", 0) > 0.03
191
+ and not reciente_glifo(n, "NAV", historial_glifos_por_nodo, pasos=6)
192
+ ):
193
+ aplicar_glifo(G, nodo, n, "NAV", historial_glifos_por_nodo, paso)
194
+ historia_glifos.append(f"{paso},{n},NAV")
195
+ nodo["estado"] = "activo"
196
+
197
+ if (
198
+ nodo["EPI"] < 0.6
199
+ and abs(nodo["ΔNFR"]) > 0.75
200
+ and nodo["Si"] < 0.25
201
+ and not reciente_glifo(n, "SHA", historial_glifos_por_nodo, pasos=6)
202
+ ):
203
+ aplicar_glifo(G, nodo, n, "SHA", historial_glifos_por_nodo, paso)
204
+ historia_glifos.append(f"{paso},{n},SHA")
205
+ continue
206
+
207
+ if (
208
+ nodo["estado"] == "latente"
209
+ and abs(nodo["ΔNFR"]) < 0.05
210
+ and nodo["Si"] > 0.3
211
+ and not reciente_glifo(n, "EN", historial_glifos_por_nodo, pasos=10)
212
+ ):
213
+ aplicar_glifo(G, nodo, n, "EN", historial_glifos_por_nodo, paso)
214
+ historia_glifos.append(f"{paso},{n},EN")
215
+
216
+ if (
217
+ nodo["glifo"] == "IL"
218
+ and nodo["Si"] > 0.55
219
+ and nodo["νf"] > 1.25
220
+ and abs(nodo["ΔNFR"]) < 0.15 # Baja necesidad de reorganización
221
+ and not reciente_glifo(n, "RA", historial_glifos_por_nodo, pasos=8)
222
+ ):
223
+ aplicar_glifo(G, nodo, n, "RA", historial_glifos_por_nodo, paso)
224
+ historia_glifos.append(f"{paso},{n},RA")
225
+
226
+ vecinos = list(G.neighbors(n))
227
+ if (
228
+ nodo["estado"] == "activo"
229
+ and vecinos
230
+ and sum(1 for v in vecinos if abs(G.nodes[v]["θ"] - nodo["θ"]) < 0.08) >= 2
231
+ and not reciente_glifo(n, "UM", historial_glifos_por_nodo, pasos=8)
232
+ ):
233
+ aplicar_glifo(G, nodo, n, "UM", historial_glifos_por_nodo, paso)
234
+ historia_glifos.append(f"{paso},{n},UM")
235
+
236
+ if (
237
+ abs(nodo.get("d2EPI_dt2", 0)) > 0.25
238
+ and nodo["Si"] > 0.6
239
+ and not reciente_glifo(n, "ZHIR", historial_glifos_por_nodo, pasos=10)
240
+ ):
241
+ aplicar_glifo(G, nodo, n, "ZHIR", historial_glifos_por_nodo, paso)
242
+ historia_glifos.append(f"{paso},{n},ZHIR")
243
+
244
+ if emergencia_nodal(nodo, media_vf, std_dNFR):
245
+ glifo = glifo_por_estructura(nodo, G)
246
+ if glifo:
247
+ aplicar_glifo(G, nodo, n, glifo, historial_glifos_por_nodo, paso)
248
+ historia_glifos.append(f"{paso},{n},{glifo}")
249
+ nodo["categoria"] = glifo_categoria.get(glifo, "ninguna")
250
+
251
+ # Evaluación glífica con umbrales dinámicos (mejora canónica)
252
+ vecinos_data = [G.nodes[v] for v in G.neighbors(n)]
253
+ glifo_dinamico = evaluar_activacion_glifica_dinamica(nodo, umbrales, vecinos_data)
254
+
255
+ if glifo_dinamico and not reciente_glifo(n, glifo_dinamico, historial_glifos_por_nodo, pasos=8):
256
+ aplicar_glifo(G, nodo, n, glifo_dinamico, historial_glifos_por_nodo, paso)
257
+ historia_glifos.append(f"{paso},{n},{glifo_dinamico}")
258
+
259
+ glifo_siguiente = transicion_glifica_canonica(nodo)
260
+ if glifo_siguiente:
261
+ aplicar_glifo(G, nodo, n, glifo_siguiente, historial_glifos_por_nodo, paso)
262
+ historia_glifos.append(f"{paso},{n},{glifo_siguiente}")
263
+ nodo["glifo"] = glifo_siguiente
264
+ nodo["categoria"] = glifo_categoria.get(glifo_siguiente, "ninguna")
265
+
266
+ # Activación estructural de VAL (expansión controlada)
267
+ if (
268
+ nodo["Si"] > 0.8
269
+ and nodo["EPI"] > 1.2
270
+ and abs(nodo["ΔNFR"]) < 0.2
271
+ and nodo.get("dEPI_dt", 0) > 0.15
272
+ and not reciente_glifo(n, "VAL", historial_glifos_por_nodo, pasos=10)
273
+ ):
274
+ if "expansiones_val" not in nodo:
275
+ nodo["expansiones_val"] = 0
276
+
277
+ if nodo["expansiones_val"] < 3:
278
+ activar_val_si_estabilidad(n, G, paso, historial_glifos_por_nodo)
279
+ nodo["expansiones_val"] += 1
280
+ else:
281
+ aplicar_glifo(G, nodo, n, "THOL", historial_glifos_por_nodo, paso)
282
+ historia_glifos.append(f"{paso},{n},THOL")
283
+
284
+ if nodo.get("glifo") == "VAL":
285
+ condiciones_contraccion = (
286
+ abs(nodo.get("d2EPI_dt2", 0)) < 0.05 and
287
+ abs(nodo.get("ΔNFR", 0)) < 0.1 and
288
+ nodo.get("νf", 1.0) < 1.0 and
289
+ abs(nodo.get("EPI", 0) - nodo.get("EPI_prev", 0)) < 0.01
290
+ )
291
+
292
+ if condiciones_contraccion:
293
+ aplicar_glifo(G, nodo, n, "NUL", historial_glifos_por_nodo, paso)
294
+ historia_glifos.append(f"{paso},{n},NUL")
295
+ nodo["glifo"] = "NUL"
296
+ nodo["categoria"] = glifo_categoria.get("NUL", "ninguna")
297
+
298
+ paso_data.append({
299
+ "nodo": n,
300
+ "paso": paso,
301
+ "EPI": round(nodo["EPI"], 2)
302
+ })
303
+ nodo["EPI_prev3"] = nodo.get("EPI_prev2", nodo["EPI_prev"])
304
+ nodo["EPI_prev2"] = nodo.get("EPI_prev", nodo["EPI"])
305
+ nodo["EPI_prev"] = nodo["EPI"] if np.isfinite(nodo["EPI"]) else 1.0
306
+
307
+ # Cálculo de ∂EPI/∂t = νf · ΔNFR
308
+ dEPI_dt = nodo["νf"] * nodo["ΔNFR"]
309
+ nodo["dEPI_dt"] = dEPI_dt
310
+ if "historial_dEPI_dt" not in nodo:
311
+ nodo["historial_dEPI_dt"] = []
312
+ nodo["historial_dEPI_dt"].append((paso, dEPI_dt))
313
+
314
+ # Registrar evolución de νf y ΔNFR
315
+ if "historial_vf" not in nodo:
316
+ nodo["historial_vf"] = []
317
+ if "historial_dNFR" not in nodo:
318
+ nodo["historial_dNFR"] = []
319
+
320
+ nodo["historial_vf"].append((paso, nodo["νf"]))
321
+ nodo["historial_dNFR"].append((paso, nodo["ΔNFR"]))
322
+
323
+ # Calcular aceleración estructural ∂²EPI/∂t² solo si los valores son válidos
324
+ if all(np.isfinite([nodo.get("EPI", 0), nodo.get("EPI_prev", 0), nodo.get("EPI_prev2", 0)])):
325
+ aceleracion = nodo["EPI"] - 2 * nodo["EPI_prev"] + nodo["EPI_prev2"]
326
+ else:
327
+ aceleracion = 0.0 # O un valor neutro que no active mutaciones erróneas
328
+
329
+ nodo["d2EPI_dt2"] = aceleracion
330
+
331
+ # Umbral de bifurcación: si se supera, aplicar THOL
332
+ resultado_bifurcaciones = integrar_bifurcaciones_canonicas_en_simulacion(
333
+ G, paso, coordinador_temporal, bifurcation_manager
334
+ )
335
+
336
+ # Evaluar contracción si hay disonancia o colapso de sentido (NU´L)
337
+ if nodo.get("estado") == "activo":
338
+ aplicar_contraccion_nul(n, G, paso, historial_glifos_por_nodo)
339
+
340
+ # === CONTROL DE EXPANSIÓN INFINITA ===
341
+ if "expansiones_val" not in nodo:
342
+ nodo["expansiones_val"] = 0
343
+
344
+ if nodo["expansiones_val"] >= 3:
345
+ continue # evita expansión si ya lo hizo demasiadas veces
346
+
347
+ # Aquí sí puede expandirse:
348
+ activar_val_si_estabilidad(n, G, paso, historial_glifos_por_nodo)
349
+ nodo["expansiones_val"] += 1
350
+
351
+ if (
352
+ nodo.get("estado") == "activo"
353
+ and nodo.get("Si", 0) > 0.8
354
+ and nodo.get("EPI", 0) > 1.1
355
+ and abs(nodo.get("ΔNFR", 0)) < 0.25
356
+ and nodo.get("dEPI_dt", 0) > 0.15
357
+ and not reciente_glifo(n, "VAL", historial_glifos_por_nodo, pasos=8)
358
+ ):
359
+ activar_val_si_estabilidad(n, G, paso, historial_glifos_por_nodo)
360
+
361
+ # Guardar aceleración para graficar más tarde
362
+ if "historial_aceleracion" not in nodo:
363
+ nodo["historial_aceleracion"] = []
364
+ nodo["historial_aceleracion"].append((paso, aceleracion))
365
+
366
+ # Gestión temporal topológica TNFR
367
+ resultado_temporal = integrar_tiempo_topologico_en_simulacion(G, paso, coordinador_temporal)
368
+ historial_temporal.append(resultado_temporal['estadisticas'])
369
+
370
+ # Gestión de conexiones con información temporal
371
+ umbrales, estadisticas_conexiones = gestionar_conexiones_canonico(G, paso, historia_Ct)
372
+
373
+ # Calcular coherencia total C(t) al final del paso
374
+ C_t = sum(G.nodes[n]["EPI"] for n in G.nodes) / len(G)
375
+ historia_Ct.append((paso, C_t))
376
+
377
+ historia_epi.append(paso_data)
378
+
379
+ G_snapshot = nx.Graph()
380
+ G_snapshot.add_nodes_from([(n, G.nodes[n].copy()) for n in G.nodes])
381
+ G_snapshot.add_edges_from(G.edges)
382
+ G_historia.append(G_snapshot)
383
+
384
+ for nodo_id in list(historial_glifos_por_nodo.keys()):
385
+ glifos = historial_glifos_por_nodo[nodo_id]
386
+
387
+ if (
388
+ len(glifos) >= 3
389
+ and glifos[-1][1] == glifos[-2][1] == glifos[-3][1]
390
+ and abs(G.nodes[nodo_id]["EPI"] - G.nodes[nodo_id]["EPI_prev"]) < 0.05
391
+ ):
392
+ aplicar_glifo(G, G.nodes[nodo_id], nodo_id, "REMESH", historial_glifos_por_nodo, paso)
393
+ historia_glifos.append(f"{paso},{nodo_id},REMESH")
394
+
395
+ aplicar_remesh_si_estabilizacion_global(G, historial_glifos_por_nodo, historia_glifos, paso)
396
+ aplicar_remesh_grupal(G, historial_glifos_por_nodo)
397
+ epi_compuestas = detectar_EPIs_compuestas(G, umbrales)
398
+ if algo_se_mueve(G, historial_glifos_por_nodo, paso):
399
+ historial_macronodos, macronodes_info = detectar_macronodos(G, historial_glifos_por_nodo, epi_compuestas, paso)
400
+
401
+ else:
402
+ macronodes_info = {'nodos': [], 'conexiones': []}
403
+
404
+ # Evaluar exceso de VAL y promover reorganización estructural
405
+ for nodo_id, glifos in historial_glifos_por_nodo.items():
406
+ ultimos = [g for _, g in glifos[-6:]] # últimos 6 glifos del nodo
407
+ if ultimos.count("VAL") >= 4 and "THOL" not in ultimos and "ZHIR" not in ultimos:
408
+ nodo = G.nodes[nodo_id]
409
+
410
+ # Se decide el glifo correctivo en función de su Si y ΔNFR
411
+ if nodo["Si"] > 0.5 and abs(nodo["ΔNFR"]) < 0.2:
412
+ aplicar_glifo(G, nodo, nodo_id, "THOL", historial_glifos_por_nodo, paso)
413
+ historia_glifos.append(f"{paso},{nodo_id},THOL")
414
+ else:
415
+ aplicar_glifo(G, nodo, nodo_id, "ZHIR", historial_glifos_por_nodo, paso)
416
+ historia_glifos.append(f"{paso},{nodo_id},ZHIR")
417
+
418
+ porcentaje = int((paso + 1) / total_pasos * 100)
419
+ barra = "█" * (porcentaje // 2) + "-" * (50 - porcentaje // 2)
420
+ nodos_activos = [n for n in G.nodes if G.nodes[n]["estado"] == "activo"]
421
+
422
+ # Limpiar bifurcaciones obsoletas cada 300 pasos
423
+ if paso % 300 == 0:
424
+ obsoletas = limpiar_bifurcaciones_obsoletas(bifurcation_manager, paso)
425
+
426
+ lecturas = interpretar_sintaxis_glífica(historial_glifos_por_nodo)
427
+
428
+ # Diagnóstico simbólico final
429
+ diagnostico = []
430
+ for nodo in G.nodes:
431
+ nombre = nodo
432
+ datos = G.nodes[nodo]
433
+ glifos_nodo = [g[1] for g in historial_glifos_por_nodo.get(nombre, [])]
434
+ mutó = "ZHIR" in glifos_nodo
435
+ en_epi = any(nombre in grupo["nodos"] for grupo in epi_compuestas)
436
+ lectura = lecturas.get(nombre, {}).get("trayectoria", [])
437
+
438
+ diagnostico.append({
439
+ "palabra": nombre,
440
+ "glifos": glifos_nodo,
441
+ "lectura_sintactica": lectura,
442
+ "mutó": mutó,
443
+ "en_epi_compuesta": en_epi,
444
+ "Si": datos.get("Si", 0),
445
+ "estado": datos.get("estado", "latente"),
446
+ "categoría": datos.get("categoria", "sin categoría")
447
+ })
448
+
449
+ nodos_pulsantes = detectar_nodos_pulsantes(historial_glifos_por_nodo)
450
+
451
+ for nodo_id in nodos_pulsantes:
452
+ nodo = G.nodes[nodo_id]
453
+ historial = historial_glifos_por_nodo.get(nodo_id, [])
454
+ ultimos = [g for _, g in historial][-6:]
455
+
456
+ if nodo["glifo"] in ["THOL", "ZHIR", "REMESH"]:
457
+ continue # ya está mutado o recursivo
458
+
459
+ nodo = G.nodes[nodo_id]
460
+
461
+ # Evaluar emergente canónico
462
+ if abs(nodo["EPI"] - nodo["EPI_prev"]) < 0.01 and abs(nodo["ΔNFR"]) < 0.05:
463
+ glifo = "REMESH"
464
+ elif abs(nodo.get("θ", 0) - nodo.get("θ_prev", 0)) > 0.2:
465
+ glifo = "ZHIR"
466
+ elif nodo.get("Si", 0) > 0.8 and nodo.get("glifo") == "UM":
467
+ glifo = "RA"
468
+ else:
469
+ glifo = "THOL"
470
+
471
+ if nodo_id in G:
472
+ promover_emergente(nodo_id, G, paso, historial_glifos_por_nodo, historia_glifos)
473
+
474
+ bifurcation_stats = bifurcation_manager.obtener_estadisticas_bifurcacion()
475
+ return historia_epi, G, epi_compuestas, lecturas, G_historia, historial_glifos_por_nodo, historial_temporal, bifurcation_stats
476
+
477
+ def aplicar_contraccion_nul(nodo_id, G, paso, historial_glifos_por_nodo):
478
+ nodo = G.nodes[nodo_id]
479
+
480
+ condiciones = (
481
+ nodo.get("Si", 1.0) < 0.3 and
482
+ abs(nodo.get("ΔNFR", 0.0)) > 0.8 and
483
+ nodo.get("estado") == "activo" and
484
+ nodo.get("d2EPI_dt2", 0) < -0.05
485
+ )
486
+
487
+ if not condiciones:
488
+ return False
489
+
490
+ # Aplicar contracción resonante
491
+ nodo["EPI"] = round(nodo["EPI"] * 0.7, 3)
492
+ nodo["estado"] = "latente"
493
+ nodo["glifo"] = "NUL"
494
+ nodo["categoria"] = "contractivo"
495
+
496
+ historial_glifos_por_nodo.setdefault(nodo_id, []).append((paso, "NUL"))
497
+
498
+ return True
499
+
500
+ def activar_val_si_estabilidad(nodo_id, G, paso, historial_glifos_por_nodo):
501
+ nodo = G.nodes[nodo_id]
502
+
503
+ # Restricción por sobreexpansión
504
+ if nodo.get("expansiones_val", 0) >= 3:
505
+ return None
506
+
507
+ condiciones = (
508
+ nodo.get("Si", 0) > 0.85 and
509
+ abs(nodo.get("ΔNFR", 0)) < 0.2 and
510
+ nodo.get("dEPI_dt", 0) > 0.18 and
511
+ nodo.get("d2EPI_dt2", 0) > 0.2 and
512
+ nodo.get("estado") == "activo"
513
+ )
514
+
515
+ if not condiciones:
516
+ return None
517
+
518
+ nuevo_id = f"{nodo_id}_VAL_{random.randint(1000, 9999)}"
519
+ if nuevo_id in G:
520
+ return None
521
+
522
+ nuevo_nodo = {
523
+ "EPI": round(nodo["EPI"] * random.uniform(1.0, 1.1), 3),
524
+ "EPI_prev": nodo["EPI"],
525
+ "EPI_prev2": nodo.get("EPI_prev", nodo["EPI"]),
526
+ "EPI_prev3": nodo.get("EPI_prev2", nodo["EPI"]),
527
+ "glifo": "VAL",
528
+ "categoria": "expansivo",
529
+ "estado": "activo",
530
+ "νf": round(nodo["νf"] * random.uniform(1.0, 1.05), 3),
531
+ "ΔNFR": round(nodo["ΔNFR"] * 0.9, 3),
532
+ "θ": round(nodo["θ"] + random.uniform(-0.01, 0.01), 3),
533
+ "Si": nodo["Si"] * 0.98,
534
+ "historial_glifos": [(paso, "VAL")],
535
+ "historial_vf": [(paso, nodo["νf"])],
536
+ "historial_dNFR": [(paso, nodo["ΔNFR"])],
537
+ "historial_dEPI_dt": [(paso, nodo.get("dEPI_dt", 0))],
538
+ "historial_Si": [(paso, nodo["Si"])]
539
+ }
540
+
541
+ G.add_node(nuevo_id, **nuevo_nodo)
542
+ G.add_edge(nodo_id, nuevo_id)
543
+
544
+ historial_glifos_por_nodo.setdefault(nodo_id, []).append((paso, "VAL"))
545
+ historial_glifos_por_nodo[nuevo_id] = [(paso, "VAL")]
546
+
547
+ nodo["expansiones_val"] = nodo.get("expansiones_val", 0) + 1
548
+
549
+ return nuevo_id
550
+
551
+ def aplicar_remesh_grupal(G, historial_glifos_por_nodo):
552
+ nodos_aplicados = set()
553
+
554
+ for nodo_id in G.nodes:
555
+ if nodo_id in nodos_aplicados:
556
+ continue
557
+
558
+ historial = historial_glifos_por_nodo.get(nodo_id, [])
559
+ if len(historial) < 3:
560
+ continue
561
+
562
+ ultimos_glifos = [g for _, g in historial[-3:]]
563
+ if len(set(ultimos_glifos)) != 1:
564
+ continue
565
+
566
+ glifo_recurrente = ultimos_glifos[0]
567
+
568
+ vecinos = list(G.neighbors(nodo_id))
569
+ grupo = [nodo_id]
570
+
571
+ for v_id in vecinos:
572
+ v_nodo = G.nodes[v_id]
573
+ v_hist = historial_glifos_por_nodo.get(v_id, [])
574
+ if len(v_hist) >= 3:
575
+ if [g for _, g in v_hist[-3:]] == ultimos_glifos:
576
+ if abs(v_nodo.get("θ", 0) - G.nodes[nodo_id].get("θ", 0)) < 0.1:
577
+ if abs(v_nodo.get("EPI", 0) - v_nodo.get("EPI_prev", v_nodo.get("EPI", 0))) < 0.01:
578
+ if v_nodo.get("ΔNFR", 1.0) < 0.2:
579
+ grupo.append(v_id)
580
+
581
+ if len(grupo) >= 3:
582
+ for g_id in grupo:
583
+ g_nodo = G.nodes[g_id]
584
+ g_nodo["EPI_prev"] = g_nodo.get("EPI_prev", g_nodo["EPI"])
585
+ g_nodo["EPI_prev2"] = g_nodo.get("EPI_prev2", g_nodo["EPI"])
586
+ g_nodo["EPI"] = (g_nodo["EPI_prev"] + g_nodo["EPI_prev2"]) / 2
587
+ g_nodo["Si"] *= 0.98
588
+ g_nodo["νf"] *= 0.98
589
+ g_nodo["ΔNFR"] *= 0.95
590
+ g_nodo["glifo"] = "REMESH"
591
+ ultimo_paso = historial_glifos_por_nodo[g_id][-1][0] if historial_glifos_por_nodo[g_id] else 0
592
+ historial_glifos_por_nodo[g_id].append((ultimo_paso + 1, "REMESH"))
593
+ nodos_aplicados.add(g_id)
594
+
595
+ def cumple_condiciones_emergencia(forma_base, campo_coherencia):
596
+ """
597
+ Evalúa si una forma puede generar un NFR según criterios TNFR.
598
+
599
+ Condiciones de emergencia nodal:
600
+ 1. Frecuencia estructural mínima νf > 0.3
601
+ 2. Coherencia interna suficiente (estructura no degenerada)
602
+ 3. Acoplamiento posible con campo de coherencia
603
+ """
604
+ if not forma_base or len(forma_base) < 2:
605
+ return False
606
+
607
+ # Evaluar diversidad estructural interna
608
+ diversidad = len(set(forma_base)) / len(forma_base)
609
+ if diversidad < 0.3: # demasiado repetitivo
610
+ return False
611
+
612
+ # Evaluar potencial de frecuencia resonante
613
+ freq_potencial = calcular_frecuencia_resonante(forma_base)
614
+ if freq_potencial < 0.3: # frecuencia insuficiente para emergencia
615
+ return False
616
+
617
+ # Evaluar compatibilidad con campo de coherencia
618
+ if campo_coherencia and len(campo_coherencia) > 0:
619
+ coherencia_promedio = np.mean([nodo.get("EPI", 1.0) for nodo in campo_coherencia.values()])
620
+ if coherencia_promedio > 0 and freq_potencial > coherencia_promedio * 2.5:
621
+ return False # demasiado energético para el campo actual
622
+
623
+ return True
624
+
625
+ def evaluar_coherencia_estructural(forma_base):
626
+ """
627
+ Calcula EPI basado en estructura interna real según TNFR.
628
+
629
+ Evalúa:
630
+ - Simetría funcional de la forma
631
+ - Estabilidad topológica interna
632
+ - Resistencia a mutaciones
633
+ """
634
+ if not forma_base:
635
+ return 1.0
636
+
637
+ # Análisis de simetría funcional
638
+ forma_norm = forma_base.lower()
639
+ longitud = len(forma_norm)
640
+
641
+ # Factor de simetría: evalúa patrones internos
642
+ def calcular_simetria(s):
643
+ centro = len(s) // 2
644
+ if len(s) % 2 == 0:
645
+ izq, der = s[:centro], s[centro:][::-1]
646
+ else:
647
+ izq, der = s[:centro], s[centro+1:][::-1]
648
+
649
+ coincidencias = sum(1 for a, b in zip(izq, der) if a == b)
650
+ return coincidencias / max(len(izq), 1)
651
+
652
+ simetria = calcular_simetria(forma_norm)
653
+
654
+ # Factor de diversidad estructural
655
+ diversidad = len(set(forma_norm)) / longitud
656
+
657
+ # Factor de estabilidad (resistencia a mutaciones puntuales)
658
+ # Basado en la distribución de caracteres
659
+ contador = Counter(forma_norm)
660
+ entropia = -sum((freq/longitud) * np.log2(freq/longitud) for freq in contador.values())
661
+ estabilidad = min(1.0, entropia / 3.0) # normalizada
662
+
663
+ # Factor de coherencia por patrones vocálicos/consonánticos
664
+ vocales = "aeiouáéíóúü"
665
+ patron_vocal = sum(1 for c in forma_norm if c in vocales) / longitud
666
+ coherencia_fonetica = min(1.0, abs(0.4 - patron_vocal) * 2.5) # óptimo cerca de 40% vocales
667
+
668
+ # Combinar factores según pesos TNFR
669
+ EPI = (
670
+ 0.3 * simetria + # simetría estructural
671
+ 0.25 * diversidad + # diversidad interna
672
+ 0.25 * estabilidad + # resistencia mutacional
673
+ 0.2 * coherencia_fonetica # coherencia fónica
674
+ )
675
+
676
+ # Normalizar al rango [0.5, 2.5] típico de EPIs
677
+ EPI_normalizada = 0.5 + EPI * 2.0
678
+
679
+ return round(EPI_normalizada, 3)
680
+
681
+ def generar_matriz_coherencia(forma_base):
682
+ """
683
+ Crea matriz Wi(t) para evaluar estabilidad topológica interna.
684
+
685
+ Modela subnodos internos como caracteres y sus acoplamientos.
686
+ """
687
+ if not forma_base or len(forma_base) < 2:
688
+ return np.array([[1.0]])
689
+
690
+ longitud = len(forma_base)
691
+ Wi = np.zeros((longitud, longitud))
692
+
693
+ # Acoplamiento entre caracteres adyacentes (fuerte)
694
+ for i in range(longitud - 1):
695
+ Wi[i][i+1] = Wi[i+1][i] = 0.8
696
+
697
+ # Acoplamiento entre caracteres similares (débil)
698
+ for i in range(longitud):
699
+ for j in range(i+2, longitud):
700
+ if forma_base[i].lower() == forma_base[j].lower():
701
+ Wi[i][j] = Wi[j][i] = 0.3
702
+
703
+ # Autocoherencia (diagonal)
704
+ np.fill_diagonal(Wi, 1.0)
705
+
706
+ # Normalizar filas para que sumen aproximadamente 1
707
+ for i in range(longitud):
708
+ suma_fila = np.sum(Wi[i])
709
+ if suma_fila > 0:
710
+ Wi[i] = Wi[i] / suma_fila
711
+
712
+ return Wi
713
+
714
+ def sincronizar_con_campo(campo_coherencia, νf_nodo):
715
+ """
716
+ Calcula fase del nodo respecto al campo de coherencia global.
717
+
718
+ La fase determina si el nodo está sincronizado o en disonancia
719
+ con el estado actual de la red.
720
+ """
721
+ if not campo_coherencia or len(campo_coherencia) == 0:
722
+ return 0.0 # fase neutra si no hay campo
723
+
724
+ # Calcular frecuencia promedio del campo
725
+ frecuencias_campo = [nodo.get("νf", 1.0) for nodo in campo_coherencia.values()]
726
+ freq_promedio_campo = np.mean(frecuencias_campo)
727
+
728
+ # Calcular diferencia de fase basada en frecuencias
729
+ diferencia_freq = abs(νf_nodo - freq_promedio_campo)
730
+
731
+ # Convertir a fase: diferencias pequeñas = sincronización, grandes = disonancia
732
+ if diferencia_freq < 0.1:
733
+ fase = 0.0 # sincronización perfecta
734
+ elif diferencia_freq < 0.3:
735
+ fase = 0.25 # sincronización parcial
736
+ elif diferencia_freq < 0.6:
737
+ fase = 0.5 # neutral
738
+ elif diferencia_freq < 1.0:
739
+ fase = 0.75 # disonancia parcial
740
+ else:
741
+ fase = 1.0 # disonancia completa
742
+
743
+ return round(fase, 3)
744
+
745
+ __all__ = [
746
+ 'inicializar_nfr_emergente',
747
+ '_deben_conectarse_canonico',
748
+ 'crear_red_desde_datos',
749
+ 'simular_emergencia',
750
+ 'aplicar_contraccion_nul',
751
+ 'activar_val_si_estabilidad',
752
+ 'aplicar_remesh_grupal',
753
+ 'cumple_condiciones_emergencia',
754
+ 'evaluar_coherencia_estructural',
755
+ 'generar_matriz_coherencia',
756
+ 'sincronizar_con_campo'
757
+ ]