tnfr 4.1.0__tar.gz → 4.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of tnfr might be problematic. Click here for more details.

Files changed (52) hide show
  1. {tnfr-4.1.0/src/tnfr.egg-info → tnfr-4.5.0}/PKG-INFO +10 -4
  2. {tnfr-4.1.0 → tnfr-4.5.0}/README.md +7 -1
  3. {tnfr-4.1.0 → tnfr-4.5.0}/pyproject.toml +5 -4
  4. {tnfr-4.1.0 → tnfr-4.5.0}/src/tnfr/__init__.py +34 -4
  5. tnfr-4.5.0/src/tnfr/cli.py +322 -0
  6. tnfr-4.5.0/src/tnfr/config.py +41 -0
  7. {tnfr-4.1.0 → tnfr-4.5.0}/src/tnfr/constants.py +102 -41
  8. {tnfr-4.1.0 → tnfr-4.5.0}/src/tnfr/dynamics.py +255 -49
  9. {tnfr-4.1.0 → tnfr-4.5.0}/src/tnfr/gamma.py +35 -8
  10. {tnfr-4.1.0 → tnfr-4.5.0}/src/tnfr/helpers.py +50 -17
  11. tnfr-4.5.0/src/tnfr/metrics.py +597 -0
  12. tnfr-4.5.0/src/tnfr/node.py +202 -0
  13. tnfr-4.5.0/src/tnfr/operators.py +525 -0
  14. {tnfr-4.1.0 → tnfr-4.5.0}/src/tnfr/presets.py +3 -0
  15. {tnfr-4.1.0 → tnfr-4.5.0}/src/tnfr/scenarios.py +9 -3
  16. {tnfr-4.1.0 → tnfr-4.5.0}/src/tnfr/sense.py +6 -21
  17. tnfr-4.5.0/src/tnfr/structural.py +201 -0
  18. {tnfr-4.1.0 → tnfr-4.5.0}/src/tnfr/trace.py +4 -20
  19. {tnfr-4.1.0 → tnfr-4.5.0}/src/tnfr/types.py +2 -1
  20. tnfr-4.5.0/src/tnfr/validators.py +38 -0
  21. {tnfr-4.1.0 → tnfr-4.5.0/src/tnfr.egg-info}/PKG-INFO +10 -4
  22. {tnfr-4.1.0 → tnfr-4.5.0}/src/tnfr.egg-info/SOURCES.txt +13 -1
  23. tnfr-4.5.0/tests/test_canon.py +30 -0
  24. {tnfr-4.1.0 → tnfr-4.5.0}/tests/test_edge_cases.py +1 -0
  25. {tnfr-4.1.0 → tnfr-4.5.0}/tests/test_grammar.py +13 -0
  26. tnfr-4.5.0/tests/test_history_series.py +27 -0
  27. tnfr-4.5.0/tests/test_integrators.py +36 -0
  28. {tnfr-4.1.0 → tnfr-4.5.0}/tests/test_invariants.py +5 -0
  29. tnfr-4.5.0/tests/test_nav.py +32 -0
  30. {tnfr-4.1.0 → tnfr-4.5.0}/tests/test_remesh.py +21 -0
  31. tnfr-4.5.0/tests/test_remesh_community.py +18 -0
  32. tnfr-4.5.0/tests/test_structural.py +47 -0
  33. tnfr-4.5.0/tests/test_validators.py +38 -0
  34. tnfr-4.5.0/tests/test_vf_coherencia.py +27 -0
  35. tnfr-4.1.0/src/tnfr/cli.py +0 -193
  36. tnfr-4.1.0/src/tnfr/metrics.py +0 -211
  37. tnfr-4.1.0/src/tnfr/operators.py +0 -330
  38. {tnfr-4.1.0 → tnfr-4.5.0}/LICENSE.md +0 -0
  39. {tnfr-4.1.0 → tnfr-4.5.0}/setup.cfg +0 -0
  40. {tnfr-4.1.0 → tnfr-4.5.0}/src/tnfr/grammar.py +0 -0
  41. {tnfr-4.1.0 → tnfr-4.5.0}/src/tnfr/main.py +0 -0
  42. {tnfr-4.1.0 → tnfr-4.5.0}/src/tnfr/observers.py +0 -0
  43. {tnfr-4.1.0 → tnfr-4.5.0}/src/tnfr/ontosim.py +0 -0
  44. {tnfr-4.1.0 → tnfr-4.5.0}/src/tnfr/program.py +0 -0
  45. {tnfr-4.1.0 → tnfr-4.5.0}/src/tnfr.egg-info/dependency_links.txt +0 -0
  46. {tnfr-4.1.0 → tnfr-4.5.0}/src/tnfr.egg-info/entry_points.txt +0 -0
  47. {tnfr-4.1.0 → tnfr-4.5.0}/src/tnfr.egg-info/requires.txt +0 -0
  48. {tnfr-4.1.0 → tnfr-4.5.0}/src/tnfr.egg-info/top_level.txt +0 -0
  49. {tnfr-4.1.0 → tnfr-4.5.0}/tests/test_cli_sanity.py +0 -0
  50. {tnfr-4.1.0 → tnfr-4.5.0}/tests/test_gamma.py +0 -0
  51. {tnfr-4.1.0 → tnfr-4.5.0}/tests/test_history.py +0 -0
  52. {tnfr-4.1.0 → tnfr-4.5.0}/tests/test_program.py +0 -0
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tnfr
3
- Version: 4.1.0
4
- Summary: TNFR canónica: dinámica glífica modular sobre redes.
3
+ Version: 4.5.0
4
+ Summary: Canonical TNFR: modular glyph-based dynamics on networks.
5
5
  Author: fmg
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://pypi.org/project/tnfr/
8
8
  Project-URL: Repository, https://github.com/fermga/Teoria-de-la-naturaleza-fractal-resonante-TNFR-
9
- Keywords: TNFR,fractal resonante,resonancia,glifos,networkx,dinámica,coherencia,EPI,Kuramoto
9
+ Keywords: TNFR,resonant fractal,resonance,glyphs,networkx,dynamics,coherence,EPI,Kuramoto
10
10
  Classifier: Programming Language :: Python :: 3
11
11
  Classifier: Programming Language :: Python :: 3 :: Only
12
12
  Classifier: Programming Language :: Python :: 3.9
@@ -25,7 +25,13 @@ License-File: LICENSE.md
25
25
  Requires-Dist: networkx>=2.6
26
26
  Dynamic: license-file
27
27
 
28
- # General Project Structure
28
+ # TNFR Python Project
29
+
30
+ Reference implementation of the Resonant Fractal Nature Theory (TNFR).
31
+ It models glyph-driven dynamics on NetworkX graphs, providing a modular
32
+ engine to simulate coherent reorganization processes.
33
+
34
+ ## General Project Structure
29
35
 
30
36
  * **Package entry point.** `__init__.py` registers modules under short names to avoid circular imports and exposes the public API: `preparar_red`, `step`, `run`, and observation utilities.
31
37
 
@@ -1,4 +1,10 @@
1
- # General Project Structure
1
+ # TNFR Python Project
2
+
3
+ Reference implementation of the Resonant Fractal Nature Theory (TNFR).
4
+ It models glyph-driven dynamics on NetworkX graphs, providing a modular
5
+ engine to simulate coherent reorganization processes.
6
+
7
+ ## General Project Structure
2
8
 
3
9
  * **Package entry point.** `__init__.py` registers modules under short names to avoid circular imports and exposes the public API: `preparar_red`, `step`, `run`, and observation utilities.
4
10
 
@@ -1,14 +1,14 @@
1
1
  [project]
2
2
  name = "tnfr"
3
- version = "4.1.0"
4
- description = "TNFR canónica: dinámica glífica modular sobre redes."
3
+ version = "4.5.0"
4
+ description = "Canonical TNFR: modular glyph-based dynamics on networks."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.9"
7
7
  license = { text = "MIT" }
8
8
  authors = [{ name = "fmg" }]
9
9
  keywords = [
10
- "TNFR", "fractal resonante", "resonancia", "glifos",
11
- "networkx", "dinámica", "coherencia", "EPI", "Kuramoto"
10
+ "TNFR", "resonant fractal", "resonance", "glyphs",
11
+ "networkx", "dynamics", "coherence", "EPI", "Kuramoto"
12
12
  ]
13
13
  classifiers = [
14
14
  "Programming Language :: Python :: 3",
@@ -39,3 +39,4 @@ requires = ["setuptools>=61", "wheel"]
39
39
  build-backend = "setuptools.build_meta"
40
40
 
41
41
 
42
+
@@ -8,10 +8,10 @@ Ecuación nodal:
8
8
  ∂EPI/∂t = νf · ΔNFR(t)
9
9
  """
10
10
 
11
- __version__ = "4.1.0"
11
+ __version__ = "4.5.0"
12
12
 
13
13
  # Re-exports de la API pública
14
- from .dynamics import step, run, set_delta_nfr_hook
14
+ from .dynamics import step, run, set_delta_nfr_hook, validate_canon
15
15
  from .ontosim import preparar_red
16
16
  from .observers import attach_standard_observer, coherencia_global, orden_kuramoto
17
17
  from .gamma import GAMMA_REGISTRY, eval_gamma, kuramoto_R_psi
@@ -26,19 +26,40 @@ from .metrics import (
26
26
  register_metrics_callbacks,
27
27
  Tg_global, Tg_by_node,
28
28
  latency_series, glifogram_series,
29
- glyph_top, glyph_dwell_stats,
29
+ glyph_top, glyph_dwell_stats, export_history,
30
30
  )
31
+ from .operators import aplicar_remesh_red_topologico
31
32
  from .trace import register_trace
32
33
  from .program import play, seq, block, target, wait, THOL, TARGET, WAIT, ejemplo_canonico_basico
33
34
  from .cli import main as cli_main
34
35
  from .scenarios import build_graph
35
36
  from .presets import get_preset
36
37
  from .types import NodeState
38
+ from .structural import (
39
+ create_nfr,
40
+ Operador,
41
+ Emision,
42
+ Recepcion,
43
+ Coherencia,
44
+ Disonancia,
45
+ Acoplamiento,
46
+ Resonancia,
47
+ Silencio,
48
+ Expansion,
49
+ Contraccion,
50
+ Autoorganizacion,
51
+ Mutacion,
52
+ Transicion,
53
+ Recursividad,
54
+ OPERADORES,
55
+ validate_sequence,
56
+ run_sequence,
57
+ )
37
58
 
38
59
 
39
60
  __all__ = [
40
61
  "preparar_red",
41
- "step", "run", "set_delta_nfr_hook",
62
+ "step", "run", "set_delta_nfr_hook", "validate_canon",
42
63
 
43
64
  "attach_standard_observer", "coherencia_global", "orden_kuramoto",
44
65
  "GAMMA_REGISTRY", "eval_gamma", "kuramoto_R_psi",
@@ -52,8 +73,17 @@ __all__ = [
52
73
  "Tg_global", "Tg_by_node",
53
74
  "latency_series", "glifogram_series",
54
75
  "glyph_top", "glyph_dwell_stats",
76
+ "export_history",
77
+ "aplicar_remesh_red_topologico",
55
78
  "play", "seq", "block", "target", "wait", "THOL", "TARGET", "WAIT",
56
79
  "cli_main", "build_graph", "get_preset", "NodeState",
57
80
  "ejemplo_canonico_basico",
81
+ "create_nfr",
82
+ "Operador", "Emision", "Recepcion", "Coherencia", "Disonancia",
83
+ "Acoplamiento", "Resonancia", "Silencio", "Expansion", "Contraccion",
84
+ "Autoorganizacion", "Mutacion", "Transicion", "Recursividad",
85
+ "OPERADORES", "validate_sequence", "run_sequence",
58
86
  "__version__",
59
87
  ]
88
+
89
+
@@ -0,0 +1,322 @@
1
+ from __future__ import annotations
2
+ import argparse
3
+ import json
4
+ from typing import Any, Dict, List, Optional
5
+
6
+ try: # pragma: no cover - opcional
7
+ import yaml # type: ignore
8
+ except Exception: # pragma: no cover - yaml es opcional
9
+ yaml = None
10
+
11
+ import networkx as nx
12
+
13
+ from .constants import inject_defaults, DEFAULTS
14
+ from .sense import register_sigma_callback, sigma_series, sigma_rose
15
+ from .metrics import (
16
+ register_metrics_callbacks,
17
+ Tg_global,
18
+ latency_series,
19
+ glifogram_series,
20
+ glyph_top,
21
+ export_history,
22
+ )
23
+ from .trace import register_trace
24
+ from .program import play, seq, block, wait, target
25
+ from .dynamics import step, _update_history, default_glyph_selector, parametric_glyph_selector, validate_canon
26
+ from .gamma import GAMMA_REGISTRY
27
+ from .scenarios import build_graph
28
+ from .presets import get_preset
29
+ from .config import apply_config
30
+
31
+
32
+ def _save_json(path: str, data: Any) -> None:
33
+ with open(path, "w", encoding="utf-8") as f:
34
+ json.dump(data, f, ensure_ascii=False, indent=2)
35
+
36
+
37
+ def _str2bool(s: str) -> bool:
38
+ s = s.lower()
39
+ if s in {"true", "1", "yes", "y"}:
40
+ return True
41
+ if s in {"false", "0", "no", "n"}:
42
+ return False
43
+ raise argparse.ArgumentTypeError("expected true/false")
44
+
45
+
46
+ def _args_to_dict(args: argparse.Namespace, prefix: str) -> Dict[str, Any]:
47
+ out: Dict[str, Any] = {}
48
+ pref = prefix.replace(".", "_")
49
+ for k, v in vars(args).items():
50
+ if k.startswith(pref) and v is not None:
51
+ out[k[len(pref):]] = v
52
+ return out
53
+
54
+
55
+ def _load_sequence(path: str) -> List[Any]:
56
+ with open(path, "r", encoding="utf-8") as f:
57
+ text = f.read()
58
+ if path.endswith(".yaml") or path.endswith(".yml"):
59
+ if not yaml:
60
+ raise RuntimeError("pyyaml no está instalado, usa JSON o instala pyyaml")
61
+ data = yaml.safe_load(text)
62
+ else:
63
+ data = json.loads(text)
64
+
65
+ def parse_token(tok: Any):
66
+ if isinstance(tok, str):
67
+ return tok
68
+ if isinstance(tok, dict):
69
+ if "WAIT" in tok:
70
+ return wait(int(tok["WAIT"]))
71
+ if "TARGET" in tok:
72
+ return target(tok["TARGET"])
73
+ if "THOL" in tok:
74
+ spec = tok["THOL"] or {}
75
+ b = [_parse_inner(x) for x in spec.get("body", [])]
76
+ return block(*b, repeat=int(spec.get("repeat", 1)), close=spec.get("close"))
77
+ raise ValueError(f"Token inválido: {tok}")
78
+
79
+ def _parse_inner(x: Any):
80
+ return parse_token(x)
81
+
82
+ return [parse_token(t) for t in data]
83
+
84
+
85
+ def _attach_callbacks(G: nx.Graph) -> None:
86
+ inject_defaults(G, DEFAULTS)
87
+ register_sigma_callback(G)
88
+ register_metrics_callbacks(G)
89
+ register_trace(G)
90
+ _update_history(G)
91
+
92
+
93
+ def cmd_run(args: argparse.Namespace) -> int:
94
+ G = build_graph(n=args.nodes, topology=args.topology, seed=args.seed)
95
+ if getattr(args, "config", None):
96
+ apply_config(G, args.config)
97
+ _attach_callbacks(G)
98
+ validate_canon(G)
99
+ if args.dt is not None:
100
+ G.graph["DT"] = float(args.dt)
101
+ if args.integrator is not None:
102
+ G.graph["INTEGRATOR_METHOD"] = str(args.integrator)
103
+ if getattr(args, "remesh_mode", None):
104
+ G.graph["REMESH_MODE"] = str(args.remesh_mode)
105
+ gcanon = dict(DEFAULTS["GRAMMAR_CANON"])
106
+ gcanon.update(_args_to_dict(args, prefix="grammar."))
107
+ if hasattr(args, "grammar_canon") and args.grammar_canon is not None:
108
+ gcanon["enabled"] = bool(args.grammar_canon)
109
+ G.graph.setdefault("GRAMMAR_CANON", {}).update(gcanon)
110
+ if args.glyph_hysteresis_window is not None:
111
+ G.graph["GLYPH_HYSTERESIS_WINDOW"] = int(args.glyph_hysteresis_window)
112
+ G.graph["glyph_selector"] = default_glyph_selector if args.selector == "basic" else parametric_glyph_selector
113
+ G.graph["GAMMA"] = {
114
+ "type": args.gamma_type,
115
+ "beta": args.gamma_beta,
116
+ "R0": args.gamma_R0,
117
+ }
118
+
119
+ if args.preset:
120
+ program = get_preset(args.preset)
121
+ play(G, program)
122
+ else:
123
+ steps = int(args.steps or 100)
124
+ for _ in range(steps):
125
+ step(G)
126
+
127
+ if args.save_history:
128
+ _save_json(args.save_history, G.graph.get("history", {}))
129
+ if args.export_history_base:
130
+ export_history(G, args.export_history_base, fmt=args.export_format)
131
+
132
+ # Resúmenes rápidos (si están activados)
133
+ if G.graph.get("COHERENCE", DEFAULTS["COHERENCE"]).get("enabled", True):
134
+ Wstats = G.graph.get("history", {}).get(
135
+ G.graph.get("COHERENCE", DEFAULTS["COHERENCE"]).get("stats_history_key", "W_stats"), []
136
+ )
137
+ if Wstats:
138
+ print("[COHERENCE] último paso:", Wstats[-1])
139
+ if G.graph.get("DIAGNOSIS", DEFAULTS["DIAGNOSIS"]).get("enabled", True):
140
+ last_diag = G.graph.get("history", {}).get(
141
+ G.graph.get("DIAGNOSIS", DEFAULTS["DIAGNOSIS"]).get("history_key", "nodal_diag"), []
142
+ )
143
+ if last_diag:
144
+ sample = list(last_diag[-1].values())[:3]
145
+ print("[DIAGNOSIS] ejemplo:", sample)
146
+
147
+ if args.summary:
148
+ tg = Tg_global(G, normalize=True)
149
+ lat = latency_series(G)
150
+ print("Top operadores por Tg:", glyph_top(G, k=5))
151
+ if lat["value"]:
152
+ print("Latencia media:", sum(lat["value"]) / max(1, len(lat["value"])) )
153
+ return 0
154
+
155
+
156
+ def cmd_sequence(args: argparse.Namespace) -> int:
157
+ G = build_graph(n=args.nodes, topology=args.topology, seed=args.seed)
158
+ if getattr(args, "config", None):
159
+ apply_config(G, args.config)
160
+ _attach_callbacks(G)
161
+ validate_canon(G)
162
+ if args.dt is not None:
163
+ G.graph["DT"] = float(args.dt)
164
+ if args.integrator is not None:
165
+ G.graph["INTEGRATOR_METHOD"] = str(args.integrator)
166
+ if getattr(args, "remesh_mode", None):
167
+ G.graph["REMESH_MODE"] = str(args.remesh_mode)
168
+ gcanon = dict(DEFAULTS["GRAMMAR_CANON"])
169
+ gcanon.update(_args_to_dict(args, prefix="grammar."))
170
+ if hasattr(args, "grammar_canon") and args.grammar_canon is not None:
171
+ gcanon["enabled"] = bool(args.grammar_canon)
172
+ G.graph.setdefault("GRAMMAR_CANON", {}).update(gcanon)
173
+ if args.glyph_hysteresis_window is not None:
174
+ G.graph["GLYPH_HYSTERESIS_WINDOW"] = int(args.glyph_hysteresis_window)
175
+ G.graph["glyph_selector"] = default_glyph_selector if args.selector == "basic" else parametric_glyph_selector
176
+ G.graph["GAMMA"] = {
177
+ "type": args.gamma_type,
178
+ "beta": args.gamma_beta,
179
+ "R0": args.gamma_R0,
180
+ }
181
+
182
+ if args.preset:
183
+ program = get_preset(args.preset)
184
+ elif args.sequence_file:
185
+ program = _load_sequence(args.sequence_file)
186
+ else:
187
+ program = seq("A’L", "E’N", "I’L", block("O’Z", "Z’HIR", "I’L", repeat=1), "R’A", "SH’A")
188
+
189
+ play(G, program)
190
+
191
+ if args.save_history:
192
+ _save_json(args.save_history, G.graph.get("history", {}))
193
+ if args.export_history_base:
194
+ export_history(G, args.export_history_base, fmt=args.export_format)
195
+ return 0
196
+
197
+
198
+ def cmd_metrics(args: argparse.Namespace) -> int:
199
+ G = build_graph(n=args.nodes, topology=args.topology, seed=args.seed)
200
+ if getattr(args, "config", None):
201
+ apply_config(G, args.config)
202
+ _attach_callbacks(G)
203
+ validate_canon(G)
204
+ if args.dt is not None:
205
+ G.graph["DT"] = float(args.dt)
206
+ if args.integrator is not None:
207
+ G.graph["INTEGRATOR_METHOD"] = str(args.integrator)
208
+ if getattr(args, "remesh_mode", None):
209
+ G.graph["REMESH_MODE"] = str(args.remesh_mode)
210
+ G.graph.setdefault("GRAMMAR_CANON", DEFAULTS["GRAMMAR_CANON"]).update({"enabled": bool(args.grammar_canon)})
211
+ G.graph["glyph_selector"] = default_glyph_selector if args.selector == "basic" else parametric_glyph_selector
212
+ G.graph["GAMMA"] = {
213
+ "type": args.gamma_type,
214
+ "beta": args.gamma_beta,
215
+ "R0": args.gamma_R0,
216
+ }
217
+ for _ in range(int(args.steps or 200)):
218
+ step(G)
219
+
220
+ tg = Tg_global(G, normalize=True)
221
+ lat = latency_series(G)
222
+ rose = sigma_rose(G)
223
+ glifo = glifogram_series(G)
224
+
225
+ out = {
226
+ "Tg_global": tg,
227
+ "latency_mean": (sum(lat["value"]) / max(1, len(lat["value"])) ) if lat["value"] else 0.0,
228
+ "rose": rose,
229
+ "glifogram": {k: v[:10] for k, v in glifo.items()},
230
+ }
231
+ if args.save:
232
+ _save_json(args.save, out)
233
+ else:
234
+ print(json.dumps(out, ensure_ascii=False, indent=2))
235
+ return 0
236
+
237
+
238
+ def main(argv: Optional[List[str]] = None) -> int:
239
+ p = argparse.ArgumentParser(prog="tnfr")
240
+ sub = p.add_subparsers(dest="cmd")
241
+
242
+ p_run = sub.add_parser("run", help="Correr escenario libre o preset y opcionalmente exportar history")
243
+ p_run.add_argument("--nodes", type=int, default=24)
244
+ p_run.add_argument("--topology", choices=["ring", "complete", "erdos"], default="ring")
245
+ p_run.add_argument("--steps", type=int, default=200)
246
+ p_run.add_argument("--seed", type=int, default=1)
247
+ p_run.add_argument("--preset", type=str, default=None)
248
+ p_run.add_argument("--config", type=str, default=None)
249
+ p_run.add_argument("--dt", type=float, default=None)
250
+ p_run.add_argument("--integrator", choices=["euler", "rk4"], default=None)
251
+ p_run.add_argument("--save-history", dest="save_history", type=str, default=None)
252
+ p_run.add_argument("--export-history-base", dest="export_history_base", type=str, default=None)
253
+ p_run.add_argument("--export-format", dest="export_format", choices=["csv", "json"], default="json")
254
+ p_run.add_argument("--summary", action="store_true")
255
+ p_run.add_argument("--remesh-mode", choices=["knn", "mst", "community"], default=None)
256
+ p_run.add_argument("--no-canon", dest="grammar_canon", action="store_false", default=True, help="Desactiva gramática canónica")
257
+ p_run.add_argument("--grammar.enabled", dest="grammar_enabled", type=_str2bool, default=None)
258
+ p_run.add_argument("--grammar.zhir_requires_oz_window", dest="grammar_zhir_requires_oz_window", type=int, default=None)
259
+ p_run.add_argument("--grammar.zhir_dnfr_min", dest="grammar_zhir_dnfr_min", type=float, default=None)
260
+ p_run.add_argument("--grammar.thol_min_len", dest="grammar_thol_min_len", type=int, default=None)
261
+ p_run.add_argument("--grammar.thol_max_len", dest="grammar_thol_max_len", type=int, default=None)
262
+ p_run.add_argument("--grammar.thol_close_dnfr", dest="grammar_thol_close_dnfr", type=float, default=None)
263
+ p_run.add_argument("--grammar.si_high", dest="grammar_si_high", type=float, default=None)
264
+ p_run.add_argument("--glyph.hysteresis_window", dest="glyph_hysteresis_window", type=int, default=None)
265
+ p_run.add_argument("--selector", choices=["basic", "param"], default="basic")
266
+ p_run.add_argument("--gamma-type", choices=list(GAMMA_REGISTRY.keys()), default="none")
267
+ p_run.add_argument("--gamma-beta", type=float, default=0.0)
268
+ p_run.add_argument("--gamma-R0", type=float, default=0.0)
269
+ p_run.set_defaults(func=cmd_run)
270
+
271
+ p_seq = sub.add_parser("sequence", help="Ejecutar una secuencia (preset o YAML/JSON)")
272
+ p_seq.add_argument("--nodes", type=int, default=24)
273
+ p_seq.add_argument("--topology", choices=["ring", "complete", "erdos"], default="ring")
274
+ p_seq.add_argument("--seed", type=int, default=1)
275
+ p_seq.add_argument("--preset", type=str, default=None)
276
+ p_seq.add_argument("--sequence-file", type=str, default=None)
277
+ p_seq.add_argument("--config", type=str, default=None)
278
+ p_seq.add_argument("--dt", type=float, default=None)
279
+ p_seq.add_argument("--integrator", choices=["euler", "rk4"], default=None)
280
+ p_seq.add_argument("--save-history", dest="save_history", type=str, default=None)
281
+ p_seq.add_argument("--export-history-base", dest="export_history_base", type=str, default=None)
282
+ p_seq.add_argument("--export-format", dest="export_format", choices=["csv", "json"], default="json")
283
+ p_seq.add_argument("--remesh-mode", choices=["knn", "mst", "community"], default=None)
284
+ p_seq.add_argument("--gamma-type", choices=list(GAMMA_REGISTRY.keys()), default="none")
285
+ p_seq.add_argument("--gamma-beta", type=float, default=0.0)
286
+ p_seq.add_argument("--gamma-R0", type=float, default=0.0)
287
+ p_seq.add_argument("--grammar.enabled", dest="grammar_enabled", type=_str2bool, default=None)
288
+ p_seq.add_argument("--grammar.zhir_requires_oz_window", dest="grammar_zhir_requires_oz_window", type=int, default=None)
289
+ p_seq.add_argument("--grammar.zhir_dnfr_min", dest="grammar_zhir_dnfr_min", type=float, default=None)
290
+ p_seq.add_argument("--grammar.thol_min_len", dest="grammar_thol_min_len", type=int, default=None)
291
+ p_seq.add_argument("--grammar.thol_max_len", dest="grammar_thol_max_len", type=int, default=None)
292
+ p_seq.add_argument("--grammar.thol_close_dnfr", dest="grammar_thol_close_dnfr", type=float, default=None)
293
+ p_seq.add_argument("--grammar.si_high", dest="grammar_si_high", type=float, default=None)
294
+ p_seq.add_argument("--glyph.hysteresis_window", dest="glyph_hysteresis_window", type=int, default=None)
295
+ p_seq.set_defaults(func=cmd_sequence)
296
+
297
+ p_met = sub.add_parser("metrics", help="Correr breve y volcar métricas clave")
298
+ p_met.add_argument("--nodes", type=int, default=24)
299
+ p_met.add_argument("--topology", choices=["ring", "complete", "erdos"], default="ring")
300
+ p_met.add_argument("--steps", type=int, default=300)
301
+ p_met.add_argument("--seed", type=int, default=1)
302
+ p_met.add_argument("--dt", type=float, default=None)
303
+ p_met.add_argument("--integrator", choices=["euler", "rk4"], default=None)
304
+ p_met.add_argument("--no-canon", dest="grammar_canon", action="store_false", default=True, help="Desactiva gramática canónica")
305
+ p_met.add_argument("--selector", choices=["basic", "param"], default="basic")
306
+ p_met.add_argument("--gamma-type", choices=list(GAMMA_REGISTRY.keys()), default="none")
307
+ p_met.add_argument("--gamma-beta", type=float, default=0.0)
308
+ p_met.add_argument("--gamma-R0", type=float, default=0.0)
309
+ p_met.add_argument("--remesh-mode", choices=["knn", "mst", "community"], default=None)
310
+ p_met.add_argument("--save", type=str, default=None)
311
+ p_met.add_argument("--config", type=str, default=None)
312
+ p_met.set_defaults(func=cmd_metrics)
313
+
314
+ args = p.parse_args(argv)
315
+ if not hasattr(args, "func"):
316
+ p.print_help()
317
+ return 1
318
+ return int(args.func(args))
319
+
320
+
321
+ if __name__ == "__main__": # pragma: no cover
322
+ raise SystemExit(main())
@@ -0,0 +1,41 @@
1
+ """Carga e inyección de configuraciones externas.
2
+
3
+ Permite definir parámetros en JSON o YAML y aplicarlos sobre ``G.graph``
4
+ reutilizando :func:`tnfr.constants.inject_defaults`.
5
+ """
6
+
7
+ from __future__ import annotations
8
+ from typing import Any, Dict
9
+ import json
10
+
11
+ try: # pragma: no cover - dependencia opcional
12
+ import yaml # type: ignore
13
+ except Exception: # pragma: no cover
14
+ yaml = None
15
+
16
+ from .constants import inject_defaults
17
+
18
+
19
+ def load_config(path: str) -> Dict[str, Any]:
20
+ """Lee un archivo JSON/YAML y devuelve un ``dict`` con los parámetros."""
21
+ with open(path, "r", encoding="utf-8") as f:
22
+ text = f.read()
23
+ if path.endswith((".yaml", ".yml")):
24
+ if not yaml: # pragma: no cover - fallo en entorno sin pyyaml
25
+ raise RuntimeError("pyyaml no está instalado")
26
+ data = yaml.safe_load(text)
27
+ else:
28
+ data = json.loads(text)
29
+ if not isinstance(data, dict):
30
+ raise ValueError("El archivo de configuración debe contener un objeto")
31
+ return data
32
+
33
+
34
+ def apply_config(G, path: str) -> None:
35
+ """Inyecta parámetros desde ``path`` sobre ``G.graph``.
36
+
37
+ Se reutiliza :func:`inject_defaults` para mantener la semántica de los
38
+ *defaults* canónicos.
39
+ """
40
+ cfg = load_config(path)
41
+ inject_defaults(G, cfg, override=True)