tnfr 4.5.2__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 +334 -50
- 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 +214 -37
- 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 +149 -556
- tnfr/cache.pyi +13 -0
- tnfr/cli/__init__.py +51 -16
- tnfr/cli/__init__.pyi +26 -0
- tnfr/cli/arguments.py +344 -32
- tnfr/cli/arguments.pyi +29 -0
- tnfr/cli/execution.py +676 -50
- tnfr/cli/execution.pyi +70 -0
- tnfr/cli/interactive_validator.py +614 -0
- tnfr/cli/utils.py +18 -3
- 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/{constants_glyphs.py → config/constants.py} +26 -20
- tnfr/config/constants.pyi +12 -0
- tnfr/config/defaults.py +54 -0
- tnfr/{constants/core.py → config/defaults_core.py} +59 -6
- 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 +51 -133
- tnfr/constants/__init__.pyi +92 -0
- tnfr/constants/aliases.py +33 -0
- tnfr/constants/aliases.pyi +27 -0
- tnfr/constants/init.py +3 -1
- tnfr/constants/init.pyi +12 -0
- tnfr/constants/metric.py +9 -15
- 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 +213 -633
- 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 +2699 -398
- 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 +496 -102
- 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 +10 -5
- 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 +77 -55
- 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 +29 -50
- tnfr/flatten.pyi +21 -0
- tnfr/gamma.py +66 -53
- tnfr/gamma.pyi +36 -0
- tnfr/glyph_history.py +144 -57
- tnfr/glyph_history.pyi +35 -0
- tnfr/glyph_runtime.py +19 -0
- tnfr/glyph_runtime.pyi +8 -0
- tnfr/immutable.py +70 -30
- tnfr/immutable.pyi +36 -0
- tnfr/initialization.py +22 -16
- tnfr/initialization.pyi +65 -0
- tnfr/io.py +5 -241
- tnfr/io.pyi +13 -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 +47 -9
- 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 +1510 -330
- tnfr/metrics/coherence.pyi +129 -0
- tnfr/metrics/common.py +23 -16
- tnfr/metrics/common.pyi +35 -0
- tnfr/metrics/core.py +251 -36
- tnfr/metrics/core.pyi +13 -0
- tnfr/metrics/diagnosis.py +709 -110
- tnfr/metrics/diagnosis.pyi +86 -0
- tnfr/metrics/emergence.py +245 -0
- tnfr/metrics/export.py +60 -18
- tnfr/metrics/export.pyi +7 -0
- tnfr/metrics/glyph_timing.py +233 -43
- 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 +63 -28
- tnfr/metrics/reporting.pyi +25 -0
- tnfr/metrics/sense_index.py +1126 -43
- tnfr/metrics/sense_index.pyi +9 -0
- tnfr/metrics/trig.py +215 -23
- tnfr/metrics/trig.pyi +13 -0
- tnfr/metrics/trig_cache.py +148 -24
- tnfr/metrics/trig_cache.pyi +10 -0
- tnfr/multiscale/__init__.py +32 -0
- tnfr/multiscale/hierarchical.py +517 -0
- tnfr/node.py +646 -140
- tnfr/node.pyi +139 -0
- tnfr/observers.py +160 -45
- tnfr/observers.pyi +31 -0
- tnfr/ontosim.py +23 -19
- tnfr/ontosim.pyi +28 -0
- tnfr/operators/__init__.py +1358 -106
- 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 +107 -38
- 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 +1415 -91
- 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 +75 -151
- 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 +59 -22
- tnfr/selector.pyi +19 -0
- tnfr/sense.py +92 -67
- 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 +639 -263
- 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 +2 -4
- 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 +300 -126
- 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 +743 -12
- 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/{callback_utils.py → utils/callbacks.py} +26 -39
- 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/{helpers → utils}/numeric.py +51 -24
- 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-4.5.2.dist-info → tnfr-8.5.0.dist-info}/entry_points.txt +1 -0
- {tnfr-4.5.2.dist-info → tnfr-8.5.0.dist-info}/licenses/LICENSE.md +1 -1
- tnfr/collections_utils.py +0 -300
- tnfr/config.py +0 -32
- tnfr/grammar.py +0 -344
- tnfr/graph_utils.py +0 -84
- tnfr/helpers/__init__.py +0 -71
- tnfr/import_utils.py +0 -228
- tnfr/json_utils.py +0 -162
- tnfr/logging_utils.py +0 -116
- tnfr/presets.py +0 -60
- tnfr/validators.py +0 -84
- tnfr/value_utils.py +0 -59
- tnfr-4.5.2.dist-info/METADATA +0 -379
- tnfr-4.5.2.dist-info/RECORD +0 -67
- {tnfr-4.5.2.dist-info → tnfr-8.5.0.dist-info}/WHEEL +0 -0
- {tnfr-4.5.2.dist-info → tnfr-8.5.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Primitive window validation helpers without heavy dependencies."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import numbers
|
|
6
|
+
|
|
7
|
+
__all__ = ["validate_window"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def validate_window(window: int, *, positive: bool = False) -> int:
|
|
11
|
+
"""Validate ``window`` as an integer and return it.
|
|
12
|
+
|
|
13
|
+
Parameters
|
|
14
|
+
----------
|
|
15
|
+
window:
|
|
16
|
+
Value to coerce into an integer window size.
|
|
17
|
+
positive:
|
|
18
|
+
When ``True`` the window must be strictly greater than zero; otherwise
|
|
19
|
+
zero is accepted. Negative values are never permitted.
|
|
20
|
+
|
|
21
|
+
Returns
|
|
22
|
+
-------
|
|
23
|
+
int
|
|
24
|
+
The validated integer window size.
|
|
25
|
+
|
|
26
|
+
Raises
|
|
27
|
+
------
|
|
28
|
+
TypeError
|
|
29
|
+
If ``window`` is not an integer value.
|
|
30
|
+
ValueError
|
|
31
|
+
If ``window`` violates the positivity constraint.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
if isinstance(window, bool) or not isinstance(window, numbers.Integral):
|
|
35
|
+
raise TypeError("'window' must be an integer")
|
|
36
|
+
if window < 0 or (positive and window == 0):
|
|
37
|
+
kind = "positive" if positive else "non-negative"
|
|
38
|
+
raise ValueError(f"'window'={window} must be {kind}")
|
|
39
|
+
return int(window)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
def validate_window(window: int, *, positive: bool = ...) -> int: ...
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Visualization tools for TNFR operator sequences and structural analysis.
|
|
2
|
+
|
|
3
|
+
This module provides advanced visualization capabilities for:
|
|
4
|
+
- Sequence flow diagrams with compatibility-colored transitions
|
|
5
|
+
- Health metrics dashboards with radar charts and gauges
|
|
6
|
+
- Pattern analysis with component highlighting
|
|
7
|
+
- Frequency timelines showing structural evolution
|
|
8
|
+
- Cascade propagation and temporal dynamics
|
|
9
|
+
- Hierarchical bifurcation structures (NEW)
|
|
10
|
+
|
|
11
|
+
Requires matplotlib for plotting (optional). Install with::
|
|
12
|
+
|
|
13
|
+
pip install tnfr[viz]
|
|
14
|
+
|
|
15
|
+
Hierarchy visualization (ASCII) has no external dependencies.
|
|
16
|
+
|
|
17
|
+
Examples
|
|
18
|
+
--------
|
|
19
|
+
>>> from tnfr.visualization import SequenceVisualizer
|
|
20
|
+
>>> from tnfr.operators.grammar import validate_sequence_with_health
|
|
21
|
+
>>>
|
|
22
|
+
>>> sequence = ["emission", "reception", "coherence", "silence"]
|
|
23
|
+
>>> result = validate_sequence_with_health(sequence)
|
|
24
|
+
>>>
|
|
25
|
+
>>> visualizer = SequenceVisualizer()
|
|
26
|
+
>>> fig, ax = visualizer.plot_sequence_flow(sequence, result.health_metrics)
|
|
27
|
+
>>> fig.savefig("sequence_flow.png")
|
|
28
|
+
|
|
29
|
+
>>> # Cascade visualization
|
|
30
|
+
>>> from tnfr.visualization import plot_cascade_propagation, plot_cascade_timeline
|
|
31
|
+
>>> fig = plot_cascade_propagation(G)
|
|
32
|
+
>>> fig.savefig("cascade_propagation.png")
|
|
33
|
+
|
|
34
|
+
>>> # Hierarchy visualization (no matplotlib required)
|
|
35
|
+
>>> from tnfr.visualization import print_bifurcation_hierarchy
|
|
36
|
+
>>> print_bifurcation_hierarchy(G, node)
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
# Always available (no dependencies)
|
|
40
|
+
from .hierarchy import (
|
|
41
|
+
print_bifurcation_hierarchy,
|
|
42
|
+
get_hierarchy_info,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
_import_error: ImportError | None = None
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
from .sequence_plotter import SequenceVisualizer
|
|
49
|
+
from .cascade_viz import (
|
|
50
|
+
plot_cascade_propagation,
|
|
51
|
+
plot_cascade_timeline,
|
|
52
|
+
plot_cascade_metrics_summary,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
__all__ = [
|
|
56
|
+
"SequenceVisualizer",
|
|
57
|
+
"plot_cascade_propagation",
|
|
58
|
+
"plot_cascade_timeline",
|
|
59
|
+
"plot_cascade_metrics_summary",
|
|
60
|
+
"print_bifurcation_hierarchy",
|
|
61
|
+
"get_hierarchy_info",
|
|
62
|
+
]
|
|
63
|
+
except ImportError as _import_err:
|
|
64
|
+
_import_error = _import_err
|
|
65
|
+
from typing import Any as _Any
|
|
66
|
+
|
|
67
|
+
def _missing_viz_dependency(*args: _Any, **kwargs: _Any) -> None:
|
|
68
|
+
missing_deps = []
|
|
69
|
+
try:
|
|
70
|
+
import matplotlib # noqa: F401
|
|
71
|
+
except ImportError:
|
|
72
|
+
missing_deps.append("matplotlib")
|
|
73
|
+
|
|
74
|
+
if missing_deps:
|
|
75
|
+
deps_str = " and ".join(missing_deps)
|
|
76
|
+
raise ImportError(
|
|
77
|
+
f"Visualization functions require {deps_str}. "
|
|
78
|
+
"Install with: pip install tnfr[viz]"
|
|
79
|
+
) from _import_error
|
|
80
|
+
else:
|
|
81
|
+
raise ImportError(
|
|
82
|
+
"Visualization functions are not available. "
|
|
83
|
+
"Install with: pip install tnfr[viz]"
|
|
84
|
+
) from _import_error
|
|
85
|
+
|
|
86
|
+
SequenceVisualizer = _missing_viz_dependency # type: ignore[assignment]
|
|
87
|
+
plot_cascade_propagation = _missing_viz_dependency # type: ignore[assignment]
|
|
88
|
+
plot_cascade_timeline = _missing_viz_dependency # type: ignore[assignment]
|
|
89
|
+
plot_cascade_metrics_summary = _missing_viz_dependency # type: ignore[assignment]
|
|
90
|
+
|
|
91
|
+
__all__ = [
|
|
92
|
+
"SequenceVisualizer",
|
|
93
|
+
"plot_cascade_propagation",
|
|
94
|
+
"plot_cascade_timeline",
|
|
95
|
+
"plot_cascade_metrics_summary",
|
|
96
|
+
"print_bifurcation_hierarchy",
|
|
97
|
+
"get_hierarchy_info",
|
|
98
|
+
]
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""Visualization tools for THOL cascade dynamics.
|
|
2
|
+
|
|
3
|
+
Provides plotting functions to visualize cascade propagation across networks,
|
|
4
|
+
temporal evolution of cascades, and collective emergence patterns.
|
|
5
|
+
|
|
6
|
+
TNFR Canonical Principle
|
|
7
|
+
-------------------------
|
|
8
|
+
From "El pulso que nos atraviesa" (TNFR Manual, §2.2.10):
|
|
9
|
+
|
|
10
|
+
"THOL actúa como modulador central de plasticidad. Es el glifo que
|
|
11
|
+
permite a la red reorganizar su topología sin intervención externa."
|
|
12
|
+
|
|
13
|
+
These visualizations make cascade dynamics observable and traceable,
|
|
14
|
+
enabling scientific validation and debugging of self-organization.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from typing import TYPE_CHECKING, Any
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from ..types import TNFRGraph
|
|
23
|
+
|
|
24
|
+
import matplotlib.pyplot as plt
|
|
25
|
+
|
|
26
|
+
from ..alias import get_attr
|
|
27
|
+
from ..constants.aliases import ALIAS_EPI
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
import networkx as nx
|
|
31
|
+
|
|
32
|
+
HAS_NETWORKX = True
|
|
33
|
+
except ImportError:
|
|
34
|
+
HAS_NETWORKX = False
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"plot_cascade_propagation",
|
|
38
|
+
"plot_cascade_timeline",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def plot_cascade_propagation(G: TNFRGraph, figsize: tuple[int, int] = (12, 8)):
|
|
43
|
+
"""Visualize THOL cascade propagation across network.
|
|
44
|
+
|
|
45
|
+
Creates network diagram with:
|
|
46
|
+
- Node size = EPI magnitude
|
|
47
|
+
- Node color = bifurcation occurred (red) or not (blue)
|
|
48
|
+
- Edge thickness = coupling strength
|
|
49
|
+
- Arrows = propagation direction
|
|
50
|
+
|
|
51
|
+
Parameters
|
|
52
|
+
----------
|
|
53
|
+
G : TNFRGraph
|
|
54
|
+
Graph with THOL propagation history
|
|
55
|
+
figsize : tuple[int, int], default (12, 8)
|
|
56
|
+
Figure size in inches (width, height)
|
|
57
|
+
|
|
58
|
+
Returns
|
|
59
|
+
-------
|
|
60
|
+
matplotlib.figure.Figure
|
|
61
|
+
Figure object containing the cascade visualization
|
|
62
|
+
|
|
63
|
+
Notes
|
|
64
|
+
-----
|
|
65
|
+
TNFR Principle: Cascade propagation shows how self-organization
|
|
66
|
+
spreads through phase-aligned neighbors. Red nodes = bifurcation source,
|
|
67
|
+
blue nodes = unaffected. Arrow thickness = propagation strength.
|
|
68
|
+
|
|
69
|
+
Examples
|
|
70
|
+
--------
|
|
71
|
+
>>> # After running THOL sequence with cascades
|
|
72
|
+
>>> fig = plot_cascade_propagation(G)
|
|
73
|
+
>>> fig.savefig("cascade_propagation.png")
|
|
74
|
+
>>> plt.show()
|
|
75
|
+
"""
|
|
76
|
+
if not HAS_NETWORKX:
|
|
77
|
+
raise ImportError("NetworkX required for cascade visualization")
|
|
78
|
+
|
|
79
|
+
propagations = G.graph.get("thol_propagations", [])
|
|
80
|
+
|
|
81
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
82
|
+
|
|
83
|
+
# Identify nodes that bifurcated (source nodes in propagations)
|
|
84
|
+
bifurcated_nodes = set()
|
|
85
|
+
for prop in propagations:
|
|
86
|
+
bifurcated_nodes.add(prop["source_node"])
|
|
87
|
+
|
|
88
|
+
# Node colors: red = bifurcated, lightblue = normal
|
|
89
|
+
node_colors = ["red" if n in bifurcated_nodes else "lightblue" for n in G.nodes]
|
|
90
|
+
|
|
91
|
+
# Node sizes based on EPI magnitude
|
|
92
|
+
node_sizes = []
|
|
93
|
+
for n in G.nodes:
|
|
94
|
+
epi = float(get_attr(G.nodes[n], ALIAS_EPI, 0.5))
|
|
95
|
+
node_sizes.append(1000 * epi)
|
|
96
|
+
|
|
97
|
+
# Compute layout
|
|
98
|
+
pos = nx.spring_layout(G, seed=42)
|
|
99
|
+
|
|
100
|
+
# Draw network structure
|
|
101
|
+
nx.draw_networkx_nodes(
|
|
102
|
+
G, pos, node_color=node_colors, node_size=node_sizes, ax=ax, alpha=0.8
|
|
103
|
+
)
|
|
104
|
+
nx.draw_networkx_edges(G, pos, alpha=0.3, ax=ax)
|
|
105
|
+
nx.draw_networkx_labels(G, pos, ax=ax, font_size=10)
|
|
106
|
+
|
|
107
|
+
# Draw propagation arrows
|
|
108
|
+
for prop in propagations:
|
|
109
|
+
source = prop["source_node"]
|
|
110
|
+
for target, strength in prop["propagations"]:
|
|
111
|
+
if source in pos and target in pos:
|
|
112
|
+
ax.annotate(
|
|
113
|
+
"",
|
|
114
|
+
xy=pos[target],
|
|
115
|
+
xytext=pos[source],
|
|
116
|
+
arrowprops=dict(
|
|
117
|
+
arrowstyle="->",
|
|
118
|
+
color="red",
|
|
119
|
+
lw=2 * strength,
|
|
120
|
+
alpha=0.7,
|
|
121
|
+
),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
ax.set_title("THOL Cascade Propagation", fontsize=14, fontweight="bold")
|
|
125
|
+
ax.axis("off")
|
|
126
|
+
plt.tight_layout()
|
|
127
|
+
return fig
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def plot_cascade_timeline(G: TNFRGraph, figsize: tuple[int, int] = (10, 5)):
|
|
131
|
+
"""Plot temporal evolution of cascade events.
|
|
132
|
+
|
|
133
|
+
Creates scatter plot showing:
|
|
134
|
+
- X-axis: Timestamp (operator sequence step)
|
|
135
|
+
- Y-axis: Number of propagation targets
|
|
136
|
+
- Size: Indicates cascade magnitude
|
|
137
|
+
|
|
138
|
+
Parameters
|
|
139
|
+
----------
|
|
140
|
+
G : TNFRGraph
|
|
141
|
+
Graph with THOL propagation history
|
|
142
|
+
figsize : tuple[int, int], default (10, 5)
|
|
143
|
+
Figure size in inches (width, height)
|
|
144
|
+
|
|
145
|
+
Returns
|
|
146
|
+
-------
|
|
147
|
+
matplotlib.figure.Figure or None
|
|
148
|
+
Figure object containing the timeline, or None if no cascades
|
|
149
|
+
|
|
150
|
+
Notes
|
|
151
|
+
-----
|
|
152
|
+
TNFR Principle: Temporal evolution reveals cascade patterns.
|
|
153
|
+
Spikes indicate strong propagation events; clusters indicate
|
|
154
|
+
sustained collective reorganization.
|
|
155
|
+
|
|
156
|
+
Examples
|
|
157
|
+
--------
|
|
158
|
+
>>> # After running THOL sequence with cascades
|
|
159
|
+
>>> fig = plot_cascade_timeline(G)
|
|
160
|
+
>>> if fig:
|
|
161
|
+
... fig.savefig("cascade_timeline.png")
|
|
162
|
+
... plt.show()
|
|
163
|
+
"""
|
|
164
|
+
propagations = G.graph.get("thol_propagations", [])
|
|
165
|
+
|
|
166
|
+
if not propagations:
|
|
167
|
+
print("No cascade events to plot")
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
timestamps = [p["timestamp"] for p in propagations]
|
|
171
|
+
cascade_sizes = [len(p["propagations"]) for p in propagations]
|
|
172
|
+
|
|
173
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
174
|
+
ax.scatter(timestamps, cascade_sizes, s=100, alpha=0.7, color="darkred")
|
|
175
|
+
ax.plot(timestamps, cascade_sizes, linestyle="--", alpha=0.5, color="gray")
|
|
176
|
+
|
|
177
|
+
ax.set_xlabel("Timestamp (operator sequence step)", fontsize=12)
|
|
178
|
+
ax.set_ylabel("Propagation Targets", fontsize=12)
|
|
179
|
+
ax.set_title("THOL Cascade Evolution", fontsize=14, fontweight="bold")
|
|
180
|
+
ax.grid(alpha=0.3)
|
|
181
|
+
|
|
182
|
+
plt.tight_layout()
|
|
183
|
+
return fig
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def plot_cascade_metrics_summary(
|
|
187
|
+
G: TNFRGraph,
|
|
188
|
+
node_metrics: dict[Any, dict[str, Any]],
|
|
189
|
+
figsize: tuple[int, int] = (14, 6),
|
|
190
|
+
):
|
|
191
|
+
"""Create comprehensive cascade metrics dashboard.
|
|
192
|
+
|
|
193
|
+
Creates multi-panel visualization showing:
|
|
194
|
+
- Panel 1: Cascade depth distribution
|
|
195
|
+
- Panel 2: Sub-EPI coherence over time
|
|
196
|
+
- Panel 3: Metabolic activity index
|
|
197
|
+
|
|
198
|
+
Parameters
|
|
199
|
+
----------
|
|
200
|
+
G : TNFRGraph
|
|
201
|
+
Graph with THOL history
|
|
202
|
+
node_metrics : dict
|
|
203
|
+
Dictionary mapping node IDs to their THOL metrics
|
|
204
|
+
figsize : tuple[int, int], default (14, 6)
|
|
205
|
+
Figure size in inches (width, height)
|
|
206
|
+
|
|
207
|
+
Returns
|
|
208
|
+
-------
|
|
209
|
+
matplotlib.figure.Figure
|
|
210
|
+
Figure object containing the dashboard
|
|
211
|
+
|
|
212
|
+
Notes
|
|
213
|
+
-----
|
|
214
|
+
TNFR Principle: Complete observability requires multiple metrics.
|
|
215
|
+
This dashboard provides holistic view of self-organization dynamics.
|
|
216
|
+
|
|
217
|
+
Examples
|
|
218
|
+
--------
|
|
219
|
+
>>> # Collect metrics during sequence
|
|
220
|
+
>>> metrics_by_node = {}
|
|
221
|
+
>>> for node in G.nodes:
|
|
222
|
+
... metrics_by_node[node] = self_organization_metrics(G, node, ...)
|
|
223
|
+
>>> fig = plot_cascade_metrics_summary(G, metrics_by_node)
|
|
224
|
+
>>> fig.savefig("cascade_metrics_dashboard.png")
|
|
225
|
+
"""
|
|
226
|
+
fig, axes = plt.subplots(1, 3, figsize=figsize)
|
|
227
|
+
|
|
228
|
+
# Panel 1: Cascade depth distribution
|
|
229
|
+
depths = [m.get("cascade_depth", 0) for m in node_metrics.values()]
|
|
230
|
+
axes[0].hist(depths, bins=range(max(depths) + 2), alpha=0.7, color="steelblue")
|
|
231
|
+
axes[0].set_xlabel("Cascade Depth", fontsize=11)
|
|
232
|
+
axes[0].set_ylabel("Count", fontsize=11)
|
|
233
|
+
axes[0].set_title("Cascade Depth Distribution", fontsize=12, fontweight="bold")
|
|
234
|
+
axes[0].grid(alpha=0.3)
|
|
235
|
+
|
|
236
|
+
# Panel 2: Sub-EPI coherence
|
|
237
|
+
coherences = [m.get("subepi_coherence", 0) for m in node_metrics.values()]
|
|
238
|
+
node_ids = list(node_metrics.keys())
|
|
239
|
+
axes[1].bar(range(len(node_ids)), coherences, alpha=0.7, color="forestgreen")
|
|
240
|
+
axes[1].set_xlabel("Node Index", fontsize=11)
|
|
241
|
+
axes[1].set_ylabel("Coherence [0,1]", fontsize=11)
|
|
242
|
+
axes[1].set_title("Sub-EPI Collective Coherence", fontsize=12, fontweight="bold")
|
|
243
|
+
axes[1].axhline(0.5, color="red", linestyle="--", alpha=0.5, label="Threshold")
|
|
244
|
+
axes[1].legend()
|
|
245
|
+
axes[1].grid(alpha=0.3)
|
|
246
|
+
|
|
247
|
+
# Panel 3: Metabolic activity index
|
|
248
|
+
activities = [m.get("metabolic_activity_index", 0) for m in node_metrics.values()]
|
|
249
|
+
axes[2].bar(range(len(node_ids)), activities, alpha=0.7, color="darkorange")
|
|
250
|
+
axes[2].set_xlabel("Node Index", fontsize=11)
|
|
251
|
+
axes[2].set_ylabel("Activity [0,1]", fontsize=11)
|
|
252
|
+
axes[2].set_title("Metabolic Activity Index", fontsize=12, fontweight="bold")
|
|
253
|
+
axes[2].grid(alpha=0.3)
|
|
254
|
+
|
|
255
|
+
plt.tight_layout()
|
|
256
|
+
return fig
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""Hierarchical bifurcation visualization for THOL operator.
|
|
2
|
+
|
|
3
|
+
Provides utilities to visualize and inspect nested THOL bifurcation structures,
|
|
4
|
+
supporting operational fractality analysis and debugging of complex emergent
|
|
5
|
+
hierarchies.
|
|
6
|
+
|
|
7
|
+
TNFR Canonical Principle
|
|
8
|
+
-------------------------
|
|
9
|
+
From THOL_ENCAPSULATION_GUIDE.md:
|
|
10
|
+
|
|
11
|
+
"THOL windows can be nested to arbitrary depth, reflecting TNFR's
|
|
12
|
+
fractal structure."
|
|
13
|
+
|
|
14
|
+
This module implements ASCII tree visualization of bifurcation hierarchies,
|
|
15
|
+
enabling rapid inspection of multi-level emergent structures without external
|
|
16
|
+
visualization dependencies.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from typing import TYPE_CHECKING
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from ..types import NodeId, TNFRGraph
|
|
25
|
+
|
|
26
|
+
from ..alias import get_attr
|
|
27
|
+
from ..constants.aliases import ALIAS_EPI
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"print_bifurcation_hierarchy",
|
|
31
|
+
"get_hierarchy_info",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def print_bifurcation_hierarchy(
|
|
36
|
+
G: TNFRGraph,
|
|
37
|
+
node: NodeId,
|
|
38
|
+
indent: int = 0,
|
|
39
|
+
max_depth: int | None = None,
|
|
40
|
+
) -> None:
|
|
41
|
+
"""Print ASCII tree of bifurcation hierarchy.
|
|
42
|
+
|
|
43
|
+
Recursively traverses THOL bifurcations and displays them as an ASCII
|
|
44
|
+
tree structure, showing EPI values and bifurcation levels at each node.
|
|
45
|
+
|
|
46
|
+
Parameters
|
|
47
|
+
----------
|
|
48
|
+
G : TNFRGraph
|
|
49
|
+
Graph containing bifurcation structure
|
|
50
|
+
node : NodeId
|
|
51
|
+
Root node to start visualization from
|
|
52
|
+
indent : int, optional
|
|
53
|
+
Current indentation level (used internally for recursion), by default 0
|
|
54
|
+
max_depth : int | None, optional
|
|
55
|
+
Maximum depth to display (None = unlimited), by default None
|
|
56
|
+
|
|
57
|
+
Notes
|
|
58
|
+
-----
|
|
59
|
+
TNFR Principle: Operational fractality (Invariant #7) enables recursive
|
|
60
|
+
bifurcation. This visualization makes hierarchical structures visible for:
|
|
61
|
+
- Debugging nested THOL sequences
|
|
62
|
+
- Validating depth constraints
|
|
63
|
+
- Analyzing emergent patterns
|
|
64
|
+
- Educational demonstrations
|
|
65
|
+
|
|
66
|
+
The format uses standard tree characters:
|
|
67
|
+
- ├─ for intermediate branches
|
|
68
|
+
- └─ for last branch at each level
|
|
69
|
+
- │ for vertical continuation
|
|
70
|
+
|
|
71
|
+
Examples
|
|
72
|
+
--------
|
|
73
|
+
>>> # Simple single-level bifurcation
|
|
74
|
+
>>> print_bifurcation_hierarchy(G, node)
|
|
75
|
+
Node 0 (EPI=0.82, level=0)
|
|
76
|
+
├─ Sub-EPI 1 (epi=0.21, level=1)
|
|
77
|
+
└─ Sub-EPI 2 (epi=0.18, level=1)
|
|
78
|
+
|
|
79
|
+
>>> # Multi-level nested bifurcation
|
|
80
|
+
>>> print_bifurcation_hierarchy(G, node)
|
|
81
|
+
Node 0 (EPI=0.82, level=0)
|
|
82
|
+
├─ Sub-EPI 1 (epi=0.21, level=1)
|
|
83
|
+
│ ├─ Sub-Sub-EPI 1.1 (epi=0.05, level=2)
|
|
84
|
+
│ └─ Sub-Sub-EPI 1.2 (epi=0.08, level=2)
|
|
85
|
+
└─ Sub-EPI 2 (epi=0.18, level=1)
|
|
86
|
+
|
|
87
|
+
>>> # Limit depth display
|
|
88
|
+
>>> print_bifurcation_hierarchy(G, node, max_depth=1)
|
|
89
|
+
Node 0 (EPI=0.82, level=0)
|
|
90
|
+
├─ Sub-EPI 1 (epi=0.21, level=1) [...]
|
|
91
|
+
└─ Sub-EPI 2 (epi=0.18, level=1) [...]
|
|
92
|
+
"""
|
|
93
|
+
# Check depth limit
|
|
94
|
+
if max_depth is not None and indent >= max_depth:
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
# Get node information
|
|
98
|
+
node_epi = float(get_attr(G.nodes[node], ALIAS_EPI, 0.0))
|
|
99
|
+
node_level = G.nodes[node].get("_bifurcation_level", 0)
|
|
100
|
+
|
|
101
|
+
# Print current node
|
|
102
|
+
prefix = " " * indent
|
|
103
|
+
print(f"{prefix}Node {node} (EPI={node_epi:.2f}, level={node_level})")
|
|
104
|
+
|
|
105
|
+
# Get sub-EPIs
|
|
106
|
+
sub_epis = G.nodes[node].get("sub_epis", [])
|
|
107
|
+
|
|
108
|
+
if not sub_epis:
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
# Print each sub-EPI
|
|
112
|
+
for i, sub_epi in enumerate(sub_epis):
|
|
113
|
+
is_last = (i == len(sub_epis) - 1)
|
|
114
|
+
branch = "└─" if is_last else "├─"
|
|
115
|
+
continuation = " " if is_last else "│ "
|
|
116
|
+
|
|
117
|
+
sub_level = sub_epi.get("bifurcation_level", 1)
|
|
118
|
+
sub_epi_value = sub_epi.get("epi", 0.0)
|
|
119
|
+
|
|
120
|
+
# Check if we'll hit depth limit on next level
|
|
121
|
+
truncated = ""
|
|
122
|
+
if max_depth is not None and indent + 1 >= max_depth:
|
|
123
|
+
# Check if sub-EPI has further nesting
|
|
124
|
+
sub_node_id = sub_epi.get("node_id")
|
|
125
|
+
if sub_node_id and sub_node_id in G.nodes:
|
|
126
|
+
sub_has_children = bool(G.nodes[sub_node_id].get("sub_epis", []))
|
|
127
|
+
if sub_has_children:
|
|
128
|
+
truncated = " [...]"
|
|
129
|
+
|
|
130
|
+
print(f"{prefix}{branch} Sub-EPI {i+1} "
|
|
131
|
+
f"(epi={sub_epi_value:.2f}, level={sub_level}){truncated}")
|
|
132
|
+
|
|
133
|
+
# Recurse into sub-node if it exists and we haven't hit depth limit
|
|
134
|
+
sub_node_id = sub_epi.get("node_id")
|
|
135
|
+
if sub_node_id and sub_node_id in G.nodes:
|
|
136
|
+
if max_depth is None or indent + 1 < max_depth:
|
|
137
|
+
# Print continuation line for all but last child
|
|
138
|
+
if not is_last:
|
|
139
|
+
child_sub_epis = G.nodes[sub_node_id].get("sub_epis", [])
|
|
140
|
+
if child_sub_epis:
|
|
141
|
+
# Prepare indentation for child's children
|
|
142
|
+
child_indent = indent + 1
|
|
143
|
+
child_prefix = " " * child_indent
|
|
144
|
+
# Print vertical continuation
|
|
145
|
+
print(f"{prefix}{continuation}")
|
|
146
|
+
# Recurse with continuation context
|
|
147
|
+
_print_sub_hierarchy(
|
|
148
|
+
G, sub_node_id, child_indent,
|
|
149
|
+
parent_continuation=continuation,
|
|
150
|
+
parent_prefix=prefix,
|
|
151
|
+
max_depth=max_depth
|
|
152
|
+
)
|
|
153
|
+
else:
|
|
154
|
+
# Last child - no continuation line
|
|
155
|
+
child_sub_epis = G.nodes[sub_node_id].get("sub_epis", [])
|
|
156
|
+
if child_sub_epis:
|
|
157
|
+
child_indent = indent + 1
|
|
158
|
+
_print_sub_hierarchy(
|
|
159
|
+
G, sub_node_id, child_indent,
|
|
160
|
+
parent_continuation=" ",
|
|
161
|
+
parent_prefix=prefix,
|
|
162
|
+
max_depth=max_depth
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _print_sub_hierarchy(
|
|
167
|
+
G: TNFRGraph,
|
|
168
|
+
node: NodeId,
|
|
169
|
+
indent: int,
|
|
170
|
+
parent_continuation: str,
|
|
171
|
+
parent_prefix: str,
|
|
172
|
+
max_depth: int | None,
|
|
173
|
+
) -> None:
|
|
174
|
+
"""Helper to print sub-hierarchy with proper indentation.
|
|
175
|
+
|
|
176
|
+
Internal function handling continuation lines for nested hierarchies.
|
|
177
|
+
"""
|
|
178
|
+
sub_epis = G.nodes[node].get("sub_epis", [])
|
|
179
|
+
|
|
180
|
+
if not sub_epis:
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
if max_depth is not None and indent >= max_depth:
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
for i, sub_epi in enumerate(sub_epis):
|
|
187
|
+
is_last = (i == len(sub_epis) - 1)
|
|
188
|
+
branch = "└─" if is_last else "├─"
|
|
189
|
+
continuation = " " if is_last else "│ "
|
|
190
|
+
|
|
191
|
+
sub_level = sub_epi.get("bifurcation_level", 1)
|
|
192
|
+
sub_epi_value = sub_epi.get("epi", 0.0)
|
|
193
|
+
|
|
194
|
+
# Build prefix with parent continuation
|
|
195
|
+
full_prefix = parent_prefix + parent_continuation
|
|
196
|
+
|
|
197
|
+
truncated = ""
|
|
198
|
+
if max_depth is not None and indent + 1 >= max_depth:
|
|
199
|
+
sub_node_id = sub_epi.get("node_id")
|
|
200
|
+
if sub_node_id and sub_node_id in G.nodes:
|
|
201
|
+
sub_has_children = bool(G.nodes[sub_node_id].get("sub_epis", []))
|
|
202
|
+
if sub_has_children:
|
|
203
|
+
truncated = " [...]"
|
|
204
|
+
|
|
205
|
+
print(f"{full_prefix}{branch} Sub-EPI {i+1} "
|
|
206
|
+
f"(epi={sub_epi_value:.2f}, level={sub_level}){truncated}")
|
|
207
|
+
|
|
208
|
+
# Recurse if node exists
|
|
209
|
+
sub_node_id = sub_epi.get("node_id")
|
|
210
|
+
if sub_node_id and sub_node_id in G.nodes:
|
|
211
|
+
if max_depth is None or indent + 1 < max_depth:
|
|
212
|
+
child_sub_epis = G.nodes[sub_node_id].get("sub_epis", [])
|
|
213
|
+
if child_sub_epis:
|
|
214
|
+
_print_sub_hierarchy(
|
|
215
|
+
G, sub_node_id, indent + 1,
|
|
216
|
+
parent_continuation=parent_continuation + continuation,
|
|
217
|
+
parent_prefix=parent_prefix,
|
|
218
|
+
max_depth=max_depth
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def get_hierarchy_info(G: TNFRGraph, node: NodeId) -> dict:
|
|
223
|
+
"""Get hierarchical bifurcation information for a node.
|
|
224
|
+
|
|
225
|
+
Returns structured data about the bifurcation hierarchy, useful for
|
|
226
|
+
programmatic analysis and testing.
|
|
227
|
+
|
|
228
|
+
Parameters
|
|
229
|
+
----------
|
|
230
|
+
G : TNFRGraph
|
|
231
|
+
Graph containing bifurcation structure
|
|
232
|
+
node : NodeId
|
|
233
|
+
Node to analyze
|
|
234
|
+
|
|
235
|
+
Returns
|
|
236
|
+
-------
|
|
237
|
+
dict
|
|
238
|
+
Hierarchy information containing:
|
|
239
|
+
- node: Node identifier
|
|
240
|
+
- epi: Current EPI value
|
|
241
|
+
- bifurcation_level: Current bifurcation level
|
|
242
|
+
- hierarchy_path: List of ancestor nodes
|
|
243
|
+
- sub_epi_count: Number of direct sub-EPIs
|
|
244
|
+
- max_depth: Maximum bifurcation depth from this node
|
|
245
|
+
- total_descendants: Total number of sub-EPIs at all levels
|
|
246
|
+
|
|
247
|
+
Examples
|
|
248
|
+
--------
|
|
249
|
+
>>> info = get_hierarchy_info(G, node)
|
|
250
|
+
>>> info["bifurcation_level"]
|
|
251
|
+
0
|
|
252
|
+
>>> info["max_depth"]
|
|
253
|
+
2
|
|
254
|
+
>>> info["total_descendants"]
|
|
255
|
+
4
|
|
256
|
+
"""
|
|
257
|
+
from ..operators.metabolism import compute_hierarchical_depth
|
|
258
|
+
|
|
259
|
+
node_epi = float(get_attr(G.nodes[node], ALIAS_EPI, 0.0))
|
|
260
|
+
node_level = G.nodes[node].get("_bifurcation_level", 0)
|
|
261
|
+
hierarchy_path = G.nodes[node].get("_hierarchy_path", [])
|
|
262
|
+
sub_epis = G.nodes[node].get("sub_epis", [])
|
|
263
|
+
|
|
264
|
+
# Compute max depth
|
|
265
|
+
max_depth = compute_hierarchical_depth(G, node)
|
|
266
|
+
|
|
267
|
+
# Count total descendants recursively
|
|
268
|
+
total_descendants = len(sub_epis)
|
|
269
|
+
for sub_epi in sub_epis:
|
|
270
|
+
sub_node_id = sub_epi.get("node_id")
|
|
271
|
+
if sub_node_id and sub_node_id in G.nodes:
|
|
272
|
+
# Recurse into sub-node
|
|
273
|
+
sub_info = get_hierarchy_info(G, sub_node_id)
|
|
274
|
+
total_descendants += sub_info["total_descendants"]
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
"node": node,
|
|
278
|
+
"epi": node_epi,
|
|
279
|
+
"bifurcation_level": node_level,
|
|
280
|
+
"hierarchy_path": hierarchy_path,
|
|
281
|
+
"sub_epi_count": len(sub_epis),
|
|
282
|
+
"max_depth": max_depth,
|
|
283
|
+
"total_descendants": total_descendants,
|
|
284
|
+
}
|