Buckets:
| """Tests for onf.graph.run.track — the sequential Bayesian belief filter over the demonstration | |
| graph (the "WHERE: belief filter" section of onf.graph.core.schema) — plus | |
| onf.graph.core.nodes.NodeTable.node_at_raw, the canonical raw-frame lookup TransitionKernel | |
| is built from. | |
| Mostly data-coupling-free (small, synthetic, in-memory graphs — mirrors tests/test_retrieve.py's | |
| fixture pattern), except for a handful of tests gated on the real long suite artifacts at | |
| outputs/long/frozen_learn1/artifacts (V=28476, 500 demos, 767k edges), which validate that the | |
| vectorised node_at_raw/TransitionKernel.build construction actually holds at real scale and | |
| report the per-check cost — skipped cleanly (tests/test_field_parity.py's skip_no_field | |
| pattern) when that directory is absent. | |
| """ | |
| from __future__ import annotations | |
| import math | |
| import time | |
| from pathlib import Path | |
| import numpy as np | |
| import pytest | |
| import torch | |
| from onf.config import GraphConfig | |
| from onf.graph.core import schema | |
| from onf.graph.core.edges import EdgeSet | |
| from onf.graph.net.gnn import GraphRetrieverNet | |
| from onf.graph.core.nodes import NodeTable | |
| from onf.graph.run.retrieve import GraphRetriever | |
| from onf.graph.core.geometry import graph_hash | |
| from onf.graph.run.track import ( | |
| TASK_PRIOR_EPS, | |
| TASK_PRIOR_MIN_CHECKS, | |
| GraphTracker, | |
| TaskPosterior, | |
| TrackParams, | |
| TransitionKernel, | |
| ) | |
| from onf.graph.run.params import TrackerCalibration | |
| from onf.graph.build.tracker_fit import build_likelihood_sequences, evaluate_kernel_log_evidence, fit_transition_kernel | |
| # ====================================================================================================== | |
| # real-artifact skip marker (tests/test_field_parity.py's skip_no_field pattern) | |
| # ====================================================================================================== | |
| REAL_GRAPH_DIR = Path(__file__).resolve().parents[1] / "outputs" / "long" / "frozen_learn1" / "artifacts" | |
| HAS_REAL_GRAPH = (REAL_GRAPH_DIR / schema.NODES_NPZ).exists() | |
| skip_no_real_graph = pytest.mark.skipif(not HAS_REAL_GRAPH, reason=f"no real graph artifacts at {REAL_GRAPH_DIR}") | |
| # ====================================================================================================== | |
| # synthetic graph fixture (mirrors tests/test_retrieve.py's _make_demo/_toy_nodes_edges/_toy_net) | |
| # ====================================================================================================== | |
| DIM = 4 | |
| N_FRAMES = 31 | |
| COARSEN = 3 | |
| HIDDEN = 16 | |
| def _make_demo(task_id: int, seed: int, n: int = N_FRAMES, dim: int = DIM) -> dict: | |
| rng = np.random.RandomState(seed) | |
| start = rng.randn(dim) * 0.01 | |
| direction = rng.randn(dim) | |
| direction /= np.linalg.norm(direction) | |
| t = np.arange(n, dtype=np.float32) | |
| q = (start[None, :] + 0.05 * t[:, None] * direction[None, :]).astype(np.float32) | |
| qdot = np.zeros_like(q) | |
| qdot[1:] = q[1:] - q[:-1] | |
| grip = np.zeros(n, dtype=np.uint8) | |
| grip[n // 2 :] = 1 | |
| stage = grip.astype(np.int16) | |
| return {"q": q, "qdot": qdot, "grip": grip, "stage": stage, "task_id": task_id} | |
| def _toy_nodes_edges(n_demos: int = 6, dim: int = DIM) -> tuple[NodeTable, EdgeSet]: | |
| demos = [_make_demo(task_id=i % 2, seed=100 + i, dim=dim) for i in range(n_demos)] | |
| nodes = NodeTable.from_demos(demos, task_names=["taskA", "taskB"], coarsen=COARSEN) | |
| edges = EdgeSet.build(nodes, k_sib=4, k_align=2, device="cpu") | |
| return nodes, edges | |
| def _toy_net(nodes: NodeTable, hidden: int = HIDDEN, layers: int = 2, seed: int = 0) -> GraphRetrieverNet: | |
| torch.manual_seed(seed) | |
| net = GraphRetrieverNet(dim=nodes.dim, hidden=hidden, layers=layers, n_rel=schema.N_RELATIONS, agg="sum") | |
| mean, std = nodes.feature_stats() | |
| net.set_stats(mean, std) | |
| net.eval() | |
| return net | |
| def _declare_ee_base(retriever: GraphRetriever) -> GraphRetriever: | |
| """Stand in for NodeTable.ee_base on a corpus that is not a 7-DoF Panda. | |
| ee_base is normally forward-kinematicked from q_raw, which needs a real robot; these demos are | |
| straight lines in a D=5 joint space. Only the readout path that GATHERS an end-effector segment | |
| is exercised here, never its values, so the demos' own declared ee is the right stand-in.""" | |
| retriever.nodes.ee_base = retriever.nodes.ee_raw | |
| return retriever | |
| def _toy_retriever(n_demos: int = 6, seed: int = 0) -> GraphRetriever: | |
| nodes, edges = _toy_nodes_edges(n_demos=n_demos) | |
| net = _toy_net(nodes, seed=seed) | |
| # readout="argmax" -- every caller of this helper feeds the retriever into a GraphTracker, whose | |
| # internal per-node finalize (onf.graph.run.track._finalize) only supports "argmax" (it hand-rolls the | |
| # top-M/softmax aggregation itself rather than delegating to onf.graph.run.readout's registered arms, | |
| # which are built for a raw network posterior, not a filtered belief b_t -- see that function's own | |
| # docstring). GraphConfig's own default is "euc_raw" (the entry-side arm), which would make | |
| # GraphTracker.step() raise here. | |
| return _declare_ee_base( | |
| GraphRetriever(net, nodes, edges, cfg=GraphConfig(device="cpu", readout="argmax")) | |
| ) | |
| # ====================================================================================================== | |
| # 1. NodeTable.node_at_raw | |
| # ====================================================================================================== | |
| def _old_node_at_raw(nodes: NodeTable, owner: int, raw_idx: int) -> int: | |
| """The per-demo reference implementation this method replaces (verbatim from | |
| onf.graph.train.data._node_at_raw) — used here only to cross-check the vectorised implementation.""" | |
| node_ids = np.asarray(nodes.demo_nodes(int(owner))) | |
| t_raws = np.asarray(nodes.t_raw)[node_ids] | |
| pos = int(np.searchsorted(t_raws, int(raw_idx), side="right")) - 1 | |
| pos = min(max(pos, 0), len(node_ids) - 1) | |
| return int(node_ids[pos]) | |
| def test_node_at_raw_scalar_matches_per_demo_reference(): | |
| nodes, _ = _toy_nodes_edges() | |
| rng = np.random.RandomState(0) | |
| for _ in range(200): | |
| j = int(rng.randint(nodes.n_demos)) | |
| demo_start, demo_end = int(nodes.raw_ptr[j]), int(nodes.raw_ptr[j + 1]) | |
| raw_idx = int(rng.randint(demo_start - 3, demo_end + 10)) | |
| assert nodes.node_at_raw(j, raw_idx) == _old_node_at_raw(nodes, j, raw_idx) | |
| def test_node_at_raw_vectorised_matches_scalar_and_is_identity_at_zero(): | |
| nodes, _ = _toy_nodes_edges() | |
| owner = np.asarray(nodes.owner, dtype=np.int64) | |
| t_raw = np.asarray(nodes.t_raw, dtype=np.int64) | |
| succ0 = nodes.node_at_raw_many(owner, t_raw + 0) | |
| assert np.array_equal(succ0, np.arange(len(nodes))) # a=0 is the identity map | |
| succ4 = nodes.node_at_raw_many(owner, t_raw + 4) | |
| expected = np.array([_old_node_at_raw(nodes, int(o), int(t) + 4) for o, t in zip(owner, t_raw)]) | |
| assert np.array_equal(succ4, expected) | |
| def test_node_at_raw_returns_python_int_for_scalar_input(): | |
| nodes, _ = _toy_nodes_edges() | |
| v = nodes.node_at_raw(0, 3) | |
| assert isinstance(v, int) | |
| # ====================================================================================================== | |
| # 2. TransitionKernel construction invariants | |
| # ====================================================================================================== | |
| def test_succ_a_never_crosses_demo_boundary_and_is_monotone(): | |
| nodes, edges = _toy_nodes_edges() | |
| kernel = TransitionKernel.build(nodes, edges, device="cpu") | |
| owner = np.asarray(nodes.owner) | |
| t_raw = np.asarray(nodes.t_raw) | |
| for i, a in enumerate(kernel.advances): | |
| succ = kernel.succ[i].cpu().numpy() | |
| assert np.array_equal(owner[succ], owner), f"advance={a} crosses a demo boundary" | |
| # monotone in t_raw: within any (owner) block, t_raw[succ[u]] is non-decreasing in t_raw[u] | |
| # (owner is itself non-decreasing/contiguous, so adjacent-index comparison suffices) | |
| same_owner_adjacent = owner[:-1] == owner[1:] | |
| t_succ = t_raw[succ] | |
| assert np.all(t_succ[1:][same_owner_adjacent] >= t_succ[:-1][same_owner_adjacent]), ( | |
| f"advance={a} is not monotone in t_raw within a demo" | |
| ) | |
| def test_kernel_rows_sum_to_one(): | |
| """b @ P sums to 1 for every one-hot b -- the row-stochastic property, checked directly rather than | |
| materialising a dense [V,V] matrix (see TransitionKernel.push's mass-conservation docstring).""" | |
| nodes, edges = _toy_nodes_edges() | |
| kernel = TransitionKernel.build(nodes, edges, beta=0.4, device="cpu") | |
| v = len(nodes) | |
| for u in [0, 1, v // 2, v - 1]: | |
| b = torch.zeros(v, dtype=torch.float64) | |
| b[u] = 1.0 | |
| pred = kernel.push(b) | |
| assert pred.sum().item() == pytest.approx(1.0, abs=1e-9), f"row {u} does not sum to 1" | |
| assert bool((pred >= 0).all()) | |
| def test_kernel_push_conserves_mass_for_arbitrary_belief(): | |
| nodes, edges = _toy_nodes_edges() | |
| kernel = TransitionKernel.build(nodes, edges, beta=0.4, device="cpu") | |
| rng = np.random.RandomState(0) | |
| raw = rng.rand(len(nodes)) | |
| b = torch.as_tensor(raw / raw.sum(), dtype=torch.float64) | |
| pred = kernel.push(b) | |
| assert pred.sum().item() == pytest.approx(1.0, abs=1e-9) | |
| def test_kernel_with_params_reuses_precomputed_structure(): | |
| nodes, edges = _toy_nodes_edges() | |
| k1 = TransitionKernel.build(nodes, edges, device="cpu") | |
| k2 = k1.with_params(beta=0.9) | |
| assert k2.succ is k1.succ # same tensor object, not a copy | |
| assert k2.sib_src is k1.sib_src | |
| assert k2.beta == 0.9 and k1.beta != 0.9 | |
| def test_kernel_advance_set_and_pi_length_match_schema(): | |
| nodes, edges = _toy_nodes_edges() | |
| kernel = TransitionKernel.build(nodes, edges, device="cpu") | |
| assert kernel.advances == schema.TRACK_ADVANCE_SET | |
| assert kernel.pi.shape[0] == len(schema.TRACK_ADVANCE_SET) | |
| assert kernel.pi.sum().item() == pytest.approx(1.0, abs=1e-9) | |
| # ====================================================================================================== | |
| # 2b. E.1 fused kernel -- scatter-vs-fused equality, lazy build, mass conservation | |
| # ====================================================================================================== | |
| def test_fused_push_matches_scatter_push_on_a_random_belief(): | |
| """The equality test the plan's E.1 asks for: fuse=True's one torch.sparse.mm must reproduce | |
| fuse=False's ~10-scatter reference to 1e-12 on a random belief -- same pi/beta, only the | |
| implementation differs.""" | |
| nodes, edges = _toy_nodes_edges() | |
| rng = np.random.RandomState(0) | |
| raw = rng.rand(len(nodes)) | |
| b = torch.as_tensor(raw / raw.sum(), dtype=torch.float64) | |
| k_scatter = TransitionKernel.build(nodes, edges, beta=0.4, device="cpu", fuse=False) | |
| k_fused = TransitionKernel.build(nodes, edges, beta=0.4, device="cpu", fuse=True) | |
| pred_scatter = k_scatter.push(b) | |
| pred_fused = k_fused.push(b) | |
| np.testing.assert_allclose(pred_fused.numpy(), pred_scatter.numpy(), atol=1e-12, rtol=0) | |
| def test_fused_build_is_lazy_and_cached(): | |
| """fuse=True must not build the sparse matrix at build()/with_params() time -- only on the FIRST | |
| push() call after -- and with_params() must never carry a stale cache forward (see the class | |
| docstring's "FUSION" section for why fit_transition_kernel's grid search depends on this).""" | |
| nodes, edges = _toy_nodes_edges() | |
| kernel = TransitionKernel.build(nodes, edges, device="cpu", fuse=True) | |
| assert kernel._fused_pt is None # not built at construction | |
| k2 = kernel.with_params(beta=0.5) | |
| assert k2._fused_pt is None # with_params never inherits a stale cache | |
| assert k2.fuse is True # ... but DOES inherit the flag | |
| b = torch.full((len(nodes),), 1.0 / len(nodes), dtype=torch.float64) | |
| k2.push(b) | |
| assert k2._fused_pt is not None # built (and cached) by the first push() | |
| cached = k2._fused_pt | |
| k2.push(b) | |
| assert k2._fused_pt is cached # second push reuses the SAME cached tensor | |
| def test_fused_kernel_rows_sum_to_one(): | |
| """Mass conservation on the fused path -- the exact twin of test_kernel_rows_sum_to_one, fuse=True.""" | |
| nodes, edges = _toy_nodes_edges() | |
| kernel = TransitionKernel.build(nodes, edges, beta=0.4, device="cpu", fuse=True) | |
| v = len(nodes) | |
| for u in [0, 1, v // 2, v - 1]: | |
| b = torch.zeros(v, dtype=torch.float64) | |
| b[u] = 1.0 | |
| pred = kernel.push(b) | |
| assert pred.sum().item() == pytest.approx(1.0, abs=1e-9), f"fused row {u} does not sum to 1" | |
| assert bool((pred >= 0).all()) | |
| def test_fused_push_ignores_precomputed_b_mid(): | |
| """The fused path already encodes the sibling mix in P -- passing a (deliberately WRONG) b_mid must | |
| change nothing, unlike the scatter path where it is consumed.""" | |
| nodes, edges = _toy_nodes_edges() | |
| v = len(nodes) | |
| b = torch.full((v,), 1.0 / v, dtype=torch.float64) | |
| bogus_b_mid = torch.zeros(v, dtype=torch.float64) | |
| bogus_b_mid[0] = 1.0 | |
| kernel = TransitionKernel.build(nodes, edges, beta=0.4, device="cpu", fuse=True) | |
| pred_default = kernel.push(b) | |
| pred_with_bogus = kernel.push(b, b_mid=bogus_b_mid) | |
| np.testing.assert_array_equal(pred_default.numpy(), pred_with_bogus.numpy()) | |
| # ====================================================================================================== | |
| # 4. backward-jump suppression | |
| # ====================================================================================================== | |
| def test_backward_jump_is_suppressed(): | |
| """Small synthetic two-branch aliasing graph: after the belief has advanced along demo 0, a query | |
| that matches an early-phase node on a DIFFERENT branch (demo 1) EQUALLY well (same L_t mass) as the | |
| dynamics-consistent continuation must NOT capture the belief -- see | |
| GraphTracker._update_belief_core's docstring for the mechanism (pred has EXACTLY zero mass on a node | |
| no forward advance can reach, so only the small leak term feeds it, while the dynamics-consistent | |
| node gets the full (1-leak)*pred term too).""" | |
| nodes, edges = _toy_nodes_edges() | |
| kernel = TransitionKernel.build(nodes, edges, device="cpu") # default uniform pi, beta=0.3 | |
| retriever = _toy_retriever() | |
| tracker = GraphTracker(retriever, kernel, calib=TrackerCalibration(leak=0.1)) | |
| demo0 = nodes.demo_nodes(0) | |
| demo1 = nodes.demo_nodes(1) | |
| u_prev = int(demo0[3]) # belief currently sits mid-early in demo 0 | |
| e_node = int(demo1[0]) # the aliased early-phase node -- a DIFFERENT branch entirely | |
| b_prev = torch.zeros(len(nodes), dtype=torch.float64) | |
| b_prev[u_prev] = 1.0 | |
| pred = kernel.push(b_prev) | |
| assert pred[e_node].item() == 0.0, "pred must never route mass backward onto a different branch" | |
| c_node = int(torch.argmax(pred).item()) | |
| L_t = torch.zeros(len(nodes), dtype=torch.float64) | |
| L_t[e_node] = 0.5 | |
| L_t[c_node] = 0.5 # the observation, on its own, cannot tell the two apart | |
| b_t, _, _, _ = tracker._update_belief_core(b_prev, L_t, len(nodes)) | |
| assert b_t[c_node].item() > b_t[e_node].item(), ( | |
| "the aliased early-phase node captured the belief despite the dynamics giving it zero support" | |
| ) | |
| assert b_t[e_node].item() < 0.5, "L_t alone gave e_node half the mass; the filter must reduce that" | |
| # ====================================================================================================== | |
| # 8. g_track.npz — graph_hash refusal | |
| # ====================================================================================================== | |
| def test_g_track_npz_round_trips(tmp_path): | |
| nodes, edges = _toy_nodes_edges() | |
| params = TrackParams( | |
| pi=np.array([0.5, 0.1, 0.1, 0.1, 0.1, 0.05, 0.025, 0.025]), beta=0.3, leak=0.1, | |
| ) | |
| out = params.save(tmp_path / "graph", nodes, edges) | |
| loaded = TrackParams.load(out, nodes, edges) | |
| np.testing.assert_allclose(loaded.pi, params.pi) | |
| assert loaded.beta == pytest.approx(params.beta) | |
| assert loaded.leak == pytest.approx(params.leak) | |
| assert math.isnan(loaded.delta_star) # reserved-slot placeholder | |
| assert loaded.s_ref_per_joint.shape == (nodes.dim,) | |
| assert np.all(np.isnan(loaded.s_ref_per_joint)) | |
| # Basin slots unset here, so they round-trip as the documented NaN placeholders. | |
| assert loaded.basin_r.shape == (nodes.n_tasks, schema.NBINS_ALIGN) | |
| assert np.all(np.isnan(loaded.basin_r)) | |
| assert loaded.basin_h.shape == (nodes.n_tasks, schema.NBINS_ALIGN) | |
| assert np.all(np.isnan(loaded.basin_h)) | |
| def test_g_track_npz_round_trips_with_basin_fields(tmp_path): | |
| """The basin slots are POPULATED this time -- pin that real geometry survives the round trip | |
| bit-for-bit, not just the empty-placeholder path the previous test exercises.""" | |
| nodes, edges = _toy_nodes_edges() | |
| n_tasks, nbins = nodes.n_tasks, schema.NBINS_ALIGN | |
| rng = np.random.RandomState(0) | |
| basin_r = rng.uniform(0.5, 1.5, size=(n_tasks, nbins)) | |
| basin_h = rng.uniform(0.1, 0.3, size=(n_tasks, nbins)) | |
| params = TrackParams(pi=np.full(8, 1 / 8), beta=0.3, leak=0.1, basin_r=basin_r, basin_h=basin_h) | |
| out = params.save(tmp_path / "graph", nodes, edges) | |
| loaded = TrackParams.load(out, nodes, edges) | |
| np.testing.assert_allclose(loaded.basin_r, basin_r) | |
| np.testing.assert_allclose(loaded.basin_h, basin_h) | |
| def test_g_track_npz_load_ignores_retired_keys(tmp_path): | |
| """Files written before the null banks were retired still carry those arrays; load must ignore | |
| the extra keys rather than fail.""" | |
| nodes, edges = _toy_nodes_edges() | |
| out = TrackParams(pi=np.full(8, 1 / 8), beta=0.3, leak=0.1).save(tmp_path / "graph", nodes, edges) | |
| payload = dict(np.load(out)) | |
| payload["null_bank_evidence"] = np.array([-1.0, 0.0, 1.0]) | |
| payload["null_bank_progress"] = np.array([0.1, 0.5, 0.9]) | |
| payload["null_bank_basin"] = np.array([-6.0, -3.0, -1.0]) | |
| np.savez(out, **payload) | |
| loaded = TrackParams.load(out, nodes, edges) | |
| assert loaded.beta == pytest.approx(0.3) | |
| assert not hasattr(loaded, "null_bank_evidence") | |
| def test_g_track_npz_graph_hash_mismatch_raises(tmp_path): | |
| nodes, edges = _toy_nodes_edges() | |
| params = TrackParams(pi=np.full(8, 1 / 8), beta=0.3, leak=0.1) | |
| out_dir = tmp_path / "graph" | |
| out_dir.mkdir() | |
| payload = { | |
| "pi": params.pi, "beta": np.array(params.beta), "leak": np.array(params.leak), | |
| "delta_star": np.array(float("nan")), "s_ref_per_joint": np.full(nodes.dim, np.nan), | |
| "graph_hash": np.array("not-the-right-hash"), | |
| } | |
| np.savez(out_dir / schema.TRACK_NPZ, **payload) | |
| with pytest.raises(ValueError, match="graph_hash mismatch"): | |
| TrackParams.load(out_dir, nodes, edges) | |
| def test_g_track_npz_missing_graph_hash_raises(tmp_path): | |
| nodes, edges = _toy_nodes_edges() | |
| out_dir = tmp_path / "graph" | |
| out_dir.mkdir() | |
| payload = { | |
| "pi": np.full(8, 1 / 8), "beta": np.array(0.3), "leak": np.array(0.1), | |
| "delta_star": np.array(float("nan")), "s_ref_per_joint": np.full(nodes.dim, np.nan), | |
| } | |
| np.savez(out_dir / schema.TRACK_NPZ, **payload) # no graph_hash key at all | |
| with pytest.raises(ValueError, match="graph_hash mismatch"): | |
| TrackParams.load(out_dir, nodes, edges) | |
| def test_graph_tracker_load_refuses_stale_track_params(tmp_path): | |
| """End-to-end: GraphTracker.load must propagate the same refusal when a saved graph directory's | |
| g_track.npz does not match its own g_nodes/g_edges.""" | |
| nodes, edges = _toy_nodes_edges() | |
| net = _toy_net(nodes) | |
| d = tmp_path / "graph" | |
| d.mkdir() | |
| nodes.save(d) | |
| edges.save(d / schema.EDGES_NPZ) | |
| head = dict(net.state_npz_dict()) | |
| head["graph_hash"] = np.array(graph_hash(nodes, edges)) | |
| np.savez(d / schema.HEAD_NPZ, **head) | |
| bad_payload = { | |
| "pi": np.full(8, 1 / 8), "beta": np.array(0.3), "leak": np.array(0.1), | |
| "delta_star": np.array(float("nan")), "s_ref_per_joint": np.full(nodes.dim, np.nan), | |
| "graph_hash": np.array("wrong"), | |
| } | |
| np.savez(d / schema.TRACK_NPZ, **bad_payload) | |
| with pytest.raises(ValueError, match="graph_hash mismatch"): | |
| GraphTracker.load(d, cfg=GraphConfig(device="cpu")) | |
| def test_graph_tracker_load_without_track_npz_falls_back_to_unfit_defaults(tmp_path): | |
| nodes, edges = _toy_nodes_edges() | |
| net = _toy_net(nodes) | |
| d = tmp_path / "graph" | |
| d.mkdir() | |
| nodes.save(d) | |
| edges.save(d / schema.EDGES_NPZ) | |
| head = dict(net.state_npz_dict()) | |
| head["graph_hash"] = np.array(graph_hash(nodes, edges)) | |
| np.savez(d / schema.HEAD_NPZ, **head) # no g_track.npz written at all | |
| # readout="argmax" -- this test calls tracker.step() below, which needs it (see _toy_retriever's | |
| # own comment for why). | |
| tracker = GraphTracker.load(d, cfg=GraphConfig(device="cpu", readout="argmax")) | |
| q0 = tracker.retriever.nodes.q[0].astype(np.float64) | |
| q_hist = np.repeat(q0[None, :], tracker.retriever.cfg.hist, axis=0) | |
| tracker.reset() | |
| step = tracker.step(q_hist) # must not raise | |
| assert step.as_retrieval().node >= 0 | |
| # ====================================================================================================== | |
| # 8b. head_npz threading -- default bit-parity, and GraphTracker.where_target | |
| # ====================================================================================================== | |
| def _write_toy_graph_dir(tmp_path, nodes, edges, net, *, head_name: str = schema.HEAD_NPZ) -> Path: | |
| d = tmp_path / "graph" | |
| d.mkdir(exist_ok=True) | |
| nodes.save(d) | |
| edges.save(d / schema.EDGES_NPZ) | |
| head = dict(net.state_npz_dict()) | |
| head["graph_hash"] = np.array(graph_hash(nodes, edges)) | |
| np.savez(d / head_name, **head) | |
| return d | |
| def test_head_npz_default_is_bit_identical_to_explicit_default(tmp_path): | |
| """schema.HEAD_NPZ (the default) must be reachable two ways -- omitting head_npz entirely, or passing | |
| it explicitly -- and produce a bit-identical retriever; every existing call site (which never passes | |
| head_npz) is therefore unaffected by this parameter's addition.""" | |
| nodes, edges = _toy_nodes_edges() | |
| net = _toy_net(nodes) | |
| d = _write_toy_graph_dir(tmp_path, nodes, edges, net) | |
| r_default = GraphRetriever.load(d, cfg=GraphConfig(device="cpu")) | |
| r_explicit = GraphRetriever.load(d, cfg=GraphConfig(device="cpu"), head_npz=schema.HEAD_NPZ) | |
| assert r_default.graph_hash == r_explicit.graph_hash | |
| for p1, p2 in zip(r_default.net.parameters(), r_explicit.net.parameters()): | |
| assert torch.equal(p1, p2) | |
| _ALT_HEAD_NPZ = "g_alt_head.npz" # any name other than schema.HEAD_NPZ; exercises head_npz's own | |
| # name-selection logic, not a specific named artifact (there is only | |
| # one head artifact any more, schema.HEAD_NPZ -- see schema.py's note | |
| # by NODES_NPZ/EDGES_NPZ/HEAD_NPZ on the removed second artifact). | |
| def test_head_npz_can_load_a_differently_named_head(tmp_path): | |
| """Passing a non-default name must load THAT file instead of g_head.npz -- head_npz selects the | |
| on-disk artifact by name, never by which weights happen to be loaded into a shared file.""" | |
| nodes, edges = _toy_nodes_edges() | |
| net = _toy_net(nodes, seed=7) | |
| d = _write_toy_graph_dir(tmp_path, nodes, edges, net, head_name=_ALT_HEAD_NPZ) | |
| with pytest.raises(FileNotFoundError): | |
| GraphRetriever.load(d, cfg=GraphConfig(device="cpu")) # g_head.npz was never written here | |
| r = GraphRetriever.load(d, cfg=GraphConfig(device="cpu"), head_npz=_ALT_HEAD_NPZ) | |
| assert r.graph_hash == graph_hash(nodes, edges) | |
| def test_graph_tracker_load_threads_head_npz(tmp_path): | |
| nodes, edges = _toy_nodes_edges() | |
| net = _toy_net(nodes) | |
| d = _write_toy_graph_dir(tmp_path, nodes, edges, net, head_name=_ALT_HEAD_NPZ) | |
| tracker = GraphTracker.load(d, cfg=GraphConfig(device="cpu"), head_npz=_ALT_HEAD_NPZ) | |
| assert tracker.retriever.graph_hash == graph_hash(nodes, edges) | |
| # ====================================================================================================== | |
| # 8c. GraphTracker.where_target -- belief top-K -> ReadoutContext -> the "basin" arm | |
| # ====================================================================================================== | |
| def test_where_target_requires_a_prior_step(): | |
| retriever = _toy_retriever() | |
| kernel = TransitionKernel.build(retriever.nodes, retriever.edges, device="cpu") | |
| tracker = GraphTracker(retriever, kernel) | |
| tracker.reset() | |
| with pytest.raises(RuntimeError, match="no step"): | |
| tracker.where_target(retriever.nodes.q[0].astype(np.float64)) | |
| def test_where_target_returns_no_op_inside_basin(): | |
| """Exercises the readout.py interface this method is written against (ReadoutContext.basin_r/ | |
| basin_h/task_id, the 'basin' arm).""" | |
| retriever = _toy_retriever() | |
| kernel = TransitionKernel.build(retriever.nodes, retriever.edges, device="cpu") | |
| n_tasks = retriever.nodes.n_tasks | |
| basin_r = np.full((n_tasks, schema.NBINS_ALIGN), 10.0) # generous radius -> everything is "inside" | |
| basin_h = np.full((n_tasks, schema.NBINS_ALIGN), 1.0) | |
| tracker = GraphTracker(retriever, kernel, calib=TrackerCalibration(basin_r=basin_r, basin_h=basin_h)) | |
| q0 = retriever.nodes.q[5].astype(np.float64) | |
| q_hist = np.repeat(q0[None, :], retriever.cfg.hist, axis=0) | |
| tracker.reset() | |
| tracker.step(q_hist) | |
| result = tracker.where_target(q0) | |
| assert result.info.get("no_op") is True | |
| def test_where_target_aims_at_the_pushed_belief_so_the_target_lies_AHEAD_along_the_strand(): | |
| """Pins the v3 WHERE change. where_target used to read the readout off the FILTERED belief b_t | |
| ("where am I now"), which makes the correction purely perpendicular to the demo -- it restores a | |
| pose while discarding everything the graph knows about direction of travel, so a merge could put the | |
| arm back on the tube still heading the wrong way. It now reads off kernel.push(b_t) ("where does | |
| the demo flow say I will be next check"), which is the filter's OWN predict step -- no new fitted | |
| quantity. The observable consequence, pinned here: the target's depth (the p-weighted phase of the | |
| support the readout aggregates) must be strictly GREATER than the depth the filtered belief would | |
| have produced, i.e. the aim point moved forward along the strand rather than sideways onto it. | |
| """ | |
| from onf.graph.run import readout as gr | |
| retriever = _toy_retriever() | |
| kernel = TransitionKernel.build(retriever.nodes, retriever.edges, device="cpu") | |
| n_tasks = retriever.nodes.n_tasks | |
| # tight radius -> never a no_op, so a real target (and a real depth) is produced | |
| basin_r = np.full((n_tasks, schema.NBINS_ALIGN), 1e-3) | |
| basin_h = np.zeros((n_tasks, schema.NBINS_ALIGN)) | |
| tracker = GraphTracker(retriever, kernel, calib=TrackerCalibration(basin_r=basin_r, basin_h=basin_h)) | |
| # start mid-demo, so there is strand left ahead to advance INTO (phase near 1.0 would clip) | |
| v_mid = int(np.argmin(np.abs(np.asarray(retriever.nodes.phase) - 0.3))) | |
| q0 = retriever.nodes.q[v_mid].astype(np.float64) | |
| q_hist = np.repeat(q0[None, :], retriever.cfg.hist, axis=0) | |
| tracker.reset() | |
| tracker.step(q_hist) | |
| pushed = tracker.where_target(q0) | |
| # the pre-change behaviour, reconstructed inline from the SAME belief: read the readout off b_t | |
| b = tracker._b | |
| k = min(schema.TRACK_BELIEF_TOPK, int(b.shape[0])) | |
| top_v, top_idx = torch.topk(b, k, largest=True, sorted=True) | |
| p_filtered = np.zeros(len(retriever.nodes), dtype=np.float64) | |
| p_filtered[top_idx.cpu().numpy()] = top_v.cpu().numpy() | |
| ctx = gr.ReadoutContext( | |
| nodes=retriever.nodes, topm=retriever.cfg.topm, q_now=q0, | |
| basin=gr.BasinGeometry(r=basin_r, h=basin_h), task_id=int(retriever.nodes.task_id[v_mid]), | |
| ) | |
| filtered = gr.make_readout("basin")(p_filtered, ctx) | |
| assert pushed.depth > filtered.depth, ( | |
| f"pushed-belief target must aim further along the strand: depth {pushed.depth:.4f} " | |
| f"vs filtered {filtered.depth:.4f}" | |
| ) | |
| # ====================================================================================================== | |
| # 9. MLE fit improves held-out predictive log-evidence over a uniform-pi baseline | |
| # ====================================================================================================== | |
| def test_mle_fit_improves_held_out_log_evidence(): | |
| retriever = _toy_retriever(n_demos=6) | |
| held_out = [0, 2, 4] | |
| result = fit_transition_kernel(retriever, held_out, n_sweeps=2) | |
| assert result.log_evidence >= result.log_evidence_uniform_pi - 1e-9 | |
| assert np.isfinite(result.log_evidence) | |
| assert result.pi.shape == (len(schema.TRACK_ADVANCE_SET),) | |
| assert result.pi.sum() == pytest.approx(1.0, abs=1e-6) | |
| assert 0.0 <= result.beta <= 1.0 | |
| assert 0.0 <= result.leak <= 1.0 | |
| assert result.n_checks > 0 | |
| def test_fit_transition_kernel_raises_on_no_usable_held_out_demos(): | |
| retriever = _toy_retriever(n_demos=6) | |
| with pytest.raises(ValueError, match="no usable fitting sequence"): | |
| fit_transition_kernel(retriever, held_out_owners=[], check_every=8) | |
| # ====================================================================================================== | |
| # 9b. build_likelihood_sequences -- clean is not the only regime that can be fit on (2026-07-30 addition) | |
| # ====================================================================================================== | |
| # Longer toy demos than the DIM=4/N_FRAMES=31 fixture above: a real drift/cross-strand rollout needs | |
| # room for several checks on top of DriftConfig's own window/check-spacing floor. | |
| _LONG_N_FRAMES = 80 | |
| def _toy_retriever_for_sequences(n_demos: int = 8, seed: int = 0) -> GraphRetriever: | |
| nodes, edges = _toy_nodes_edges(n_demos=n_demos) | |
| # rebuild with longer demos (the module-level _make_demo/_toy_nodes_edges default to N_FRAMES=31, | |
| # too short for a multi-check drift/cross-strand rollout under the default check_every=8) | |
| demos = [_make_demo(task_id=i % 2, seed=100 + i, n=_LONG_N_FRAMES) for i in range(n_demos)] | |
| nodes = NodeTable.from_demos(demos, task_names=["taskA", "taskB"], coarsen=COARSEN) | |
| edges = EdgeSet.build(nodes, k_sib=4, k_align=2, device="cpu") | |
| net = _toy_net(nodes, seed=seed) | |
| # readout="argmax" -- see _toy_retriever's comment; every caller here feeds into GraphTracker too. | |
| return GraphRetriever(net, nodes, edges, cfg=GraphConfig(device="cpu", readout="argmax")) | |
| def _assert_valid_L_sequences(sequences, v): | |
| assert len(sequences) > 0 | |
| for L in sequences: | |
| assert L.ndim == 2 and L.shape[0] >= 2 and L.shape[1] == v | |
| row_sums = L.sum(dim=1) | |
| # every row is either a valid distribution (sums to 1) or all-zero (nothing reachable under | |
| # this window -- should not happen for these toy queries, but a hard 1.0 assertion here would | |
| # make the test brittle to unrelated toy-graph changes; check the common case strictly instead. | |
| assert torch.all((row_sums > 0.999) & (row_sums < 1.001)) | |
| def test_build_likelihood_sequences_clean(): | |
| retriever = _toy_retriever_for_sequences() | |
| seqs = build_likelihood_sequences(retriever, [0, 2, 4], kind="clean", check_every=8) | |
| _assert_valid_L_sequences(seqs, len(retriever.nodes)) | |
| assert len(seqs) <= 3 # at most one sequence per requested owner | |
| def test_build_likelihood_sequences_drift(): | |
| retriever = _toy_retriever_for_sequences() | |
| seqs = build_likelihood_sequences( | |
| retriever, [0, 2, 4, 6], kind="drift", n_sequences=8, check_every=8, seed=1, | |
| ) | |
| _assert_valid_L_sequences(seqs, len(retriever.nodes)) | |
| def test_build_likelihood_sequences_cross_strand(): | |
| retriever = _toy_retriever_for_sequences() | |
| seqs = build_likelihood_sequences( | |
| retriever, [0, 2, 4, 6], kind="cross_strand", n_sequences=8, check_every=8, seed=2, | |
| ) | |
| _assert_valid_L_sequences(seqs, len(retriever.nodes)) | |
| def test_build_likelihood_sequences_rejects_unknown_kind(): | |
| retriever = _toy_retriever_for_sequences() | |
| with pytest.raises(ValueError, match="kind must be one of"): | |
| build_likelihood_sequences(retriever, [0], kind="not_a_real_kind") | |
| def test_build_likelihood_sequences_empty_owners_returns_empty_list(): | |
| retriever = _toy_retriever_for_sequences() | |
| assert build_likelihood_sequences(retriever, [], kind="clean") == [] | |
| assert build_likelihood_sequences(retriever, [], kind="drift") == [] | |
| def test_fit_transition_kernel_accepts_sequences_directly(): | |
| """The 2026-07-30 fix this whole section exists for: fitting is no longer hard-wired to | |
| clean-demo-only sequences -- a caller can hand it ANY [T,V] sequence set, e.g. drift or | |
| cross-strand, and the fit optimises against exactly that set.""" | |
| retriever = _toy_retriever_for_sequences() | |
| drift_seqs = build_likelihood_sequences( | |
| retriever, [0, 2, 4, 6], kind="drift", n_sequences=6, check_every=8, seed=3, | |
| ) | |
| result = fit_transition_kernel(retriever, sequences=drift_seqs, n_sweeps=1) | |
| assert result.log_evidence >= result.log_evidence_uniform_pi - 1e-9 | |
| assert result.n_checks == sum(L.shape[0] - 1 for L in drift_seqs) | |
| def test_fit_transition_kernel_combines_sequences_and_held_out_owners(): | |
| retriever = _toy_retriever_for_sequences() | |
| drift_seqs = build_likelihood_sequences( | |
| retriever, [0, 2], kind="drift", n_sequences=3, check_every=8, seed=4, | |
| ) | |
| result = fit_transition_kernel(retriever, held_out_owners=[4, 6], sequences=drift_seqs, n_sweeps=1) | |
| clean_seqs = build_likelihood_sequences(retriever, [4, 6], kind="clean", check_every=8) | |
| assert result.n_checks == sum(L.shape[0] - 1 for L in drift_seqs) + sum(L.shape[0] - 1 for L in clean_seqs) | |
| def test_evaluate_kernel_log_evidence_matches_fit_internal_computation(): | |
| """evaluate_kernel_log_evidence is the SAME quantity fit_transition_kernel's coordinate ascent | |
| scores internally -- pin that a standalone call against a FIXED kernel reproduces exactly what the | |
| fit found at its own optimum (beta=0 as a probe point, not assuming the fit landed there).""" | |
| retriever = _toy_retriever_for_sequences() | |
| seqs = build_likelihood_sequences( | |
| retriever, [0, 2, 4, 6], kind="cross_strand", n_sequences=6, check_every=8, seed=5, | |
| ) | |
| kernel = TransitionKernel.build(retriever.nodes, retriever.edges, beta=0.0, device="cpu") | |
| ev, n = evaluate_kernel_log_evidence(kernel, leak=0.1, sequences=seqs) | |
| assert np.isfinite(ev) | |
| assert n == sum(L.shape[0] - 1 for L in seqs) | |
| # ====================================================================================================== | |
| # 10. real-artifact-gated: scale, cost, and end-to-end sanity on the actual long graph | |
| # ====================================================================================================== | |
| def test_real_graph_transition_kernel_builds_and_succ_a_is_well_formed(): | |
| nodes = NodeTable.load(REAL_GRAPH_DIR / schema.NODES_NPZ) | |
| edges = EdgeSet.load(REAL_GRAPH_DIR / schema.EDGES_NPZ) | |
| kernel = TransitionKernel.build(nodes, edges, device="cpu") | |
| owner = np.asarray(nodes.owner) | |
| for i, a in enumerate(kernel.advances): | |
| succ = kernel.succ[i].cpu().numpy() | |
| assert np.array_equal(owner[succ], owner), f"advance={a} crosses a demo boundary on the real graph" | |
| assert kernel.succ.shape == (len(schema.TRACK_ADVANCE_SET), len(nodes)) | |
| def test_real_graph_kernel_push_per_check_cost(): | |
| """Measures TransitionKernel.push's per-check cost on the real V=28476 graph -- the module | |
| docstring's performance budget target (~1M float ops, 0.1-1% of observe()'s ~150M). Reported via | |
| print (captured by pytest -s / -q -rA), not asserted tightly (wall-clock is not a portability-safe | |
| thing to pin), but bounded generously so a real regression still fails the test.""" | |
| nodes = NodeTable.load(REAL_GRAPH_DIR / schema.NODES_NPZ) | |
| edges = EdgeSet.load(REAL_GRAPH_DIR / schema.EDGES_NPZ) | |
| device = "cuda" if torch.cuda.is_available() else "cpu" | |
| kernel = TransitionKernel.build(nodes, edges, device=device) | |
| v = len(nodes) | |
| b = torch.full((v,), 1.0 / v, dtype=torch.float64, device=device) | |
| kernel.push(b) # warm-up (cuda context / caching allocator) | |
| if device == "cuda": | |
| torch.cuda.synchronize() | |
| n_reps = 50 | |
| t0 = time.perf_counter() | |
| for _ in range(n_reps): | |
| pred = kernel.push(b) | |
| if device == "cuda": | |
| torch.cuda.synchronize() | |
| dt = (time.perf_counter() - t0) / n_reps | |
| print(f"\n[test_real_graph_kernel_push_per_check_cost] V={v} device={device} push()={dt * 1e3:.3f} ms/call") | |
| assert dt < 0.5, f"push() took {dt * 1e3:.1f} ms/call on V={v} -- unexpectedly slow" | |
| assert pred.sum().item() == pytest.approx(1.0, abs=1e-6) | |
| def test_real_graph_kernel_rows_sum_to_one(): | |
| nodes = NodeTable.load(REAL_GRAPH_DIR / schema.NODES_NPZ) | |
| edges = EdgeSet.load(REAL_GRAPH_DIR / schema.EDGES_NPZ) | |
| kernel = TransitionKernel.build(nodes, edges, beta=0.3, device="cpu") | |
| v = len(nodes) | |
| rng = np.random.RandomState(0) | |
| for u in rng.choice(v, size=20, replace=False): | |
| b = torch.zeros(v, dtype=torch.float64) | |
| b[int(u)] = 1.0 | |
| pred = kernel.push(b) | |
| assert pred.sum().item() == pytest.approx(1.0, abs=1e-6) | |
| def test_real_graph_step_overhead_vs_retrieve(): | |
| """E.2's acceptance gate: GraphTracker.step()'s SINGLE-SYNC belief-tracking machinery, fused kernel | |
| included, must cost <= 10% over plain retrieve() on the real long graph (V=28476) -- reported via | |
| print (captured by -s/-rA), not tightly asserted (wall-clock is not portability-safe to pin), | |
| but bounded generously so a real regression still fails. Measured on this machine's CPU (no GPU in | |
| this sandbox): step() actually comes in FASTER than retrieve() here, consistent with the module | |
| docstring's performance budget (observe()'s ~150M FLOPs dominate the belief update's ~1M). | |
| Loads TWO retrievers off the same on-disk graph, one per readout: onf.graph.run.track._finalize (the | |
| belief-filtered aggregation tracker.step() uses) only supports cfg.readout == "argmax", while | |
| retriever.retrieve()'s registered arms are ("euc_raw", "basin") (`onf.graph.core.schema. | |
| READOUT_ARMS`) -- "argmax" is not one of them any more. This is still a fair overhead comparison: | |
| both retrievers share the identical trained network/nodes/edges, and observe() (~150M FLOPs) is | |
| the cost this measures, dominating either aggregation (~1M FLOPs) by 2 orders of magnitude. | |
| """ | |
| retriever = GraphRetriever.load(REAL_GRAPH_DIR, cfg=GraphConfig(device="cpu")) | |
| retriever_argmax = GraphRetriever.load(REAL_GRAPH_DIR, cfg=GraphConfig(device="cpu", readout="argmax")) | |
| nodes = retriever.nodes | |
| q0 = nodes.q[100].astype(np.float64) | |
| q_hist = np.repeat(q0[None, :], retriever.cfg.hist, axis=0) | |
| n_reps = 20 | |
| retriever.retrieve(q_hist) # warm-up | |
| t0 = time.perf_counter() | |
| for _ in range(n_reps): | |
| retriever.retrieve(q_hist) | |
| t_retrieve = (time.perf_counter() - t0) / n_reps | |
| kernel = TransitionKernel.build(nodes, retriever_argmax.edges, device="cpu", fuse=True) | |
| tracker = GraphTracker(retriever_argmax, kernel) | |
| tracker.reset() | |
| tracker.step(q_hist) # ENTRY step (no transition applied) | |
| tracker.step(q_hist) # a real step -- also builds+caches the | |
| # fused matrix, so it is warmed up too | |
| t0 = time.perf_counter() | |
| for _ in range(n_reps): | |
| tracker.step(q_hist) | |
| t_step = (time.perf_counter() - t0) / n_reps | |
| overhead_pct = 100.0 * (t_step - t_retrieve) / t_retrieve | |
| print( | |
| f"\n[test_real_graph_step_overhead_vs_retrieve] V={len(nodes)} retrieve()={t_retrieve * 1e3:.3f}ms " | |
| f"step()={t_step * 1e3:.3f}ms overhead={overhead_pct:.1f}%" | |
| ) | |
| assert overhead_pct <= 25.0, f"step() overhead {overhead_pct:.1f}% exceeds the E.2 acceptance gate" | |
| # ====================================================================================================== | |
| # 11. TaskPosterior -- the behavioural task lane (GR_TASK_PRIOR_W) | |
| # ====================================================================================================== | |
| def _task_posterior(weight: float, task_id=(0, 0, 1, 1, 2, 2)) -> TaskPosterior: | |
| return TaskPosterior(torch.as_tensor(task_id, dtype=torch.int64), max(task_id) + 1, weight=weight) | |
| def _belief(mass: list[float]) -> torch.Tensor: | |
| b = torch.as_tensor(mass, dtype=torch.float64) | |
| return b / b.sum() | |
| def test_task_posterior_gates_and_stays_soft(): | |
| """The marginal is a plain per-task sum of b_t, and the prior it yields is withheld on all three | |
| gates (off / too little evidence / no opinion) and is FINITE when it does apply -- a -inf entry | |
| would make it the hard mask this deliberately is not.""" | |
| confident = _belief([0.002, 0.002, 0.0, 0.0, 0.5, 0.496]) | |
| uniform = _belief([1.0] * 6) | |
| enough = TASK_PRIOR_MIN_CHECKS | |
| tp = _task_posterior(1.0) | |
| assert tp.marginal(confident).tolist() == pytest.approx([0.004, 0.0, 0.996]) | |
| assert _task_posterior(0.0).log_prior(confident, enough) is None # off by default | |
| assert tp.log_prior(None, enough) is None # no belief yet | |
| assert tp.log_prior(confident, enough - 1) is None # too little evidence | |
| assert torch.equal(tp.log_prior(uniform, enough), torch.zeros(3, dtype=torch.float64)) | |
| prior = tp.log_prior(confident, enough) | |
| assert bool(torch.isfinite(prior).all()) | |
| assert int(torch.argmax(prior).item()) == 2 | |
| # A task with exactly zero belief mass is floored at -log(eps), not -inf. | |
| assert prior[1].item() == pytest.approx(math.log(TASK_PRIOR_EPS)) | |
| # ...and the winner is worth a few nats over it, at weight 1. | |
| assert 3.0 <= (prior[2] - prior[1]).item() <= 10.0 | |
| def test_task_prior_is_inert_when_the_text_lane_resolves(): | |
| """Strictly additive: a large GR_TASK_PRIOR_W must reproduce the weight-0 run bit-for-bit whenever | |
| the lane resolved from the instruction string, and must be withheld while the marginal is still | |
| undecided. The last block hands the filter a marginal that IS decided, so the equalities above | |
| cannot be passing merely because nothing is wired up.""" | |
| nodes, edges = _toy_nodes_edges() | |
| net = _toy_net(nodes) | |
| kernel = TransitionKernel.build(nodes, edges, device="cpu") | |
| def tracker_at(weight: float) -> GraphTracker: | |
| cfg = GraphConfig(device="cpu", readout="argmax", task_prior_w=weight) | |
| return GraphTracker(GraphRetriever(net, nodes, edges, cfg=cfg), kernel) | |
| def run(weight: float, task: str) -> list[np.ndarray]: | |
| tracker = tracker_at(weight) | |
| rng = np.random.RandomState(7) | |
| q_hist = np.asarray(nodes.q[: tracker.retriever.cfg.hist], dtype=np.float64) | |
| out = [] | |
| for _ in range(2 * TASK_PRIOR_MIN_CHECKS): | |
| out.append(tracker.step(q_hist, task=task).as_retrieval().p_node) | |
| q_hist = q_hist + 0.01 * rng.randn(*q_hist.shape) | |
| return out | |
| for task in ("taskA", ""): | |
| # "taskA" resolves the lane, so the prior is dropped; "" resolves nothing, but the untrained | |
| # fixture head leaves the marginal at normalised entropy ~0.98, so the gate withholds it. | |
| for off, on in zip(run(0.0, task), run(8.0, task)): | |
| np.testing.assert_array_equal(off, on) | |
| def one_step_on_a_committed_belief(weight: float, task: str) -> np.ndarray: | |
| tracker = tracker_at(weight) | |
| # Reaching normalised entropy < TASK_PRIOR_MAX_ENTROPY takes a TRAINED head and ~8 real checks | |
| # (measured on the long tapes); the fixture head never gets there. What is under test here is | |
| # the wiring and the lane precedence, not the head, so the belief is handed over directly. | |
| b = torch.as_tensor(np.asarray(nodes.task_id) == 1, dtype=torch.float64) | |
| tracker._b = b / b.sum() | |
| tracker._n_checks = TASK_PRIOR_MIN_CHECKS | |
| q_hist = np.asarray(nodes.q[: tracker.retriever.cfg.hist], dtype=np.float64) | |
| return tracker.step(q_hist, task=task).as_retrieval().p_node | |
| np.testing.assert_array_equal( | |
| one_step_on_a_committed_belief(0.0, "taskA"), one_step_on_a_committed_belief(8.0, "taskA"), | |
| ) | |
| assert not np.array_equal( | |
| one_step_on_a_committed_belief(0.0, ""), one_step_on_a_committed_belief(8.0, ""), | |
| ), "a committed marginal with no text lane changed nothing -- the prior is not wired up" | |
Xet Storage Details
- Size:
- 45 kB
- Xet hash:
- 2e0f38101f88b9be49bc271bfdbb1eedfd480335d3d129c5070f72ba4c664248
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.