tnfr 4.5.1__py3-none-any.whl → 6.0.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.
Files changed (170) hide show
  1. tnfr/__init__.py +270 -90
  2. tnfr/__init__.pyi +40 -0
  3. tnfr/_compat.py +11 -0
  4. tnfr/_version.py +7 -0
  5. tnfr/_version.pyi +7 -0
  6. tnfr/alias.py +631 -0
  7. tnfr/alias.pyi +140 -0
  8. tnfr/cache.py +732 -0
  9. tnfr/cache.pyi +232 -0
  10. tnfr/callback_utils.py +381 -0
  11. tnfr/callback_utils.pyi +105 -0
  12. tnfr/cli/__init__.py +89 -0
  13. tnfr/cli/__init__.pyi +47 -0
  14. tnfr/cli/arguments.py +199 -0
  15. tnfr/cli/arguments.pyi +33 -0
  16. tnfr/cli/execution.py +322 -0
  17. tnfr/cli/execution.pyi +80 -0
  18. tnfr/cli/utils.py +34 -0
  19. tnfr/cli/utils.pyi +8 -0
  20. tnfr/config/__init__.py +12 -0
  21. tnfr/config/__init__.pyi +8 -0
  22. tnfr/config/constants.py +104 -0
  23. tnfr/config/constants.pyi +12 -0
  24. tnfr/config/init.py +36 -0
  25. tnfr/config/init.pyi +8 -0
  26. tnfr/config/operator_names.py +106 -0
  27. tnfr/config/operator_names.pyi +28 -0
  28. tnfr/config/presets.py +104 -0
  29. tnfr/config/presets.pyi +7 -0
  30. tnfr/constants/__init__.py +228 -0
  31. tnfr/constants/__init__.pyi +104 -0
  32. tnfr/constants/core.py +158 -0
  33. tnfr/constants/core.pyi +17 -0
  34. tnfr/constants/init.py +31 -0
  35. tnfr/constants/init.pyi +12 -0
  36. tnfr/constants/metric.py +102 -0
  37. tnfr/constants/metric.pyi +19 -0
  38. tnfr/constants_glyphs.py +16 -0
  39. tnfr/constants_glyphs.pyi +12 -0
  40. tnfr/dynamics/__init__.py +136 -0
  41. tnfr/dynamics/__init__.pyi +83 -0
  42. tnfr/dynamics/adaptation.py +201 -0
  43. tnfr/dynamics/aliases.py +22 -0
  44. tnfr/dynamics/coordination.py +343 -0
  45. tnfr/dynamics/dnfr.py +2315 -0
  46. tnfr/dynamics/dnfr.pyi +33 -0
  47. tnfr/dynamics/integrators.py +561 -0
  48. tnfr/dynamics/integrators.pyi +35 -0
  49. tnfr/dynamics/runtime.py +521 -0
  50. tnfr/dynamics/sampling.py +34 -0
  51. tnfr/dynamics/sampling.pyi +7 -0
  52. tnfr/dynamics/selectors.py +680 -0
  53. tnfr/execution.py +216 -0
  54. tnfr/execution.pyi +65 -0
  55. tnfr/flatten.py +283 -0
  56. tnfr/flatten.pyi +28 -0
  57. tnfr/gamma.py +320 -89
  58. tnfr/gamma.pyi +40 -0
  59. tnfr/glyph_history.py +337 -0
  60. tnfr/glyph_history.pyi +53 -0
  61. tnfr/grammar.py +23 -153
  62. tnfr/grammar.pyi +13 -0
  63. tnfr/helpers/__init__.py +151 -0
  64. tnfr/helpers/__init__.pyi +66 -0
  65. tnfr/helpers/numeric.py +88 -0
  66. tnfr/helpers/numeric.pyi +12 -0
  67. tnfr/immutable.py +214 -0
  68. tnfr/immutable.pyi +37 -0
  69. tnfr/initialization.py +199 -0
  70. tnfr/initialization.pyi +73 -0
  71. tnfr/io.py +311 -0
  72. tnfr/io.pyi +11 -0
  73. tnfr/locking.py +37 -0
  74. tnfr/locking.pyi +7 -0
  75. tnfr/metrics/__init__.py +41 -0
  76. tnfr/metrics/__init__.pyi +20 -0
  77. tnfr/metrics/coherence.py +1469 -0
  78. tnfr/metrics/common.py +149 -0
  79. tnfr/metrics/common.pyi +15 -0
  80. tnfr/metrics/core.py +259 -0
  81. tnfr/metrics/core.pyi +13 -0
  82. tnfr/metrics/diagnosis.py +840 -0
  83. tnfr/metrics/diagnosis.pyi +89 -0
  84. tnfr/metrics/export.py +151 -0
  85. tnfr/metrics/glyph_timing.py +369 -0
  86. tnfr/metrics/reporting.py +152 -0
  87. tnfr/metrics/reporting.pyi +12 -0
  88. tnfr/metrics/sense_index.py +294 -0
  89. tnfr/metrics/sense_index.pyi +9 -0
  90. tnfr/metrics/trig.py +216 -0
  91. tnfr/metrics/trig.pyi +12 -0
  92. tnfr/metrics/trig_cache.py +105 -0
  93. tnfr/metrics/trig_cache.pyi +10 -0
  94. tnfr/node.py +255 -177
  95. tnfr/node.pyi +161 -0
  96. tnfr/observers.py +154 -150
  97. tnfr/observers.pyi +46 -0
  98. tnfr/ontosim.py +135 -134
  99. tnfr/ontosim.pyi +33 -0
  100. tnfr/operators/__init__.py +452 -0
  101. tnfr/operators/__init__.pyi +31 -0
  102. tnfr/operators/definitions.py +181 -0
  103. tnfr/operators/definitions.pyi +92 -0
  104. tnfr/operators/jitter.py +266 -0
  105. tnfr/operators/jitter.pyi +11 -0
  106. tnfr/operators/registry.py +80 -0
  107. tnfr/operators/registry.pyi +15 -0
  108. tnfr/operators/remesh.py +569 -0
  109. tnfr/presets.py +10 -23
  110. tnfr/presets.pyi +7 -0
  111. tnfr/py.typed +0 -0
  112. tnfr/rng.py +440 -0
  113. tnfr/rng.pyi +14 -0
  114. tnfr/selector.py +217 -0
  115. tnfr/selector.pyi +19 -0
  116. tnfr/sense.py +307 -142
  117. tnfr/sense.pyi +30 -0
  118. tnfr/structural.py +69 -164
  119. tnfr/structural.pyi +46 -0
  120. tnfr/telemetry/__init__.py +13 -0
  121. tnfr/telemetry/verbosity.py +37 -0
  122. tnfr/tokens.py +61 -0
  123. tnfr/tokens.pyi +41 -0
  124. tnfr/trace.py +520 -95
  125. tnfr/trace.pyi +68 -0
  126. tnfr/types.py +382 -17
  127. tnfr/types.pyi +145 -0
  128. tnfr/utils/__init__.py +158 -0
  129. tnfr/utils/__init__.pyi +133 -0
  130. tnfr/utils/cache.py +755 -0
  131. tnfr/utils/cache.pyi +156 -0
  132. tnfr/utils/data.py +267 -0
  133. tnfr/utils/data.pyi +73 -0
  134. tnfr/utils/graph.py +87 -0
  135. tnfr/utils/graph.pyi +10 -0
  136. tnfr/utils/init.py +746 -0
  137. tnfr/utils/init.pyi +85 -0
  138. tnfr/utils/io.py +157 -0
  139. tnfr/utils/io.pyi +10 -0
  140. tnfr/utils/validators.py +130 -0
  141. tnfr/utils/validators.pyi +19 -0
  142. tnfr/validation/__init__.py +25 -0
  143. tnfr/validation/__init__.pyi +17 -0
  144. tnfr/validation/compatibility.py +59 -0
  145. tnfr/validation/compatibility.pyi +8 -0
  146. tnfr/validation/grammar.py +149 -0
  147. tnfr/validation/grammar.pyi +11 -0
  148. tnfr/validation/rules.py +194 -0
  149. tnfr/validation/rules.pyi +18 -0
  150. tnfr/validation/syntax.py +151 -0
  151. tnfr/validation/syntax.pyi +7 -0
  152. tnfr-6.0.0.dist-info/METADATA +135 -0
  153. tnfr-6.0.0.dist-info/RECORD +157 -0
  154. tnfr/cli.py +0 -322
  155. tnfr/config.py +0 -41
  156. tnfr/constants.py +0 -277
  157. tnfr/dynamics.py +0 -814
  158. tnfr/helpers.py +0 -264
  159. tnfr/main.py +0 -47
  160. tnfr/metrics.py +0 -597
  161. tnfr/operators.py +0 -525
  162. tnfr/program.py +0 -176
  163. tnfr/scenarios.py +0 -34
  164. tnfr/validators.py +0 -38
  165. tnfr-4.5.1.dist-info/METADATA +0 -221
  166. tnfr-4.5.1.dist-info/RECORD +0 -28
  167. {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/WHEEL +0 -0
  168. {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/entry_points.txt +0 -0
  169. {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/licenses/LICENSE.md +0 -0
  170. {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/top_level.txt +0 -0
tnfr/cli/__init__.py ADDED
@@ -0,0 +1,89 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import logging
5
+ import sys
6
+ from typing import Optional
7
+
8
+ from .arguments import (
9
+ add_common_args,
10
+ add_grammar_args,
11
+ add_grammar_selector_args,
12
+ add_history_export_args,
13
+ add_canon_toggle,
14
+ _add_run_parser,
15
+ _add_sequence_parser,
16
+ _add_metrics_parser,
17
+ )
18
+ from .execution import (
19
+ build_basic_graph,
20
+ apply_cli_config,
21
+ register_callbacks_and_observer,
22
+ run_program,
23
+ resolve_program,
24
+ )
25
+ from .. import __version__
26
+ from ..utils import _configure_root, get_logger
27
+
28
+ logger = get_logger(__name__)
29
+
30
+ __all__ = (
31
+ "main",
32
+ "add_common_args",
33
+ "add_grammar_args",
34
+ "add_grammar_selector_args",
35
+ "add_history_export_args",
36
+ "add_canon_toggle",
37
+ "build_basic_graph",
38
+ "apply_cli_config",
39
+ "register_callbacks_and_observer",
40
+ "run_program",
41
+ "resolve_program",
42
+ )
43
+
44
+
45
+ def main(argv: Optional[list[str]] = None) -> int:
46
+ _configure_root()
47
+
48
+ root = logging.getLogger()
49
+ root.setLevel(logging.INFO)
50
+
51
+ formatter = logging.Formatter("%(message)s")
52
+ for handler in list(root.handlers):
53
+ root.removeHandler(handler)
54
+
55
+ handler = logging.StreamHandler(stream=sys.stdout)
56
+ handler.setLevel(logging.INFO)
57
+ handler.setFormatter(formatter)
58
+ root.addHandler(handler)
59
+
60
+ p = argparse.ArgumentParser(
61
+ prog="tnfr",
62
+ formatter_class=argparse.RawDescriptionHelpFormatter,
63
+ epilog=(
64
+ "Example: tnfr sequence --sequence-file sequence.json\n"
65
+ "sequence.json:\n"
66
+ '[\n {"WAIT": 1},\n {"TARGET": "A"}\n]'
67
+ ),
68
+ )
69
+ p.add_argument(
70
+ "--version",
71
+ action="store_true",
72
+ help=(
73
+ "show the actual version and exit (reads pyproject.toml in development)"
74
+ ),
75
+ )
76
+ sub = p.add_subparsers(dest="cmd")
77
+
78
+ _add_run_parser(sub)
79
+ _add_sequence_parser(sub)
80
+ _add_metrics_parser(sub)
81
+
82
+ args = p.parse_args(argv)
83
+ if args.version:
84
+ logger.info("%s", __version__)
85
+ return 0
86
+ if not hasattr(args, "func"):
87
+ p.print_help()
88
+ return 1
89
+ return int(args.func(args))
tnfr/cli/__init__.pyi ADDED
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from typing import Optional
5
+
6
+ from ..types import ProgramTokens, TNFRGraph
7
+
8
+ __all__: tuple[str, ...]
9
+
10
+
11
+ def main(argv: Optional[list[str]] = None) -> int: ...
12
+
13
+
14
+ def add_common_args(parser: argparse.ArgumentParser) -> None: ...
15
+
16
+
17
+ def add_grammar_args(parser: argparse.ArgumentParser) -> None: ...
18
+
19
+
20
+ def add_grammar_selector_args(parser: argparse.ArgumentParser) -> None: ...
21
+
22
+
23
+ def add_history_export_args(parser: argparse.ArgumentParser) -> None: ...
24
+
25
+
26
+ def add_canon_toggle(parser: argparse.ArgumentParser) -> None: ...
27
+
28
+
29
+ def build_basic_graph(args: argparse.Namespace) -> TNFRGraph: ...
30
+
31
+
32
+ def apply_cli_config(G: TNFRGraph, args: argparse.Namespace) -> None: ...
33
+
34
+
35
+ def register_callbacks_and_observer(G: TNFRGraph) -> None: ...
36
+
37
+
38
+ def resolve_program(
39
+ args: argparse.Namespace, default: Optional[ProgramTokens] = None
40
+ ) -> Optional[ProgramTokens]: ...
41
+
42
+
43
+ def run_program(
44
+ G: Optional[TNFRGraph],
45
+ program: Optional[ProgramTokens],
46
+ args: argparse.Namespace,
47
+ ) -> TNFRGraph: ...
tnfr/cli/arguments.py ADDED
@@ -0,0 +1,199 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from typing import Any, Iterable
5
+
6
+ from ..config.presets import PREFERRED_PRESET_NAMES
7
+ from ..gamma import GAMMA_REGISTRY
8
+ from ..telemetry.verbosity import TELEMETRY_VERBOSITY_LEVELS
9
+ from ..types import ArgSpec
10
+ from .utils import spec
11
+
12
+
13
+ _PRESET_HELP = "Available presets: {}.".format(
14
+ ", ".join(PREFERRED_PRESET_NAMES),
15
+ )
16
+
17
+ TELEMETRY_VERBOSITY_CHOICES = TELEMETRY_VERBOSITY_LEVELS
18
+
19
+
20
+ GRAMMAR_ARG_SPECS: tuple[ArgSpec, ...] = (
21
+ spec("--grammar.enabled", action=argparse.BooleanOptionalAction),
22
+ spec("--grammar.zhir_requires_oz_window", type=int),
23
+ spec("--grammar.zhir_dnfr_min", type=float),
24
+ spec("--grammar.thol_min_len", type=int),
25
+ spec("--grammar.thol_max_len", type=int),
26
+ spec("--grammar.thol_close_dnfr", type=float),
27
+ spec("--grammar.si_high", type=float),
28
+ spec("--glyph.hysteresis_window", type=int),
29
+ )
30
+
31
+
32
+ # History export/save specifications
33
+ HISTORY_ARG_SPECS: tuple[ArgSpec, ...] = (
34
+ spec("--save-history", type=str),
35
+ spec("--export-history-base", type=str),
36
+ spec("--export-format", choices=["csv", "json"], default="json"),
37
+ )
38
+
39
+
40
+ # Arguments shared by CLI subcommands
41
+ COMMON_ARG_SPECS: tuple[ArgSpec, ...] = (
42
+ spec("--nodes", type=int, default=24),
43
+ spec("--topology", choices=["ring", "complete", "erdos"], default="ring"),
44
+ spec("--seed", type=int, default=1),
45
+ spec(
46
+ "--p",
47
+ type=float,
48
+ help="Edge probability when topology=erdos",
49
+ ),
50
+ spec("--observer", action="store_true", help="Attach standard observer"),
51
+ spec(
52
+ "--trace-verbosity",
53
+ choices=TELEMETRY_VERBOSITY_CHOICES,
54
+ help="Select the trace capture preset",
55
+ ),
56
+ spec(
57
+ "--metrics-verbosity",
58
+ choices=TELEMETRY_VERBOSITY_CHOICES,
59
+ help="Select the metrics capture preset",
60
+ ),
61
+ spec("--config", type=str),
62
+ spec("--dt", type=float),
63
+ spec("--integrator", choices=["euler", "rk4"]),
64
+ spec("--remesh-mode", choices=["knn", "mst", "community"]),
65
+ spec("--gamma-type", choices=list(GAMMA_REGISTRY.keys()), default="none"),
66
+ spec("--gamma-beta", type=float, default=0.0),
67
+ spec("--gamma-R0", type=float, default=0.0),
68
+ )
69
+
70
+
71
+ def add_arg_specs(
72
+ parser: argparse._ActionsContainer, specs: Iterable[ArgSpec]
73
+ ) -> None:
74
+ """Register arguments from ``specs`` on ``parser``."""
75
+ for opt, kwargs in specs:
76
+ parser.add_argument(opt, **kwargs)
77
+
78
+
79
+ def _args_to_dict(args: argparse.Namespace, prefix: str) -> dict[str, Any]:
80
+ """Extract arguments matching a prefix."""
81
+ return {
82
+ k.removeprefix(prefix): v
83
+ for k, v in vars(args).items()
84
+ if k.startswith(prefix) and v is not None
85
+ }
86
+
87
+
88
+ def add_common_args(parser: argparse.ArgumentParser) -> None:
89
+ """Add arguments shared across subcommands."""
90
+ add_arg_specs(parser, COMMON_ARG_SPECS)
91
+
92
+
93
+ def add_grammar_args(parser: argparse.ArgumentParser) -> None:
94
+ """Add grammar and glyph hysteresis options."""
95
+ group = parser.add_argument_group("Grammar")
96
+ add_arg_specs(group, GRAMMAR_ARG_SPECS)
97
+
98
+
99
+ def add_grammar_selector_args(parser: argparse.ArgumentParser) -> None:
100
+ """Add grammar options and glyph selector."""
101
+ add_grammar_args(parser)
102
+ parser.add_argument(
103
+ "--selector", choices=["basic", "param"], default="basic"
104
+ )
105
+
106
+
107
+ def add_history_export_args(parser: argparse.ArgumentParser) -> None:
108
+ """Add arguments to save or export history."""
109
+ add_arg_specs(parser, HISTORY_ARG_SPECS)
110
+
111
+
112
+ def add_canon_toggle(parser: argparse.ArgumentParser) -> None:
113
+ """Add option to disable canonical grammar."""
114
+ parser.add_argument(
115
+ "--no-canon",
116
+ dest="grammar_canon",
117
+ action="store_false",
118
+ default=True,
119
+ help="Disable canonical grammar",
120
+ )
121
+
122
+
123
+ def _add_run_parser(sub: argparse._SubParsersAction) -> None:
124
+ """Configure the ``run`` subcommand."""
125
+ from .execution import cmd_run, DEFAULT_SUMMARY_SERIES_LIMIT
126
+
127
+ p_run = sub.add_parser(
128
+ "run",
129
+ help=(
130
+ "Run a free scenario or preset and optionally export history"
131
+ ),
132
+ )
133
+ add_common_args(p_run)
134
+ p_run.add_argument("--steps", type=int, default=100)
135
+ add_canon_toggle(p_run)
136
+ add_grammar_selector_args(p_run)
137
+ add_history_export_args(p_run)
138
+ p_run.add_argument("--preset", type=str, default=None, help=_PRESET_HELP)
139
+ p_run.add_argument("--sequence-file", type=str, default=None)
140
+ p_run.add_argument("--summary", action="store_true")
141
+ p_run.add_argument(
142
+ "--summary-limit",
143
+ type=int,
144
+ default=DEFAULT_SUMMARY_SERIES_LIMIT,
145
+ help=(
146
+ "Maximum number of samples per series in the summary (<=0 to"
147
+ " disable trimming)"
148
+ ),
149
+ )
150
+ p_run.set_defaults(func=cmd_run)
151
+
152
+
153
+ def _add_sequence_parser(sub: argparse._SubParsersAction) -> None:
154
+ """Configure the ``sequence`` subcommand."""
155
+ from .execution import cmd_sequence
156
+
157
+ p_seq = sub.add_parser(
158
+ "sequence",
159
+ help="Execute a sequence (preset or YAML/JSON)",
160
+ formatter_class=argparse.RawDescriptionHelpFormatter,
161
+ epilog=(
162
+ "JSON sequence example:\n"
163
+ "[\n"
164
+ ' "A",\n'
165
+ ' {"WAIT": 1},\n'
166
+ ' {"THOL": {"body": ["A", {"WAIT": 2}], "repeat": 2}}\n'
167
+ "]"
168
+ ),
169
+ )
170
+ add_common_args(p_seq)
171
+ p_seq.add_argument("--preset", type=str, default=None, help=_PRESET_HELP)
172
+ p_seq.add_argument("--sequence-file", type=str, default=None)
173
+ add_history_export_args(p_seq)
174
+ add_grammar_args(p_seq)
175
+ p_seq.set_defaults(func=cmd_sequence)
176
+
177
+
178
+ def _add_metrics_parser(sub: argparse._SubParsersAction) -> None:
179
+ """Configure the ``metrics`` subcommand."""
180
+ from .execution import cmd_metrics
181
+
182
+ p_met = sub.add_parser(
183
+ "metrics", help="Run briefly and export key metrics"
184
+ )
185
+ add_common_args(p_met)
186
+ p_met.add_argument("--steps", type=int, default=None)
187
+ add_canon_toggle(p_met)
188
+ add_grammar_selector_args(p_met)
189
+ p_met.add_argument("--save", type=str, default=None)
190
+ p_met.add_argument(
191
+ "--summary-limit",
192
+ type=int,
193
+ default=None,
194
+ help=(
195
+ "Maximum number of samples per series in the summary (<=0 to"
196
+ " disable trimming)"
197
+ ),
198
+ )
199
+ p_met.set_defaults(func=cmd_metrics)
tnfr/cli/arguments.pyi ADDED
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from typing import Any, Iterable
5
+
6
+ from ..gamma import GAMMA_REGISTRY
7
+ from ..types import ArgSpec
8
+ from .utils import spec
9
+
10
+ GRAMMAR_ARG_SPECS: tuple[ArgSpec, ...]
11
+ HISTORY_ARG_SPECS: tuple[ArgSpec, ...]
12
+ COMMON_ARG_SPECS: tuple[ArgSpec, ...]
13
+
14
+
15
+ def add_arg_specs(parser: argparse._ActionsContainer, specs: Iterable[ArgSpec]) -> None: ...
16
+
17
+ def _args_to_dict(args: argparse.Namespace, prefix: str) -> dict[str, Any]: ...
18
+
19
+ def add_common_args(parser: argparse.ArgumentParser) -> None: ...
20
+
21
+ def add_grammar_args(parser: argparse.ArgumentParser) -> None: ...
22
+
23
+ def add_grammar_selector_args(parser: argparse.ArgumentParser) -> None: ...
24
+
25
+ def add_history_export_args(parser: argparse.ArgumentParser) -> None: ...
26
+
27
+ def add_canon_toggle(parser: argparse.ArgumentParser) -> None: ...
28
+
29
+ def _add_run_parser(sub: argparse._SubParsersAction) -> None: ...
30
+
31
+ def _add_sequence_parser(sub: argparse._SubParsersAction) -> None: ...
32
+
33
+ def _add_metrics_parser(sub: argparse._SubParsersAction) -> None: ...
tnfr/cli/execution.py ADDED
@@ -0,0 +1,322 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+
5
+ from copy import deepcopy
6
+ from pathlib import Path
7
+ from typing import Any, Optional
8
+
9
+ import networkx as nx
10
+
11
+ from ..constants import METRIC_DEFAULTS
12
+ from ..sense import register_sigma_callback
13
+ from ..metrics import (
14
+ register_metrics_callbacks,
15
+ glyph_top,
16
+ export_metrics,
17
+ build_metrics_summary,
18
+ )
19
+ from ..metrics.core import _metrics_step
20
+ from ..trace import register_trace
21
+ from ..execution import CANONICAL_PRESET_NAME, play
22
+ from ..dynamics import (
23
+ run,
24
+ default_glyph_selector,
25
+ parametric_glyph_selector,
26
+ validate_canon,
27
+ )
28
+ from ..config.presets import (
29
+ PREFERRED_PRESET_NAMES,
30
+ get_preset,
31
+ legacy_preset_guidance,
32
+ )
33
+ from ..config import apply_config
34
+ from ..io import read_structured_file, safe_write, StructuredFileError
35
+ from ..glyph_history import ensure_history
36
+ from ..ontosim import prepare_network
37
+ from ..types import ProgramTokens
38
+ from ..utils import get_logger, json_dumps
39
+ from ..flatten import parse_program_tokens
40
+
41
+ from .arguments import _args_to_dict
42
+
43
+ logger = get_logger(__name__)
44
+
45
+
46
+ # CLI summaries should remain concise by default while allowing callers to
47
+ # inspect the full glyphogram series when needed.
48
+ DEFAULT_SUMMARY_SERIES_LIMIT = 10
49
+
50
+ _PREFERRED_PRESETS_DISPLAY = ", ".join(PREFERRED_PRESET_NAMES)
51
+
52
+
53
+ def _save_json(path: str, data: Any) -> None:
54
+ payload = json_dumps(data, ensure_ascii=False, indent=2, default=list)
55
+ safe_write(path, lambda f: f.write(payload))
56
+
57
+
58
+ def _attach_callbacks(G: "nx.Graph") -> None:
59
+ register_sigma_callback(G)
60
+ register_metrics_callbacks(G)
61
+ register_trace(G)
62
+ _metrics_step(G, ctx=None)
63
+
64
+
65
+ def _persist_history(G: "nx.Graph", args: argparse.Namespace) -> None:
66
+ if getattr(args, "save_history", None) or getattr(args, "export_history_base", None):
67
+ history = ensure_history(G)
68
+ if getattr(args, "save_history", None):
69
+ _save_json(args.save_history, history)
70
+ if getattr(args, "export_history_base", None):
71
+ export_metrics(G, args.export_history_base, fmt=args.export_format)
72
+
73
+
74
+ def build_basic_graph(args: argparse.Namespace) -> "nx.Graph":
75
+ n = args.nodes
76
+ topology = getattr(args, "topology", "ring").lower()
77
+ seed = getattr(args, "seed", None)
78
+ if topology == "ring":
79
+ G = nx.cycle_graph(n)
80
+ elif topology == "complete":
81
+ G = nx.complete_graph(n)
82
+ elif topology == "erdos":
83
+ if getattr(args, "p", None) is not None:
84
+ prob = float(args.p)
85
+ else:
86
+ if n <= 0:
87
+ fallback = 0.0
88
+ else:
89
+ fallback = 3.0 / n
90
+ prob = min(max(fallback, 0.0), 1.0)
91
+ if not 0.0 <= prob <= 1.0:
92
+ raise ValueError(f"p must be between 0 and 1; received {prob}")
93
+ G = nx.gnp_random_graph(n, prob, seed=seed)
94
+ else:
95
+ raise ValueError(
96
+ f"Invalid topology '{topology}'. Accepted options are: ring, complete, erdos"
97
+ )
98
+ if seed is not None:
99
+ G.graph["RANDOM_SEED"] = int(seed)
100
+ return G
101
+
102
+
103
+ def apply_cli_config(G: "nx.Graph", args: argparse.Namespace) -> None:
104
+ if args.config:
105
+ apply_config(G, Path(args.config))
106
+ arg_map = {
107
+ "dt": ("DT", float),
108
+ "integrator": ("INTEGRATOR_METHOD", str),
109
+ "remesh_mode": ("REMESH_MODE", str),
110
+ "glyph_hysteresis_window": ("GLYPH_HYSTERESIS_WINDOW", int),
111
+ }
112
+ for attr, (key, conv) in arg_map.items():
113
+ val = getattr(args, attr, None)
114
+ if val is not None:
115
+ G.graph[key] = conv(val)
116
+
117
+ gcanon = {
118
+ **METRIC_DEFAULTS["GRAMMAR_CANON"],
119
+ **_args_to_dict(args, prefix="grammar_"),
120
+ }
121
+ if getattr(args, "grammar_canon", None) is not None:
122
+ gcanon["enabled"] = bool(args.grammar_canon)
123
+ G.graph["GRAMMAR_CANON"] = gcanon
124
+
125
+ selector = getattr(args, "selector", None)
126
+ if selector is not None:
127
+ sel_map = {
128
+ "basic": default_glyph_selector,
129
+ "param": parametric_glyph_selector,
130
+ }
131
+ G.graph["glyph_selector"] = sel_map.get(
132
+ selector, default_glyph_selector
133
+ )
134
+
135
+ if hasattr(args, "gamma_type"):
136
+ G.graph["GAMMA"] = {
137
+ "type": args.gamma_type,
138
+ "beta": args.gamma_beta,
139
+ "R0": args.gamma_R0,
140
+ }
141
+
142
+ for attr, key in (
143
+ ("trace_verbosity", "TRACE"),
144
+ ("metrics_verbosity", "METRICS"),
145
+ ):
146
+ cfg = G.graph.get(key)
147
+ if not isinstance(cfg, dict):
148
+ cfg = deepcopy(METRIC_DEFAULTS[key])
149
+ G.graph[key] = cfg
150
+ value = getattr(args, attr, None)
151
+ if value is not None:
152
+ cfg["verbosity"] = value
153
+
154
+
155
+ def register_callbacks_and_observer(G: "nx.Graph") -> None:
156
+ _attach_callbacks(G)
157
+ validate_canon(G)
158
+
159
+
160
+ def _build_graph_from_args(args: argparse.Namespace) -> "nx.Graph":
161
+ G = build_basic_graph(args)
162
+ apply_cli_config(G, args)
163
+ if getattr(args, "observer", False):
164
+ G.graph["ATTACH_STD_OBSERVER"] = True
165
+ prepare_network(G)
166
+ register_callbacks_and_observer(G)
167
+ return G
168
+
169
+
170
+ def _load_sequence(path: Path) -> ProgramTokens:
171
+ try:
172
+ data = read_structured_file(path)
173
+ except (StructuredFileError, OSError) as exc:
174
+ if isinstance(exc, StructuredFileError):
175
+ message = str(exc)
176
+ else:
177
+ message = str(StructuredFileError(path, exc))
178
+ logger.error("%s", message)
179
+ raise SystemExit(1) from exc
180
+ return parse_program_tokens(data)
181
+
182
+
183
+ def resolve_program(
184
+ args: argparse.Namespace, default: Optional[ProgramTokens] = None
185
+ ) -> Optional[ProgramTokens]:
186
+ if getattr(args, "preset", None):
187
+ try:
188
+ return get_preset(args.preset)
189
+ except KeyError as exc:
190
+ guidance = legacy_preset_guidance(args.preset)
191
+ if guidance is not None:
192
+ details = guidance
193
+ else:
194
+ details = (
195
+ exc.args[0]
196
+ if exc.args
197
+ else "Legacy preset identifier rejected."
198
+ )
199
+ logger.error(
200
+ (
201
+ "Unknown preset '%s'. Available presets: %s. %s "
202
+ "Use --sequence-file to execute custom sequences."
203
+ ),
204
+ args.preset,
205
+ _PREFERRED_PRESETS_DISPLAY,
206
+ details,
207
+ )
208
+ raise SystemExit(1) from exc
209
+ if getattr(args, "sequence_file", None):
210
+ return _load_sequence(Path(args.sequence_file))
211
+ return default
212
+
213
+
214
+ def run_program(
215
+ G: Optional["nx.Graph"],
216
+ program: Optional[ProgramTokens],
217
+ args: argparse.Namespace,
218
+ ) -> "nx.Graph":
219
+ if G is None:
220
+ G = _build_graph_from_args(args)
221
+
222
+ if program is None:
223
+ steps = getattr(args, "steps", 100)
224
+ steps = 100 if steps is None else int(steps)
225
+ if steps < 0:
226
+ steps = 0
227
+
228
+ run_kwargs: dict[str, Any] = {}
229
+ for attr in ("dt", "use_Si", "apply_glyphs"):
230
+ value = getattr(args, attr, None)
231
+ if value is not None:
232
+ run_kwargs[attr] = value
233
+
234
+ run(G, steps=steps, **run_kwargs)
235
+ else:
236
+ play(G, program)
237
+
238
+ _persist_history(G, args)
239
+ return G
240
+
241
+
242
+ def _run_cli_program(
243
+ args: argparse.Namespace,
244
+ *,
245
+ default_program: Optional[ProgramTokens] = None,
246
+ graph: Optional["nx.Graph"] = None,
247
+ ) -> tuple[int, Optional["nx.Graph"]]:
248
+ try:
249
+ program = resolve_program(args, default=default_program)
250
+ except SystemExit as exc:
251
+ code = exc.code if isinstance(exc.code, int) else 1
252
+ return code or 1, None
253
+
254
+ result_graph = run_program(graph, program, args)
255
+ return 0, result_graph
256
+
257
+
258
+ def _log_run_summaries(G: "nx.Graph", args: argparse.Namespace) -> None:
259
+ cfg_coh = G.graph.get("COHERENCE", METRIC_DEFAULTS["COHERENCE"])
260
+ cfg_diag = G.graph.get("DIAGNOSIS", METRIC_DEFAULTS["DIAGNOSIS"])
261
+ hist = ensure_history(G)
262
+
263
+ if cfg_coh.get("enabled", True):
264
+ Wstats = hist.get(cfg_coh.get("stats_history_key", "W_stats"), [])
265
+ if Wstats:
266
+ logger.info("[COHERENCE] last step: %s", Wstats[-1])
267
+
268
+ if cfg_diag.get("enabled", True):
269
+ last_diag = hist.get(cfg_diag.get("history_key", "nodal_diag"), [])
270
+ if last_diag:
271
+ sample = list(last_diag[-1].values())[:3]
272
+ logger.info("[DIAGNOSIS] sample: %s", sample)
273
+
274
+ if args.summary:
275
+ summary_limit = getattr(args, "summary_limit", DEFAULT_SUMMARY_SERIES_LIMIT)
276
+ summary, has_latency_values = build_metrics_summary(
277
+ G, series_limit=summary_limit
278
+ )
279
+ logger.info("Global Tg: %s", summary["Tg_global"])
280
+ logger.info("Top operators by Tg: %s", glyph_top(G, k=5))
281
+ if has_latency_values:
282
+ logger.info("Average latency: %s", summary["latency_mean"])
283
+
284
+
285
+ def cmd_run(args: argparse.Namespace) -> int:
286
+ code, graph = _run_cli_program(args)
287
+ if code != 0:
288
+ return code
289
+
290
+ if graph is not None:
291
+ _log_run_summaries(graph, args)
292
+ return 0
293
+
294
+
295
+ def cmd_sequence(args: argparse.Namespace) -> int:
296
+ if args.preset and args.sequence_file:
297
+ logger.error(
298
+ "Cannot use --preset and --sequence-file at the same time"
299
+ )
300
+ return 1
301
+ code, _ = _run_cli_program(
302
+ args, default_program=get_preset(CANONICAL_PRESET_NAME)
303
+ )
304
+ return code
305
+
306
+
307
+ def cmd_metrics(args: argparse.Namespace) -> int:
308
+ if getattr(args, "steps", None) is None:
309
+ # Default a longer run for metrics stability
310
+ args.steps = 200
311
+
312
+ code, graph = _run_cli_program(args)
313
+ if code != 0 or graph is None:
314
+ return code
315
+
316
+ summary_limit = getattr(args, "summary_limit", None)
317
+ out, _ = build_metrics_summary(graph, series_limit=summary_limit)
318
+ if args.save:
319
+ _save_json(args.save, out)
320
+ else:
321
+ logger.info("%s", json_dumps(out))
322
+ return 0