tnfr 3.0.3__py3-none-any.whl → 8.5.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.
Potentially problematic release.
This version of tnfr might be problematic. Click here for more details.
- tnfr/__init__.py +375 -56
- tnfr/__init__.pyi +33 -0
- tnfr/_compat.py +10 -0
- tnfr/_generated_version.py +34 -0
- tnfr/_version.py +49 -0
- tnfr/_version.pyi +7 -0
- tnfr/alias.py +723 -0
- tnfr/alias.pyi +108 -0
- tnfr/backends/__init__.py +354 -0
- tnfr/backends/jax_backend.py +173 -0
- tnfr/backends/numpy_backend.py +238 -0
- tnfr/backends/optimized_numpy.py +420 -0
- tnfr/backends/torch_backend.py +408 -0
- tnfr/cache.py +171 -0
- tnfr/cache.pyi +13 -0
- tnfr/cli/__init__.py +110 -0
- tnfr/cli/__init__.pyi +26 -0
- tnfr/cli/arguments.py +489 -0
- tnfr/cli/arguments.pyi +29 -0
- tnfr/cli/execution.py +914 -0
- tnfr/cli/execution.pyi +70 -0
- tnfr/cli/interactive_validator.py +614 -0
- tnfr/cli/utils.py +51 -0
- tnfr/cli/utils.pyi +7 -0
- tnfr/cli/validate.py +236 -0
- tnfr/compat/__init__.py +85 -0
- tnfr/compat/dataclass.py +136 -0
- tnfr/compat/jsonschema_stub.py +61 -0
- tnfr/compat/matplotlib_stub.py +73 -0
- tnfr/compat/numpy_stub.py +155 -0
- tnfr/config/__init__.py +224 -0
- tnfr/config/__init__.pyi +10 -0
- tnfr/config/constants.py +104 -0
- tnfr/config/constants.pyi +12 -0
- tnfr/config/defaults.py +54 -0
- tnfr/config/defaults_core.py +212 -0
- tnfr/config/defaults_init.py +33 -0
- tnfr/config/defaults_metric.py +104 -0
- tnfr/config/feature_flags.py +81 -0
- tnfr/config/feature_flags.pyi +16 -0
- tnfr/config/glyph_constants.py +31 -0
- tnfr/config/init.py +77 -0
- tnfr/config/init.pyi +8 -0
- tnfr/config/operator_names.py +254 -0
- tnfr/config/operator_names.pyi +36 -0
- tnfr/config/physics_derivation.py +354 -0
- tnfr/config/presets.py +83 -0
- tnfr/config/presets.pyi +7 -0
- tnfr/config/security.py +927 -0
- tnfr/config/thresholds.py +114 -0
- tnfr/config/tnfr_config.py +498 -0
- tnfr/constants/__init__.py +92 -0
- tnfr/constants/__init__.pyi +92 -0
- tnfr/constants/aliases.py +33 -0
- tnfr/constants/aliases.pyi +27 -0
- tnfr/constants/init.py +33 -0
- tnfr/constants/init.pyi +12 -0
- tnfr/constants/metric.py +104 -0
- tnfr/constants/metric.pyi +19 -0
- tnfr/core/__init__.py +33 -0
- tnfr/core/container.py +226 -0
- tnfr/core/default_implementations.py +329 -0
- tnfr/core/interfaces.py +279 -0
- tnfr/dynamics/__init__.py +238 -0
- tnfr/dynamics/__init__.pyi +83 -0
- tnfr/dynamics/adaptation.py +267 -0
- tnfr/dynamics/adaptation.pyi +7 -0
- tnfr/dynamics/adaptive_sequences.py +189 -0
- tnfr/dynamics/adaptive_sequences.pyi +14 -0
- tnfr/dynamics/aliases.py +23 -0
- tnfr/dynamics/aliases.pyi +19 -0
- tnfr/dynamics/bifurcation.py +232 -0
- tnfr/dynamics/canonical.py +229 -0
- tnfr/dynamics/canonical.pyi +48 -0
- tnfr/dynamics/coordination.py +385 -0
- tnfr/dynamics/coordination.pyi +25 -0
- tnfr/dynamics/dnfr.py +3034 -0
- tnfr/dynamics/dnfr.pyi +26 -0
- tnfr/dynamics/dynamic_limits.py +225 -0
- tnfr/dynamics/feedback.py +252 -0
- tnfr/dynamics/feedback.pyi +24 -0
- tnfr/dynamics/fused_dnfr.py +454 -0
- tnfr/dynamics/homeostasis.py +157 -0
- tnfr/dynamics/homeostasis.pyi +14 -0
- tnfr/dynamics/integrators.py +661 -0
- tnfr/dynamics/integrators.pyi +36 -0
- tnfr/dynamics/learning.py +310 -0
- tnfr/dynamics/learning.pyi +33 -0
- tnfr/dynamics/metabolism.py +254 -0
- tnfr/dynamics/nbody.py +796 -0
- tnfr/dynamics/nbody_tnfr.py +783 -0
- tnfr/dynamics/propagation.py +326 -0
- tnfr/dynamics/runtime.py +908 -0
- tnfr/dynamics/runtime.pyi +77 -0
- tnfr/dynamics/sampling.py +36 -0
- tnfr/dynamics/sampling.pyi +7 -0
- tnfr/dynamics/selectors.py +711 -0
- tnfr/dynamics/selectors.pyi +85 -0
- tnfr/dynamics/structural_clip.py +207 -0
- tnfr/errors/__init__.py +37 -0
- tnfr/errors/contextual.py +492 -0
- tnfr/execution.py +223 -0
- tnfr/execution.pyi +45 -0
- tnfr/extensions/__init__.py +205 -0
- tnfr/extensions/__init__.pyi +18 -0
- tnfr/extensions/base.py +173 -0
- tnfr/extensions/base.pyi +35 -0
- tnfr/extensions/business/__init__.py +71 -0
- tnfr/extensions/business/__init__.pyi +11 -0
- tnfr/extensions/business/cookbook.py +88 -0
- tnfr/extensions/business/cookbook.pyi +8 -0
- tnfr/extensions/business/health_analyzers.py +202 -0
- tnfr/extensions/business/health_analyzers.pyi +9 -0
- tnfr/extensions/business/patterns.py +183 -0
- tnfr/extensions/business/patterns.pyi +8 -0
- tnfr/extensions/medical/__init__.py +73 -0
- tnfr/extensions/medical/__init__.pyi +11 -0
- tnfr/extensions/medical/cookbook.py +88 -0
- tnfr/extensions/medical/cookbook.pyi +8 -0
- tnfr/extensions/medical/health_analyzers.py +181 -0
- tnfr/extensions/medical/health_analyzers.pyi +9 -0
- tnfr/extensions/medical/patterns.py +163 -0
- tnfr/extensions/medical/patterns.pyi +8 -0
- tnfr/flatten.py +262 -0
- tnfr/flatten.pyi +21 -0
- tnfr/gamma.py +354 -0
- tnfr/gamma.pyi +36 -0
- tnfr/glyph_history.py +377 -0
- tnfr/glyph_history.pyi +35 -0
- tnfr/glyph_runtime.py +19 -0
- tnfr/glyph_runtime.pyi +8 -0
- tnfr/immutable.py +218 -0
- tnfr/immutable.pyi +36 -0
- tnfr/initialization.py +203 -0
- tnfr/initialization.pyi +65 -0
- tnfr/io.py +10 -0
- tnfr/io.pyi +13 -0
- tnfr/locking.py +37 -0
- tnfr/locking.pyi +7 -0
- tnfr/mathematics/__init__.py +79 -0
- tnfr/mathematics/backend.py +453 -0
- tnfr/mathematics/backend.pyi +99 -0
- tnfr/mathematics/dynamics.py +408 -0
- tnfr/mathematics/dynamics.pyi +90 -0
- tnfr/mathematics/epi.py +391 -0
- tnfr/mathematics/epi.pyi +65 -0
- tnfr/mathematics/generators.py +242 -0
- tnfr/mathematics/generators.pyi +29 -0
- tnfr/mathematics/metrics.py +119 -0
- tnfr/mathematics/metrics.pyi +16 -0
- tnfr/mathematics/operators.py +239 -0
- tnfr/mathematics/operators.pyi +59 -0
- tnfr/mathematics/operators_factory.py +124 -0
- tnfr/mathematics/operators_factory.pyi +11 -0
- tnfr/mathematics/projection.py +87 -0
- tnfr/mathematics/projection.pyi +33 -0
- tnfr/mathematics/runtime.py +182 -0
- tnfr/mathematics/runtime.pyi +64 -0
- tnfr/mathematics/spaces.py +256 -0
- tnfr/mathematics/spaces.pyi +83 -0
- tnfr/mathematics/transforms.py +305 -0
- tnfr/mathematics/transforms.pyi +62 -0
- tnfr/metrics/__init__.py +79 -0
- tnfr/metrics/__init__.pyi +20 -0
- tnfr/metrics/buffer_cache.py +163 -0
- tnfr/metrics/buffer_cache.pyi +24 -0
- tnfr/metrics/cache_utils.py +214 -0
- tnfr/metrics/coherence.py +2009 -0
- tnfr/metrics/coherence.pyi +129 -0
- tnfr/metrics/common.py +158 -0
- tnfr/metrics/common.pyi +35 -0
- tnfr/metrics/core.py +316 -0
- tnfr/metrics/core.pyi +13 -0
- tnfr/metrics/diagnosis.py +833 -0
- tnfr/metrics/diagnosis.pyi +86 -0
- tnfr/metrics/emergence.py +245 -0
- tnfr/metrics/export.py +179 -0
- tnfr/metrics/export.pyi +7 -0
- tnfr/metrics/glyph_timing.py +379 -0
- tnfr/metrics/glyph_timing.pyi +81 -0
- tnfr/metrics/learning_metrics.py +280 -0
- tnfr/metrics/learning_metrics.pyi +21 -0
- tnfr/metrics/phase_coherence.py +351 -0
- tnfr/metrics/phase_compatibility.py +349 -0
- tnfr/metrics/reporting.py +183 -0
- tnfr/metrics/reporting.pyi +25 -0
- tnfr/metrics/sense_index.py +1203 -0
- tnfr/metrics/sense_index.pyi +9 -0
- tnfr/metrics/trig.py +373 -0
- tnfr/metrics/trig.pyi +13 -0
- tnfr/metrics/trig_cache.py +233 -0
- tnfr/metrics/trig_cache.pyi +10 -0
- tnfr/multiscale/__init__.py +32 -0
- tnfr/multiscale/hierarchical.py +517 -0
- tnfr/node.py +763 -0
- tnfr/node.pyi +139 -0
- tnfr/observers.py +255 -130
- tnfr/observers.pyi +31 -0
- tnfr/ontosim.py +144 -137
- tnfr/ontosim.pyi +28 -0
- tnfr/operators/__init__.py +1672 -0
- tnfr/operators/__init__.pyi +31 -0
- tnfr/operators/algebra.py +277 -0
- tnfr/operators/canonical_patterns.py +420 -0
- tnfr/operators/cascade.py +267 -0
- tnfr/operators/cycle_detection.py +358 -0
- tnfr/operators/definitions.py +4108 -0
- tnfr/operators/definitions.pyi +78 -0
- tnfr/operators/grammar.py +1164 -0
- tnfr/operators/grammar.pyi +140 -0
- tnfr/operators/hamiltonian.py +710 -0
- tnfr/operators/health_analyzer.py +809 -0
- tnfr/operators/jitter.py +272 -0
- tnfr/operators/jitter.pyi +11 -0
- tnfr/operators/lifecycle.py +314 -0
- tnfr/operators/metabolism.py +618 -0
- tnfr/operators/metrics.py +2138 -0
- tnfr/operators/network_analysis/__init__.py +27 -0
- tnfr/operators/network_analysis/source_detection.py +186 -0
- tnfr/operators/nodal_equation.py +395 -0
- tnfr/operators/pattern_detection.py +660 -0
- tnfr/operators/patterns.py +669 -0
- tnfr/operators/postconditions/__init__.py +38 -0
- tnfr/operators/postconditions/mutation.py +236 -0
- tnfr/operators/preconditions/__init__.py +1226 -0
- tnfr/operators/preconditions/coherence.py +305 -0
- tnfr/operators/preconditions/dissonance.py +236 -0
- tnfr/operators/preconditions/emission.py +128 -0
- tnfr/operators/preconditions/mutation.py +580 -0
- tnfr/operators/preconditions/reception.py +125 -0
- tnfr/operators/preconditions/resonance.py +364 -0
- tnfr/operators/registry.py +74 -0
- tnfr/operators/registry.pyi +9 -0
- tnfr/operators/remesh.py +1809 -0
- tnfr/operators/remesh.pyi +26 -0
- tnfr/operators/structural_units.py +268 -0
- tnfr/operators/unified_grammar.py +105 -0
- tnfr/parallel/__init__.py +54 -0
- tnfr/parallel/auto_scaler.py +234 -0
- tnfr/parallel/distributed.py +384 -0
- tnfr/parallel/engine.py +238 -0
- tnfr/parallel/gpu_engine.py +420 -0
- tnfr/parallel/monitoring.py +248 -0
- tnfr/parallel/partitioner.py +459 -0
- tnfr/py.typed +0 -0
- tnfr/recipes/__init__.py +22 -0
- tnfr/recipes/cookbook.py +743 -0
- tnfr/rng.py +178 -0
- tnfr/rng.pyi +26 -0
- tnfr/schemas/__init__.py +8 -0
- tnfr/schemas/grammar.json +94 -0
- tnfr/sdk/__init__.py +107 -0
- tnfr/sdk/__init__.pyi +19 -0
- tnfr/sdk/adaptive_system.py +173 -0
- tnfr/sdk/adaptive_system.pyi +21 -0
- tnfr/sdk/builders.py +370 -0
- tnfr/sdk/builders.pyi +51 -0
- tnfr/sdk/fluent.py +1121 -0
- tnfr/sdk/fluent.pyi +74 -0
- tnfr/sdk/templates.py +342 -0
- tnfr/sdk/templates.pyi +41 -0
- tnfr/sdk/utils.py +341 -0
- tnfr/secure_config.py +46 -0
- tnfr/security/__init__.py +70 -0
- tnfr/security/database.py +514 -0
- tnfr/security/subprocess.py +503 -0
- tnfr/security/validation.py +290 -0
- tnfr/selector.py +247 -0
- tnfr/selector.pyi +19 -0
- tnfr/sense.py +378 -0
- tnfr/sense.pyi +23 -0
- tnfr/services/__init__.py +17 -0
- tnfr/services/orchestrator.py +325 -0
- tnfr/sparse/__init__.py +39 -0
- tnfr/sparse/representations.py +492 -0
- tnfr/structural.py +705 -0
- tnfr/structural.pyi +83 -0
- tnfr/telemetry/__init__.py +35 -0
- tnfr/telemetry/cache_metrics.py +226 -0
- tnfr/telemetry/cache_metrics.pyi +64 -0
- tnfr/telemetry/nu_f.py +422 -0
- tnfr/telemetry/nu_f.pyi +108 -0
- tnfr/telemetry/verbosity.py +36 -0
- tnfr/telemetry/verbosity.pyi +15 -0
- tnfr/tokens.py +58 -0
- tnfr/tokens.pyi +36 -0
- tnfr/tools/__init__.py +20 -0
- tnfr/tools/domain_templates.py +478 -0
- tnfr/tools/sequence_generator.py +846 -0
- tnfr/topology/__init__.py +13 -0
- tnfr/topology/asymmetry.py +151 -0
- tnfr/trace.py +543 -0
- tnfr/trace.pyi +42 -0
- tnfr/tutorials/__init__.py +38 -0
- tnfr/tutorials/autonomous_evolution.py +285 -0
- tnfr/tutorials/interactive.py +1576 -0
- tnfr/tutorials/structural_metabolism.py +238 -0
- tnfr/types.py +775 -0
- tnfr/types.pyi +357 -0
- tnfr/units.py +68 -0
- tnfr/units.pyi +13 -0
- tnfr/utils/__init__.py +282 -0
- tnfr/utils/__init__.pyi +215 -0
- tnfr/utils/cache.py +4223 -0
- tnfr/utils/cache.pyi +470 -0
- tnfr/utils/callbacks.py +375 -0
- tnfr/utils/callbacks.pyi +49 -0
- tnfr/utils/chunks.py +108 -0
- tnfr/utils/chunks.pyi +22 -0
- tnfr/utils/data.py +428 -0
- tnfr/utils/data.pyi +74 -0
- tnfr/utils/graph.py +85 -0
- tnfr/utils/graph.pyi +10 -0
- tnfr/utils/init.py +821 -0
- tnfr/utils/init.pyi +80 -0
- tnfr/utils/io.py +559 -0
- tnfr/utils/io.pyi +66 -0
- tnfr/utils/numeric.py +114 -0
- tnfr/utils/numeric.pyi +21 -0
- tnfr/validation/__init__.py +257 -0
- tnfr/validation/__init__.pyi +85 -0
- tnfr/validation/compatibility.py +460 -0
- tnfr/validation/compatibility.pyi +6 -0
- tnfr/validation/config.py +73 -0
- tnfr/validation/graph.py +139 -0
- tnfr/validation/graph.pyi +18 -0
- tnfr/validation/input_validation.py +755 -0
- tnfr/validation/invariants.py +712 -0
- tnfr/validation/rules.py +253 -0
- tnfr/validation/rules.pyi +44 -0
- tnfr/validation/runtime.py +279 -0
- tnfr/validation/runtime.pyi +28 -0
- tnfr/validation/sequence_validator.py +162 -0
- tnfr/validation/soft_filters.py +170 -0
- tnfr/validation/soft_filters.pyi +32 -0
- tnfr/validation/spectral.py +164 -0
- tnfr/validation/spectral.pyi +42 -0
- tnfr/validation/validator.py +1266 -0
- tnfr/validation/window.py +39 -0
- tnfr/validation/window.pyi +1 -0
- tnfr/visualization/__init__.py +98 -0
- tnfr/visualization/cascade_viz.py +256 -0
- tnfr/visualization/hierarchy.py +284 -0
- tnfr/visualization/sequence_plotter.py +784 -0
- tnfr/viz/__init__.py +60 -0
- tnfr/viz/matplotlib.py +278 -0
- tnfr/viz/matplotlib.pyi +35 -0
- tnfr-8.5.0.dist-info/METADATA +573 -0
- tnfr-8.5.0.dist-info/RECORD +353 -0
- tnfr-8.5.0.dist-info/entry_points.txt +3 -0
- tnfr-3.0.3.dist-info/licenses/LICENSE.txt → tnfr-8.5.0.dist-info/licenses/LICENSE.md +1 -1
- tnfr/constants.py +0 -183
- tnfr/dynamics.py +0 -543
- tnfr/helpers.py +0 -198
- tnfr/main.py +0 -37
- tnfr/operators.py +0 -296
- tnfr-3.0.3.dist-info/METADATA +0 -35
- tnfr-3.0.3.dist-info/RECORD +0 -13
- {tnfr-3.0.3.dist-info → tnfr-8.5.0.dist-info}/WHEEL +0 -0
- {tnfr-3.0.3.dist-info → tnfr-8.5.0.dist-info}/top_level.txt +0 -0
tnfr/utils/callbacks.py
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
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
|
+
import threading
|
|
13
|
+
import traceback
|
|
14
|
+
from collections import defaultdict, deque
|
|
15
|
+
from collections.abc import Callable, Iterable, Mapping
|
|
16
|
+
from enum import Enum
|
|
17
|
+
from typing import Any, NamedTuple
|
|
18
|
+
|
|
19
|
+
import networkx as nx
|
|
20
|
+
|
|
21
|
+
from ..constants import DEFAULTS
|
|
22
|
+
from ..locking import get_lock
|
|
23
|
+
from .init import get_logger
|
|
24
|
+
from .data import is_non_string_sequence
|
|
25
|
+
from ..types import CallbackError
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CallbackSpec(NamedTuple):
|
|
29
|
+
"""Specification for a registered callback."""
|
|
30
|
+
|
|
31
|
+
name: str | None
|
|
32
|
+
func: Callable[..., Any]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
__all__ = (
|
|
36
|
+
"CallbackEvent",
|
|
37
|
+
"CallbackManager",
|
|
38
|
+
"callback_manager",
|
|
39
|
+
"CallbackError",
|
|
40
|
+
"CallbackSpec",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
logger = get_logger(__name__)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class CallbackEvent(str, Enum):
|
|
47
|
+
"""Supported callback events."""
|
|
48
|
+
|
|
49
|
+
BEFORE_STEP = "before_step"
|
|
50
|
+
AFTER_STEP = "after_step"
|
|
51
|
+
ON_REMESH = "on_remesh"
|
|
52
|
+
CACHE_METRICS = "cache_metrics"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class CallbackManager:
|
|
56
|
+
"""Centralised registry and error tracking for callbacks."""
|
|
57
|
+
|
|
58
|
+
def __init__(self) -> None:
|
|
59
|
+
self._lock = get_lock("callbacks")
|
|
60
|
+
self._error_limit_lock = threading.Lock()
|
|
61
|
+
self._error_limit = 100
|
|
62
|
+
self._error_limit_cache = self._error_limit
|
|
63
|
+
|
|
64
|
+
# ------------------------------------------------------------------
|
|
65
|
+
# Error limit management
|
|
66
|
+
# ------------------------------------------------------------------
|
|
67
|
+
def get_callback_error_limit(self) -> int:
|
|
68
|
+
"""Return the current callback error retention limit."""
|
|
69
|
+
with self._error_limit_lock:
|
|
70
|
+
return self._error_limit
|
|
71
|
+
|
|
72
|
+
def set_callback_error_limit(self, limit: int) -> int:
|
|
73
|
+
"""Set the maximum number of callback errors retained."""
|
|
74
|
+
if limit < 1:
|
|
75
|
+
raise ValueError("limit must be positive")
|
|
76
|
+
with self._error_limit_lock:
|
|
77
|
+
previous = self._error_limit
|
|
78
|
+
self._error_limit = int(limit)
|
|
79
|
+
self._error_limit_cache = self._error_limit
|
|
80
|
+
return previous
|
|
81
|
+
|
|
82
|
+
# ------------------------------------------------------------------
|
|
83
|
+
# Registry helpers
|
|
84
|
+
# ------------------------------------------------------------------
|
|
85
|
+
def _record_callback_error(
|
|
86
|
+
self,
|
|
87
|
+
G: "nx.Graph",
|
|
88
|
+
event: str,
|
|
89
|
+
ctx: dict[str, Any],
|
|
90
|
+
spec: CallbackSpec,
|
|
91
|
+
err: Exception,
|
|
92
|
+
) -> None:
|
|
93
|
+
"""Log and store a callback error for later inspection."""
|
|
94
|
+
|
|
95
|
+
logger.exception("callback %r failed for %s: %s", spec.name, event, err)
|
|
96
|
+
limit = self._error_limit_cache
|
|
97
|
+
err_list = G.graph.setdefault(
|
|
98
|
+
"_callback_errors", deque[CallbackError](maxlen=limit)
|
|
99
|
+
)
|
|
100
|
+
if err_list.maxlen != limit:
|
|
101
|
+
err_list = deque[CallbackError](err_list, maxlen=limit)
|
|
102
|
+
G.graph["_callback_errors"] = err_list
|
|
103
|
+
error: CallbackError = {
|
|
104
|
+
"event": event,
|
|
105
|
+
"step": ctx.get("step"),
|
|
106
|
+
"error": repr(err),
|
|
107
|
+
"traceback": traceback.format_exc(),
|
|
108
|
+
"fn": _func_id(spec.func),
|
|
109
|
+
"name": spec.name,
|
|
110
|
+
}
|
|
111
|
+
err_list.append(error)
|
|
112
|
+
|
|
113
|
+
def _ensure_callbacks_nolock(self, G: "nx.Graph") -> CallbackRegistry:
|
|
114
|
+
cbs = G.graph.setdefault("callbacks", defaultdict(dict))
|
|
115
|
+
dirty: set[str] = set(G.graph.pop("_callbacks_dirty", ()))
|
|
116
|
+
return _validate_registry(G, cbs, dirty)
|
|
117
|
+
|
|
118
|
+
def _ensure_callbacks(self, G: "nx.Graph") -> CallbackRegistry:
|
|
119
|
+
with self._lock:
|
|
120
|
+
return self._ensure_callbacks_nolock(G)
|
|
121
|
+
|
|
122
|
+
def register_callback(
|
|
123
|
+
self,
|
|
124
|
+
G: "nx.Graph",
|
|
125
|
+
event: CallbackEvent | str,
|
|
126
|
+
func: Callback,
|
|
127
|
+
*,
|
|
128
|
+
name: str | None = None,
|
|
129
|
+
) -> Callback:
|
|
130
|
+
"""Register ``func`` as callback for ``event``."""
|
|
131
|
+
|
|
132
|
+
event = _normalize_event(event)
|
|
133
|
+
_ensure_known_event(event)
|
|
134
|
+
if not callable(func):
|
|
135
|
+
raise TypeError("func must be callable")
|
|
136
|
+
with self._lock:
|
|
137
|
+
cbs = self._ensure_callbacks_nolock(G)
|
|
138
|
+
|
|
139
|
+
cb_name = name or getattr(func, "__name__", None)
|
|
140
|
+
spec = CallbackSpec(cb_name, func)
|
|
141
|
+
existing_map = cbs[event]
|
|
142
|
+
strict = bool(G.graph.get("CALLBACKS_STRICT", DEFAULTS["CALLBACKS_STRICT"]))
|
|
143
|
+
key = _reconcile_callback(event, existing_map, spec, strict)
|
|
144
|
+
|
|
145
|
+
existing_map[key] = spec
|
|
146
|
+
dirty = G.graph.setdefault("_callbacks_dirty", set())
|
|
147
|
+
dirty.add(event)
|
|
148
|
+
return func
|
|
149
|
+
|
|
150
|
+
def invoke_callbacks(
|
|
151
|
+
self,
|
|
152
|
+
G: "nx.Graph",
|
|
153
|
+
event: CallbackEvent | str,
|
|
154
|
+
ctx: dict[str, Any] | None = None,
|
|
155
|
+
) -> None:
|
|
156
|
+
"""Invoke all callbacks registered for ``event`` with context ``ctx``."""
|
|
157
|
+
|
|
158
|
+
event = _normalize_event(event)
|
|
159
|
+
with self._lock:
|
|
160
|
+
cbs = dict(self._ensure_callbacks_nolock(G).get(event, {}))
|
|
161
|
+
strict = bool(G.graph.get("CALLBACKS_STRICT", DEFAULTS["CALLBACKS_STRICT"]))
|
|
162
|
+
if ctx is None:
|
|
163
|
+
ctx = {}
|
|
164
|
+
for spec in cbs.values():
|
|
165
|
+
try:
|
|
166
|
+
spec.func(G, ctx)
|
|
167
|
+
except (
|
|
168
|
+
RuntimeError,
|
|
169
|
+
ValueError,
|
|
170
|
+
TypeError,
|
|
171
|
+
) as e:
|
|
172
|
+
with self._lock:
|
|
173
|
+
self._record_callback_error(G, event, ctx, spec, e)
|
|
174
|
+
if strict:
|
|
175
|
+
raise
|
|
176
|
+
except nx.NetworkXError as err:
|
|
177
|
+
with self._lock:
|
|
178
|
+
self._record_callback_error(G, event, ctx, spec, err)
|
|
179
|
+
logger.exception(
|
|
180
|
+
"callback %r raised NetworkXError for %s with ctx=%r",
|
|
181
|
+
spec.name,
|
|
182
|
+
event,
|
|
183
|
+
ctx,
|
|
184
|
+
)
|
|
185
|
+
raise
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
Callback = Callable[["nx.Graph", dict[str, Any]], None]
|
|
189
|
+
CallbackRegistry = dict[str, dict[str, "CallbackSpec"]]
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _func_id(fn: Callable[..., Any]) -> str:
|
|
193
|
+
"""Return a deterministic identifier for ``fn``.
|
|
194
|
+
|
|
195
|
+
Combines the function's module and qualified name to avoid the
|
|
196
|
+
nondeterminism of ``repr(fn)`` which includes the memory address.
|
|
197
|
+
"""
|
|
198
|
+
module = getattr(fn, "__module__", fn.__class__.__module__)
|
|
199
|
+
qualname = getattr(
|
|
200
|
+
fn,
|
|
201
|
+
"__qualname__",
|
|
202
|
+
getattr(fn, "__name__", fn.__class__.__qualname__),
|
|
203
|
+
)
|
|
204
|
+
return f"{module}.{qualname}"
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _validate_registry(G: "nx.Graph", cbs: Any, dirty: set[str]) -> CallbackRegistry:
|
|
208
|
+
"""Validate and normalise the callback registry.
|
|
209
|
+
|
|
210
|
+
``cbs`` is coerced to a ``defaultdict(dict)`` and any events listed in
|
|
211
|
+
``dirty`` are rebuilt using :func:`_normalize_callbacks`. Unknown events are
|
|
212
|
+
removed. The cleaned registry is stored back on the graph and returned.
|
|
213
|
+
"""
|
|
214
|
+
|
|
215
|
+
if not isinstance(cbs, Mapping):
|
|
216
|
+
logger.warning(
|
|
217
|
+
"Invalid callbacks registry on graph; resetting to empty",
|
|
218
|
+
)
|
|
219
|
+
cbs = defaultdict(dict)
|
|
220
|
+
elif not isinstance(cbs, defaultdict) or cbs.default_factory is not dict:
|
|
221
|
+
cbs = defaultdict(
|
|
222
|
+
dict,
|
|
223
|
+
{
|
|
224
|
+
event: _normalize_callbacks(entries)
|
|
225
|
+
for event, entries in dict(cbs).items()
|
|
226
|
+
if _is_known_event(event)
|
|
227
|
+
},
|
|
228
|
+
)
|
|
229
|
+
else:
|
|
230
|
+
for event in dirty:
|
|
231
|
+
if _is_known_event(event):
|
|
232
|
+
cbs[event] = _normalize_callbacks(cbs.get(event))
|
|
233
|
+
else:
|
|
234
|
+
cbs.pop(event, None)
|
|
235
|
+
|
|
236
|
+
G.graph["callbacks"] = cbs
|
|
237
|
+
return cbs
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _normalize_callbacks(entries: Any) -> dict[str, CallbackSpec]:
|
|
241
|
+
"""Return ``entries`` normalised into a callback mapping."""
|
|
242
|
+
if isinstance(entries, Mapping):
|
|
243
|
+
entries_iter = entries.values()
|
|
244
|
+
elif isinstance(entries, Iterable) and not isinstance(
|
|
245
|
+
entries, (str, bytes, bytearray)
|
|
246
|
+
):
|
|
247
|
+
entries_iter = entries
|
|
248
|
+
else:
|
|
249
|
+
return {}
|
|
250
|
+
|
|
251
|
+
new_map: dict[str, CallbackSpec] = {}
|
|
252
|
+
for entry in entries_iter:
|
|
253
|
+
spec = _normalize_callback_entry(entry)
|
|
254
|
+
if spec is None:
|
|
255
|
+
continue
|
|
256
|
+
key = spec.name or _func_id(spec.func)
|
|
257
|
+
new_map[key] = spec
|
|
258
|
+
return new_map
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _normalize_event(event: CallbackEvent | str) -> str:
|
|
262
|
+
"""Return ``event`` as a string."""
|
|
263
|
+
return event.value if isinstance(event, CallbackEvent) else str(event)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _is_known_event(event: str) -> bool:
|
|
267
|
+
"""Return ``True`` when ``event`` matches a declared :class:`CallbackEvent`."""
|
|
268
|
+
|
|
269
|
+
try:
|
|
270
|
+
CallbackEvent(event)
|
|
271
|
+
except ValueError:
|
|
272
|
+
return False
|
|
273
|
+
else:
|
|
274
|
+
return True
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _ensure_known_event(event: str) -> None:
|
|
278
|
+
"""Raise :class:`ValueError` when ``event`` is not a known callback."""
|
|
279
|
+
|
|
280
|
+
try:
|
|
281
|
+
CallbackEvent(event)
|
|
282
|
+
except ValueError as exc: # pragma: no cover - defensive branch
|
|
283
|
+
raise ValueError(f"Unknown event: {event}") from exc
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _normalize_callback_entry(entry: Any) -> "CallbackSpec | None":
|
|
287
|
+
"""Normalize a callback specification.
|
|
288
|
+
|
|
289
|
+
Supported formats
|
|
290
|
+
-----------------
|
|
291
|
+
* :class:`CallbackSpec` instances (returned unchanged).
|
|
292
|
+
* Sequences ``(name: str, func: Callable)`` such as lists, tuples or other
|
|
293
|
+
iterables.
|
|
294
|
+
* Bare callables ``func`` whose name is taken from ``func.__name__``.
|
|
295
|
+
|
|
296
|
+
``None`` is returned when ``entry`` does not match any of the accepted
|
|
297
|
+
formats. The original ``entry`` is never mutated. Sequence inputs are
|
|
298
|
+
converted to ``tuple`` before validation to support generators; the
|
|
299
|
+
materialization consumes the iterable and failure results in ``None``.
|
|
300
|
+
"""
|
|
301
|
+
|
|
302
|
+
if isinstance(entry, CallbackSpec):
|
|
303
|
+
return entry
|
|
304
|
+
elif is_non_string_sequence(entry):
|
|
305
|
+
try:
|
|
306
|
+
entry = tuple(entry)
|
|
307
|
+
except TypeError:
|
|
308
|
+
return None
|
|
309
|
+
if len(entry) != 2:
|
|
310
|
+
return None
|
|
311
|
+
name, fn = entry
|
|
312
|
+
if not isinstance(name, str) or not callable(fn):
|
|
313
|
+
return None
|
|
314
|
+
return CallbackSpec(name, fn)
|
|
315
|
+
elif callable(entry):
|
|
316
|
+
name = getattr(entry, "__name__", None)
|
|
317
|
+
return CallbackSpec(name, entry)
|
|
318
|
+
else:
|
|
319
|
+
return None
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _reconcile_callback(
|
|
323
|
+
event: str,
|
|
324
|
+
existing_map: dict[str, CallbackSpec],
|
|
325
|
+
spec: CallbackSpec,
|
|
326
|
+
strict: bool,
|
|
327
|
+
) -> str:
|
|
328
|
+
"""Reconcile ``spec`` with ``existing_map``.
|
|
329
|
+
|
|
330
|
+
Ensures that callbacks remain unique by explicit name or function identity.
|
|
331
|
+
When a name collision occurs with a different function, ``strict`` controls
|
|
332
|
+
whether a :class:`ValueError` is raised or a warning is logged.
|
|
333
|
+
|
|
334
|
+
Parameters
|
|
335
|
+
----------
|
|
336
|
+
event:
|
|
337
|
+
Event under which ``spec`` will be registered. Only used for messages.
|
|
338
|
+
existing_map:
|
|
339
|
+
Current mapping of callbacks for ``event``.
|
|
340
|
+
spec:
|
|
341
|
+
Callback specification being registered.
|
|
342
|
+
strict:
|
|
343
|
+
Whether to raise on name collisions instead of logging a warning.
|
|
344
|
+
|
|
345
|
+
Returns
|
|
346
|
+
-------
|
|
347
|
+
str
|
|
348
|
+
Key under which ``spec`` should be stored in ``existing_map``.
|
|
349
|
+
"""
|
|
350
|
+
|
|
351
|
+
key = spec.name or _func_id(spec.func)
|
|
352
|
+
|
|
353
|
+
if spec.name is not None:
|
|
354
|
+
existing_spec = existing_map.get(key)
|
|
355
|
+
if existing_spec is not None and existing_spec.func is not spec.func:
|
|
356
|
+
msg = f"Callback {spec.name!r} already registered for {event}"
|
|
357
|
+
if strict:
|
|
358
|
+
raise ValueError(msg)
|
|
359
|
+
logger.warning(msg)
|
|
360
|
+
|
|
361
|
+
# Remove existing entries under the same key and any other using the same
|
|
362
|
+
# function identity to avoid duplicates.
|
|
363
|
+
existing_map.pop(key, None)
|
|
364
|
+
fn_key = next((k for k, s in existing_map.items() if s.func is spec.func), None)
|
|
365
|
+
if fn_key is not None:
|
|
366
|
+
existing_map.pop(fn_key, None)
|
|
367
|
+
|
|
368
|
+
return key
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
# ---------------------------------------------------------------------------
|
|
372
|
+
# Default manager instance and convenience wrappers
|
|
373
|
+
# ---------------------------------------------------------------------------
|
|
374
|
+
|
|
375
|
+
callback_manager = CallbackManager()
|
tnfr/utils/callbacks.pyi
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import networkx as nx
|
|
4
|
+
from ..types import CallbackError as CallbackError
|
|
5
|
+
from _typeshed import Incomplete
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import Any, NamedTuple
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"CallbackEvent",
|
|
12
|
+
"CallbackManager",
|
|
13
|
+
"callback_manager",
|
|
14
|
+
"CallbackError",
|
|
15
|
+
"CallbackSpec",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
Callback = Callable[[nx.Graph, dict[str, Any]], None]
|
|
19
|
+
|
|
20
|
+
class CallbackSpec(NamedTuple):
|
|
21
|
+
name: str | None
|
|
22
|
+
func: Callable[..., Any]
|
|
23
|
+
|
|
24
|
+
class CallbackEvent(str, Enum):
|
|
25
|
+
BEFORE_STEP = "before_step"
|
|
26
|
+
AFTER_STEP = "after_step"
|
|
27
|
+
ON_REMESH = "on_remesh"
|
|
28
|
+
CACHE_METRICS = "cache_metrics"
|
|
29
|
+
|
|
30
|
+
class CallbackManager:
|
|
31
|
+
def __init__(self) -> None: ...
|
|
32
|
+
def get_callback_error_limit(self) -> int: ...
|
|
33
|
+
def set_callback_error_limit(self, limit: int) -> int: ...
|
|
34
|
+
def register_callback(
|
|
35
|
+
self,
|
|
36
|
+
G: nx.Graph,
|
|
37
|
+
event: CallbackEvent | str,
|
|
38
|
+
func: Callback,
|
|
39
|
+
*,
|
|
40
|
+
name: str | None = None,
|
|
41
|
+
) -> Callback: ...
|
|
42
|
+
def invoke_callbacks(
|
|
43
|
+
self, G: nx.Graph, event: CallbackEvent | str, ctx: dict[str, Any] | None = None
|
|
44
|
+
) -> None: ...
|
|
45
|
+
|
|
46
|
+
callback_manager: Incomplete
|
|
47
|
+
|
|
48
|
+
def _normalize_callbacks(entries: Any) -> dict[str, CallbackSpec]: ...
|
|
49
|
+
def _normalize_callback_entry(entry: Any) -> CallbackSpec | None: ...
|
tnfr/utils/chunks.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Chunk sizing heuristics for batching structural computations.
|
|
2
|
+
|
|
3
|
+
The helpers in this module determine how large each processing block should be
|
|
4
|
+
when splitting work across workers or vectorised loops. They take into account
|
|
5
|
+
the number of items involved, approximate memory pressure, and available CPU
|
|
6
|
+
parallelism so the caller can balance throughput with deterministic behaviour.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import math
|
|
12
|
+
import os
|
|
13
|
+
from typing import Final
|
|
14
|
+
|
|
15
|
+
DEFAULT_APPROX_BYTES_PER_ITEM: Final[int] = 64
|
|
16
|
+
DEFAULT_CHUNK_CLAMP: Final[int] | None = 131_072
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _estimate_available_memory() -> int | None:
|
|
20
|
+
"""Best-effort estimation of free memory available to the process."""
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
page_size = os.sysconf("SC_PAGE_SIZE")
|
|
24
|
+
avail_pages = os.sysconf("SC_AVPHYS_PAGES")
|
|
25
|
+
except (
|
|
26
|
+
AttributeError,
|
|
27
|
+
ValueError,
|
|
28
|
+
OSError,
|
|
29
|
+
): # pragma: no cover - platform specific
|
|
30
|
+
return None
|
|
31
|
+
if page_size <= 0 or avail_pages <= 0:
|
|
32
|
+
return None
|
|
33
|
+
return int(page_size) * int(avail_pages)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def auto_chunk_size(
|
|
37
|
+
total_items: int,
|
|
38
|
+
*,
|
|
39
|
+
minimum: int = 1,
|
|
40
|
+
approx_bytes_per_item: int = DEFAULT_APPROX_BYTES_PER_ITEM,
|
|
41
|
+
clamp_to: int | None = DEFAULT_CHUNK_CLAMP,
|
|
42
|
+
) -> int:
|
|
43
|
+
"""Infer a safe chunk length when the caller does not specify one."""
|
|
44
|
+
|
|
45
|
+
if total_items <= 0:
|
|
46
|
+
return 0
|
|
47
|
+
|
|
48
|
+
minimum = max(1, minimum)
|
|
49
|
+
approx_bytes_per_item = max(1, approx_bytes_per_item)
|
|
50
|
+
|
|
51
|
+
available_memory = _estimate_available_memory()
|
|
52
|
+
if available_memory is not None and available_memory > 0:
|
|
53
|
+
safe_bytes = max(approx_bytes_per_item * minimum, available_memory // 8)
|
|
54
|
+
mem_bound = max(minimum, min(total_items, safe_bytes // approx_bytes_per_item))
|
|
55
|
+
else:
|
|
56
|
+
mem_bound = total_items
|
|
57
|
+
|
|
58
|
+
if clamp_to is not None:
|
|
59
|
+
mem_bound = min(mem_bound, clamp_to)
|
|
60
|
+
|
|
61
|
+
cpu_count = os.cpu_count() or 1
|
|
62
|
+
target_chunks = max(1, cpu_count * 4)
|
|
63
|
+
cpu_chunk = max(minimum, math.ceil(total_items / target_chunks))
|
|
64
|
+
baseline = max(minimum, min(total_items, 1024))
|
|
65
|
+
target = max(cpu_chunk, baseline)
|
|
66
|
+
|
|
67
|
+
chunk = min(mem_bound, target)
|
|
68
|
+
chunk = max(minimum, min(total_items, chunk))
|
|
69
|
+
return chunk
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def resolve_chunk_size(
|
|
73
|
+
chunk_size: int | None,
|
|
74
|
+
total_items: int,
|
|
75
|
+
*,
|
|
76
|
+
minimum: int = 1,
|
|
77
|
+
approx_bytes_per_item: int = DEFAULT_APPROX_BYTES_PER_ITEM,
|
|
78
|
+
clamp_to: int | None = DEFAULT_CHUNK_CLAMP,
|
|
79
|
+
) -> int:
|
|
80
|
+
"""Return a sanitised chunk size honouring automatic fallbacks."""
|
|
81
|
+
|
|
82
|
+
if total_items <= 0:
|
|
83
|
+
return 0
|
|
84
|
+
|
|
85
|
+
resolved: int | None
|
|
86
|
+
if chunk_size is None:
|
|
87
|
+
resolved = None
|
|
88
|
+
else:
|
|
89
|
+
try:
|
|
90
|
+
resolved = int(chunk_size)
|
|
91
|
+
except (TypeError, ValueError):
|
|
92
|
+
resolved = None
|
|
93
|
+
else:
|
|
94
|
+
if resolved <= 0:
|
|
95
|
+
resolved = None
|
|
96
|
+
|
|
97
|
+
if resolved is None:
|
|
98
|
+
resolved = auto_chunk_size(
|
|
99
|
+
total_items,
|
|
100
|
+
minimum=minimum,
|
|
101
|
+
approx_bytes_per_item=approx_bytes_per_item,
|
|
102
|
+
clamp_to=clamp_to,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
return max(minimum, min(total_items, resolved))
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
__all__ = ["auto_chunk_size", "resolve_chunk_size"]
|
tnfr/utils/chunks.pyi
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Final
|
|
4
|
+
|
|
5
|
+
DEFAULT_APPROX_BYTES_PER_ITEM: Final[int]
|
|
6
|
+
DEFAULT_CHUNK_CLAMP: Final[int | None]
|
|
7
|
+
|
|
8
|
+
def auto_chunk_size(
|
|
9
|
+
total_items: int,
|
|
10
|
+
*,
|
|
11
|
+
minimum: int = ...,
|
|
12
|
+
approx_bytes_per_item: int = ...,
|
|
13
|
+
clamp_to: int | None = ...,
|
|
14
|
+
) -> int: ...
|
|
15
|
+
def resolve_chunk_size(
|
|
16
|
+
chunk_size: int | None,
|
|
17
|
+
total_items: int,
|
|
18
|
+
*,
|
|
19
|
+
minimum: int = ...,
|
|
20
|
+
approx_bytes_per_item: int = ...,
|
|
21
|
+
clamp_to: int | None = ...,
|
|
22
|
+
) -> int: ...
|