From 8c1d951e87c655d7e676cb0377dfd1a474697e10 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 9 Nov 2025 22:00:01 +0000 Subject: [PATCH 1/3] Initial plan From 585fb57d7f2387da5f2203219e0cdfa7277cdf23 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 9 Nov 2025 22:09:17 +0000 Subject: [PATCH 2/3] Add comprehensive ZHIR test suite (initial implementation with known issues) Co-authored-by: fermga <203334638+fermga@users.noreply.github.com> --- .../test_mutation_network_impact.py | 302 +++++++++++++ tests/integration/test_mutation_sequences.py | 423 ++++++++++++++++++ .../unit/operators/test_mutation_contracts.py | 341 ++++++++++++++ .../operators/test_mutation_edge_cases.py | 354 +++++++++++++++ .../unit/operators/test_mutation_identity.py | 317 +++++++++++++ .../operators/test_mutation_preconditions.py | 230 ++++++++++ 6 files changed, 1967 insertions(+) create mode 100644 tests/integration/test_mutation_network_impact.py create mode 100644 tests/integration/test_mutation_sequences.py create mode 100644 tests/unit/operators/test_mutation_contracts.py create mode 100644 tests/unit/operators/test_mutation_edge_cases.py create mode 100644 tests/unit/operators/test_mutation_identity.py create mode 100644 tests/unit/operators/test_mutation_preconditions.py diff --git a/tests/integration/test_mutation_network_impact.py b/tests/integration/test_mutation_network_impact.py new file mode 100644 index 000000000..48a0366c8 --- /dev/null +++ b/tests/integration/test_mutation_network_impact.py @@ -0,0 +1,302 @@ +"""Tests for ZHIR (Mutation) network impact and neighbor effects. + +This module tests how ZHIR affects neighboring nodes in the network, +capturing the structural physics of phase transformation propagation. + +Test Coverage: +1. Impact on directly connected neighbors +2. Phase coherence with neighbors +3. Network-wide effects +4. Isolated node behavior + +References: +- AGENTS.md §11 (Mutation operator) +- test_mutation_metrics_comprehensive.py (network_impact metrics) +""" + +import pytest +import math +from tnfr.structural import create_nfr, run_sequence +from tnfr.operators.definitions import Mutation, Coherence, Dissonance + + +class TestZHIRNetworkImpact: + """Test ZHIR impact on network neighbors.""" + + def test_zhir_affects_neighbors(self): + """ZHIR should have measurable impact on connected neighbors.""" + G, node = create_nfr("test", epi=0.5, vf=1.0, theta=0.5) + G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] + + # Add neighbors with proper TNFR attributes + neighbors = [] + for i in range(3): + neighbor_id = f"neighbor_{i}" + G.add_node( + neighbor_id, + EPI=0.5, + epi=0.5, + theta=0.5 + i * 0.1, + **{"νf": 1.0}, # Greek letter for canonical + vf=1.0, + dnfr=0.0, + delta_nfr=0.0, + theta_history=[0.5, 0.5 + i * 0.1], + epi_history=[0.4, 0.5], + ) + G.add_edge(node, neighbor_id) + neighbors.append(neighbor_id) + + G.graph["COLLECT_OPERATOR_METRICS"] = True + + # Store neighbor states before mutation + neighbors_theta_before = {n: G.nodes[n]["theta"] for n in neighbors} + + # Apply mutation + Mutation()(G, node) + + # Check metrics captured network impact + metrics = G.graph["operator_metrics"][-1] + + assert "neighbor_count" in metrics + assert metrics["neighbor_count"] == 3 + + assert "network_impact_radius" in metrics + # Impact radius should be non-zero with neighbors + # (actual value depends on implementation) + + assert "phase_coherence_neighbors" in metrics + # Should have computed phase coherence with neighbors + + def test_zhir_phase_coherence_with_neighbors(self): + """ZHIR should consider phase coherence with neighbors.""" + G, node = create_nfr("test", epi=0.5, vf=1.0, theta=0.5) + G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] + + # Add neighbor with similar phase (coherent) + G.add_node( + "coherent_neighbor", + epi=0.5, + vf=1.0, + theta=0.52, # Very close phase + delta_nfr=0.0, + theta_history=[0.5, 0.52], + ) + G.add_edge(node, "coherent_neighbor") + + # Add neighbor with opposite phase (incoherent) + G.add_node( + "incoherent_neighbor", + epi=0.5, + vf=1.0, + theta=0.5 + math.pi, # Opposite phase + delta_nfr=0.0, + theta_history=[0.5 + math.pi, 0.5 + math.pi], + ) + G.add_edge(node, "incoherent_neighbor") + + G.graph["COLLECT_OPERATOR_METRICS"] = True + + # Apply mutation + Mutation()(G, node) + + metrics = G.graph["operator_metrics"][-1] + + # Should have measured phase coherence + assert "phase_coherence_neighbors" in metrics + assert "impacted_neighbors" in metrics + + def test_zhir_isolated_node_zero_impact(self): + """ZHIR on isolated node should have zero network impact.""" + G, node = create_nfr("test", epi=0.5, vf=1.0) + # No neighbors + G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] + G.graph["COLLECT_OPERATOR_METRICS"] = True + + Mutation()(G, node) + + metrics = G.graph["operator_metrics"][-1] + + # Isolated node should have zero neighbors + assert metrics["neighbor_count"] == 0 + assert metrics["impacted_neighbors"] == 0 + assert metrics["network_impact_radius"] == 0.0 + assert metrics["phase_coherence_neighbors"] == 0.0 + + def test_zhir_network_impact_radius_calculation(self): + """Network impact radius should be calculated correctly.""" + G, node = create_nfr("test", epi=0.5, vf=1.0, theta=0.5) + G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] + + # Add neighbors at different "distances" (via phase difference) + # Close phase = high impact + G.add_node("close", epi=0.5, vf=1.0, theta=0.51, delta_nfr=0.0) + G.add_edge(node, "close") + + # Far phase = low impact + G.add_node("far", epi=0.5, vf=1.0, theta=0.5 + 1.5, delta_nfr=0.0) + G.add_edge(node, "far") + + G.graph["COLLECT_OPERATOR_METRICS"] = True + + Mutation()(G, node) + + metrics = G.graph["operator_metrics"][-1] + + # Should have computed impact radius + assert "network_impact_radius" in metrics + assert 0.0 <= metrics["network_impact_radius"] <= 1.0 + + def test_zhir_in_dense_network(self): + """ZHIR in dense network should track all neighbors.""" + G, node = create_nfr("test", epi=0.5, vf=1.0, theta=0.5) + G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] + + # Add many neighbors (dense network) + for i in range(10): + neighbor_id = f"n{i}" + G.add_node( + neighbor_id, + epi=0.5, + vf=1.0, + theta=0.5 + i * 0.1, + delta_nfr=0.0, + ) + G.add_edge(node, neighbor_id) + + G.graph["COLLECT_OPERATOR_METRICS"] = True + + Mutation()(G, node) + + metrics = G.graph["operator_metrics"][-1] + + # Should track all neighbors + assert metrics["neighbor_count"] == 10 + + def test_zhir_with_bidirectional_edges(self): + """ZHIR should handle bidirectional connections correctly.""" + G, node = create_nfr("test", epi=0.5, vf=1.0, theta=0.5) + G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] + + # Add bidirectional neighbor + G.add_node("neighbor", epi=0.5, vf=1.0, theta=0.52, delta_nfr=0.0) + G.add_edge(node, "neighbor") + G.add_edge("neighbor", node) # Bidirectional + + G.graph["COLLECT_OPERATOR_METRICS"] = True + + # Should not raise error + Mutation()(G, node) + + metrics = G.graph["operator_metrics"][-1] + + # Should count neighbor once (not twice for bidirectional) + assert metrics["neighbor_count"] >= 1 + + +class TestZHIRNeighborPhaseCompatibility: + """Test phase compatibility checking with neighbors.""" + + def test_zhir_with_compatible_neighbors(self): + """ZHIR with phase-compatible neighbors should work smoothly.""" + G, node = create_nfr("test", epi=0.5, vf=1.0, theta=0.5) + G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] + + # Add neighbors with compatible phases (within π/2) + for i in range(3): + G.add_node( + f"n{i}", + epi=0.5, + vf=1.0, + theta=0.5 + i * 0.3, # Within compatible range + delta_nfr=0.0, + ) + G.add_edge(node, f"n{i}") + + G.graph["COLLECT_OPERATOR_METRICS"] = True + + # Should work without issues + Mutation()(G, node) + + metrics = G.graph["operator_metrics"][-1] + + # Phase coherence should be relatively high + if "phase_coherence_neighbors" in metrics: + # Should be positive (compatible phases) + assert metrics["phase_coherence_neighbors"] >= 0 + + def test_zhir_with_incompatible_neighbors(self): + """ZHIR with phase-incompatible neighbors should still work.""" + G, node = create_nfr("test", epi=0.5, vf=1.0, theta=0.5) + G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] + + # Add neighbors with incompatible phases (antiphase) + for i in range(3): + G.add_node( + f"n{i}", + epi=0.5, + vf=1.0, + theta=0.5 + math.pi + i * 0.1, # Opposite phase + delta_nfr=0.0, + ) + G.add_edge(node, f"n{i}") + + # Should not raise error (ZHIR is internal transformation) + Mutation()(G, node) + + # Node should still be viable + assert G.nodes[node]["vf"] > 0 + + +class TestZHIRNetworkPropagation: + """Test mutation effects propagation through network.""" + + def test_zhir_sequence_with_resonance_propagates(self): + """ZHIR → RA should propagate transformed state.""" + from tnfr.operators.definitions import Resonance + + G, node = create_nfr("test", epi=0.5, vf=1.0, theta=0.5) + G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] + + # Add neighbor with compatible phase + G.add_node("neighbor", epi=0.5, vf=1.0, theta=0.52, delta_nfr=0.0) + G.add_edge(node, "neighbor") + + theta_before = G.nodes[node]["theta"] + + # Apply mutation then resonance + run_sequence(G, node, [ + Coherence(), + Dissonance(), + Mutation(), # Transform phase + Resonance(), # Propagate to neighbors + ]) + + theta_after = G.nodes[node]["theta"] + + # Phase should have changed + assert theta_after != theta_before + + def test_zhir_does_not_directly_modify_neighbors(self): + """ZHIR should not directly modify neighbor states (internal transformation).""" + G, node = create_nfr("test", epi=0.5, vf=1.0, theta=0.5) + G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] + + # Add neighbor + G.add_node("neighbor", epi=0.5, vf=1.0, theta=0.52, delta_nfr=0.0) + G.add_edge(node, "neighbor") + + # Store neighbor state + neighbor_theta_before = G.nodes["neighbor"]["theta"] + neighbor_epi_before = G.nodes["neighbor"]["epi"] + + # Apply mutation to main node + Mutation()(G, node) + + # Neighbor should not be directly modified + assert G.nodes["neighbor"]["theta"] == neighbor_theta_before + assert G.nodes["neighbor"]["epi"] == neighbor_epi_before + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/tests/integration/test_mutation_sequences.py b/tests/integration/test_mutation_sequences.py new file mode 100644 index 000000000..dd6b31d15 --- /dev/null +++ b/tests/integration/test_mutation_sequences.py @@ -0,0 +1,423 @@ +"""Integration tests for ZHIR (Mutation) in canonical operator sequences. + +This module tests ZHIR behavior in complete, canonical operator sequences +as specified in AGENTS.md and UNIFIED_GRAMMAR_RULES.md: + +Test Coverage: +1. Canonical mutation cycle (IL → OZ → ZHIR → IL) +2. Mutation with self-organization (OZ → ZHIR → THOL) +3. Bootstrap with mutation (AL → IL → OZ → ZHIR → NAV) +4. Extended sequences with multiple mutations +5. Sequence validation with grammar rules + +References: +- AGENTS.md §Operator Composition +- UNIFIED_GRAMMAR_RULES.md (U1-U4) +- test_canonical_sequences.py (similar integration tests) +""" + +import pytest +from tnfr.structural import create_nfr, run_sequence +from tnfr.operators.definitions import ( + Emission, + Coherence, + Dissonance, + Mutation, + SelfOrganization, + Transition, + Silence, + Resonance, +) + + +class TestCanonicalMutationCycle: + """Test the canonical IL → OZ → ZHIR → IL mutation cycle.""" + + def test_canonical_mutation_cycle_completes(self): + """IL → OZ → ZHIR → IL should complete successfully.""" + G, node = create_nfr("test", epi=0.5, vf=1.0) + G.nodes[node]["epi_history"] = [0.35, 0.42, 0.50] + G.graph["COLLECT_OPERATOR_METRICS"] = True + + # Apply canonical cycle + run_sequence(G, node, [ + Coherence(), # IL: Stabilize + Dissonance(), # OZ: Destabilize + Mutation(), # ZHIR: Transform + Coherence(), # IL: Stabilize + ]) + + # Verify all operators executed + metrics = G.graph["operator_metrics"] + glyphs = [m["glyph"] for m in metrics] + + assert "IL" in glyphs + assert "OZ" in glyphs + assert "ZHIR" in glyphs + assert glyphs.count("IL") >= 2 # IL appears twice + + def test_mutation_cycle_improves_coherence(self): + """Mutation cycle should end with stabilized state.""" + from tnfr.metrics import compute_coherence + + G, node = create_nfr("test", epi=0.5, vf=1.0) + G.nodes[node]["epi_history"] = [0.35, 0.42, 0.50] + + # Measure initial coherence + C_initial = compute_coherence(G) + + # Apply mutation cycle + run_sequence(G, node, [ + Coherence(), + Dissonance(), + Mutation(), + Coherence(), + ]) + + # Measure final coherence + C_final = compute_coherence(G) + + # Final coherence should be reasonable (not collapsed) + assert C_final > 0.3, f"Coherence collapsed: {C_initial} → {C_final}" + + def test_mutation_cycle_preserves_node_viability(self): + """After mutation cycle, node should remain viable (νf > 0).""" + G, node = create_nfr("test", epi=0.5, vf=1.0) + G.nodes[node]["epi_history"] = [0.35, 0.42, 0.50] + + run_sequence(G, node, [ + Coherence(), + Dissonance(), + Mutation(), + Coherence(), + ]) + + # Node should remain viable + vf_final = G.nodes[node]["vf"] + assert vf_final > 0, "Mutation cycle killed node (νf → 0)" + assert vf_final > 0.2, "Mutation cycle severely weakened node" + + def test_mutation_cycle_theta_transformed(self): + """Mutation cycle should transform phase (θ changes).""" + import math + + G, node = create_nfr("test", epi=0.5, vf=1.0, theta=0.5) + G.nodes[node]["epi_history"] = [0.35, 0.42, 0.50] + + theta_initial = G.nodes[node]["theta"] + + run_sequence(G, node, [ + Coherence(), + Dissonance(), + Mutation(), + Coherence(), + ]) + + theta_final = G.nodes[node]["theta"] + + # Phase should have changed during ZHIR + assert theta_final != theta_initial, "ZHIR did not transform phase" + + def test_multiple_mutation_cycles(self): + """Multiple mutation cycles should work correctly.""" + G, node = create_nfr("test", epi=0.5, vf=1.0) + G.nodes[node]["epi_history"] = [0.35, 0.42, 0.50] + + # Apply 3 mutation cycles + for i in range(3): + run_sequence(G, node, [ + Coherence(), + Dissonance(), + Mutation(), + Coherence(), + ]) + + # Node should still be viable + assert G.nodes[node]["vf"] > 0 + assert -1.0 <= G.nodes[node]["epi"] <= 1.0 + + +class TestMutationWithSelfOrganization: + """Test OZ → ZHIR → THOL sequence.""" + + def test_mutation_then_self_organization(self): + """OZ → ZHIR → THOL should complete successfully.""" + G, node = create_nfr("test", epi=0.5, vf=1.0) + # Add a neighbor for THOL to work with + G.add_node("neighbor", epi=0.4, vf=1.0, theta=0.5) + G.add_edge(node, "neighbor") + + G.nodes[node]["epi_history"] = [0.35, 0.42, 0.50] + G.graph["COLLECT_OPERATOR_METRICS"] = True + + # Apply sequence + run_sequence(G, node, [ + Coherence(), # Stabilize + Dissonance(), # Destabilize + Mutation(), # Transform + SelfOrganization(), # Organize + ]) + + # Verify sequence completed + metrics = G.graph["operator_metrics"] + glyphs = [m["glyph"] for m in metrics] + + assert "OZ" in glyphs + assert "ZHIR" in glyphs + assert "THOL" in glyphs + + def test_mutation_enables_self_organization(self): + """ZHIR transformation should enable effective THOL.""" + G, node = create_nfr("test", epi=0.5, vf=1.0, theta=0.2) + # Add neighbors + for i in range(2): + neighbor_id = f"n{i}" + G.add_node(neighbor_id, epi=0.4, vf=1.0, theta=0.3 + i * 0.1) + G.add_edge(node, neighbor_id) + + G.nodes[node]["epi_history"] = [0.35, 0.42, 0.50] + + # Apply sequence + run_sequence(G, node, [ + Dissonance(), # Create instability + Mutation(), # Transform phase + SelfOrganization(), # Should work with transformed state + ]) + + # Verify THOL was applied + # (exact effects depend on implementation) + + +class TestBootstrapWithMutation: + """Test complete bootstrap sequence including mutation.""" + + def test_bootstrap_with_mutation(self): + """AL → IL → OZ → ZHIR → NAV should complete lifecycle.""" + G, node = create_nfr("test", epi=0.0, vf=0.5) + G.graph["COLLECT_OPERATOR_METRICS"] = True + + # Complete bootstrap sequence + run_sequence(G, node, [ + Emission(), # AL: Generate from vacuum + Coherence(), # IL: Stabilize + Dissonance(), # OZ: Destabilize + Mutation(), # ZHIR: Transform + Transition(), # NAV: Regime shift + ]) + + # Verify all operators executed + metrics = G.graph["operator_metrics"] + glyphs = [m["glyph"] for m in metrics] + + assert "AL" in glyphs + assert "IL" in glyphs + assert "OZ" in glyphs + assert "ZHIR" in glyphs + assert "NAV" in glyphs + + def test_bootstrap_sequence_grammar_valid(self): + """Bootstrap sequence should satisfy all grammar rules.""" + G, node = create_nfr("test", epi=0.0, vf=0.5) + + # With strict validation enabled + G.graph["VALIDATE_OPERATOR_PRECONDITIONS"] = True + + # Sequence should pass grammar validation + run_sequence(G, node, [ + Emission(), # U1a: Generator for EPI=0 + Coherence(), # U2: Stabilizer after generation + Dissonance(), # U2: Destabilizer (needs stabilizer after) + Mutation(), # U4b: Transformer (has IL + destabilizer) + Coherence(), # U2: Stabilizer after mutation + Transition(), # U1b: Closure + ]) + + # Should complete without grammar violations + + def test_bootstrap_node_becomes_viable(self): + """After bootstrap, node should be fully viable.""" + G, node = create_nfr("test", epi=0.0, vf=0.3) + + run_sequence(G, node, [ + Emission(), + Coherence(), + Dissonance(), + Mutation(), + Transition(), + ]) + + # Node should be viable + assert G.nodes[node]["epi"] != 0.0 + assert G.nodes[node]["vf"] > 0 + assert 0 <= G.nodes[node]["theta"] < 6.28319 # 2π + + +class TestExtendedMutationSequences: + """Test extended sequences with multiple operations.""" + + def test_oz_zhir_oz_zhir_sequence(self): + """Multiple mutation applications in one sequence.""" + G, node = create_nfr("test", epi=0.5, vf=1.0) + G.nodes[node]["epi_history"] = [0.35, 0.42, 0.50] + + # Double mutation sequence + run_sequence(G, node, [ + Coherence(), # Stabilize + Dissonance(), # Destabilize 1 + Mutation(), # Transform 1 + Coherence(), # Stabilize + Dissonance(), # Destabilize 2 + Mutation(), # Transform 2 + Coherence(), # Stabilize + ]) + + # Node should still be viable + assert G.nodes[node]["vf"] > 0 + assert -1.0 <= G.nodes[node]["epi"] <= 1.0 + + def test_resonance_after_mutation(self): + """RA (Resonance) after ZHIR should propagate transformed state.""" + G, node = create_nfr("test", epi=0.5, vf=1.0, theta=0.5) + # Add neighbor with compatible phase + G.add_node("neighbor", epi=0.4, vf=1.0, theta=0.6) + G.add_edge(node, "neighbor") + + G.nodes[node]["epi_history"] = [0.35, 0.42, 0.50] + + # Apply sequence + run_sequence(G, node, [ + Coherence(), + Dissonance(), + Mutation(), # Transform phase + Resonance(), # Propagate transformed pattern + ]) + + # Resonance should have executed + # (exact effects depend on phase compatibility) + + def test_silence_after_mutation(self): + """SHA (Silence) after ZHIR should freeze transformed state.""" + G, node = create_nfr("test", epi=0.5, vf=1.0, theta=0.5) + G.nodes[node]["epi_history"] = [0.35, 0.42, 0.50] + + # Apply mutation then silence + run_sequence(G, node, [ + Coherence(), + Dissonance(), + Mutation(), + Silence(), + ]) + + # SHA should have frozen state + # νf should be near zero during silence + # (actual implementation may vary) + + +class TestSequenceGrammarValidation: + """Test that mutation sequences satisfy grammar rules.""" + + def test_u4b_satisfied_in_canonical_sequence(self): + """Canonical sequence should satisfy U4b.""" + G, node = create_nfr("test", epi=0.5, vf=1.0) + G.nodes[node]["epi_history"] = [0.35, 0.42, 0.50] + G.graph["VALIDATE_OPERATOR_PRECONDITIONS"] = True + + # U4b requires: IL precedence + recent destabilizer + run_sequence(G, node, [ + Coherence(), # IL precedence ✓ + Dissonance(), # Recent destabilizer ✓ + Mutation(), # U4b satisfied ✓ + ]) + + # Should complete without error + + def test_u2_satisfied_with_stabilizers(self): + """U2 (Convergence) satisfied with stabilizers after destabilizers.""" + G, node = create_nfr("test", epi=0.5, vf=1.0) + G.nodes[node]["epi_history"] = [0.35, 0.42, 0.50] + + # U2 requires stabilizers after destabilizers + run_sequence(G, node, [ + Dissonance(), # Destabilizer + Mutation(), # Destabilizer/Transformer + Coherence(), # Stabilizer ✓ + ]) + + # Integral should converge (not diverge) + + def test_u1b_closure_satisfied(self): + """Sequences should end with closure operators.""" + G, node = create_nfr("test", epi=0.5, vf=1.0) + G.nodes[node]["epi_history"] = [0.35, 0.42, 0.50] + + # Test various closure operators + closure_operators = [ + Silence(), + Transition(), + # Dissonance also counts as closure + ] + + for closure_op in closure_operators[:2]: # Test first two + run_sequence(G, node, [ + Coherence(), + Dissonance(), + Mutation(), + closure_op, # U1b: Closure ✓ + ]) + + # Should complete successfully + + +class TestMutationSequenceMetrics: + """Test metrics collection in sequences with mutation.""" + + def test_sequence_metrics_captured(self): + """All operators in sequence should have metrics.""" + G, node = create_nfr("test", epi=0.5, vf=1.0) + G.nodes[node]["epi_history"] = [0.35, 0.42, 0.50] + G.graph["COLLECT_OPERATOR_METRICS"] = True + + run_sequence(G, node, [ + Coherence(), + Dissonance(), + Mutation(), + Coherence(), + ]) + + metrics = G.graph["operator_metrics"] + + # Should have 4 metric entries + assert len(metrics) >= 4 + + # Verify operators are tracked + operators = [m["operator"] for m in metrics] + assert "Coherence" in operators + assert "Dissonance" in operators + assert "Mutation" in operators + + def test_mutation_context_captured(self): + """ZHIR metrics should include context from preceding operators.""" + G, node = create_nfr("test", epi=0.5, vf=1.0) + G.nodes[node]["epi_history"] = [0.35, 0.42, 0.50] + G.graph["COLLECT_OPERATOR_METRICS"] = True + + run_sequence(G, node, [ + Coherence(), + Dissonance(), + Mutation(), + ]) + + # Find ZHIR metrics + metrics = G.graph["operator_metrics"] + zhir_metrics = [m for m in metrics if m["glyph"] == "ZHIR"] + + assert len(zhir_metrics) > 0 + zhir_metric = zhir_metrics[-1] + + # Should capture context + assert "destabilizer_type" in zhir_metric + assert "destabilizer_operator" in zhir_metric + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/tests/unit/operators/test_mutation_contracts.py b/tests/unit/operators/test_mutation_contracts.py new file mode 100644 index 000000000..66f5a5d0e --- /dev/null +++ b/tests/unit/operators/test_mutation_contracts.py @@ -0,0 +1,341 @@ +"""Tests for ZHIR (Mutation) canonical contracts. + +This module tests the canonical contracts that ZHIR MUST satisfy, +as specified in AGENTS.md and TNFR theory: + +1. Preserves EPI sign (structural identity at sign level) +2. Does not collapse νf (maintains reorganization capacity) +3. Satisfies monotonicity requirements +4. Maintains structural bounds + +Test Coverage: +- EPI sign preservation +- νf preservation (no collapse to zero) +- Structural bounds maintenance +- Contract validation in various scenarios + +References: +- AGENTS.md §11 (Mutation contracts) +- TNFR.pdf §2.2.11 (ZHIR physics) +- Canonical Invariants (AGENTS.md §Invariants) +""" + +import pytest +import math +from tnfr.structural import create_nfr, run_sequence +from tnfr.operators.definitions import ( + Mutation, + Coherence, + Dissonance, + Emission, +) + + +class TestZHIREPISignPreservation: + """Test ZHIR preserves EPI sign (identity at sign level).""" + + def test_zhir_preserves_positive_epi_sign(self): + """ZHIR MUST NOT change positive EPI to negative.""" + G, node = create_nfr("test", epi=0.5, vf=1.0) + G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] + G.nodes[node]["delta_nfr"] = 0.3 + + epi_before = G.nodes[node]["epi"] + assert epi_before > 0, "Test setup: EPI should be positive" + + # Apply mutation + Mutation()(G, node) + + epi_after = G.nodes[node]["epi"] + + # CRITICAL CONTRACT: Positive EPI must remain positive + assert epi_after > 0, \ + f"ZHIR violated sign preservation: positive EPI {epi_before} became {epi_after}" + + def test_zhir_preserves_negative_epi_sign(self): + """ZHIR MUST NOT change negative EPI to positive.""" + G, node = create_nfr("test", epi=-0.5, vf=1.0) + G.nodes[node]["epi_history"] = [-0.7, -0.6, -0.5] + G.nodes[node]["delta_nfr"] = 0.3 + + epi_before = G.nodes[node]["epi"] + assert epi_before < 0, "Test setup: EPI should be negative" + + # Apply mutation + Mutation()(G, node) + + epi_after = G.nodes[node]["epi"] + + # CRITICAL CONTRACT: Negative EPI must remain negative + assert epi_after < 0, \ + f"ZHIR violated sign preservation: negative EPI {epi_before} became {epi_after}" + + def test_zhir_preserves_sign_in_canonical_sequence(self): + """EPI sign preserved through IL → OZ → ZHIR sequence.""" + G, node = create_nfr("test", epi=0.6, vf=1.0) + G.nodes[node]["epi_history"] = [0.4, 0.5, 0.6] + + epi_initial = G.nodes[node]["epi"] + sign_initial = 1 if epi_initial > 0 else -1 + + # Apply canonical sequence + run_sequence(G, node, [Coherence(), Dissonance(), Mutation()]) + + epi_final = G.nodes[node]["epi"] + sign_final = 1 if epi_final > 0 else -1 + + # Sign must be preserved + assert sign_initial == sign_final, \ + f"Sign changed from {sign_initial} to {sign_final}" + + def test_zhir_handles_zero_epi(self): + """ZHIR with EPI=0 is edge case (no sign to preserve).""" + G, node = create_nfr("test", epi=0.0, vf=1.0) + G.nodes[node]["epi_history"] = [-0.05, 0.0, 0.0] + G.nodes[node]["delta_nfr"] = 0.1 + + # Should not raise error + Mutation()(G, node) + + epi_after = G.nodes[node]["epi"] + # Result can be positive, negative, or zero (no sign to preserve at 0) + # Contract is satisfied as long as it doesn't crash + + def test_zhir_preserves_sign_with_high_transformation(self): + """Sign preserved even with strong transformation (high ΔNFR).""" + G, node = create_nfr("test", epi=0.7, vf=1.5) + G.nodes[node]["epi_history"] = [0.4, 0.55, 0.7] + G.nodes[node]["delta_nfr"] = 0.8 # High transformation pressure + + epi_before = G.nodes[node]["epi"] + assert epi_before > 0 + + # Apply with strong destabilizer first + run_sequence(G, node, [Dissonance(), Mutation()]) + + epi_after = G.nodes[node]["epi"] + + # Even with strong transformation, sign must be preserved + assert epi_after > 0, \ + "Strong transformation violated sign preservation" + + +class TestZHIRVfPreservation: + """Test ZHIR does not collapse structural frequency (νf).""" + + def test_zhir_does_not_collapse_vf(self): + """ZHIR MUST NOT reduce νf to zero (would kill the node).""" + G, node = create_nfr("test", epi=0.5, vf=1.0) + G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] + + vf_before = G.nodes[node]["vf"] + assert vf_before > 0, "Test setup: νf should be positive" + + # Apply mutation + Mutation()(G, node) + + vf_after = G.nodes[node]["vf"] + + # CRITICAL CONTRACT: νf must remain positive + assert vf_after > 0, \ + f"ZHIR collapsed νf: {vf_before} → {vf_after} (node death)" + + def test_zhir_does_not_drastically_reduce_vf(self): + """ZHIR should not drastically reduce νf (>50% reduction suspicious).""" + G, node = create_nfr("test", epi=0.5, vf=1.0) + G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] + + vf_before = G.nodes[node]["vf"] + + # Apply mutation + Mutation()(G, node) + + vf_after = G.nodes[node]["vf"] + + # νf should not drop by more than 50% in single mutation + # (This is a soft contract - strong drops indicate potential issue) + assert vf_after > 0.5 * vf_before, \ + f"ZHIR drastically reduced νf: {vf_before} → {vf_after} (>50% drop)" + + def test_zhir_preserves_vf_in_multiple_applications(self): + """Multiple ZHIR applications should not progressively collapse νf.""" + G, node = create_nfr("test", epi=0.5, vf=1.0) + G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] + + vf_initial = G.nodes[node]["vf"] + + # Apply 5 mutation cycles + for i in range(5): + run_sequence(G, node, [Coherence(), Dissonance(), Mutation()]) + + vf_final = G.nodes[node]["vf"] + + # νf should not have collapsed to near-zero + assert vf_final > 0.1 * vf_initial, \ + f"Multiple ZHIR collapsed νf: {vf_initial} → {vf_final}" + + def test_zhir_with_low_initial_vf(self): + """ZHIR with already low νf should not collapse it further.""" + G, node = create_nfr("test", epi=0.5, vf=0.2) # Already low + G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] + + vf_before = 0.2 + + # Apply mutation + Mutation()(G, node) + + vf_after = G.nodes[node]["vf"] + + # Should remain positive + assert vf_after > 0, "ZHIR collapsed already-low νf" + + # Should not drop drastically + assert vf_after > 0.5 * vf_before, \ + f"ZHIR made low νf worse: {vf_before} → {vf_after}" + + def test_zhir_vf_metrics_tracked(self): + """νf changes should be tracked in metrics.""" + G, node = create_nfr("test", epi=0.5, vf=1.0) + G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] + G.graph["COLLECT_OPERATOR_METRICS"] = True + + vf_before = G.nodes[node]["vf"] + + Mutation()(G, node) + + metrics = G.graph["operator_metrics"][-1] + + # Metrics should track νf + assert "vf_final" in metrics + assert "delta_vf" in metrics + + # delta_vf should reflect actual change + vf_after = G.nodes[node]["vf"] + expected_delta = vf_after - vf_before + assert abs(metrics["delta_vf"] - expected_delta) < 0.01 + + +class TestZHIRStructuralBounds: + """Test ZHIR maintains structural bounds (EPI ∈ [-1, 1]).""" + + def test_zhir_respects_epi_upper_bound(self): + """ZHIR should not push EPI > 1.0.""" + G, node = create_nfr("test", epi=0.95, vf=1.0) + G.nodes[node]["epi_history"] = [0.85, 0.90, 0.95] + G.nodes[node]["delta_nfr"] = 0.5 # Strong expansion pressure + + # Apply with destabilizer + run_sequence(G, node, [Dissonance(), Mutation()]) + + epi_after = G.nodes[node]["epi"] + + # Should respect upper bound + assert epi_after <= 1.0, \ + f"ZHIR violated upper bound: EPI = {epi_after} > 1.0" + + def test_zhir_respects_epi_lower_bound(self): + """ZHIR should not push EPI < -1.0.""" + G, node = create_nfr("test", epi=-0.95, vf=1.0) + G.nodes[node]["epi_history"] = [-0.85, -0.90, -0.95] + G.nodes[node]["delta_nfr"] = -0.5 # Strong contraction pressure + + # Apply with destabilizer + run_sequence(G, node, [Dissonance(), Mutation()]) + + epi_after = G.nodes[node]["epi"] + + # Should respect lower bound + assert epi_after >= -1.0, \ + f"ZHIR violated lower bound: EPI = {epi_after} < -1.0" + + def test_zhir_phase_wraps_correctly(self): + """Phase (θ) should wrap in [0, 2π).""" + G, node = create_nfr("test", epi=0.5, vf=1.0) + G.nodes[node]["theta"] = 1.9 * math.pi # Near 2π + G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] + G.nodes[node]["delta_nfr"] = 0.5 # Will push beyond 2π + G.graph["GLYPH_FACTORS"] = {"ZHIR_theta_shift_factor": 0.5} + + Mutation()(G, node) + + theta_after = G.nodes[node]["theta"] + + # Phase must be in valid range + assert 0 <= theta_after < 2 * math.pi, \ + f"ZHIR failed to wrap phase: θ = {theta_after} not in [0, 2π)" + + +class TestZHIRContractIntegration: + """Integration tests for contract satisfaction.""" + + def test_all_contracts_satisfied_in_typical_use(self): + """Typical ZHIR usage should satisfy all contracts.""" + G, node = create_nfr("test", epi=0.5, vf=1.0, theta=1.0) + G.nodes[node]["epi_kind"] = "test_pattern" + G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] + + epi_before = G.nodes[node]["epi"] + vf_before = G.nodes[node]["vf"] + sign_before = 1 if epi_before > 0 else -1 + identity_before = G.nodes[node]["epi_kind"] + + # Apply canonical sequence + run_sequence(G, node, [Coherence(), Dissonance(), Mutation(), Coherence()]) + + epi_after = G.nodes[node]["epi"] + vf_after = G.nodes[node]["vf"] + sign_after = 1 if epi_after > 0 else -1 + identity_after = G.nodes[node]["epi_kind"] + theta_after = G.nodes[node]["theta"] + + # Check all contracts + assert sign_after == sign_before, "Sign preservation violated" + assert vf_after > 0, "νf collapse violated" + assert identity_after == identity_before, "Identity preservation violated" + assert -1.0 <= epi_after <= 1.0, "EPI bounds violated" + assert 0 <= theta_after < 2 * math.pi, "Phase bounds violated" + + def test_contracts_under_extreme_conditions(self): + """Contracts should hold even under extreme transformations.""" + G, node = create_nfr("test", epi=0.8, vf=2.0, theta=0.1) + G.nodes[node]["epi_kind"] = "extreme_pattern" + G.nodes[node]["epi_history"] = [0.5, 0.65, 0.8] + G.nodes[node]["delta_nfr"] = 0.9 # Very high pressure + + epi_before = G.nodes[node]["epi"] + vf_before = G.nodes[node]["vf"] + sign_before = 1 if epi_before > 0 else -1 + + # Apply with extreme conditions + G.graph["GLYPH_FACTORS"] = {"ZHIR_theta_shift_factor": 0.9} + run_sequence(G, node, [Dissonance(), Mutation()]) + + epi_after = G.nodes[node]["epi"] + vf_after = G.nodes[node]["vf"] + sign_after = 1 if epi_after > 0 else -1 + + # Contracts must still hold + assert sign_after == sign_before, "Extreme: Sign violated" + assert vf_after > 0, "Extreme: νf collapsed" + assert -1.0 <= epi_after <= 1.0, "Extreme: EPI bounds violated" + + def test_contracts_with_negative_dnfr(self): + """Contracts should hold with negative ΔNFR (contraction).""" + G, node = create_nfr("test", epi=0.5, vf=1.0) + G.nodes[node]["epi_history"] = [0.7, 0.6, 0.5] # Decreasing + G.nodes[node]["delta_nfr"] = -0.4 # Negative pressure + + epi_before = G.nodes[node]["epi"] + sign_before = 1 + + Mutation()(G, node) + + epi_after = G.nodes[node]["epi"] + sign_after = 1 if epi_after > 0 else -1 + + # Sign must be preserved even with negative ΔNFR + assert sign_after == sign_before, "Negative ΔNFR violated sign" + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/tests/unit/operators/test_mutation_edge_cases.py b/tests/unit/operators/test_mutation_edge_cases.py new file mode 100644 index 000000000..b14a4f90e --- /dev/null +++ b/tests/unit/operators/test_mutation_edge_cases.py @@ -0,0 +1,354 @@ +"""Edge case tests for ZHIR (Mutation) operator. + +This module tests ZHIR behavior in edge cases and boundary conditions +to ensure robust operation across the full parameter space. + +Test Coverage: +1. Isolated nodes +2. Phase boundaries +3. EPI extremes +4. Reproducibility +5. Unusual configurations + +References: +- AGENTS.md §11 (Mutation operator) +- test_zhir_phase_transformation.py (complementary tests) +""" + +import pytest +import math +import random +import numpy as np +from tnfr.structural import create_nfr, run_sequence +from tnfr.operators.definitions import Mutation, Coherence, Dissonance + + +class TestZHIRIsolatedNodes: + """Test ZHIR on isolated nodes (no connections).""" + + def test_zhir_on_isolated_node_succeeds(self): + """ZHIR should work on isolated nodes (internal transformation).""" + G, node = create_nfr("test", epi=0.5, vf=1.0) + # No edges - completely isolated + assert G.degree(node) == 0 + + G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] + + # Should not raise error (mutation is internal) + Mutation()(G, node) + + # Node should still be viable + assert G.nodes[node]["vf"] > 0 + assert -1.0 <= G.nodes[node]["epi"] <= 1.0 + + def test_zhir_isolated_node_transforms_phase(self): + """Isolated node's phase should still transform.""" + G, node = create_nfr("test", epi=0.5, vf=1.0, theta=0.5) + G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] + G.nodes[node]["delta_nfr"] = 0.4 + + theta_before = G.nodes[node]["theta"] + + Mutation()(G, node) + + theta_after = G.nodes[node]["theta"] + + # Phase transformation should occur even without neighbors + assert theta_after != theta_before, \ + "Isolated node phase not transformed" + + def test_zhir_isolated_preserves_all_contracts(self): + """Isolated node should satisfy all ZHIR contracts.""" + G, node = create_nfr("test", epi=0.6, vf=1.2) + G.nodes[node]["epi_kind"] = "isolated_pattern" + G.nodes[node]["epi_history"] = [0.4, 0.5, 0.6] + + epi_before = G.nodes[node]["epi"] + vf_before = G.nodes[node]["vf"] + sign_before = 1 if epi_before > 0 else -1 + + Mutation()(G, node) + + epi_after = G.nodes[node]["epi"] + vf_after = G.nodes[node]["vf"] + sign_after = 1 if epi_after > 0 else -1 + + # All contracts should hold + assert sign_after == sign_before, "Sign contract violated" + assert vf_after > 0, "νf contract violated" + assert G.nodes[node]["epi_kind"] == "isolated_pattern", "Identity contract violated" + + +class TestZHIRPhaseBoundaries: + """Test ZHIR behavior at phase boundaries (0, π/2, π, 3π/2, 2π).""" + + def test_zhir_at_zero_phase(self): + """ZHIR should work correctly at θ=0.""" + G, node = create_nfr("test", epi=0.5, vf=1.0, theta=0.0) + G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] + G.nodes[node]["delta_nfr"] = 0.3 + + Mutation()(G, node) + + theta_after = G.nodes[node]["theta"] + + # Should be in valid range + assert 0 <= theta_after < 2 * math.pi + + def test_zhir_at_pi_over_2(self): + """ZHIR should work correctly at θ=π/2 (quadrant boundary).""" + G, node = create_nfr("test", epi=0.5, vf=1.0, theta=math.pi / 2) + G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] + G.nodes[node]["delta_nfr"] = 0.3 + + Mutation()(G, node) + + theta_after = G.nodes[node]["theta"] + + assert 0 <= theta_after < 2 * math.pi + + def test_zhir_at_pi(self): + """ZHIR should work correctly at θ=π.""" + G, node = create_nfr("test", epi=0.5, vf=1.0, theta=math.pi) + G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] + G.nodes[node]["delta_nfr"] = 0.3 + + Mutation()(G, node) + + theta_after = G.nodes[node]["theta"] + + assert 0 <= theta_after < 2 * math.pi + + def test_zhir_near_2pi_wraps(self): + """ZHIR near 2π should wrap correctly to [0, 2π).""" + G, node = create_nfr("test", epi=0.5, vf=1.0, theta=1.95 * math.pi) + G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] + G.nodes[node]["delta_nfr"] = 0.5 # Will push past 2π + G.graph["GLYPH_FACTORS"] = {"ZHIR_theta_shift_factor": 0.4} + + Mutation()(G, node) + + theta_after = G.nodes[node]["theta"] + + # Should wrap into valid range + assert 0 <= theta_after < 2 * math.pi + # Should have wrapped to small value + assert theta_after < 1.0, f"Failed to wrap: θ={theta_after}" + + def test_zhir_all_boundaries_tested(self): + """Test all major phase boundaries.""" + boundaries = [ + 0.0, + math.pi / 2, + math.pi, + 3 * math.pi / 2, + 1.99 * math.pi, # Just before 2π + ] + + for boundary in boundaries: + G, node = create_nfr(f"test_{boundary}", epi=0.5, vf=1.0, theta=boundary) + G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] + G.nodes[node]["delta_nfr"] = 0.3 + + # Should not raise error + Mutation()(G, node) + + theta_after = G.nodes[node]["theta"] + assert 0 <= theta_after < 2 * math.pi, \ + f"Invalid phase after mutation at boundary {boundary}: {theta_after}" + + +class TestZHIRReproducibility: + """Test deterministic behavior with seeds.""" + + def test_zhir_reproducible_with_same_seed(self): + """Same seed should produce identical ZHIR results.""" + # First run + random.seed(42) + np.random.seed(42) + G1, node1 = create_nfr("test1", epi=0.5, vf=1.0, theta=1.0) + G1.nodes[node1]["epi_history"] = [0.3, 0.4, 0.5] + G1.nodes[node1]["delta_nfr"] = 0.3 + Mutation()(G1, node1) + result1 = { + "theta": G1.nodes[node1]["theta"], + "epi": G1.nodes[node1]["epi"], + "vf": G1.nodes[node1]["vf"], + } + + # Second run with same seed + random.seed(42) + np.random.seed(42) + G2, node2 = create_nfr("test2", epi=0.5, vf=1.0, theta=1.0) + G2.nodes[node2]["epi_history"] = [0.3, 0.4, 0.5] + G2.nodes[node2]["delta_nfr"] = 0.3 + Mutation()(G2, node2) + result2 = { + "theta": G2.nodes[node2]["theta"], + "epi": G2.nodes[node2]["epi"], + "vf": G2.nodes[node2]["vf"], + } + + # Should be identical + assert abs(result1["theta"] - result2["theta"]) < 1e-10 + assert abs(result1["epi"] - result2["epi"]) < 1e-10 + assert abs(result1["vf"] - result2["vf"]) < 1e-10 + + def test_zhir_different_seeds_produce_different_results(self): + """Different seeds should produce different results (if stochastic).""" + # Run with seed 1 + random.seed(1) + np.random.seed(1) + G1, node1 = create_nfr("test1", epi=0.5, vf=1.0, theta=1.0) + G1.nodes[node1]["epi_history"] = [0.3, 0.4, 0.5] + G1.nodes[node1]["delta_nfr"] = 0.3 + Mutation()(G1, node1) + theta1 = G1.nodes[node1]["theta"] + + # Run with seed 2 + random.seed(2) + np.random.seed(2) + G2, node2 = create_nfr("test2", epi=0.5, vf=1.0, theta=1.0) + G2.nodes[node2]["epi_history"] = [0.3, 0.4, 0.5] + G2.nodes[node2]["delta_nfr"] = 0.3 + Mutation()(G2, node2) + theta2 = G2.nodes[node2]["theta"] + + # Results may be different if operator uses randomness + # (If deterministic, they'll be the same - that's OK too) + + def test_zhir_sequence_reproducible(self): + """Full sequence with ZHIR should be reproducible.""" + # First run + random.seed(100) + np.random.seed(100) + G1, node1 = create_nfr("test1", epi=0.5, vf=1.0) + G1.nodes[node1]["epi_history"] = [0.35, 0.42, 0.50] + run_sequence(G1, node1, [Coherence(), Dissonance(), Mutation()]) + state1 = G1.nodes[node1]["theta"] + + # Second run with same seed + random.seed(100) + np.random.seed(100) + G2, node2 = create_nfr("test2", epi=0.5, vf=1.0) + G2.nodes[node2]["epi_history"] = [0.35, 0.42, 0.50] + run_sequence(G2, node2, [Coherence(), Dissonance(), Mutation()]) + state2 = G2.nodes[node2]["theta"] + + # Should be identical + assert abs(state1 - state2) < 1e-10 + + +class TestZHIRExtremeCases: + """Test ZHIR with extreme parameter values.""" + + def test_zhir_with_very_high_epi(self): + """ZHIR near upper EPI bound.""" + G, node = create_nfr("test", epi=0.95, vf=1.0) + G.nodes[node]["epi_history"] = [0.85, 0.90, 0.95] + + Mutation()(G, node) + + # Should not exceed bounds + assert G.nodes[node]["epi"] <= 1.0 + + def test_zhir_with_very_low_epi(self): + """ZHIR near lower EPI bound.""" + G, node = create_nfr("test", epi=-0.95, vf=1.0) + G.nodes[node]["epi_history"] = [-0.85, -0.90, -0.95] + + Mutation()(G, node) + + # Should not exceed bounds + assert G.nodes[node]["epi"] >= -1.0 + + def test_zhir_with_very_high_vf(self): + """ZHIR with very high structural frequency.""" + G, node = create_nfr("test", epi=0.5, vf=100.0) + G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] + + # Should not raise error + Mutation()(G, node) + + # Should still be viable + assert G.nodes[node]["vf"] > 0 + + def test_zhir_with_very_high_dnfr(self): + """ZHIR with extreme ΔNFR values.""" + G, node = create_nfr("test", epi=0.5, vf=1.0) + G.nodes[node]["epi_history"] = [0.2, 0.35, 0.5] + G.nodes[node]["delta_nfr"] = 5.0 # Very high + + # Should not crash + Mutation()(G, node) + + # Should maintain bounds + assert -1.0 <= G.nodes[node]["epi"] <= 1.0 + + def test_zhir_with_negative_dnfr(self): + """ZHIR with negative ΔNFR (contraction pressure).""" + G, node = create_nfr("test", epi=0.5, vf=1.0, theta=1.0) + G.nodes[node]["epi_history"] = [0.7, 0.6, 0.5] + G.nodes[node]["delta_nfr"] = -0.8 # Strong contraction + + theta_before = G.nodes[node]["theta"] + + Mutation()(G, node) + + theta_after = G.nodes[node]["theta"] + + # Should still transform (direction depends on sign of ΔNFR) + # Phase should have shifted backward + shift = theta_after - theta_before + if shift > math.pi: + shift -= 2 * math.pi + # Negative ΔNFR should produce backward shift + # (implementation-dependent) + + +class TestZHIRUnusualConfigurations: + """Test ZHIR with unusual graph/node configurations.""" + + def test_zhir_without_history_initialization(self): + """ZHIR should handle missing history keys gracefully.""" + G, node = create_nfr("test", epi=0.5, vf=1.0) + # Explicitly don't set epi_history + + # Should not crash (may log warning) + Mutation()(G, node) + + def test_zhir_with_empty_history(self): + """ZHIR with empty history should handle gracefully.""" + G, node = create_nfr("test", epi=0.5, vf=1.0) + G.nodes[node]["epi_history"] = [] # Empty + + # Should not crash (may log warning) + Mutation()(G, node) + + def test_zhir_with_nan_handling(self): + """ZHIR should handle NaN values gracefully (if they occur).""" + G, node = create_nfr("test", epi=0.5, vf=1.0) + G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] + # Don't inject NaN - just ensure it wouldn't crash + # (Actual NaN injection would violate TNFR invariants) + + # Normal operation should work + Mutation()(G, node) + + def test_zhir_repeated_immediate_applications(self): + """Multiple immediate ZHIR applications (no intermediate ops).""" + G, node = create_nfr("test", epi=0.5, vf=1.0) + G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] + + # Apply 5 times immediately + for i in range(5): + Mutation()(G, node) + + # Node should still be viable + assert G.nodes[node]["vf"] > 0 + assert -1.0 <= G.nodes[node]["epi"] <= 1.0 + assert 0 <= G.nodes[node]["theta"] < 2 * math.pi + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/tests/unit/operators/test_mutation_identity.py b/tests/unit/operators/test_mutation_identity.py new file mode 100644 index 000000000..007f302b5 --- /dev/null +++ b/tests/unit/operators/test_mutation_identity.py @@ -0,0 +1,317 @@ +"""Tests for ZHIR (Mutation) structural identity preservation. + +This module tests the canonical requirement that ZHIR preserves structural +identity (epi_kind) during phase transformation, as specified in: +- AGENTS.md §11 (Mutation operator: "Contract: Preserves identity") +- TNFR.pdf §2.2.11 (ZHIR physics - identity preservation) + +Test Coverage: +1. Single ZHIR preserves epi_kind +2. Multiple ZHIR applications preserve identity +3. Identity preservation across different node types +4. Validation that identity changes would raise errors (when implemented) + +References: +- AGENTS.md: Invariant #7 (Operational Fractality) +- test_mutation_metrics_comprehensive.py (identity_preserved metric) +""" + +import pytest +from tnfr.structural import create_nfr, run_sequence +from tnfr.operators.definitions import ( + Mutation, + Coherence, + Dissonance, + Emission, +) + + +class TestZHIRIdentityPreservation: + """Test ZHIR preserves structural identity (epi_kind).""" + + def test_zhir_preserves_epi_kind(self): + """ZHIR MUST preserve epi_kind during phase transformation.""" + G, node = create_nfr("test", epi=0.5, vf=1.0) + + # Set structural identity + epi_kind_original = "coherent_oscillator" + G.nodes[node]["epi_kind"] = epi_kind_original + G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] + G.nodes[node]["delta_nfr"] = 0.3 + + # Apply mutation + Mutation()(G, node) + + # Identity MUST be preserved + epi_kind_after = G.nodes[node].get("epi_kind") + assert epi_kind_after == epi_kind_original, \ + f"ZHIR violated identity preservation: {epi_kind_original} → {epi_kind_after}" + + def test_zhir_preserves_identity_in_canonical_sequence(self): + """Identity preserved in IL → OZ → ZHIR → IL sequence.""" + G, node = create_nfr("test", epi=0.4, vf=1.0) + + # Set identity + G.nodes[node]["epi_kind"] = "test_pattern" + G.nodes[node]["epi_history"] = [0.35, 0.38, 0.40] + + # Apply canonical sequence + run_sequence(G, node, [ + Coherence(), # IL + Dissonance(), # OZ + Mutation(), # ZHIR - must preserve identity + Coherence(), # IL + ]) + + # Identity must be preserved through entire sequence + assert G.nodes[node]["epi_kind"] == "test_pattern" + + def test_multiple_zhir_preserve_identity(self): + """Multiple ZHIR applications (with IL between) preserve identity.""" + G, node = create_nfr("test", epi=0.5, vf=1.0) + + # Set identity + original_identity = "fractal_structure" + G.nodes[node]["epi_kind"] = original_identity + G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] + + # Apply 3 mutation cycles + for i in range(3): + run_sequence(G, node, [ + Coherence(), # Stabilize + Dissonance(), # Destabilize + Mutation(), # Transform + Coherence(), # Stabilize + ]) + + # Identity must still be preserved + assert G.nodes[node]["epi_kind"] == original_identity, \ + "Multiple ZHIR violated identity preservation" + + def test_zhir_without_epi_kind_set(self): + """ZHIR should work when epi_kind is not set (no violation possible).""" + G, node = create_nfr("test", epi=0.5, vf=1.0) + # Don't set epi_kind + G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] + + # Should not raise error + Mutation()(G, node) + + # epi_kind should remain unset or undefined + epi_kind = G.nodes[node].get("epi_kind") + # It's OK if it's None or undefined + + def test_zhir_preserves_custom_identities(self): + """ZHIR preserves various custom identity types.""" + identity_types = [ + "wave_packet", + "vortex_structure", + "nested_pattern", + "emergent_form", + "synchronized_oscillator", + ] + + for identity_type in identity_types: + G, node = create_nfr(f"test_{identity_type}", epi=0.5, vf=1.0) + G.nodes[node]["epi_kind"] = identity_type + G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] + + # Apply mutation + run_sequence(G, node, [Coherence(), Dissonance(), Mutation()]) + + # Verify preservation + assert G.nodes[node]["epi_kind"] == identity_type, \ + f"Failed to preserve identity: {identity_type}" + + +class TestZHIRIdentityMetrics: + """Test identity preservation is captured in metrics.""" + + def test_identity_preserved_in_metrics(self): + """Metrics should track identity preservation.""" + G, node = create_nfr("test", epi=0.5, vf=1.0) + G.nodes[node]["epi_kind"] = "test_identity" + G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] + G.graph["COLLECT_OPERATOR_METRICS"] = True + + Mutation()(G, node) + + # Get metrics + metrics_list = G.graph.get("operator_metrics", []) + assert len(metrics_list) > 0 + + zhir_metric = metrics_list[-1] + assert zhir_metric["glyph"] == "ZHIR" + + # Check identity metrics + assert "identity_preserved" in zhir_metric + assert "epi_kind_before" in zhir_metric + assert "epi_kind_after" in zhir_metric + + # Identity should be preserved + assert zhir_metric["identity_preserved"] is True + assert zhir_metric["epi_kind_before"] == "test_identity" + assert zhir_metric["epi_kind_after"] == "test_identity" + + def test_identity_metrics_without_epi_kind(self): + """Metrics should handle nodes without epi_kind gracefully.""" + G, node = create_nfr("test", epi=0.5, vf=1.0) + # No epi_kind set + G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] + G.graph["COLLECT_OPERATOR_METRICS"] = True + + Mutation()(G, node) + + metrics = G.graph["operator_metrics"][-1] + + # Should have identity metrics + assert "identity_preserved" in metrics + # Should indicate no identity to preserve (trivially preserved) + # or should be True (no violation occurred) + assert metrics["identity_preserved"] in [True, None] + + +class TestZHIRIdentityWithTransformations: + """Test identity preservation despite various transformations.""" + + def test_identity_preserved_despite_phase_change(self): + """Identity preserved even when phase changes significantly.""" + import math + + G, node = create_nfr("test", epi=0.5, vf=1.0) + G.nodes[node]["epi_kind"] = "rotating_pattern" + G.nodes[node]["theta"] = 0.0 + G.nodes[node]["delta_nfr"] = 0.8 # Strong transformation + G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] + + # Apply mutation with strong phase shift + G.graph["GLYPH_FACTORS"] = {"ZHIR_theta_shift_factor": 0.8} + Mutation()(G, node) + + theta_after = G.nodes[node]["theta"] + + # Phase should have changed significantly + assert theta_after != 0.0 + assert abs(theta_after) > 0.5 + + # But identity MUST be preserved + assert G.nodes[node]["epi_kind"] == "rotating_pattern" + + def test_identity_preserved_with_high_epi_change(self): + """Identity preserved even when EPI changes (within bounds).""" + G, node = create_nfr("test", epi=0.3, vf=1.0) + G.nodes[node]["epi_kind"] = "adaptive_pattern" + G.nodes[node]["epi_history"] = [0.1, 0.2, 0.3] + G.nodes[node]["delta_nfr"] = 0.5 + + # Apply sequence that increases EPI + run_sequence(G, node, [Dissonance(), Mutation()]) + + # EPI may have changed + epi_after = G.nodes[node]["epi"] + # (exact value depends on implementation) + + # Identity must be preserved + assert G.nodes[node]["epi_kind"] == "adaptive_pattern" + + def test_identity_preserved_across_regime_change(self): + """Identity preserved when phase crosses regime boundary.""" + import math + + G, node = create_nfr("test", epi=0.5, vf=1.0) + G.nodes[node]["epi_kind"] = "regime_crossing_pattern" + G.nodes[node]["theta"] = math.pi / 2 - 0.1 # Near boundary + G.nodes[node]["delta_nfr"] = 0.6 # Strong shift + G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] + G.graph["GLYPH_FACTORS"] = {"ZHIR_theta_shift_factor": 0.5} + + # Apply mutation - should cross regime boundary + Mutation()(G, node) + + # Check if regime changed + theta_after = G.nodes[node]["theta"] + regime_before = int((math.pi / 2 - 0.1) // (math.pi / 2)) + regime_after = int(theta_after // (math.pi / 2)) + + # Identity must be preserved regardless of regime change + assert G.nodes[node]["epi_kind"] == "regime_crossing_pattern" + + +class TestZHIRIdentityEdgeCases: + """Test identity preservation in edge cases.""" + + def test_identity_with_negative_epi(self): + """Identity preserved when EPI is negative.""" + G, node = create_nfr("test", epi=-0.5, vf=1.0) + G.nodes[node]["epi_kind"] = "negative_epi_pattern" + G.nodes[node]["epi_history"] = [-0.7, -0.6, -0.5] + + Mutation()(G, node) + + assert G.nodes[node]["epi_kind"] == "negative_epi_pattern" + + def test_identity_with_zero_epi(self): + """Identity preserved when EPI crosses zero.""" + G, node = create_nfr("test", epi=0.0, vf=1.0) + G.nodes[node]["epi_kind"] = "zero_crossing_pattern" + G.nodes[node]["epi_history"] = [-0.1, 0.0, 0.0] + + Mutation()(G, node) + + assert G.nodes[node]["epi_kind"] == "zero_crossing_pattern" + + def test_identity_with_special_characters(self): + """Identity strings with special characters should be preserved.""" + G, node = create_nfr("test", epi=0.5, vf=1.0) + G.nodes[node]["epi_kind"] = "pattern_v2.0_α-β" + G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] + + Mutation()(G, node) + + assert G.nodes[node]["epi_kind"] == "pattern_v2.0_α-β" + + def test_identity_with_long_name(self): + """Long identity names should be preserved.""" + G, node = create_nfr("test", epi=0.5, vf=1.0) + long_identity = "very_long_identity_name_" * 10 # 260+ characters + G.nodes[node]["epi_kind"] = long_identity + G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] + + Mutation()(G, node) + + assert G.nodes[node]["epi_kind"] == long_identity + + +class TestZHIRIdentityViolationDetection: + """Test detection of identity violations (when enforcement is added).""" + + def test_identity_violation_placeholder(self): + """Placeholder: Identity violation detection not yet implemented. + + This test documents the expected behavior when identity validation + is added to the operator. Currently, ZHIR does not actively enforce + identity preservation beyond maintaining the epi_kind attribute. + + Future implementation should: + 1. Check epi_kind before/after transformation + 2. Raise OperatorPostconditionError if changed + 3. Log identity violations to telemetry + """ + # This is a placeholder test + # When identity enforcement is implemented, this should become: + # + # G, node = create_nfr("test", epi=0.5, vf=1.0) + # G.nodes[node]["epi_kind"] = "original_identity" + # G.graph["VALIDATE_OPERATOR_POSTCONDITIONS"] = True + # + # # Simulate identity violation (would require internal modification) + # with pytest.raises(OperatorPostconditionError) as exc_info: + # # Apply mutation that somehow changes epi_kind + # pass + # + # assert "identity" in str(exc_info.value).lower() + pass + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/tests/unit/operators/test_mutation_preconditions.py b/tests/unit/operators/test_mutation_preconditions.py new file mode 100644 index 000000000..23655e91d --- /dev/null +++ b/tests/unit/operators/test_mutation_preconditions.py @@ -0,0 +1,230 @@ +"""Additional precondition tests for ZHIR (Mutation) operator. + +This module complements test_mutation_threshold.py with tests for +EPI history requirements and other preconditions not covered elsewhere. + +Test Coverage: +1. EPI history requirement (must have sufficient history) +2. Node initialization requirements +3. Edge cases for precondition validation + +References: +- AGENTS.md §11 (Mutation operator contracts) +- test_mutation_threshold.py (complementary tests) +- src/tnfr/operators/preconditions/__init__.py +""" + +import pytest +from tnfr.structural import create_nfr, run_sequence +from tnfr.operators.definitions import Mutation, Coherence, Dissonance +from tnfr.operators.preconditions import validate_mutation, OperatorPreconditionError + + +class TestZHIRHistoryRequirements: + """Test ZHIR requirements for EPI history.""" + + def test_zhir_requires_epi_history_for_threshold(self, caplog): + """ZHIR should warn when epi_history is missing or insufficient.""" + G, node = create_nfr("test", epi=0.5, vf=1.0) + G.graph["ZHIR_THRESHOLD_XI"] = 0.1 + + # No EPI history at all + # Don't set epi_history + + import logging + with caplog.at_level(logging.WARNING): + validate_mutation(G, node) + + # Should log warning about insufficient history + assert any( + "without sufficient EPI history" in record.message or + "history" in record.message.lower() + for record in caplog.records + ) + + # Should set unknown threshold flag + assert G.nodes[node].get("_zhir_threshold_unknown") is True + + def test_zhir_accepts_minimal_history(self): + """ZHIR should work with minimal history (2 points for velocity).""" + G, node = create_nfr("test", epi=0.5, vf=1.0) + G.graph["ZHIR_THRESHOLD_XI"] = 0.1 + + # Minimal history for velocity computation (2 points) + G.nodes[node]["epi_history"] = [0.3, 0.5] + + # Should not raise error + validate_mutation(G, node) + + # Should compute velocity even with minimal history + # (bifurcation detection needs 3 points, but threshold only needs 2) + depi_dt = abs(0.5 - 0.3) + if depi_dt > 0.1: + assert G.nodes[node].get("_zhir_threshold_met") is True + else: + assert G.nodes[node].get("_zhir_threshold_warning") is True + + def test_zhir_accepts_long_history(self): + """ZHIR should handle long EPI histories correctly.""" + G, node = create_nfr("test", epi=0.8, vf=1.0) + G.graph["ZHIR_THRESHOLD_XI"] = 0.1 + + # Long history (uses last 3 points for computations) + G.nodes[node]["epi_history"] = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8] + + # Should not raise error + validate_mutation(G, node) + + # Should use recent history for velocity computation + # Recent velocity: 0.8 - 0.7 = 0.1 (meets threshold) + assert G.nodes[node].get("_zhir_threshold_met") is True + + def test_zhir_with_both_history_keys(self): + """ZHIR should work with either 'epi_history' or '_epi_history'.""" + # Test with underscore prefix + G1, n1 = create_nfr("test1", epi=0.5, vf=1.0) + G1.nodes[n1]["_epi_history"] = [0.3, 0.4, 0.5] + G1.graph["ZHIR_THRESHOLD_XI"] = 0.1 + + # Should not raise + validate_mutation(G1, n1) + assert G1.nodes[n1].get("_zhir_threshold_met") or G1.nodes[n1].get("_zhir_threshold_warning") + + # Test with standard key + G2, n2 = create_nfr("test2", epi=0.5, vf=1.0) + G2.nodes[n2]["epi_history"] = [0.3, 0.4, 0.5] + G2.graph["ZHIR_THRESHOLD_XI"] = 0.1 + + # Should not raise + validate_mutation(G2, n2) + assert G2.nodes[n2].get("_zhir_threshold_met") or G2.nodes[n2].get("_zhir_threshold_warning") + + +class TestZHIRNodeValidation: + """Test ZHIR validation of node state.""" + + def test_zhir_requires_positive_vf(self): + """ZHIR must fail if νf <= 0 (node is dead/frozen).""" + G, node = create_nfr("test", epi=0.5, vf=0.0) # Dead node + G.graph["ZHIR_MIN_VF"] = 0.05 + G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] + + # Should raise error + with pytest.raises(OperatorPreconditionError) as exc_info: + validate_mutation(G, node) + + assert "Structural frequency too low" in str(exc_info.value) + + def test_zhir_accepts_small_positive_vf(self): + """ZHIR should work with small but positive νf.""" + G, node = create_nfr("test", epi=0.5, vf=0.06) # Just above minimum + G.graph["ZHIR_MIN_VF"] = 0.05 + G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] + + # Should not raise + validate_mutation(G, node) + + # Should succeed or warn (depending on threshold) + assert G.nodes[node].get("_zhir_threshold_met") or G.nodes[node].get("_zhir_threshold_warning") + + def test_zhir_works_without_min_vf_config(self): + """ZHIR should work when ZHIR_MIN_VF not configured (no enforcement).""" + G, node = create_nfr("test", epi=0.5, vf=0.01) # Very low vf + # Don't set ZHIR_MIN_VF + G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] + + # Should not raise (enforcement disabled) + validate_mutation(G, node) + + +class TestZHIREdgeCases: + """Test edge cases for ZHIR preconditions.""" + + def test_zhir_with_negative_epi(self): + """ZHIR should work with negative EPI values.""" + G, node = create_nfr("test", epi=-0.5, vf=1.0) + G.nodes[node]["epi_history"] = [-0.7, -0.6, -0.5] + G.graph["ZHIR_THRESHOLD_XI"] = 0.1 + + # Should not raise + validate_mutation(G, node) + + # Velocity should still be computed + depi_dt = abs(-0.5 - (-0.6)) + assert depi_dt == pytest.approx(0.1, abs=0.01) + + def test_zhir_with_zero_epi(self): + """ZHIR should work with EPI=0 (crossing zero is valid).""" + G, node = create_nfr("test", epi=0.0, vf=1.0) + G.nodes[node]["epi_history"] = [-0.1, 0.0, 0.0] + G.graph["ZHIR_THRESHOLD_XI"] = 0.1 + + # Should not raise + validate_mutation(G, node) + + def test_zhir_with_very_high_vf(self): + """ZHIR should handle very high νf values.""" + G, node = create_nfr("test", epi=0.5, vf=10.0) # Very high frequency + G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] + G.graph["ZHIR_THRESHOLD_XI"] = 0.1 + + # Should not raise + validate_mutation(G, node) + + # Should still compute threshold correctly + assert G.nodes[node].get("_zhir_threshold_met") or G.nodes[node].get("_zhir_threshold_warning") + + def test_zhir_preserves_history_after_validation(self): + """ZHIR validation should not modify EPI history.""" + G, node = create_nfr("test", epi=0.5, vf=1.0) + history_original = [0.3, 0.4, 0.5] + G.nodes[node]["epi_history"] = history_original.copy() + + validate_mutation(G, node) + + # History should be unchanged + history_after = G.nodes[node]["epi_history"] + assert history_after == history_original + + +class TestZHIRPreconditionConfiguration: + """Test configuration flags for precondition validation.""" + + def test_strict_validation_enforces_all_checks(self): + """VALIDATE_OPERATOR_PRECONDITIONS=True should enforce all checks.""" + G, node = create_nfr("test", epi=0.5, vf=0.01) # Low vf + G.graph["VALIDATE_OPERATOR_PRECONDITIONS"] = True + G.graph["ZHIR_MIN_VF"] = 0.05 + G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] + + # Should raise error with strict validation + with pytest.raises(OperatorPreconditionError): + validate_mutation(G, node) + + def test_soft_validation_allows_borderline_cases(self): + """Default soft validation should allow borderline cases with warnings.""" + G, node = create_nfr("test", epi=0.5, vf=0.04) # Below threshold + # Don't enable strict validation (default is soft) + G.graph["ZHIR_MIN_VF"] = 0.05 + G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] + + # Should not raise (soft validation logs warnings) + validate_mutation(G, node) + + def test_individual_flags_override_defaults(self): + """Individual requirement flags should work independently.""" + G, node = create_nfr("test", epi=0.5, vf=1.0) + # Set individual flag without global strict validation + G.graph["ZHIR_REQUIRE_IL_PRECEDENCE"] = True + + from tnfr.types import Glyph + G.nodes[node]["glyph_history"] = [Glyph.OZ] # No IL + G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] + + # Should fail on IL even without global strict validation + with pytest.raises(OperatorPreconditionError): + validate_mutation(G, node) + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) From 873894d09a1e74f8c75e397d76c3ad5ab9cc2ad7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 9 Nov 2025 22:12:09 +0000 Subject: [PATCH 3/3] Fix attribute access and node initialization in ZHIR tests Co-authored-by: fermga <203334638+fermga@users.noreply.github.com> --- .../test_mutation_network_impact.py | 33 +++++---- tests/integration/test_mutation_sequences.py | 36 +++++----- .../unit/operators/test_mutation_contracts.py | 68 +++++++++---------- .../operators/test_mutation_edge_cases.py | 42 ++++++------ .../unit/operators/test_mutation_identity.py | 48 ++++++------- 5 files changed, 117 insertions(+), 110 deletions(-) diff --git a/tests/integration/test_mutation_network_impact.py b/tests/integration/test_mutation_network_impact.py index 48a0366c8..a8e2327d1 100644 --- a/tests/integration/test_mutation_network_impact.py +++ b/tests/integration/test_mutation_network_impact.py @@ -232,20 +232,21 @@ def test_zhir_with_incompatible_neighbors(self): # Add neighbors with incompatible phases (antiphase) for i in range(3): + neighbor_id = f"n{i}" G.add_node( - f"n{i}", - epi=0.5, - vf=1.0, + neighbor_id, + EPI=0.5, + **{"νf": 1.0}, theta=0.5 + math.pi + i * 0.1, # Opposite phase delta_nfr=0.0, ) - G.add_edge(node, f"n{i}") + G.add_edge(node, neighbor_id) # Should not raise error (ZHIR is internal transformation) Mutation()(G, node) # Node should still be viable - assert G.nodes[node]["vf"] > 0 + assert G.nodes[node]["νf"] > 0 class TestZHIRNetworkPropagation: @@ -258,9 +259,10 @@ def test_zhir_sequence_with_resonance_propagates(self): G, node = create_nfr("test", epi=0.5, vf=1.0, theta=0.5) G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] - # Add neighbor with compatible phase - G.add_node("neighbor", epi=0.5, vf=1.0, theta=0.52, delta_nfr=0.0) - G.add_edge(node, "neighbor") + # Add neighbor with compatible phase and proper initialization + neighbor_id = "neighbor" + G.add_node(neighbor_id, EPI=0.5, **{"νf": 1.0}, theta=0.52, delta_nfr=0.0) + G.add_edge(node, neighbor_id) theta_before = G.nodes[node]["theta"] @@ -282,20 +284,21 @@ def test_zhir_does_not_directly_modify_neighbors(self): G, node = create_nfr("test", epi=0.5, vf=1.0, theta=0.5) G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] - # Add neighbor - G.add_node("neighbor", epi=0.5, vf=1.0, theta=0.52, delta_nfr=0.0) - G.add_edge(node, "neighbor") + # Add neighbor with proper initialization + neighbor_id = "neighbor" + G.add_node(neighbor_id, EPI=0.5, **{"νf": 1.0}, theta=0.52, delta_nfr=0.0) + G.add_edge(node, neighbor_id) # Store neighbor state - neighbor_theta_before = G.nodes["neighbor"]["theta"] - neighbor_epi_before = G.nodes["neighbor"]["epi"] + neighbor_theta_before = G.nodes[neighbor_id]["theta"] + neighbor_epi_before = G.nodes[neighbor_id]["EPI"] # Apply mutation to main node Mutation()(G, node) # Neighbor should not be directly modified - assert G.nodes["neighbor"]["theta"] == neighbor_theta_before - assert G.nodes["neighbor"]["epi"] == neighbor_epi_before + assert G.nodes[neighbor_id]["theta"] == neighbor_theta_before + assert G.nodes[neighbor_id]["EPI"] == neighbor_epi_before if __name__ == "__main__": diff --git a/tests/integration/test_mutation_sequences.py b/tests/integration/test_mutation_sequences.py index dd6b31d15..d9507cb78 100644 --- a/tests/integration/test_mutation_sequences.py +++ b/tests/integration/test_mutation_sequences.py @@ -58,7 +58,7 @@ def test_canonical_mutation_cycle_completes(self): def test_mutation_cycle_improves_coherence(self): """Mutation cycle should end with stabilized state.""" - from tnfr.metrics import compute_coherence + from tnfr.metrics.coherence import compute_coherence G, node = create_nfr("test", epi=0.5, vf=1.0) G.nodes[node]["epi_history"] = [0.35, 0.42, 0.50] @@ -93,7 +93,7 @@ def test_mutation_cycle_preserves_node_viability(self): ]) # Node should remain viable - vf_final = G.nodes[node]["vf"] + vf_final = G.nodes[node]["νf"] assert vf_final > 0, "Mutation cycle killed node (νf → 0)" assert vf_final > 0.2, "Mutation cycle severely weakened node" @@ -133,8 +133,8 @@ def test_multiple_mutation_cycles(self): ]) # Node should still be viable - assert G.nodes[node]["vf"] > 0 - assert -1.0 <= G.nodes[node]["epi"] <= 1.0 + assert G.nodes[node]["νf"] > 0 + assert -1.0 <= G.nodes[node]["EPI"] <= 1.0 class TestMutationWithSelfOrganization: @@ -144,8 +144,9 @@ def test_mutation_then_self_organization(self): """OZ → ZHIR → THOL should complete successfully.""" G, node = create_nfr("test", epi=0.5, vf=1.0) # Add a neighbor for THOL to work with - G.add_node("neighbor", epi=0.4, vf=1.0, theta=0.5) - G.add_edge(node, "neighbor") + neighbor_node = "neighbor" + G.add_node(neighbor_node, EPI=0.4, **{"νf": 1.0}, theta=0.5, delta_nfr=0.0) + G.add_edge(node, neighbor_node) G.nodes[node]["epi_history"] = [0.35, 0.42, 0.50] G.graph["COLLECT_OPERATOR_METRICS"] = True @@ -169,10 +170,10 @@ def test_mutation_then_self_organization(self): def test_mutation_enables_self_organization(self): """ZHIR transformation should enable effective THOL.""" G, node = create_nfr("test", epi=0.5, vf=1.0, theta=0.2) - # Add neighbors + # Add neighbors with proper initialization for i in range(2): neighbor_id = f"n{i}" - G.add_node(neighbor_id, epi=0.4, vf=1.0, theta=0.3 + i * 0.1) + G.add_node(neighbor_id, EPI=0.4, **{"νf": 1.0}, theta=0.3 + i * 0.1, delta_nfr=0.0) G.add_edge(node, neighbor_id) G.nodes[node]["epi_history"] = [0.35, 0.42, 0.50] @@ -223,13 +224,15 @@ def test_bootstrap_sequence_grammar_valid(self): G.graph["VALIDATE_OPERATOR_PRECONDITIONS"] = True # Sequence should pass grammar validation + # Note: Removed Transition at end as it requires perturbation before it run_sequence(G, node, [ Emission(), # U1a: Generator for EPI=0 Coherence(), # U2: Stabilizer after generation Dissonance(), # U2: Destabilizer (needs stabilizer after) Mutation(), # U4b: Transformer (has IL + destabilizer) Coherence(), # U2: Stabilizer after mutation - Transition(), # U1b: Closure + # Transition requires perturbation, so use Silence for closure + Silence(), # U1b: Closure ]) # Should complete without grammar violations @@ -247,8 +250,8 @@ def test_bootstrap_node_becomes_viable(self): ]) # Node should be viable - assert G.nodes[node]["epi"] != 0.0 - assert G.nodes[node]["vf"] > 0 + assert G.nodes[node]["EPI"] != 0.0 + assert G.nodes[node]["νf"] > 0 assert 0 <= G.nodes[node]["theta"] < 6.28319 # 2π @@ -272,15 +275,16 @@ def test_oz_zhir_oz_zhir_sequence(self): ]) # Node should still be viable - assert G.nodes[node]["vf"] > 0 - assert -1.0 <= G.nodes[node]["epi"] <= 1.0 + assert G.nodes[node]["νf"] > 0 + assert -1.0 <= G.nodes[node]["EPI"] <= 1.0 def test_resonance_after_mutation(self): """RA (Resonance) after ZHIR should propagate transformed state.""" G, node = create_nfr("test", epi=0.5, vf=1.0, theta=0.5) - # Add neighbor with compatible phase - G.add_node("neighbor", epi=0.4, vf=1.0, theta=0.6) - G.add_edge(node, "neighbor") + # Add neighbor with compatible phase and proper initialization + neighbor_id = "neighbor" + G.add_node(neighbor_id, EPI=0.4, **{"νf": 1.0}, theta=0.6, delta_nfr=0.0) + G.add_edge(node, neighbor_id) G.nodes[node]["epi_history"] = [0.35, 0.42, 0.50] diff --git a/tests/unit/operators/test_mutation_contracts.py b/tests/unit/operators/test_mutation_contracts.py index 66f5a5d0e..484a48a1f 100644 --- a/tests/unit/operators/test_mutation_contracts.py +++ b/tests/unit/operators/test_mutation_contracts.py @@ -40,13 +40,13 @@ def test_zhir_preserves_positive_epi_sign(self): G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] G.nodes[node]["delta_nfr"] = 0.3 - epi_before = G.nodes[node]["epi"] + epi_before = G.nodes[node]["EPI"] assert epi_before > 0, "Test setup: EPI should be positive" # Apply mutation Mutation()(G, node) - epi_after = G.nodes[node]["epi"] + epi_after = G.nodes[node]["EPI"] # CRITICAL CONTRACT: Positive EPI must remain positive assert epi_after > 0, \ @@ -58,13 +58,13 @@ def test_zhir_preserves_negative_epi_sign(self): G.nodes[node]["epi_history"] = [-0.7, -0.6, -0.5] G.nodes[node]["delta_nfr"] = 0.3 - epi_before = G.nodes[node]["epi"] + epi_before = G.nodes[node]["EPI"] assert epi_before < 0, "Test setup: EPI should be negative" # Apply mutation Mutation()(G, node) - epi_after = G.nodes[node]["epi"] + epi_after = G.nodes[node]["EPI"] # CRITICAL CONTRACT: Negative EPI must remain negative assert epi_after < 0, \ @@ -75,13 +75,13 @@ def test_zhir_preserves_sign_in_canonical_sequence(self): G, node = create_nfr("test", epi=0.6, vf=1.0) G.nodes[node]["epi_history"] = [0.4, 0.5, 0.6] - epi_initial = G.nodes[node]["epi"] + epi_initial = G.nodes[node]["EPI"] sign_initial = 1 if epi_initial > 0 else -1 # Apply canonical sequence run_sequence(G, node, [Coherence(), Dissonance(), Mutation()]) - epi_final = G.nodes[node]["epi"] + epi_final = G.nodes[node]["EPI"] sign_final = 1 if epi_final > 0 else -1 # Sign must be preserved @@ -97,7 +97,7 @@ def test_zhir_handles_zero_epi(self): # Should not raise error Mutation()(G, node) - epi_after = G.nodes[node]["epi"] + epi_after = G.nodes[node]["EPI"] # Result can be positive, negative, or zero (no sign to preserve at 0) # Contract is satisfied as long as it doesn't crash @@ -107,13 +107,13 @@ def test_zhir_preserves_sign_with_high_transformation(self): G.nodes[node]["epi_history"] = [0.4, 0.55, 0.7] G.nodes[node]["delta_nfr"] = 0.8 # High transformation pressure - epi_before = G.nodes[node]["epi"] + epi_before = G.nodes[node]["EPI"] assert epi_before > 0 # Apply with strong destabilizer first run_sequence(G, node, [Dissonance(), Mutation()]) - epi_after = G.nodes[node]["epi"] + epi_after = G.nodes[node]["EPI"] # Even with strong transformation, sign must be preserved assert epi_after > 0, \ @@ -128,13 +128,13 @@ def test_zhir_does_not_collapse_vf(self): G, node = create_nfr("test", epi=0.5, vf=1.0) G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] - vf_before = G.nodes[node]["vf"] + vf_before = G.nodes[node]["νf"] assert vf_before > 0, "Test setup: νf should be positive" # Apply mutation Mutation()(G, node) - vf_after = G.nodes[node]["vf"] + vf_after = G.nodes[node]["νf"] # CRITICAL CONTRACT: νf must remain positive assert vf_after > 0, \ @@ -145,12 +145,12 @@ def test_zhir_does_not_drastically_reduce_vf(self): G, node = create_nfr("test", epi=0.5, vf=1.0) G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] - vf_before = G.nodes[node]["vf"] + vf_before = G.nodes[node]["νf"] # Apply mutation Mutation()(G, node) - vf_after = G.nodes[node]["vf"] + vf_after = G.nodes[node]["νf"] # νf should not drop by more than 50% in single mutation # (This is a soft contract - strong drops indicate potential issue) @@ -162,13 +162,13 @@ def test_zhir_preserves_vf_in_multiple_applications(self): G, node = create_nfr("test", epi=0.5, vf=1.0) G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] - vf_initial = G.nodes[node]["vf"] + vf_initial = G.nodes[node]["νf"] # Apply 5 mutation cycles for i in range(5): run_sequence(G, node, [Coherence(), Dissonance(), Mutation()]) - vf_final = G.nodes[node]["vf"] + vf_final = G.nodes[node]["νf"] # νf should not have collapsed to near-zero assert vf_final > 0.1 * vf_initial, \ @@ -184,7 +184,7 @@ def test_zhir_with_low_initial_vf(self): # Apply mutation Mutation()(G, node) - vf_after = G.nodes[node]["vf"] + vf_after = G.nodes[node]["νf"] # Should remain positive assert vf_after > 0, "ZHIR collapsed already-low νf" @@ -199,7 +199,7 @@ def test_zhir_vf_metrics_tracked(self): G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] G.graph["COLLECT_OPERATOR_METRICS"] = True - vf_before = G.nodes[node]["vf"] + vf_before = G.nodes[node]["νf"] Mutation()(G, node) @@ -210,7 +210,7 @@ def test_zhir_vf_metrics_tracked(self): assert "delta_vf" in metrics # delta_vf should reflect actual change - vf_after = G.nodes[node]["vf"] + vf_after = G.nodes[node]["νf"] expected_delta = vf_after - vf_before assert abs(metrics["delta_vf"] - expected_delta) < 0.01 @@ -227,7 +227,7 @@ def test_zhir_respects_epi_upper_bound(self): # Apply with destabilizer run_sequence(G, node, [Dissonance(), Mutation()]) - epi_after = G.nodes[node]["epi"] + epi_after = G.nodes[node]["EPI"] # Should respect upper bound assert epi_after <= 1.0, \ @@ -242,7 +242,7 @@ def test_zhir_respects_epi_lower_bound(self): # Apply with destabilizer run_sequence(G, node, [Dissonance(), Mutation()]) - epi_after = G.nodes[node]["epi"] + epi_after = G.nodes[node]["EPI"] # Should respect lower bound assert epi_after >= -1.0, \ @@ -271,21 +271,21 @@ class TestZHIRContractIntegration: def test_all_contracts_satisfied_in_typical_use(self): """Typical ZHIR usage should satisfy all contracts.""" G, node = create_nfr("test", epi=0.5, vf=1.0, theta=1.0) - G.nodes[node]["epi_kind"] = "test_pattern" + G.nodes[node]["EPI_kind"] = "test_pattern" G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] - epi_before = G.nodes[node]["epi"] - vf_before = G.nodes[node]["vf"] + epi_before = G.nodes[node]["EPI"] + vf_before = G.nodes[node]["νf"] sign_before = 1 if epi_before > 0 else -1 - identity_before = G.nodes[node]["epi_kind"] + identity_before = G.nodes[node]["EPI_kind"] # Apply canonical sequence run_sequence(G, node, [Coherence(), Dissonance(), Mutation(), Coherence()]) - epi_after = G.nodes[node]["epi"] - vf_after = G.nodes[node]["vf"] + epi_after = G.nodes[node]["EPI"] + vf_after = G.nodes[node]["νf"] sign_after = 1 if epi_after > 0 else -1 - identity_after = G.nodes[node]["epi_kind"] + identity_after = G.nodes[node]["EPI_kind"] theta_after = G.nodes[node]["theta"] # Check all contracts @@ -298,20 +298,20 @@ def test_all_contracts_satisfied_in_typical_use(self): def test_contracts_under_extreme_conditions(self): """Contracts should hold even under extreme transformations.""" G, node = create_nfr("test", epi=0.8, vf=2.0, theta=0.1) - G.nodes[node]["epi_kind"] = "extreme_pattern" + G.nodes[node]["EPI_kind"] = "extreme_pattern" G.nodes[node]["epi_history"] = [0.5, 0.65, 0.8] G.nodes[node]["delta_nfr"] = 0.9 # Very high pressure - epi_before = G.nodes[node]["epi"] - vf_before = G.nodes[node]["vf"] + epi_before = G.nodes[node]["EPI"] + vf_before = G.nodes[node]["νf"] sign_before = 1 if epi_before > 0 else -1 # Apply with extreme conditions G.graph["GLYPH_FACTORS"] = {"ZHIR_theta_shift_factor": 0.9} run_sequence(G, node, [Dissonance(), Mutation()]) - epi_after = G.nodes[node]["epi"] - vf_after = G.nodes[node]["vf"] + epi_after = G.nodes[node]["EPI"] + vf_after = G.nodes[node]["νf"] sign_after = 1 if epi_after > 0 else -1 # Contracts must still hold @@ -325,12 +325,12 @@ def test_contracts_with_negative_dnfr(self): G.nodes[node]["epi_history"] = [0.7, 0.6, 0.5] # Decreasing G.nodes[node]["delta_nfr"] = -0.4 # Negative pressure - epi_before = G.nodes[node]["epi"] + epi_before = G.nodes[node]["EPI"] sign_before = 1 Mutation()(G, node) - epi_after = G.nodes[node]["epi"] + epi_after = G.nodes[node]["EPI"] sign_after = 1 if epi_after > 0 else -1 # Sign must be preserved even with negative ΔNFR diff --git a/tests/unit/operators/test_mutation_edge_cases.py b/tests/unit/operators/test_mutation_edge_cases.py index b14a4f90e..5f2059e04 100644 --- a/tests/unit/operators/test_mutation_edge_cases.py +++ b/tests/unit/operators/test_mutation_edge_cases.py @@ -38,8 +38,8 @@ def test_zhir_on_isolated_node_succeeds(self): Mutation()(G, node) # Node should still be viable - assert G.nodes[node]["vf"] > 0 - assert -1.0 <= G.nodes[node]["epi"] <= 1.0 + assert G.nodes[node]["νf"] > 0 + assert -1.0 <= G.nodes[node]["EPI"] <= 1.0 def test_zhir_isolated_node_transforms_phase(self): """Isolated node's phase should still transform.""" @@ -60,23 +60,23 @@ def test_zhir_isolated_node_transforms_phase(self): def test_zhir_isolated_preserves_all_contracts(self): """Isolated node should satisfy all ZHIR contracts.""" G, node = create_nfr("test", epi=0.6, vf=1.2) - G.nodes[node]["epi_kind"] = "isolated_pattern" + G.nodes[node]["EPI_kind"] = "isolated_pattern" G.nodes[node]["epi_history"] = [0.4, 0.5, 0.6] - epi_before = G.nodes[node]["epi"] - vf_before = G.nodes[node]["vf"] + epi_before = G.nodes[node]["EPI"] + vf_before = G.nodes[node]["νf"] sign_before = 1 if epi_before > 0 else -1 Mutation()(G, node) - epi_after = G.nodes[node]["epi"] - vf_after = G.nodes[node]["vf"] + epi_after = G.nodes[node]["EPI"] + vf_after = G.nodes[node]["νf"] sign_after = 1 if epi_after > 0 else -1 # All contracts should hold assert sign_after == sign_before, "Sign contract violated" assert vf_after > 0, "νf contract violated" - assert G.nodes[node]["epi_kind"] == "isolated_pattern", "Identity contract violated" + assert G.nodes[node]["EPI_kind"] == "isolated_pattern", "Identity contract violated" class TestZHIRPhaseBoundaries: @@ -172,8 +172,8 @@ def test_zhir_reproducible_with_same_seed(self): Mutation()(G1, node1) result1 = { "theta": G1.nodes[node1]["theta"], - "epi": G1.nodes[node1]["epi"], - "vf": G1.nodes[node1]["vf"], + "epi": G1.nodes[node1]["EPI"], + "vf": G1.nodes[node1]["νf"], } # Second run with same seed @@ -185,14 +185,14 @@ def test_zhir_reproducible_with_same_seed(self): Mutation()(G2, node2) result2 = { "theta": G2.nodes[node2]["theta"], - "epi": G2.nodes[node2]["epi"], - "vf": G2.nodes[node2]["vf"], + "epi": G2.nodes[node2]["EPI"], + "vf": G2.nodes[node2]["νf"], } # Should be identical assert abs(result1["theta"] - result2["theta"]) < 1e-10 - assert abs(result1["epi"] - result2["epi"]) < 1e-10 - assert abs(result1["vf"] - result2["vf"]) < 1e-10 + assert abs(result1["EPI"] - result2["EPI"]) < 1e-10 + assert abs(result1["νf"] - result2["νf"]) < 1e-10 def test_zhir_different_seeds_produce_different_results(self): """Different seeds should produce different results (if stochastic).""" @@ -250,7 +250,7 @@ def test_zhir_with_very_high_epi(self): Mutation()(G, node) # Should not exceed bounds - assert G.nodes[node]["epi"] <= 1.0 + assert G.nodes[node]["EPI"] <= 1.0 def test_zhir_with_very_low_epi(self): """ZHIR near lower EPI bound.""" @@ -260,18 +260,18 @@ def test_zhir_with_very_low_epi(self): Mutation()(G, node) # Should not exceed bounds - assert G.nodes[node]["epi"] >= -1.0 + assert G.nodes[node]["EPI"] >= -1.0 def test_zhir_with_very_high_vf(self): """ZHIR with very high structural frequency.""" - G, node = create_nfr("test", epi=0.5, vf=100.0) + G, node = create_nfr("test", epi=0.5, vf=9.5) # Near maximum (10.0) G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] # Should not raise error Mutation()(G, node) # Should still be viable - assert G.nodes[node]["vf"] > 0 + assert G.nodes[node]["νf"] > 0 def test_zhir_with_very_high_dnfr(self): """ZHIR with extreme ΔNFR values.""" @@ -283,7 +283,7 @@ def test_zhir_with_very_high_dnfr(self): Mutation()(G, node) # Should maintain bounds - assert -1.0 <= G.nodes[node]["epi"] <= 1.0 + assert -1.0 <= G.nodes[node]["EPI"] <= 1.0 def test_zhir_with_negative_dnfr(self): """ZHIR with negative ΔNFR (contraction pressure).""" @@ -345,8 +345,8 @@ def test_zhir_repeated_immediate_applications(self): Mutation()(G, node) # Node should still be viable - assert G.nodes[node]["vf"] > 0 - assert -1.0 <= G.nodes[node]["epi"] <= 1.0 + assert G.nodes[node]["νf"] > 0 + assert -1.0 <= G.nodes[node]["EPI"] <= 1.0 assert 0 <= G.nodes[node]["theta"] < 2 * math.pi diff --git a/tests/unit/operators/test_mutation_identity.py b/tests/unit/operators/test_mutation_identity.py index 007f302b5..d29eede91 100644 --- a/tests/unit/operators/test_mutation_identity.py +++ b/tests/unit/operators/test_mutation_identity.py @@ -35,7 +35,7 @@ def test_zhir_preserves_epi_kind(self): # Set structural identity epi_kind_original = "coherent_oscillator" - G.nodes[node]["epi_kind"] = epi_kind_original + G.nodes[node]["EPI_kind"] = epi_kind_original G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] G.nodes[node]["delta_nfr"] = 0.3 @@ -52,7 +52,7 @@ def test_zhir_preserves_identity_in_canonical_sequence(self): G, node = create_nfr("test", epi=0.4, vf=1.0) # Set identity - G.nodes[node]["epi_kind"] = "test_pattern" + G.nodes[node]["EPI_kind"] = "test_pattern" G.nodes[node]["epi_history"] = [0.35, 0.38, 0.40] # Apply canonical sequence @@ -64,7 +64,7 @@ def test_zhir_preserves_identity_in_canonical_sequence(self): ]) # Identity must be preserved through entire sequence - assert G.nodes[node]["epi_kind"] == "test_pattern" + assert G.nodes[node]["EPI_kind"] == "test_pattern" def test_multiple_zhir_preserve_identity(self): """Multiple ZHIR applications (with IL between) preserve identity.""" @@ -72,7 +72,7 @@ def test_multiple_zhir_preserve_identity(self): # Set identity original_identity = "fractal_structure" - G.nodes[node]["epi_kind"] = original_identity + G.nodes[node]["EPI_kind"] = original_identity G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] # Apply 3 mutation cycles @@ -85,7 +85,7 @@ def test_multiple_zhir_preserve_identity(self): ]) # Identity must still be preserved - assert G.nodes[node]["epi_kind"] == original_identity, \ + assert G.nodes[node]["EPI_kind"] == original_identity, \ "Multiple ZHIR violated identity preservation" def test_zhir_without_epi_kind_set(self): @@ -113,14 +113,14 @@ def test_zhir_preserves_custom_identities(self): for identity_type in identity_types: G, node = create_nfr(f"test_{identity_type}", epi=0.5, vf=1.0) - G.nodes[node]["epi_kind"] = identity_type + G.nodes[node]["EPI_kind"] = identity_type G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] # Apply mutation run_sequence(G, node, [Coherence(), Dissonance(), Mutation()]) # Verify preservation - assert G.nodes[node]["epi_kind"] == identity_type, \ + assert G.nodes[node]["EPI_kind"] == identity_type, \ f"Failed to preserve identity: {identity_type}" @@ -130,7 +130,7 @@ class TestZHIRIdentityMetrics: def test_identity_preserved_in_metrics(self): """Metrics should track identity preservation.""" G, node = create_nfr("test", epi=0.5, vf=1.0) - G.nodes[node]["epi_kind"] = "test_identity" + G.nodes[node]["EPI_kind"] = "test_identity" G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] G.graph["COLLECT_OPERATOR_METRICS"] = True @@ -179,7 +179,7 @@ def test_identity_preserved_despite_phase_change(self): import math G, node = create_nfr("test", epi=0.5, vf=1.0) - G.nodes[node]["epi_kind"] = "rotating_pattern" + G.nodes[node]["EPI_kind"] = "rotating_pattern" G.nodes[node]["theta"] = 0.0 G.nodes[node]["delta_nfr"] = 0.8 # Strong transformation G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] @@ -195,12 +195,12 @@ def test_identity_preserved_despite_phase_change(self): assert abs(theta_after) > 0.5 # But identity MUST be preserved - assert G.nodes[node]["epi_kind"] == "rotating_pattern" + assert G.nodes[node]["EPI_kind"] == "rotating_pattern" def test_identity_preserved_with_high_epi_change(self): """Identity preserved even when EPI changes (within bounds).""" G, node = create_nfr("test", epi=0.3, vf=1.0) - G.nodes[node]["epi_kind"] = "adaptive_pattern" + G.nodes[node]["EPI_kind"] = "adaptive_pattern" G.nodes[node]["epi_history"] = [0.1, 0.2, 0.3] G.nodes[node]["delta_nfr"] = 0.5 @@ -208,18 +208,18 @@ def test_identity_preserved_with_high_epi_change(self): run_sequence(G, node, [Dissonance(), Mutation()]) # EPI may have changed - epi_after = G.nodes[node]["epi"] + epi_after = G.nodes[node]["EPI"] # (exact value depends on implementation) # Identity must be preserved - assert G.nodes[node]["epi_kind"] == "adaptive_pattern" + assert G.nodes[node]["EPI_kind"] == "adaptive_pattern" def test_identity_preserved_across_regime_change(self): """Identity preserved when phase crosses regime boundary.""" import math G, node = create_nfr("test", epi=0.5, vf=1.0) - G.nodes[node]["epi_kind"] = "regime_crossing_pattern" + G.nodes[node]["EPI_kind"] = "regime_crossing_pattern" G.nodes[node]["theta"] = math.pi / 2 - 0.1 # Near boundary G.nodes[node]["delta_nfr"] = 0.6 # Strong shift G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] @@ -234,7 +234,7 @@ def test_identity_preserved_across_regime_change(self): regime_after = int(theta_after // (math.pi / 2)) # Identity must be preserved regardless of regime change - assert G.nodes[node]["epi_kind"] == "regime_crossing_pattern" + assert G.nodes[node]["EPI_kind"] == "regime_crossing_pattern" class TestZHIRIdentityEdgeCases: @@ -243,43 +243,43 @@ class TestZHIRIdentityEdgeCases: def test_identity_with_negative_epi(self): """Identity preserved when EPI is negative.""" G, node = create_nfr("test", epi=-0.5, vf=1.0) - G.nodes[node]["epi_kind"] = "negative_epi_pattern" + G.nodes[node]["EPI_kind"] = "negative_epi_pattern" G.nodes[node]["epi_history"] = [-0.7, -0.6, -0.5] Mutation()(G, node) - assert G.nodes[node]["epi_kind"] == "negative_epi_pattern" + assert G.nodes[node]["EPI_kind"] == "negative_epi_pattern" def test_identity_with_zero_epi(self): """Identity preserved when EPI crosses zero.""" G, node = create_nfr("test", epi=0.0, vf=1.0) - G.nodes[node]["epi_kind"] = "zero_crossing_pattern" + G.nodes[node]["EPI_kind"] = "zero_crossing_pattern" G.nodes[node]["epi_history"] = [-0.1, 0.0, 0.0] Mutation()(G, node) - assert G.nodes[node]["epi_kind"] == "zero_crossing_pattern" + assert G.nodes[node]["EPI_kind"] == "zero_crossing_pattern" def test_identity_with_special_characters(self): """Identity strings with special characters should be preserved.""" G, node = create_nfr("test", epi=0.5, vf=1.0) - G.nodes[node]["epi_kind"] = "pattern_v2.0_α-β" + G.nodes[node]["EPI_kind"] = "pattern_v2.0_α-β" G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] Mutation()(G, node) - assert G.nodes[node]["epi_kind"] == "pattern_v2.0_α-β" + assert G.nodes[node]["EPI_kind"] == "pattern_v2.0_α-β" def test_identity_with_long_name(self): """Long identity names should be preserved.""" G, node = create_nfr("test", epi=0.5, vf=1.0) long_identity = "very_long_identity_name_" * 10 # 260+ characters - G.nodes[node]["epi_kind"] = long_identity + G.nodes[node]["EPI_kind"] = long_identity G.nodes[node]["epi_history"] = [0.3, 0.4, 0.5] Mutation()(G, node) - assert G.nodes[node]["epi_kind"] == long_identity + assert G.nodes[node]["EPI_kind"] == long_identity class TestZHIRIdentityViolationDetection: @@ -301,7 +301,7 @@ def test_identity_violation_placeholder(self): # When identity enforcement is implemented, this should become: # # G, node = create_nfr("test", epi=0.5, vf=1.0) - # G.nodes[node]["epi_kind"] = "original_identity" + # G.nodes[node]["EPI_kind"] = "original_identity" # G.graph["VALIDATE_OPERATOR_POSTCONDITIONS"] = True # # # Simulate identity violation (would require internal modification)