""" Tests for asynchronous logit grafting and NGC warm-start settling. """ import os import sys import time import numpy as np import pytest ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) if ROOT not in sys.path: sys.path.insert(0, ROOT) def _load_module(name: str, relpath: str): import importlib.util path = os.path.join(ROOT, relpath) spec = importlib.util.spec_from_file_location(name, path) mod = importlib.util.module_from_spec(spec) assert spec.loader is not None spec.loader.exec_module(mod) return mod def test_ngc_warm_start_fewer_steps(): mod = _load_module("tensegrity_engine_ngc_test", os.path.join("tensegrity", "engine", "ngc.py")) PredictiveCodingCircuit = mod.PredictiveCodingCircuit ngc = PredictiveCodingCircuit( layer_sizes=[16, 8, 4], settle_steps=30, settle_steps_warm=4, obs_change_threshold=1e-6, adaptive_precision=False, ) pattern = np.random.RandomState(0).randn(16) full = ngc.settle(pattern) assert full["settle_steps"] == 30 warm = ngc.settle(pattern) assert warm["settle_steps"] == 4 def test_async_beliefs_processor_matches_sync(): try: import torch except ImportError: return mod = _load_module("logit_bias_test", os.path.join("tensegrity", "graft", "logit_bias.py")) TensegrityLogitsProcessor = mod.TensegrityLogitsProcessor hypothesis_tokens = {"up": {10, 11}, "down": {20, 21}} posteriors = {"up": 0.88, "down": 0.12} def belief_fn(): return posteriors sync = TensegrityLogitsProcessor( hypothesis_tokens=hypothesis_tokens, belief_fn=belief_fn, vocab_size=128, entropy_gate=0.95, min_confidence=0.2, async_beliefs=False, ) async_p = TensegrityLogitsProcessor( hypothesis_tokens=hypothesis_tokens, belief_fn=belief_fn, vocab_size=128, entropy_gate=0.95, min_confidence=0.2, async_beliefs=True, belief_poll_s=0.002, ) time.sleep(0.05) fake_ids = torch.zeros(1, 3, dtype=torch.long) scores = torch.randn(1, 128) out_sync = sync(fake_ids, scores.clone()) out_async = async_p(fake_ids, scores.clone()) async_p.close() if async_p.state.bias_emitted and sync.state.bias_emitted: assert torch.allclose(out_sync, out_async, atol=1e-5) def test_associative_access_decay(): mod = _load_module("associative_test", os.path.join("tensegrity", "memory", "associative.py")) AssociativeMemory = mod.AssociativeMemory mem = AssociativeMemory( pattern_dim=8, beta=1.0, decay_every_n_retrieves=1, access_decay=0.5, max_patterns=100, ) mem.store(np.array([1.0, 0, 0, 0, 0, 0, 0, 0])) mem.retrieve(np.array([1.0, 0, 0, 0, 0, 0, 0, 0])) assert mem._access_counts[0] >= 1.0 mem._decay_access_counts() assert mem._access_counts[0] < 1.0 def test_build_scm_from_proposal(): try: import networkx # noqa: F401 except ImportError: return from tensegrity.broca.schemas import ProposedSCM, CausalEdge from tensegrity.causal.from_proposal import build_scm_from_proposal p = ProposedSCM( name="test_model", description="x drives y", edges=[ CausalEdge(source="x", target="y", mechanism="causes"), CausalEdge(source="y", target="z", mechanism="enables"), ], ) scm = build_scm_from_proposal(p) assert "x" in scm.variables and "z" in scm.variables def test_scm_marginalizes_missing_parents_and_counterfactual_changes_descendants(): pytest.importorskip("networkx") from tensegrity.causal.scm import StructuralCausalModel scm = StructuralCausalModel("two_node") scm.add_variable("X", n_values=2) scm.add_variable("Y", n_values=2, parents=["X"]) scm.mechanisms["X"].cpt_params = np.array([[100.0], [1.0]]) scm.mechanisms["Y"].cpt_params = np.array([ [100.0, 1.0], [1.0, 100.0], ]) p_x0 = 100.0 / 101.0 p_x1 = 1.0 / 101.0 expected_p_y1 = p_x0 * (1.0 / 101.0) + p_x1 * (100.0 / 101.0) assert np.isclose( scm.observe({"Y": 1})["log_likelihood"], np.log(expected_p_y1), ) cf = scm.counterfactual({"X": 0, "Y": 0}, {"X": 1}, ["Y"]) assert cf["Y"][1] > 0.98 assert cf["Y"][0] < 0.02 def test_eliminated_hypothesis_stays_eliminated_after_engine_blend(): from tensegrity.broca.controller import CognitiveController controller = CognitiveController( n_hypotheses=2, hypothesis_labels=["a", "b"], use_llm=False, ) controller.belief_state.hypotheses[0].probability = 0.0 controller.belief_state.hypotheses[1].probability = 1.0 controller.belief_state.eliminated_hypotheses = ["H0"] controller._update_hypotheses_from_inference( { "belief_state": np.array([1.0, 0.0]), "arena": {"tension": 0.5}, "free_energy": 0.0, }, np.array([0.0, 0.0]), ) assert controller.belief_state.hypotheses[0].probability == 0.0 assert controller.belief_state.hypotheses[1].probability == 1.0 assert controller.belief_state.eliminated_hypotheses == ["H0"] def test_canonical_soft_reset_preserves_ngc_weights(): from tensegrity.pipeline.canonical import CanonicalPipeline pipeline = CanonicalPipeline(["a", "b"]) before = [w.copy() for w in pipeline.controller.agent.field.ngc.W] pipeline._soft_reset_in_place(["a", "b"]) for old, new in zip(before, pipeline.controller.agent.field.ngc.W): assert np.allclose(old, new) def test_canonical_feedback_persists_memory_and_reload(tmp_path): from tensegrity.bench.tasks import TaskSample from tensegrity.pipeline.canonical import CanonicalPipeline state_path = tmp_path / "agent_state.pkl" sample = TaskSample( id="demo_1", prompt="A coolant leak causes the engine to overheat.", choices=["low oil", "coolant leak"], gold=1, metadata={"task": "demo"}, ) pipeline = CanonicalPipeline( ["_empty_0", "_empty_1"], persistent_state_path=str(state_path), ) before = len(pipeline.controller.agent.episodic.episodes) pipeline.learn_from_feedback(sample, committed_idx=0) assert state_path.exists() assert len(pipeline.controller.agent.episodic.episodes) > before reloaded = CanonicalPipeline( ["_empty_0", "_empty_1"], persistent_state_path=str(state_path), ) assert len(reloaded.controller.agent.episodic.episodes) > 0 memory_scores = reloaded._memory_choice_scores(sample) assert memory_scores.shape == (2,) assert np.all(np.isfinite(memory_scores)) def test_canonical_integrates_linguistic_scores_inside_posterior(): from tensegrity.bench.tasks import TaskSample from tensegrity.pipeline.canonical import CanonicalPipeline sample = TaskSample( id="linguistic_1", prompt="Pick the candidate supported by the language evidence.", choices=["alpha", "beta"], gold=1, ) pipeline = CanonicalPipeline( ["_empty_0", "_empty_1"], max_iterations=1, llm_evidence_weight=10.0, ) result = pipeline.score_multichoice(sample, linguistic_scores=[-5.0, 5.0]) assert result.committed_idx == 1 assert result.linguistic_scores == [-5.0, 5.0] def test_canonical_benchmark_path_uses_broca_transducer(): from tensegrity.bench.tasks import TaskSample from tensegrity.broca.schemas import ParsedObservation from tensegrity.pipeline.canonical import CanonicalPipeline class FakeBroca: def __init__(self): self.parse_calls = 0 def parse(self, text, context=None): self.parse_calls += 1 return ParsedObservation( entities=[], relations=[], implicit_relations=[], is_question=text.strip().endswith("?"), is_assertion=not text.strip().endswith("?"), is_command=False, negation_present=False, temporal_marker=None, confidence_linguistic=0.5, ) broca = FakeBroca() sample = TaskSample( id="broca_1", prompt="Which candidate is supported?", choices=["alpha", "beta"], gold=0, ) pipeline = CanonicalPipeline( ["_empty_0", "_empty_1"], broca=broca, use_llm_broca=True, max_iterations=1, ) pipeline.score_multichoice(sample, linguistic_scores=[1.0, 0.0]) assert broca.parse_calls > 0 def test_deterministic_broca_proposes_arena_compatible_scm(): from tensegrity.broca.interface import DeterministicBrocaInterface from tensegrity.causal.from_proposal import build_scm_from_proposal broca = DeterministicBrocaInterface() proposal = broca.propose_causal_hypothesis( "A blocked valve prevents pressure from reaching the chamber.", ["direct_causal", "mediated_causal", "broca_contextual_causal"], ) scm = build_scm_from_proposal(proposal) assert proposal.name not in {"direct_causal", "mediated_causal", "broca_contextual_causal"} assert "state" in scm.variables assert "observation" in scm.variables assert ("state", "observation") in scm.edges def test_runner_live_emission_choice_constraints(): from tensegrity.bench.runner import EvalRunner seqs = { 0: [[11, 12], [9, 11, 12]], 1: [[21]], } grounding = EvalRunner._choice_token_grounding(seqs) assert grounding == {"H0": {9, 11, 12}, "H1": {21}} allowed = EvalRunner._build_prefix_allowed_fn( prompt_len=3, choice_sequences=seqs, eos_token_id=99, ) assert set(allowed(0, [1, 2, 3])) == {9, 11, 21} assert allowed(0, [1, 2, 3, 11]) == [12] assert allowed(0, [1, 2, 3, 11, 12]) == [99] assert EvalRunner._parse_emitted_choice([11, 12, 99], {(11, 12): 0, (21,): 1}, 99) == 0 def test_runner_local_emission_verbalizes_committed_choice_under_graft(): import torch from types import SimpleNamespace from tensegrity.bench.runner import EvalRunner from tensegrity.bench.tasks import TaskSample class FakeTokenizer: vocab_size = 128 eos_token_id = 99 pad_token_id = 99 def __len__(self): return self.vocab_size def encode(self, text, add_special_tokens=False): stripped = text.strip() prefix = [] if text.startswith(" "): prefix = [10] elif text.startswith("\n"): prefix = [13] if stripped == "alpha": return prefix + [11] if stripped == "beta": return prefix + [21] return [1] def apply_chat_template(self, messages, return_tensors="pt", add_generation_prompt=True): return torch.tensor([[1, 2, 3]], dtype=torch.long) def decode(self, ids, skip_special_tokens=True): pieces = [] for token_id in [int(t) for t in ids]: if token_id == 99 and skip_special_tokens: continue if token_id in {10, 13}: continue if token_id == 11: pieces.append("alpha") elif token_id == 21: pieces.append("beta") return "".join(pieces) class FakeModel: device = torch.device("cpu") def generate( self, input_ids, attention_mask, logits_processor, prefix_allowed_tokens_fn, max_new_tokens, do_sample, pad_token_id, eos_token_id, ): assert attention_mask is not None cur = input_ids.clone() vocab = 140 for _ in range(max_new_tokens): allowed = prefix_allowed_tokens_fn(0, cur[0]) scores = torch.full((1, vocab), -1e9, dtype=torch.float32) for token_id in allowed: scores[0, int(token_id)] = 0.0 scores = logits_processor(cur, scores) next_id = int(torch.argmax(scores[0]).item()) cur = torch.cat([cur, torch.tensor([[next_id]], dtype=torch.long)], dim=1) if next_id == eos_token_id: break return cur runner = EvalRunner(mode="local", lam=1.0) runner._tokenizer = FakeTokenizer() runner._model = FakeModel() sample = TaskSample( id="emit_1", prompt="Choose the supported answer.", choices=["alpha", "beta"], gold=1, ) commit = SimpleNamespace(committed_idx=1, belief=[0.1, 0.9]) pred, text, mode, graft_state = runner._emit_answer(sample, commit) assert pred == 1 assert text == "beta" assert mode == "logit_grafted_verbalizer" assert graft_state["bias_emitted"] is True if __name__ == "__main__": test_ngc_warm_start_fewer_steps() test_async_beliefs_processor_matches_sync() test_associative_access_decay() test_build_scm_from_proposal() test_scm_marginalizes_missing_parents_and_counterfactual_changes_descendants() test_eliminated_hypothesis_stays_eliminated_after_engine_blend() test_canonical_soft_reset_preserves_ngc_weights() print("ok")