tnfr 4.5.1__py3-none-any.whl → 4.5.2__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.
Potentially problematic release.
This version of tnfr might be problematic. Click here for more details.
- tnfr/__init__.py +91 -90
- tnfr/alias.py +546 -0
- tnfr/cache.py +578 -0
- tnfr/callback_utils.py +388 -0
- tnfr/cli/__init__.py +75 -0
- tnfr/cli/arguments.py +177 -0
- tnfr/cli/execution.py +288 -0
- tnfr/cli/utils.py +36 -0
- tnfr/collections_utils.py +300 -0
- tnfr/config.py +19 -28
- tnfr/constants/__init__.py +174 -0
- tnfr/constants/core.py +159 -0
- tnfr/constants/init.py +31 -0
- tnfr/constants/metric.py +110 -0
- tnfr/constants_glyphs.py +98 -0
- tnfr/dynamics/__init__.py +658 -0
- tnfr/dynamics/dnfr.py +733 -0
- tnfr/dynamics/integrators.py +267 -0
- tnfr/dynamics/sampling.py +31 -0
- tnfr/execution.py +201 -0
- tnfr/flatten.py +283 -0
- tnfr/gamma.py +302 -88
- tnfr/glyph_history.py +290 -0
- tnfr/grammar.py +285 -96
- tnfr/graph_utils.py +84 -0
- tnfr/helpers/__init__.py +71 -0
- tnfr/helpers/numeric.py +87 -0
- tnfr/immutable.py +178 -0
- tnfr/import_utils.py +228 -0
- tnfr/initialization.py +197 -0
- tnfr/io.py +246 -0
- tnfr/json_utils.py +162 -0
- tnfr/locking.py +37 -0
- tnfr/logging_utils.py +116 -0
- tnfr/metrics/__init__.py +41 -0
- tnfr/metrics/coherence.py +829 -0
- tnfr/metrics/common.py +151 -0
- tnfr/metrics/core.py +101 -0
- tnfr/metrics/diagnosis.py +234 -0
- tnfr/metrics/export.py +137 -0
- tnfr/metrics/glyph_timing.py +189 -0
- tnfr/metrics/reporting.py +148 -0
- tnfr/metrics/sense_index.py +120 -0
- tnfr/metrics/trig.py +181 -0
- tnfr/metrics/trig_cache.py +109 -0
- tnfr/node.py +214 -159
- tnfr/observers.py +126 -136
- tnfr/ontosim.py +134 -134
- tnfr/operators/__init__.py +420 -0
- tnfr/operators/jitter.py +203 -0
- tnfr/operators/remesh.py +485 -0
- tnfr/presets.py +46 -14
- tnfr/rng.py +254 -0
- tnfr/selector.py +210 -0
- tnfr/sense.py +284 -131
- tnfr/structural.py +207 -79
- tnfr/tokens.py +60 -0
- tnfr/trace.py +329 -94
- tnfr/types.py +43 -17
- tnfr/validators.py +70 -24
- tnfr/value_utils.py +59 -0
- tnfr-4.5.2.dist-info/METADATA +379 -0
- tnfr-4.5.2.dist-info/RECORD +67 -0
- tnfr/cli.py +0 -322
- tnfr/constants.py +0 -277
- tnfr/dynamics.py +0 -814
- tnfr/helpers.py +0 -264
- tnfr/main.py +0 -47
- tnfr/metrics.py +0 -597
- tnfr/operators.py +0 -525
- tnfr/program.py +0 -176
- tnfr/scenarios.py +0 -34
- tnfr-4.5.1.dist-info/METADATA +0 -221
- tnfr-4.5.1.dist-info/RECORD +0 -28
- {tnfr-4.5.1.dist-info → tnfr-4.5.2.dist-info}/WHEEL +0 -0
- {tnfr-4.5.1.dist-info → tnfr-4.5.2.dist-info}/entry_points.txt +0 -0
- {tnfr-4.5.1.dist-info → tnfr-4.5.2.dist-info}/licenses/LICENSE.md +0 -0
- {tnfr-4.5.1.dist-info → tnfr-4.5.2.dist-info}/top_level.txt +0 -0
tnfr/callback_utils.py
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
"""Callback registration and invocation helpers.
|
|
2
|
+
|
|
3
|
+
This module is thread-safe: all mutations of the callback registry stored in a
|
|
4
|
+
graph's ``G.graph`` are serialised using a process-wide lock obtained via
|
|
5
|
+
``locking.get_lock("callbacks")``. Callback functions themselves execute
|
|
6
|
+
outside of the lock and must therefore be independently thread-safe if they
|
|
7
|
+
modify shared state.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
from typing import Any, TypedDict
|
|
14
|
+
from enum import Enum
|
|
15
|
+
from collections import defaultdict, deque
|
|
16
|
+
from collections.abc import Callable, Mapping, Iterable
|
|
17
|
+
|
|
18
|
+
import traceback
|
|
19
|
+
import threading
|
|
20
|
+
from .logging_utils import get_logger
|
|
21
|
+
from .constants import DEFAULTS
|
|
22
|
+
from .locking import get_lock
|
|
23
|
+
|
|
24
|
+
from .trace import CallbackSpec
|
|
25
|
+
from .collections_utils import is_non_string_sequence
|
|
26
|
+
|
|
27
|
+
import networkx as nx # type: ignore[import-untyped]
|
|
28
|
+
|
|
29
|
+
__all__ = (
|
|
30
|
+
"CallbackEvent",
|
|
31
|
+
"CallbackManager",
|
|
32
|
+
"callback_manager",
|
|
33
|
+
"CallbackError",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
logger = get_logger(__name__)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class CallbackEvent(str, Enum):
|
|
40
|
+
"""Supported callback events."""
|
|
41
|
+
|
|
42
|
+
BEFORE_STEP = "before_step"
|
|
43
|
+
AFTER_STEP = "after_step"
|
|
44
|
+
ON_REMESH = "on_remesh"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class CallbackManager:
|
|
48
|
+
"""Centralised registry and error tracking for callbacks."""
|
|
49
|
+
|
|
50
|
+
def __init__(self) -> None:
|
|
51
|
+
self._lock = get_lock("callbacks")
|
|
52
|
+
self._error_limit_lock = threading.Lock()
|
|
53
|
+
self._error_limit = 100
|
|
54
|
+
self._error_limit_cache = self._error_limit
|
|
55
|
+
|
|
56
|
+
# ------------------------------------------------------------------
|
|
57
|
+
# Error limit management
|
|
58
|
+
# ------------------------------------------------------------------
|
|
59
|
+
def get_callback_error_limit(self) -> int:
|
|
60
|
+
"""Return the current callback error retention limit."""
|
|
61
|
+
with self._error_limit_lock:
|
|
62
|
+
return self._error_limit
|
|
63
|
+
|
|
64
|
+
def set_callback_error_limit(self, limit: int) -> int:
|
|
65
|
+
"""Set the maximum number of callback errors retained."""
|
|
66
|
+
if limit < 1:
|
|
67
|
+
raise ValueError("limit must be positive")
|
|
68
|
+
with self._error_limit_lock:
|
|
69
|
+
previous = self._error_limit
|
|
70
|
+
self._error_limit = int(limit)
|
|
71
|
+
self._error_limit_cache = self._error_limit
|
|
72
|
+
return previous
|
|
73
|
+
|
|
74
|
+
# ------------------------------------------------------------------
|
|
75
|
+
# Registry helpers
|
|
76
|
+
# ------------------------------------------------------------------
|
|
77
|
+
def _record_callback_error(
|
|
78
|
+
self,
|
|
79
|
+
G: "nx.Graph",
|
|
80
|
+
event: str,
|
|
81
|
+
ctx: dict[str, Any],
|
|
82
|
+
spec: CallbackSpec,
|
|
83
|
+
err: Exception,
|
|
84
|
+
) -> None:
|
|
85
|
+
"""Log and store a callback error for later inspection."""
|
|
86
|
+
|
|
87
|
+
logger.exception("callback %r failed for %s: %s", spec.name, event, err)
|
|
88
|
+
limit = self._error_limit_cache
|
|
89
|
+
err_list = G.graph.setdefault(
|
|
90
|
+
"_callback_errors", deque[CallbackError](maxlen=limit)
|
|
91
|
+
)
|
|
92
|
+
if err_list.maxlen != limit:
|
|
93
|
+
err_list = deque[CallbackError](err_list, maxlen=limit)
|
|
94
|
+
G.graph["_callback_errors"] = err_list
|
|
95
|
+
error: CallbackError = {
|
|
96
|
+
"event": event,
|
|
97
|
+
"step": ctx.get("step"),
|
|
98
|
+
"error": repr(err),
|
|
99
|
+
"traceback": traceback.format_exc(),
|
|
100
|
+
"fn": _func_id(spec.func),
|
|
101
|
+
"name": spec.name,
|
|
102
|
+
}
|
|
103
|
+
err_list.append(error)
|
|
104
|
+
|
|
105
|
+
def _ensure_callbacks_nolock(self, G: "nx.Graph") -> CallbackRegistry:
|
|
106
|
+
cbs = G.graph.setdefault("callbacks", defaultdict(dict))
|
|
107
|
+
dirty: set[str] = set(G.graph.pop("_callbacks_dirty", ()))
|
|
108
|
+
return _validate_registry(G, cbs, dirty)
|
|
109
|
+
|
|
110
|
+
def _ensure_callbacks(self, G: "nx.Graph") -> CallbackRegistry:
|
|
111
|
+
with self._lock:
|
|
112
|
+
return self._ensure_callbacks_nolock(G)
|
|
113
|
+
|
|
114
|
+
def register_callback(
|
|
115
|
+
self,
|
|
116
|
+
G: "nx.Graph",
|
|
117
|
+
event: CallbackEvent | str,
|
|
118
|
+
func: Callback,
|
|
119
|
+
*,
|
|
120
|
+
name: str | None = None,
|
|
121
|
+
) -> Callback:
|
|
122
|
+
"""Register ``func`` as callback for ``event``."""
|
|
123
|
+
|
|
124
|
+
event = _normalize_event(event)
|
|
125
|
+
_ensure_known_event(event)
|
|
126
|
+
if not callable(func):
|
|
127
|
+
raise TypeError("func must be callable")
|
|
128
|
+
with self._lock:
|
|
129
|
+
cbs = self._ensure_callbacks_nolock(G)
|
|
130
|
+
|
|
131
|
+
cb_name = name or getattr(func, "__name__", None)
|
|
132
|
+
spec = CallbackSpec(cb_name, func)
|
|
133
|
+
existing_map = cbs[event]
|
|
134
|
+
strict = bool(
|
|
135
|
+
G.graph.get("CALLBACKS_STRICT", DEFAULTS["CALLBACKS_STRICT"])
|
|
136
|
+
)
|
|
137
|
+
key = _reconcile_callback(event, existing_map, spec, strict)
|
|
138
|
+
|
|
139
|
+
existing_map[key] = spec
|
|
140
|
+
dirty = G.graph.setdefault("_callbacks_dirty", set())
|
|
141
|
+
dirty.add(event)
|
|
142
|
+
return func
|
|
143
|
+
|
|
144
|
+
def invoke_callbacks(
|
|
145
|
+
self,
|
|
146
|
+
G: "nx.Graph",
|
|
147
|
+
event: CallbackEvent | str,
|
|
148
|
+
ctx: dict[str, Any] | None = None,
|
|
149
|
+
) -> None:
|
|
150
|
+
"""Invoke all callbacks registered for ``event`` with context ``ctx``."""
|
|
151
|
+
|
|
152
|
+
event = _normalize_event(event)
|
|
153
|
+
with self._lock:
|
|
154
|
+
cbs = dict(self._ensure_callbacks_nolock(G).get(event, {}))
|
|
155
|
+
strict = bool(
|
|
156
|
+
G.graph.get("CALLBACKS_STRICT", DEFAULTS["CALLBACKS_STRICT"])
|
|
157
|
+
)
|
|
158
|
+
if ctx is None:
|
|
159
|
+
ctx = {}
|
|
160
|
+
for spec in cbs.values():
|
|
161
|
+
try:
|
|
162
|
+
spec.func(G, ctx)
|
|
163
|
+
except (
|
|
164
|
+
RuntimeError,
|
|
165
|
+
ValueError,
|
|
166
|
+
TypeError,
|
|
167
|
+
) as e:
|
|
168
|
+
with self._lock:
|
|
169
|
+
self._record_callback_error(G, event, ctx, spec, e)
|
|
170
|
+
if strict:
|
|
171
|
+
raise
|
|
172
|
+
except nx.NetworkXError as err:
|
|
173
|
+
with self._lock:
|
|
174
|
+
self._record_callback_error(G, event, ctx, spec, err)
|
|
175
|
+
logger.exception(
|
|
176
|
+
"callback %r raised NetworkXError for %s with ctx=%r",
|
|
177
|
+
spec.name,
|
|
178
|
+
event,
|
|
179
|
+
ctx,
|
|
180
|
+
)
|
|
181
|
+
raise
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
Callback = Callable[["nx.Graph", dict[str, Any]], None]
|
|
185
|
+
CallbackRegistry = dict[str, dict[str, "CallbackSpec"]]
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class CallbackError(TypedDict):
|
|
189
|
+
"""Metadata for a failed callback invocation."""
|
|
190
|
+
|
|
191
|
+
event: str
|
|
192
|
+
step: int | None
|
|
193
|
+
error: str
|
|
194
|
+
traceback: str
|
|
195
|
+
fn: str
|
|
196
|
+
name: str | None
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _func_id(fn: Callable[..., Any]) -> str:
|
|
200
|
+
"""Return a deterministic identifier for ``fn``.
|
|
201
|
+
|
|
202
|
+
Combines the function's module and qualified name to avoid the
|
|
203
|
+
nondeterminism of ``repr(fn)`` which includes the memory address.
|
|
204
|
+
"""
|
|
205
|
+
module = getattr(fn, "__module__", fn.__class__.__module__)
|
|
206
|
+
qualname = getattr(
|
|
207
|
+
fn,
|
|
208
|
+
"__qualname__",
|
|
209
|
+
getattr(fn, "__name__", fn.__class__.__qualname__),
|
|
210
|
+
)
|
|
211
|
+
return f"{module}.{qualname}"
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _validate_registry(
|
|
215
|
+
G: "nx.Graph", cbs: Any, dirty: set[str]
|
|
216
|
+
) -> CallbackRegistry:
|
|
217
|
+
"""Validate and normalise the callback registry.
|
|
218
|
+
|
|
219
|
+
``cbs`` is coerced to a ``defaultdict(dict)`` and any events listed in
|
|
220
|
+
``dirty`` are rebuilt using :func:`_normalize_callbacks`. Unknown events are
|
|
221
|
+
removed. The cleaned registry is stored back on the graph and returned.
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
if not isinstance(cbs, Mapping):
|
|
225
|
+
logger.warning(
|
|
226
|
+
"Invalid callbacks registry on graph; resetting to empty",
|
|
227
|
+
)
|
|
228
|
+
cbs = defaultdict(dict)
|
|
229
|
+
elif not isinstance(cbs, defaultdict) or cbs.default_factory is not dict:
|
|
230
|
+
cbs = defaultdict(
|
|
231
|
+
dict,
|
|
232
|
+
{
|
|
233
|
+
event: _normalize_callbacks(entries)
|
|
234
|
+
for event, entries in dict(cbs).items()
|
|
235
|
+
if _is_known_event(event)
|
|
236
|
+
},
|
|
237
|
+
)
|
|
238
|
+
else:
|
|
239
|
+
for event in dirty:
|
|
240
|
+
if _is_known_event(event):
|
|
241
|
+
cbs[event] = _normalize_callbacks(cbs.get(event))
|
|
242
|
+
else:
|
|
243
|
+
cbs.pop(event, None)
|
|
244
|
+
|
|
245
|
+
G.graph["callbacks"] = cbs
|
|
246
|
+
return cbs
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _normalize_callbacks(entries: Any) -> dict[str, CallbackSpec]:
|
|
252
|
+
"""Return ``entries`` normalised into a callback mapping."""
|
|
253
|
+
if isinstance(entries, Mapping):
|
|
254
|
+
entries_iter = entries.values()
|
|
255
|
+
elif isinstance(entries, Iterable) and not isinstance(entries, (str, bytes, bytearray)):
|
|
256
|
+
entries_iter = entries
|
|
257
|
+
else:
|
|
258
|
+
return {}
|
|
259
|
+
|
|
260
|
+
new_map: dict[str, CallbackSpec] = {}
|
|
261
|
+
for entry in entries_iter:
|
|
262
|
+
spec = _normalize_callback_entry(entry)
|
|
263
|
+
if spec is None:
|
|
264
|
+
continue
|
|
265
|
+
key = spec.name or _func_id(spec.func)
|
|
266
|
+
new_map[key] = spec
|
|
267
|
+
return new_map
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _normalize_event(event: CallbackEvent | str) -> str:
|
|
271
|
+
"""Return ``event`` as a string."""
|
|
272
|
+
return event.value if isinstance(event, CallbackEvent) else str(event)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _is_known_event(event: str) -> bool:
|
|
276
|
+
"""Return ``True`` when ``event`` matches a declared :class:`CallbackEvent`."""
|
|
277
|
+
|
|
278
|
+
try:
|
|
279
|
+
CallbackEvent(event)
|
|
280
|
+
except ValueError:
|
|
281
|
+
return False
|
|
282
|
+
else:
|
|
283
|
+
return True
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _ensure_known_event(event: str) -> None:
|
|
287
|
+
"""Raise :class:`ValueError` when ``event`` is not a known callback."""
|
|
288
|
+
|
|
289
|
+
try:
|
|
290
|
+
CallbackEvent(event)
|
|
291
|
+
except ValueError as exc: # pragma: no cover - defensive branch
|
|
292
|
+
raise ValueError(f"Unknown event: {event}") from exc
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _normalize_callback_entry(entry: Any) -> "CallbackSpec | None":
|
|
296
|
+
"""Normalize a callback specification.
|
|
297
|
+
|
|
298
|
+
Supported formats
|
|
299
|
+
-----------------
|
|
300
|
+
* :class:`CallbackSpec` instances (returned unchanged).
|
|
301
|
+
* Sequences ``(name: str, func: Callable)`` such as lists, tuples or other
|
|
302
|
+
iterables.
|
|
303
|
+
* Bare callables ``func`` whose name is taken from ``func.__name__``.
|
|
304
|
+
|
|
305
|
+
``None`` is returned when ``entry`` does not match any of the accepted
|
|
306
|
+
formats. The original ``entry`` is never mutated. Sequence inputs are
|
|
307
|
+
converted to ``tuple`` before validation to support generators; the
|
|
308
|
+
materialization consumes the iterable and failure results in ``None``.
|
|
309
|
+
"""
|
|
310
|
+
|
|
311
|
+
if isinstance(entry, CallbackSpec):
|
|
312
|
+
return entry
|
|
313
|
+
elif is_non_string_sequence(entry):
|
|
314
|
+
try:
|
|
315
|
+
entry = tuple(entry)
|
|
316
|
+
except TypeError:
|
|
317
|
+
return None
|
|
318
|
+
if len(entry) != 2:
|
|
319
|
+
return None
|
|
320
|
+
name, fn = entry
|
|
321
|
+
if not isinstance(name, str) or not callable(fn):
|
|
322
|
+
return None
|
|
323
|
+
return CallbackSpec(name, fn)
|
|
324
|
+
elif callable(entry):
|
|
325
|
+
name = getattr(entry, "__name__", None)
|
|
326
|
+
return CallbackSpec(name, entry)
|
|
327
|
+
else:
|
|
328
|
+
return None
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _reconcile_callback(
|
|
333
|
+
event: str,
|
|
334
|
+
existing_map: dict[str, CallbackSpec],
|
|
335
|
+
spec: CallbackSpec,
|
|
336
|
+
strict: bool,
|
|
337
|
+
) -> str:
|
|
338
|
+
"""Reconcile ``spec`` with ``existing_map``.
|
|
339
|
+
|
|
340
|
+
Ensures that callbacks remain unique by explicit name or function identity.
|
|
341
|
+
When a name collision occurs with a different function, ``strict`` controls
|
|
342
|
+
whether a :class:`ValueError` is raised or a warning is logged.
|
|
343
|
+
|
|
344
|
+
Parameters
|
|
345
|
+
----------
|
|
346
|
+
event:
|
|
347
|
+
Event under which ``spec`` will be registered. Only used for messages.
|
|
348
|
+
existing_map:
|
|
349
|
+
Current mapping of callbacks for ``event``.
|
|
350
|
+
spec:
|
|
351
|
+
Callback specification being registered.
|
|
352
|
+
strict:
|
|
353
|
+
Whether to raise on name collisions instead of logging a warning.
|
|
354
|
+
|
|
355
|
+
Returns
|
|
356
|
+
-------
|
|
357
|
+
str
|
|
358
|
+
Key under which ``spec`` should be stored in ``existing_map``.
|
|
359
|
+
"""
|
|
360
|
+
|
|
361
|
+
key = spec.name or _func_id(spec.func)
|
|
362
|
+
|
|
363
|
+
if spec.name is not None:
|
|
364
|
+
existing_spec = existing_map.get(key)
|
|
365
|
+
if existing_spec is not None and existing_spec.func is not spec.func:
|
|
366
|
+
msg = f"Callback {spec.name!r} already registered for {event}"
|
|
367
|
+
if strict:
|
|
368
|
+
raise ValueError(msg)
|
|
369
|
+
logger.warning(msg)
|
|
370
|
+
|
|
371
|
+
# Remove existing entries under the same key and any other using the same
|
|
372
|
+
# function identity to avoid duplicates.
|
|
373
|
+
existing_map.pop(key, None)
|
|
374
|
+
fn_key = next((k for k, s in existing_map.items() if s.func is spec.func), None)
|
|
375
|
+
if fn_key is not None:
|
|
376
|
+
existing_map.pop(fn_key, None)
|
|
377
|
+
|
|
378
|
+
return key
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
# ---------------------------------------------------------------------------
|
|
382
|
+
# Default manager instance and convenience wrappers
|
|
383
|
+
# ---------------------------------------------------------------------------
|
|
384
|
+
|
|
385
|
+
callback_manager = CallbackManager()
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
|
tnfr/cli/__init__.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
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 ..logging_utils import get_logger
|
|
26
|
+
from .. import __version__
|
|
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
|
+
logging.basicConfig(
|
|
47
|
+
level=logging.INFO, format="%(message)s", stream=sys.stdout, force=True
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
p = argparse.ArgumentParser(
|
|
51
|
+
prog="tnfr",
|
|
52
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
53
|
+
epilog=(
|
|
54
|
+
"Ejemplo: tnfr sequence --sequence-file secuencia.json\n"
|
|
55
|
+
"secuencia.json:\n"
|
|
56
|
+
'[\n {"WAIT": 1},\n {"TARGET": "A"}\n]'
|
|
57
|
+
),
|
|
58
|
+
)
|
|
59
|
+
p.add_argument(
|
|
60
|
+
"--version", action="store_true", help="muestra versión y sale"
|
|
61
|
+
)
|
|
62
|
+
sub = p.add_subparsers(dest="cmd")
|
|
63
|
+
|
|
64
|
+
_add_run_parser(sub)
|
|
65
|
+
_add_sequence_parser(sub)
|
|
66
|
+
_add_metrics_parser(sub)
|
|
67
|
+
|
|
68
|
+
args = p.parse_args(argv)
|
|
69
|
+
if args.version:
|
|
70
|
+
logger.info("%s", __version__)
|
|
71
|
+
return 0
|
|
72
|
+
if not hasattr(args, "func"):
|
|
73
|
+
p.print_help()
|
|
74
|
+
return 1
|
|
75
|
+
return int(args.func(args))
|
tnfr/cli/arguments.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from ..gamma import GAMMA_REGISTRY
|
|
7
|
+
from .utils import spec
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
GRAMMAR_ARG_SPECS = (
|
|
11
|
+
spec("--grammar.enabled", action=argparse.BooleanOptionalAction),
|
|
12
|
+
spec("--grammar.zhir_requires_oz_window", type=int),
|
|
13
|
+
spec("--grammar.zhir_dnfr_min", type=float),
|
|
14
|
+
spec("--grammar.thol_min_len", type=int),
|
|
15
|
+
spec("--grammar.thol_max_len", type=int),
|
|
16
|
+
spec("--grammar.thol_close_dnfr", type=float),
|
|
17
|
+
spec("--grammar.si_high", type=float),
|
|
18
|
+
spec("--glyph.hysteresis_window", type=int),
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# Especificaciones para opciones relacionadas con el histórico
|
|
23
|
+
HISTORY_ARG_SPECS = (
|
|
24
|
+
spec("--save-history", type=str),
|
|
25
|
+
spec("--export-history-base", type=str),
|
|
26
|
+
spec("--export-format", choices=["csv", "json"], default="json"),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# Argumentos comunes a los subcomandos
|
|
31
|
+
COMMON_ARG_SPECS = (
|
|
32
|
+
spec("--nodes", type=int, default=24),
|
|
33
|
+
spec("--topology", choices=["ring", "complete", "erdos"], default="ring"),
|
|
34
|
+
spec("--seed", type=int, default=1),
|
|
35
|
+
spec(
|
|
36
|
+
"--p",
|
|
37
|
+
type=float,
|
|
38
|
+
help="Probabilidad de arista si topology=erdos",
|
|
39
|
+
),
|
|
40
|
+
spec("--observer", action="store_true", help="Adjunta observador estándar"),
|
|
41
|
+
spec("--config", type=str),
|
|
42
|
+
spec("--dt", type=float),
|
|
43
|
+
spec("--integrator", choices=["euler", "rk4"]),
|
|
44
|
+
spec("--remesh-mode", choices=["knn", "mst", "community"]),
|
|
45
|
+
spec("--gamma-type", choices=list(GAMMA_REGISTRY.keys()), default="none"),
|
|
46
|
+
spec("--gamma-beta", type=float, default=0.0),
|
|
47
|
+
spec("--gamma-R0", type=float, default=0.0),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def add_arg_specs(parser: argparse._ActionsContainer, specs) -> None:
|
|
52
|
+
"""Register arguments from ``specs`` on ``parser``."""
|
|
53
|
+
for opt, kwargs in specs:
|
|
54
|
+
parser.add_argument(opt, **kwargs)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _args_to_dict(args: argparse.Namespace, prefix: str) -> dict[str, Any]:
|
|
58
|
+
"""Extract arguments matching a prefix."""
|
|
59
|
+
return {
|
|
60
|
+
k.removeprefix(prefix): v
|
|
61
|
+
for k, v in vars(args).items()
|
|
62
|
+
if k.startswith(prefix) and v is not None
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def add_common_args(parser: argparse.ArgumentParser) -> None:
|
|
67
|
+
"""Add arguments shared across subcommands."""
|
|
68
|
+
add_arg_specs(parser, COMMON_ARG_SPECS)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def add_grammar_args(parser: argparse.ArgumentParser) -> None:
|
|
72
|
+
"""Add grammar and glyph hysteresis options."""
|
|
73
|
+
group = parser.add_argument_group("Grammar")
|
|
74
|
+
add_arg_specs(group, GRAMMAR_ARG_SPECS)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def add_grammar_selector_args(parser: argparse.ArgumentParser) -> None:
|
|
78
|
+
"""Add grammar options and glyph selector."""
|
|
79
|
+
add_grammar_args(parser)
|
|
80
|
+
parser.add_argument(
|
|
81
|
+
"--selector", choices=["basic", "param"], default="basic"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def add_history_export_args(parser: argparse.ArgumentParser) -> None:
|
|
86
|
+
"""Add arguments to save or export history."""
|
|
87
|
+
add_arg_specs(parser, HISTORY_ARG_SPECS)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def add_canon_toggle(parser: argparse.ArgumentParser) -> None:
|
|
91
|
+
"""Add option to disable canonical grammar."""
|
|
92
|
+
parser.add_argument(
|
|
93
|
+
"--no-canon",
|
|
94
|
+
dest="grammar_canon",
|
|
95
|
+
action="store_false",
|
|
96
|
+
default=True,
|
|
97
|
+
help="Desactiva gramática canónica",
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _add_run_parser(sub: argparse._SubParsersAction) -> None:
|
|
102
|
+
"""Configure the ``run`` subcommand."""
|
|
103
|
+
from .execution import cmd_run, DEFAULT_SUMMARY_SERIES_LIMIT
|
|
104
|
+
|
|
105
|
+
p_run = sub.add_parser(
|
|
106
|
+
"run",
|
|
107
|
+
help=(
|
|
108
|
+
"Correr escenario libre o preset y opcionalmente exportar history"
|
|
109
|
+
),
|
|
110
|
+
)
|
|
111
|
+
add_common_args(p_run)
|
|
112
|
+
p_run.add_argument("--steps", type=int, default=100)
|
|
113
|
+
add_canon_toggle(p_run)
|
|
114
|
+
add_grammar_selector_args(p_run)
|
|
115
|
+
add_history_export_args(p_run)
|
|
116
|
+
p_run.add_argument("--preset", type=str, default=None)
|
|
117
|
+
p_run.add_argument("--sequence-file", type=str, default=None)
|
|
118
|
+
p_run.add_argument("--summary", action="store_true")
|
|
119
|
+
p_run.add_argument(
|
|
120
|
+
"--summary-limit",
|
|
121
|
+
type=int,
|
|
122
|
+
default=DEFAULT_SUMMARY_SERIES_LIMIT,
|
|
123
|
+
help=(
|
|
124
|
+
"Número máximo de muestras por serie en el resumen (<=0 para"
|
|
125
|
+
" desactivar el recorte)"
|
|
126
|
+
),
|
|
127
|
+
)
|
|
128
|
+
p_run.set_defaults(func=cmd_run)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _add_sequence_parser(sub: argparse._SubParsersAction) -> None:
|
|
132
|
+
"""Configure the ``sequence`` subcommand."""
|
|
133
|
+
from .execution import cmd_sequence
|
|
134
|
+
|
|
135
|
+
p_seq = sub.add_parser(
|
|
136
|
+
"sequence",
|
|
137
|
+
help="Ejecutar una secuencia (preset o YAML/JSON)",
|
|
138
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
139
|
+
epilog=(
|
|
140
|
+
"Ejemplo de secuencia JSON:\n"
|
|
141
|
+
"[\n"
|
|
142
|
+
' "A",\n'
|
|
143
|
+
' {"WAIT": 1},\n'
|
|
144
|
+
' {"THOL": {"body": ["A", {"WAIT": 2}], "repeat": 2}}\n'
|
|
145
|
+
"]"
|
|
146
|
+
),
|
|
147
|
+
)
|
|
148
|
+
add_common_args(p_seq)
|
|
149
|
+
p_seq.add_argument("--preset", type=str, default=None)
|
|
150
|
+
p_seq.add_argument("--sequence-file", type=str, default=None)
|
|
151
|
+
add_history_export_args(p_seq)
|
|
152
|
+
add_grammar_args(p_seq)
|
|
153
|
+
p_seq.set_defaults(func=cmd_sequence)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _add_metrics_parser(sub: argparse._SubParsersAction) -> None:
|
|
157
|
+
"""Configure the ``metrics`` subcommand."""
|
|
158
|
+
from .execution import cmd_metrics
|
|
159
|
+
|
|
160
|
+
p_met = sub.add_parser(
|
|
161
|
+
"metrics", help="Correr breve y volcar métricas clave"
|
|
162
|
+
)
|
|
163
|
+
add_common_args(p_met)
|
|
164
|
+
p_met.add_argument("--steps", type=int, default=None)
|
|
165
|
+
add_canon_toggle(p_met)
|
|
166
|
+
add_grammar_selector_args(p_met)
|
|
167
|
+
p_met.add_argument("--save", type=str, default=None)
|
|
168
|
+
p_met.add_argument(
|
|
169
|
+
"--summary-limit",
|
|
170
|
+
type=int,
|
|
171
|
+
default=None,
|
|
172
|
+
help=(
|
|
173
|
+
"Número máximo de muestras por serie en el resumen (<=0 para"
|
|
174
|
+
" desactivar el recorte)"
|
|
175
|
+
),
|
|
176
|
+
)
|
|
177
|
+
p_met.set_defaults(func=cmd_metrics)
|