tnfr 4.5.2__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 (161) hide show
  1. tnfr/__init__.py +228 -49
  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 +106 -21
  7. tnfr/alias.pyi +140 -0
  8. tnfr/cache.py +666 -512
  9. tnfr/cache.pyi +232 -0
  10. tnfr/callback_utils.py +2 -9
  11. tnfr/callback_utils.pyi +105 -0
  12. tnfr/cli/__init__.py +21 -7
  13. tnfr/cli/__init__.pyi +47 -0
  14. tnfr/cli/arguments.py +42 -20
  15. tnfr/cli/arguments.pyi +33 -0
  16. tnfr/cli/execution.py +54 -20
  17. tnfr/cli/execution.pyi +80 -0
  18. tnfr/cli/utils.py +0 -2
  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.py → config/init.py} +11 -7
  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 +78 -24
  31. tnfr/constants/__init__.pyi +104 -0
  32. tnfr/constants/core.py +1 -2
  33. tnfr/constants/core.pyi +17 -0
  34. tnfr/constants/init.pyi +12 -0
  35. tnfr/constants/metric.py +4 -12
  36. tnfr/constants/metric.pyi +19 -0
  37. tnfr/constants_glyphs.py +9 -91
  38. tnfr/constants_glyphs.pyi +12 -0
  39. tnfr/dynamics/__init__.py +112 -634
  40. tnfr/dynamics/__init__.pyi +83 -0
  41. tnfr/dynamics/adaptation.py +201 -0
  42. tnfr/dynamics/aliases.py +22 -0
  43. tnfr/dynamics/coordination.py +343 -0
  44. tnfr/dynamics/dnfr.py +1936 -354
  45. tnfr/dynamics/dnfr.pyi +33 -0
  46. tnfr/dynamics/integrators.py +369 -75
  47. tnfr/dynamics/integrators.pyi +35 -0
  48. tnfr/dynamics/runtime.py +521 -0
  49. tnfr/dynamics/sampling.py +8 -5
  50. tnfr/dynamics/sampling.pyi +7 -0
  51. tnfr/dynamics/selectors.py +680 -0
  52. tnfr/execution.py +56 -41
  53. tnfr/execution.pyi +65 -0
  54. tnfr/flatten.py +7 -7
  55. tnfr/flatten.pyi +28 -0
  56. tnfr/gamma.py +54 -37
  57. tnfr/gamma.pyi +40 -0
  58. tnfr/glyph_history.py +85 -38
  59. tnfr/glyph_history.pyi +53 -0
  60. tnfr/grammar.py +19 -338
  61. tnfr/grammar.pyi +13 -0
  62. tnfr/helpers/__init__.py +110 -30
  63. tnfr/helpers/__init__.pyi +66 -0
  64. tnfr/helpers/numeric.py +1 -0
  65. tnfr/helpers/numeric.pyi +12 -0
  66. tnfr/immutable.py +55 -19
  67. tnfr/immutable.pyi +37 -0
  68. tnfr/initialization.py +12 -10
  69. tnfr/initialization.pyi +73 -0
  70. tnfr/io.py +99 -34
  71. tnfr/io.pyi +11 -0
  72. tnfr/locking.pyi +7 -0
  73. tnfr/metrics/__init__.pyi +20 -0
  74. tnfr/metrics/coherence.py +934 -294
  75. tnfr/metrics/common.py +1 -3
  76. tnfr/metrics/common.pyi +15 -0
  77. tnfr/metrics/core.py +192 -34
  78. tnfr/metrics/core.pyi +13 -0
  79. tnfr/metrics/diagnosis.py +707 -101
  80. tnfr/metrics/diagnosis.pyi +89 -0
  81. tnfr/metrics/export.py +27 -13
  82. tnfr/metrics/glyph_timing.py +218 -38
  83. tnfr/metrics/reporting.py +22 -18
  84. tnfr/metrics/reporting.pyi +12 -0
  85. tnfr/metrics/sense_index.py +199 -25
  86. tnfr/metrics/sense_index.pyi +9 -0
  87. tnfr/metrics/trig.py +53 -18
  88. tnfr/metrics/trig.pyi +12 -0
  89. tnfr/metrics/trig_cache.py +3 -7
  90. tnfr/metrics/trig_cache.pyi +10 -0
  91. tnfr/node.py +148 -125
  92. tnfr/node.pyi +161 -0
  93. tnfr/observers.py +44 -30
  94. tnfr/observers.pyi +46 -0
  95. tnfr/ontosim.py +14 -13
  96. tnfr/ontosim.pyi +33 -0
  97. tnfr/operators/__init__.py +84 -52
  98. tnfr/operators/__init__.pyi +31 -0
  99. tnfr/operators/definitions.py +181 -0
  100. tnfr/operators/definitions.pyi +92 -0
  101. tnfr/operators/jitter.py +86 -23
  102. tnfr/operators/jitter.pyi +11 -0
  103. tnfr/operators/registry.py +80 -0
  104. tnfr/operators/registry.pyi +15 -0
  105. tnfr/operators/remesh.py +141 -57
  106. tnfr/presets.py +9 -54
  107. tnfr/presets.pyi +7 -0
  108. tnfr/py.typed +0 -0
  109. tnfr/rng.py +259 -73
  110. tnfr/rng.pyi +14 -0
  111. tnfr/selector.py +24 -17
  112. tnfr/selector.pyi +19 -0
  113. tnfr/sense.py +55 -43
  114. tnfr/sense.pyi +30 -0
  115. tnfr/structural.py +44 -267
  116. tnfr/structural.pyi +46 -0
  117. tnfr/telemetry/__init__.py +13 -0
  118. tnfr/telemetry/verbosity.py +37 -0
  119. tnfr/tokens.py +3 -2
  120. tnfr/tokens.pyi +41 -0
  121. tnfr/trace.py +272 -82
  122. tnfr/trace.pyi +68 -0
  123. tnfr/types.py +345 -6
  124. tnfr/types.pyi +145 -0
  125. tnfr/utils/__init__.py +158 -0
  126. tnfr/utils/__init__.pyi +133 -0
  127. tnfr/utils/cache.py +755 -0
  128. tnfr/utils/cache.pyi +156 -0
  129. tnfr/{collections_utils.py → utils/data.py} +57 -90
  130. tnfr/utils/data.pyi +73 -0
  131. tnfr/utils/graph.py +87 -0
  132. tnfr/utils/graph.pyi +10 -0
  133. tnfr/utils/init.py +746 -0
  134. tnfr/utils/init.pyi +85 -0
  135. tnfr/{json_utils.py → utils/io.py} +13 -18
  136. tnfr/utils/io.pyi +10 -0
  137. tnfr/utils/validators.py +130 -0
  138. tnfr/utils/validators.pyi +19 -0
  139. tnfr/validation/__init__.py +25 -0
  140. tnfr/validation/__init__.pyi +17 -0
  141. tnfr/validation/compatibility.py +59 -0
  142. tnfr/validation/compatibility.pyi +8 -0
  143. tnfr/validation/grammar.py +149 -0
  144. tnfr/validation/grammar.pyi +11 -0
  145. tnfr/validation/rules.py +194 -0
  146. tnfr/validation/rules.pyi +18 -0
  147. tnfr/validation/syntax.py +151 -0
  148. tnfr/validation/syntax.pyi +7 -0
  149. tnfr-6.0.0.dist-info/METADATA +135 -0
  150. tnfr-6.0.0.dist-info/RECORD +157 -0
  151. tnfr/graph_utils.py +0 -84
  152. tnfr/import_utils.py +0 -228
  153. tnfr/logging_utils.py +0 -116
  154. tnfr/validators.py +0 -84
  155. tnfr/value_utils.py +0 -59
  156. tnfr-4.5.2.dist-info/METADATA +0 -379
  157. tnfr-4.5.2.dist-info/RECORD +0 -67
  158. {tnfr-4.5.2.dist-info → tnfr-6.0.0.dist-info}/WHEEL +0 -0
  159. {tnfr-4.5.2.dist-info → tnfr-6.0.0.dist-info}/entry_points.txt +0 -0
  160. {tnfr-4.5.2.dist-info → tnfr-6.0.0.dist-info}/licenses/LICENSE.md +0 -0
  161. {tnfr-4.5.2.dist-info → tnfr-6.0.0.dist-info}/top_level.txt +0 -0
tnfr/node.py CHANGED
@@ -1,13 +1,24 @@
1
1
  """Node utilities and structures for TNFR graphs."""
2
2
 
3
3
  from __future__ import annotations
4
- from typing import Iterable, MutableMapping, Optional, Protocol, TypeVar
4
+ from typing import (
5
+ Any,
6
+ Callable,
7
+ Iterable,
8
+ MutableMapping,
9
+ Optional,
10
+ Protocol,
11
+ SupportsFloat,
12
+ TypeVar,
13
+ )
5
14
  from collections.abc import Hashable
6
15
  import math
16
+ from dataclasses import dataclass
7
17
 
8
18
  from .constants import get_aliases
9
19
  from .alias import (
10
20
  get_attr,
21
+ get_theta_attr,
11
22
  get_attr_str,
12
23
  set_attr,
13
24
  set_attr_str,
@@ -15,12 +26,23 @@ from .alias import (
15
26
  set_dnfr,
16
27
  set_theta,
17
28
  )
18
- from .cache import (
29
+ from .types import (
30
+ CouplingWeight,
31
+ DeltaNFR,
32
+ EPIValue,
33
+ NodeId,
34
+ Phase,
35
+ SecondDerivativeEPI,
36
+ SenseIndex,
37
+ StructuralFrequency,
38
+ TNFRGraph,
39
+ )
40
+ from .utils import (
19
41
  cached_node_list,
20
42
  ensure_node_offset_map,
21
43
  increment_edge_version,
44
+ supports_add_edge,
22
45
  )
23
- from .graph_utils import supports_add_edge
24
46
  from .locking import get_lock
25
47
 
26
48
  ALIAS_EPI = get_aliases("EPI")
@@ -31,88 +53,77 @@ ALIAS_EPI_KIND = get_aliases("EPI_KIND")
31
53
  ALIAS_DNFR = get_aliases("DNFR")
32
54
  ALIAS_D2EPI = get_aliases("D2EPI")
33
55
 
34
- # Mapping of NodoNX attribute specifications used to generate property
35
- # descriptors. Each entry defines the keyword arguments passed to
36
- # ``_nx_attr_property`` for a given attribute name.
37
- ATTR_SPECS: dict[str, dict] = {
38
- "EPI": {"aliases": ALIAS_EPI},
39
- "vf": {
40
- "aliases": ALIAS_VF,
41
- "setter": set_vf,
42
- "use_graph_setter": True,
43
- },
44
- "theta": {
45
- "aliases": ALIAS_THETA,
46
- "setter": set_theta,
47
- "use_graph_setter": True,
48
- },
49
- "Si": {"aliases": ALIAS_SI},
50
- "epi_kind": {
51
- "aliases": ALIAS_EPI_KIND,
52
- "default": "",
53
- "getter": get_attr_str,
54
- "setter": set_attr_str,
55
- "to_python": str,
56
- "to_storage": str,
57
- },
58
- "dnfr": {
59
- "aliases": ALIAS_DNFR,
60
- "setter": set_dnfr,
61
- "use_graph_setter": True,
62
- },
63
- "d2EPI": {"aliases": ALIAS_D2EPI},
64
- }
65
-
66
56
  T = TypeVar("T")
67
57
 
68
- __all__ = ("NodoNX", "NodoProtocol", "add_edge")
69
-
70
-
71
- def _nx_attr_property(
72
- aliases: tuple[str, ...],
73
- *,
74
- default=0.0,
75
- getter=get_attr,
76
- setter=set_attr,
77
- to_python=float,
78
- to_storage=float,
79
- use_graph_setter=False,
80
- ):
81
- """Generate ``NodoNX`` property descriptors.
82
-
83
- Parameters
84
- ----------
85
- aliases:
86
- Immutable tuple of aliases used to access the attribute in the
87
- underlying ``networkx`` node.
88
- default:
89
- Value returned when the attribute is missing.
90
- getter, setter:
91
- Helper functions used to retrieve or store the value. ``setter`` can
92
- either accept ``(mapping, aliases, value)`` or, when
93
- ``use_graph_setter`` is ``True``, ``(G, n, value)``.
94
- to_python, to_storage:
95
- Conversion helpers applied when getting or setting the value,
96
- respectively.
97
- use_graph_setter:
98
- Whether ``setter`` expects ``(G, n, value)`` instead of
99
- ``(mapping, aliases, value)``.
58
+ __all__ = ("NodeNX", "NodeProtocol", "add_edge")
59
+
60
+
61
+ @dataclass(frozen=True)
62
+ class AttrSpec:
63
+ """Configuration required to expose a ``networkx`` node attribute.
64
+
65
+ ``AttrSpec`` mirrors the defaults previously used by
66
+ :func:`_nx_attr_property` and centralises the descriptor generation
67
+ logic to keep a single source of truth for NodeNX attribute access.
100
68
  """
101
69
 
102
- def fget(self) -> T:
103
- return to_python(getter(self.G.nodes[self.n], aliases, default))
70
+ aliases: tuple[str, ...]
71
+ default: Any = 0.0
72
+ getter: Callable[[MutableMapping[str, Any], tuple[str, ...], Any], Any] = get_attr
73
+ setter: Callable[..., None] = set_attr
74
+ to_python: Callable[[Any], Any] = float
75
+ to_storage: Callable[[Any], Any] = float
76
+ use_graph_setter: bool = False
104
77
 
105
- def fset(self, value: T) -> None:
106
- value = to_storage(value)
107
- if use_graph_setter:
108
- setter(self.G, self.n, value)
109
- else:
110
- setter(self.G.nodes[self.n], aliases, value)
78
+ def build_property(self) -> property:
79
+ """Create the property descriptor for ``NodeNX`` attributes."""
80
+
81
+ def fget(instance: "NodeNX") -> T:
82
+ return self.to_python(
83
+ self.getter(instance.G.nodes[instance.n], self.aliases, self.default)
84
+ )
85
+
86
+ def fset(instance: "NodeNX", value: T) -> None:
87
+ value = self.to_storage(value)
88
+ if self.use_graph_setter:
89
+ self.setter(instance.G, instance.n, value)
90
+ else:
91
+ self.setter(instance.G.nodes[instance.n], self.aliases, value)
92
+
93
+ return property(fget, fset)
111
94
 
112
- return property(fget, fset)
95
+
96
+ # Mapping of NodeNX attribute specifications used to generate property
97
+ # descriptors. Each entry defines the keyword arguments passed to
98
+ # ``AttrSpec.build_property`` for a given attribute name.
99
+ ATTR_SPECS: dict[str, AttrSpec] = {
100
+ "EPI": AttrSpec(aliases=ALIAS_EPI),
101
+ "vf": AttrSpec(aliases=ALIAS_VF, setter=set_vf, use_graph_setter=True),
102
+ "theta": AttrSpec(
103
+ aliases=ALIAS_THETA,
104
+ getter=lambda mapping, _aliases, default: get_theta_attr(mapping, default),
105
+ setter=set_theta,
106
+ use_graph_setter=True,
107
+ ),
108
+ "Si": AttrSpec(aliases=ALIAS_SI),
109
+ "epi_kind": AttrSpec(
110
+ aliases=ALIAS_EPI_KIND,
111
+ default="",
112
+ getter=get_attr_str,
113
+ setter=set_attr_str,
114
+ to_python=str,
115
+ to_storage=str,
116
+ ),
117
+ "dnfr": AttrSpec(aliases=ALIAS_DNFR, setter=set_dnfr, use_graph_setter=True),
118
+ "d2EPI": AttrSpec(aliases=ALIAS_D2EPI),
119
+ }
113
120
 
114
121
 
115
- def _add_edge_common(n1, n2, weight) -> Optional[float]:
122
+ def _add_edge_common(
123
+ n1: NodeId,
124
+ n2: NodeId,
125
+ weight: CouplingWeight | SupportsFloat | str,
126
+ ) -> Optional[CouplingWeight]:
116
127
  """Validate basic edge constraints.
117
128
 
118
129
  Returns the parsed weight if the edge can be added. ``None`` is returned
@@ -132,12 +143,12 @@ def _add_edge_common(n1, n2, weight) -> Optional[float]:
132
143
 
133
144
 
134
145
  def add_edge(
135
- graph,
136
- n1,
137
- n2,
138
- weight,
146
+ graph: TNFRGraph,
147
+ n1: NodeId,
148
+ n2: NodeId,
149
+ weight: CouplingWeight | SupportsFloat | str,
139
150
  overwrite: bool = False,
140
- ):
151
+ ) -> None:
141
152
  """Add an edge between ``n1`` and ``n2`` in a ``networkx`` graph."""
142
153
 
143
154
  weight = _add_edge_common(n1, n2, weight)
@@ -154,60 +165,70 @@ def add_edge(
154
165
  increment_edge_version(graph)
155
166
 
156
167
 
157
- class NodoProtocol(Protocol):
168
+ class NodeProtocol(Protocol):
158
169
  """Minimal protocol for TNFR nodes."""
159
170
 
160
- EPI: float
161
- vf: float
162
- theta: float
163
- Si: float
171
+ EPI: EPIValue
172
+ vf: StructuralFrequency
173
+ theta: Phase
174
+ Si: SenseIndex
164
175
  epi_kind: str
165
- dnfr: float
166
- d2EPI: float
167
- graph: dict[str, object]
176
+ dnfr: DeltaNFR
177
+ d2EPI: SecondDerivativeEPI
178
+ graph: MutableMapping[str, Any]
168
179
 
169
- def neighbors(self) -> Iterable[NodoProtocol | Hashable]: ...
180
+ def neighbors(self) -> Iterable[NodeProtocol | Hashable]:
181
+ ...
170
182
 
171
- def _glyph_storage(self) -> MutableMapping[str, object]: ...
183
+ def _glyph_storage(self) -> MutableMapping[str, object]:
184
+ ...
172
185
 
173
- def has_edge(self, other: "NodoProtocol") -> bool: ...
186
+ def has_edge(self, other: "NodeProtocol") -> bool:
187
+ ...
174
188
 
175
189
  def add_edge(
176
- self, other: "NodoProtocol", weight: float, *, overwrite: bool = False
177
- ) -> None: ...
190
+ self,
191
+ other: NodeProtocol,
192
+ weight: CouplingWeight,
193
+ *,
194
+ overwrite: bool = False,
195
+ ) -> None:
196
+ ...
178
197
 
179
- def offset(self) -> int: ...
198
+ def offset(self) -> int:
199
+ ...
180
200
 
181
- def all_nodes(self) -> Iterable["NodoProtocol"]: ...
201
+ def all_nodes(self) -> Iterable[NodeProtocol]:
202
+ ...
182
203
 
183
204
 
184
- class NodoNX(NodoProtocol):
205
+ class NodeNX(NodeProtocol):
185
206
  """Adapter for ``networkx`` nodes."""
186
207
 
187
- # Statically defined property descriptors for ``NodoNX`` attributes.
208
+ # Statically defined property descriptors for ``NodeNX`` attributes.
188
209
  # Declaring them here makes the attributes discoverable by type checkers
189
210
  # and IDEs, avoiding the previous runtime ``setattr`` loop.
190
- EPI: float = _nx_attr_property(**ATTR_SPECS["EPI"])
191
- vf: float = _nx_attr_property(**ATTR_SPECS["vf"])
192
- theta: float = _nx_attr_property(**ATTR_SPECS["theta"])
193
- Si: float = _nx_attr_property(**ATTR_SPECS["Si"])
194
- epi_kind: str = _nx_attr_property(**ATTR_SPECS["epi_kind"])
195
- dnfr: float = _nx_attr_property(**ATTR_SPECS["dnfr"])
196
- d2EPI: float = _nx_attr_property(**ATTR_SPECS["d2EPI"])
197
-
198
- def __init__(self, G, n):
199
- self.G = G
200
- self.n = n
201
- self.graph = G.graph
211
+ EPI: EPIValue = ATTR_SPECS["EPI"].build_property()
212
+ vf: StructuralFrequency = ATTR_SPECS["vf"].build_property()
213
+ theta: Phase = ATTR_SPECS["theta"].build_property()
214
+ Si: SenseIndex = ATTR_SPECS["Si"].build_property()
215
+ epi_kind: str = ATTR_SPECS["epi_kind"].build_property()
216
+ dnfr: DeltaNFR = ATTR_SPECS["dnfr"].build_property()
217
+ d2EPI: SecondDerivativeEPI = ATTR_SPECS["d2EPI"].build_property()
218
+
219
+ def __init__(self, G: TNFRGraph, n: NodeId) -> None:
220
+ self.G: TNFRGraph = G
221
+ self.n: NodeId = n
222
+ self.graph: MutableMapping[str, Any] = G.graph
202
223
  G.graph.setdefault("_node_cache", {})[n] = self
203
224
 
204
- def _glyph_storage(self):
225
+ def _glyph_storage(self) -> MutableMapping[str, Any]:
205
226
  return self.G.nodes[self.n]
206
227
 
207
228
  @classmethod
208
- def from_graph(cls, G, n):
209
- """Return cached ``NodoNX`` for ``(G, n)`` with thread safety."""
210
- lock = get_lock(f"nodonx_cache_{id(G)}")
229
+ def from_graph(cls, G: TNFRGraph, n: NodeId) -> "NodeNX":
230
+ """Return cached ``NodeNX`` for ``(G, n)`` with thread safety."""
231
+ lock = get_lock(f"node_nx_cache_{id(G)}")
211
232
  with lock:
212
233
  cache = G.graph.setdefault("_node_cache", {})
213
234
  node = cache.get(n)
@@ -215,23 +236,27 @@ class NodoNX(NodoProtocol):
215
236
  node = cls(G, n)
216
237
  return node
217
238
 
218
- def neighbors(self) -> Iterable[Hashable]:
239
+ def neighbors(self) -> Iterable[NodeId]:
219
240
  """Iterate neighbour identifiers (IDs).
220
241
 
221
242
  Wrap each resulting ID with :meth:`from_graph` to obtain the cached
222
- ``NodoNX`` instance when actual node objects are required.
243
+ ``NodeNX`` instance when actual node objects are required.
223
244
  """
224
245
  return self.G.neighbors(self.n)
225
246
 
226
- def has_edge(self, other: NodoProtocol) -> bool:
227
- if isinstance(other, NodoNX):
247
+ def has_edge(self, other: NodeProtocol) -> bool:
248
+ if isinstance(other, NodeNX):
228
249
  return self.G.has_edge(self.n, other.n)
229
250
  raise NotImplementedError
230
251
 
231
252
  def add_edge(
232
- self, other: NodoProtocol, weight: float, *, overwrite: bool = False
253
+ self,
254
+ other: NodeProtocol,
255
+ weight: CouplingWeight,
256
+ *,
257
+ overwrite: bool = False,
233
258
  ) -> None:
234
- if isinstance(other, NodoNX):
259
+ if isinstance(other, NodeNX):
235
260
  add_edge(
236
261
  self.G,
237
262
  self.n,
@@ -246,12 +271,10 @@ class NodoNX(NodoProtocol):
246
271
  mapping = ensure_node_offset_map(self.G)
247
272
  return mapping.get(self.n, 0)
248
273
 
249
- def all_nodes(self) -> Iterable[NodoProtocol]:
274
+ def all_nodes(self) -> Iterable[NodeProtocol]:
250
275
  override = self.graph.get("_all_nodes")
251
276
  if override is not None:
252
277
  return override
253
278
 
254
279
  nodes = cached_node_list(self.G)
255
- return tuple(NodoNX.from_graph(self.G, v) for v in nodes)
256
-
257
-
280
+ return tuple(NodeNX.from_graph(self.G, v) for v in nodes)
tnfr/node.pyi ADDED
@@ -0,0 +1,161 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Hashable, Iterable, MutableMapping
4
+ from typing import Any, Callable, Optional, Protocol, SupportsFloat, TypeVar
5
+
6
+ from .types import (
7
+ CouplingWeight,
8
+ DeltaNFR,
9
+ EPIValue,
10
+ NodeId,
11
+ Phase,
12
+ SecondDerivativeEPI,
13
+ SenseIndex,
14
+ StructuralFrequency,
15
+ TNFRGraph,
16
+ )
17
+
18
+ T = TypeVar("T")
19
+
20
+ __all__ = ("NodeNX", "NodeProtocol", "add_edge")
21
+
22
+
23
+ class AttrSpec:
24
+ aliases: tuple[str, ...]
25
+ default: Any
26
+ getter: Callable[[MutableMapping[str, Any], tuple[str, ...], Any], Any]
27
+ setter: Callable[..., None]
28
+ to_python: Callable[[Any], Any]
29
+ to_storage: Callable[[Any], Any]
30
+ use_graph_setter: bool
31
+
32
+ def build_property(self) -> property: ...
33
+
34
+
35
+ ALIAS_EPI: tuple[str, ...]
36
+ ALIAS_VF: tuple[str, ...]
37
+ ALIAS_THETA: tuple[str, ...]
38
+ ALIAS_SI: tuple[str, ...]
39
+ ALIAS_EPI_KIND: tuple[str, ...]
40
+ ALIAS_DNFR: tuple[str, ...]
41
+ ALIAS_D2EPI: tuple[str, ...]
42
+
43
+ ATTR_SPECS: dict[str, AttrSpec]
44
+
45
+
46
+ def _add_edge_common(
47
+ n1: NodeId,
48
+ n2: NodeId,
49
+ weight: CouplingWeight | SupportsFloat | str,
50
+ ) -> Optional[CouplingWeight]: ...
51
+
52
+
53
+ def add_edge(
54
+ graph: TNFRGraph,
55
+ n1: NodeId,
56
+ n2: NodeId,
57
+ weight: CouplingWeight | SupportsFloat | str,
58
+ overwrite: bool = ...,
59
+ ) -> None: ...
60
+
61
+
62
+ class NodeProtocol(Protocol):
63
+ EPI: EPIValue
64
+ vf: StructuralFrequency
65
+ theta: Phase
66
+ Si: SenseIndex
67
+ epi_kind: str
68
+ dnfr: DeltaNFR
69
+ d2EPI: SecondDerivativeEPI
70
+ graph: MutableMapping[str, Any]
71
+
72
+ def neighbors(self) -> Iterable[NodeProtocol | Hashable]: ...
73
+
74
+ def _glyph_storage(self) -> MutableMapping[str, object]: ...
75
+
76
+ def has_edge(self, other: NodeProtocol) -> bool: ...
77
+
78
+ def add_edge(
79
+ self,
80
+ other: NodeProtocol,
81
+ weight: CouplingWeight,
82
+ *,
83
+ overwrite: bool = ...,
84
+ ) -> None: ...
85
+
86
+ def offset(self) -> int: ...
87
+
88
+ def all_nodes(self) -> Iterable[NodeProtocol]: ...
89
+
90
+
91
+ class NodeNX(NodeProtocol):
92
+ G: TNFRGraph
93
+ n: NodeId
94
+ graph: MutableMapping[str, Any]
95
+
96
+ def __init__(self, G: TNFRGraph, n: NodeId) -> None: ...
97
+
98
+ @classmethod
99
+ def from_graph(cls, G: TNFRGraph, n: NodeId) -> "NodeNX": ...
100
+
101
+ def _glyph_storage(self) -> MutableMapping[str, Any]: ...
102
+
103
+ @property
104
+ def EPI(self) -> EPIValue: ...
105
+
106
+ @EPI.setter
107
+ def EPI(self, value: EPIValue) -> None: ...
108
+
109
+ @property
110
+ def vf(self) -> StructuralFrequency: ...
111
+
112
+ @vf.setter
113
+ def vf(self, value: StructuralFrequency) -> None: ...
114
+
115
+ @property
116
+ def theta(self) -> Phase: ...
117
+
118
+ @theta.setter
119
+ def theta(self, value: Phase) -> None: ...
120
+
121
+ @property
122
+ def Si(self) -> SenseIndex: ...
123
+
124
+ @Si.setter
125
+ def Si(self, value: SenseIndex) -> None: ...
126
+
127
+ @property
128
+ def epi_kind(self) -> str: ...
129
+
130
+ @epi_kind.setter
131
+ def epi_kind(self, value: str) -> None: ...
132
+
133
+ @property
134
+ def dnfr(self) -> DeltaNFR: ...
135
+
136
+ @dnfr.setter
137
+ def dnfr(self, value: DeltaNFR) -> None: ...
138
+
139
+ @property
140
+ def d2EPI(self) -> SecondDerivativeEPI: ...
141
+
142
+ @d2EPI.setter
143
+ def d2EPI(self, value: SecondDerivativeEPI) -> None: ...
144
+
145
+ def neighbors(self) -> Iterable[NodeId]: ...
146
+
147
+ def has_edge(self, other: NodeProtocol) -> bool: ...
148
+
149
+ def add_edge(
150
+ self,
151
+ other: NodeProtocol,
152
+ weight: CouplingWeight,
153
+ *,
154
+ overwrite: bool = ...,
155
+ ) -> None: ...
156
+
157
+ def offset(self) -> int: ...
158
+
159
+ def all_nodes(self) -> Iterable[NodeProtocol]: ...
160
+
161
+
tnfr/observers.py CHANGED
@@ -1,12 +1,13 @@
1
1
  """Observer management."""
2
2
 
3
3
  from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
4
6
  from functools import partial
5
7
  import statistics
6
8
  from statistics import StatisticsError, pvariance
7
9
 
8
- from .constants import get_aliases
9
- from .alias import get_attr
10
+ from .alias import get_theta_attr
10
11
  from .helpers.numeric import angle_diff
11
12
  from .callback_utils import CallbackEvent, callback_manager
12
13
  from .glyph_history import (
@@ -14,15 +15,17 @@ from .glyph_history import (
14
15
  count_glyphs,
15
16
  append_metric,
16
17
  )
17
- from .collections_utils import normalize_counter, mix_groups
18
- from .constants_glyphs import GLYPH_GROUPS
18
+ from .types import Glyph, GlyphLoadDistribution, TNFRGraph
19
+ from .utils import (
20
+ get_logger,
21
+ get_numpy,
22
+ mix_groups,
23
+ normalize_counter,
24
+ validate_window,
25
+ )
26
+ from .config.constants import GLYPH_GROUPS
19
27
  from .gamma import kuramoto_R_psi
20
- from .logging_utils import get_logger
21
- from .import_utils import get_numpy
22
28
  from .metrics.common import compute_coherence
23
- from .validators import validate_window
24
-
25
- ALIAS_THETA = get_aliases("THETA")
26
29
 
27
30
  __all__ = (
28
31
  "attach_standard_observer",
@@ -42,11 +45,10 @@ DEFAULT_GLYPH_LOAD_SPAN = 50
42
45
  DEFAULT_WBAR_SPAN = 25
43
46
 
44
47
 
45
-
46
48
  # -------------------------
47
- # Observador estándar Γ(R)
49
+ # Standard Γ(R) observer
48
50
  # -------------------------
49
- def _std_log(kind: str, G, ctx: dict):
51
+ def _std_log(kind: str, G: TNFRGraph, ctx: Mapping[str, object]) -> None:
50
52
  """Store compact events in ``history['events']``."""
51
53
  h = ensure_history(G)
52
54
  append_metric(h, "events", (kind, dict(ctx)))
@@ -59,7 +61,7 @@ _STD_CALLBACKS = {
59
61
  }
60
62
 
61
63
 
62
- def attach_standard_observer(G):
64
+ def attach_standard_observer(G: TNFRGraph) -> TNFRGraph:
63
65
  """Register standard callbacks: before_step, after_step, on_remesh."""
64
66
  if G.graph.get("_STD_OBSERVER"):
65
67
  return G
@@ -69,12 +71,12 @@ def attach_standard_observer(G):
69
71
  return G
70
72
 
71
73
 
72
- def _ensure_nodes(G) -> bool:
74
+ def _ensure_nodes(G: TNFRGraph) -> bool:
73
75
  """Return ``True`` when the graph has nodes."""
74
76
  return bool(G.number_of_nodes())
75
77
 
76
78
 
77
- def kuramoto_metrics(G) -> tuple[float, float]:
79
+ def kuramoto_metrics(G: TNFRGraph) -> tuple[float, float]:
78
80
  """Return Kuramoto order ``R`` and mean phase ``ψ``.
79
81
 
80
82
  Delegates to :func:`kuramoto_R_psi` and performs the computation exactly
@@ -83,7 +85,11 @@ def kuramoto_metrics(G) -> tuple[float, float]:
83
85
  return kuramoto_R_psi(G)
84
86
 
85
87
 
86
- def phase_sync(G, R: float | None = None, psi: float | None = None) -> float:
88
+ def phase_sync(
89
+ G: TNFRGraph,
90
+ R: float | None = None,
91
+ psi: float | None = None,
92
+ ) -> float:
87
93
  if not _ensure_nodes(G):
88
94
  return 1.0
89
95
  if R is None or psi is None:
@@ -92,10 +98,11 @@ def phase_sync(G, R: float | None = None, psi: float | None = None) -> float:
92
98
  R = R_calc
93
99
  if psi is None:
94
100
  psi = psi_calc
95
- diffs = (
96
- angle_diff(get_attr(data, ALIAS_THETA, 0.0), psi)
97
- for _, data in G.nodes(data=True)
98
- )
101
+ def _theta(nd: Mapping[str, object]) -> float:
102
+ value = get_theta_attr(nd, 0.0)
103
+ return float(value) if value is not None else 0.0
104
+
105
+ diffs = (angle_diff(_theta(data), psi) for _, data in G.nodes(data=True))
99
106
  # Try NumPy for a vectorised population variance
100
107
  np = get_numpy()
101
108
  if np is not None:
@@ -110,7 +117,7 @@ def phase_sync(G, R: float | None = None, psi: float | None = None) -> float:
110
117
 
111
118
 
112
119
  def kuramoto_order(
113
- G, R: float | None = None, psi: float | None = None
120
+ G: TNFRGraph, R: float | None = None, psi: float | None = None
114
121
  ) -> float:
115
122
  """R in [0,1], 1 means perfectly aligned phases."""
116
123
  if not _ensure_nodes(G):
@@ -120,7 +127,7 @@ def kuramoto_order(
120
127
  return float(R)
121
128
 
122
129
 
123
- def glyph_load(G, window: int | None = None) -> dict:
130
+ def glyph_load(G: TNFRGraph, window: int | None = None) -> GlyphLoadDistribution:
124
131
  """Return distribution of glyphs applied in the network.
125
132
 
126
133
  - ``window``: if provided, count only the last ``window`` events per node;
@@ -128,21 +135,28 @@ def glyph_load(G, window: int | None = None) -> dict:
128
135
  Returns a dict with proportions per glyph and useful aggregates.
129
136
  """
130
137
  if window == 0:
131
- return {"_count": 0}
138
+ return {"_count": 0.0}
132
139
  if window is None:
133
140
  window_int = DEFAULT_GLYPH_LOAD_SPAN
134
141
  else:
135
142
  window_int = validate_window(window, positive=True)
136
143
  total = count_glyphs(G, window=window_int, last_only=(window_int == 1))
137
- dist, count = normalize_counter(total)
144
+ dist_raw, count = normalize_counter(total)
138
145
  if count == 0:
139
- return {"_count": 0}
140
- dist = mix_groups(dist, GLYPH_GROUPS)
141
- dist["_count"] = count
142
- return dist
146
+ return {"_count": 0.0}
147
+ dist = mix_groups(dist_raw, GLYPH_GROUPS)
148
+ glyph_dist: GlyphLoadDistribution = {}
149
+ for key, value in dist.items():
150
+ try:
151
+ glyph_key: Glyph | str = Glyph(key)
152
+ except ValueError:
153
+ glyph_key = key
154
+ glyph_dist[glyph_key] = value
155
+ glyph_dist["_count"] = float(count)
156
+ return glyph_dist
143
157
 
144
158
 
145
- def wbar(G, window: int | None = None) -> float:
159
+ def wbar(G: TNFRGraph, window: int | None = None) -> float:
146
160
  """Return W̄ = mean of ``C(t)`` over a recent window.
147
161
 
148
162
  Uses :func:`ensure_history` to obtain ``G.graph['history']`` and falls back
@@ -151,7 +165,7 @@ def wbar(G, window: int | None = None) -> float:
151
165
  hist = ensure_history(G)
152
166
  cs = list(hist.get("C_steps", []))
153
167
  if not cs:
154
- # fallback: coherencia instantánea
168
+ # fallback: instantaneous coherence
155
169
  return compute_coherence(G)
156
170
  w_param = DEFAULT_WBAR_SPAN if window is None else window
157
171
  w = validate_window(w_param, positive=True)