twanghcmut's picture
download
raw
25.6 kB
"""Tests for onf.graph.build.from_demos — the HDF5 -> demonstration-graph pipeline.
Runs entirely against a fabricated hdf5 fixture (_write_fixture) that matches the REAL
LIBERO key layout (obs/joint_states, obs/gripper_states, actions, one file per task, one
group per demo) so it exercises the exact same code path the real 500-demos-per-suite builds do,
without requiring the real dataset. Tests that DO need the real data are marked with
skip_no_data, following tests/test_parity.py's skip_no_field pattern.
"""
from __future__ import annotations
from pathlib import Path
import numpy as np
import pytest
from onf.config import Paths
from onf.graph.core import schema
from onf.graph.build.from_demos import (
DemoLoader,
EdgeBuilder,
GraphBuilder,
LoadLimits,
NodeBuilder,
measured_grip,
stage_labels,
)
from onf.graph.core.edges import EdgeSet
from onf.graph.core.nodes import NodeTable
# ==================================================================================================
# real-data skip marker (tests/test_parity.py's skip_no_field pattern)
# ==================================================================================================
_PATHS = Paths.resolve()
def _has_real_hdf5(name: str = "libero_object") -> bool:
try:
_PATHS.hdf5(name)
return True
except FileNotFoundError:
return False
HAS_REAL_DATA = _has_real_hdf5()
skip_no_data = pytest.mark.skipif(not HAS_REAL_DATA, reason="no real LIBERO hdf5 data found")
# ==================================================================================================
# stage adapters -- the builder API is class-per-stage (DemoLoader/NodeBuilder/EdgeBuilder), but each
# assertion below is about ONE stage's output, so these three helpers name that stage and hand back
# the plain value the assertions read. They add no logic: each is a construct-and-call.
# ==================================================================================================
def load_demos(hdf5_dir, *, limit_tasks=None, limit_demos=None):
"""(per-demo payload dicts, task names) -- what NodeTable.from_demos consumes."""
batch = DemoLoader(hdf5_dir, limits=LoadLimits(tasks=limit_tasks, demos=limit_demos)).load()
return batch.as_payloads(), batch.task_names
def build_nodes(hdf5_dir, out_dir, *, coarsen):
"""HDF5 dir -> written g_nodes.npz path, as a str."""
batch = DemoLoader(hdf5_dir).load()
builder = NodeBuilder(coarsen=coarsen)
return str(builder.save(builder.build(batch), Path(out_dir)))
def build_graph(suite, *, limit_tasks=None, limit_demos=None, **kw):
"""Full chain -> the metrics dict (GraphArtifacts.to_dict())."""
limits = LoadLimits(tasks=limit_tasks, demos=limit_demos)
return GraphBuilder(suite, limits=limits, **kw).build().to_dict()
def build_edges(nodes_path_or_table, out_dir):
"""NodeTable (live or on disk) -> written g_edges.npz path, as a str."""
table = (nodes_path_or_table if isinstance(nodes_path_or_table, NodeTable)
else NodeTable.load(nodes_path_or_table))
builder = EdgeBuilder()
return str(builder.save(builder.build(table), Path(out_dir)))
# ==================================================================================================
# every build_graph(logger=None) call below owns a fresh StageLogger, which ALWAYS writes its run
# directory under Paths.outputs() regardless of build_graph's own out_dir argument (that argument
# only controls where g_nodes.npz/g_edges.npz land, not the run/manifest bookkeeping dir) -- redirect
# it into this test's own tmp_path so the suite never pollutes the real repo outputs/ tree, or worse,
# repoints outputs/<real-suite-name>/latest (e.g. "object") away from an actual build.
# ==================================================================================================
@pytest.fixture(autouse=True)
def _isolate_stagelogger_outputs(tmp_path, monkeypatch):
monkeypatch.setenv("ONF_OUTPUTS", str(tmp_path / "_stagelogger_outputs"))
# ==================================================================================================
# fixture: a tiny hdf5 tree matching the real key layout
# ==================================================================================================
D_JOINT = 7
def _synth_demo(t: int, *, has_gripper_states: bool = True, seed: int = 0, lag: int = 2) -> dict:
"""One synthetic demo's raw arrays, matching the real per-demo hdf5 layout. Scripts ONE clean
gripper close/open cycle (a rise then a fall, roughly the middle third of the episode) so both
grip (measured-channel) and stage (command-channel release-count) transitions are exercised
by every demo. The measured gripper_states channel is built to LAG the commanded
actions[:, 6] by lag frames — mirroring the real command-leads-measurement offset the
module docstring documents — and to sit on the correct side of schema.GRIP_OPEN_THR in each
state (near 0 when closed, comfortably above threshold when open).
"""
rng = np.random.RandomState(seed)
q = np.cumsum(rng.randn(t, D_JOINT).astype(np.float64) * 0.01, axis=0)
rise, fall = t // 3, (2 * t) // 3
cmd = np.full(t, -1.0)
cmd[rise:fall] = 1.0
actions = np.zeros((t, 7), dtype=np.float64)
actions[:, :6] = rng.randn(t, 6).astype(np.float64) * 0.05
actions[:, 6] = cmd
closed = np.zeros(t, dtype=bool)
closed[min(t, rise + lag):min(t, fall + lag)] = True
finger = np.where(closed, 0.01, 4 * schema.GRIP_OPEN_THR) # closed near 0; open well above thr
# force grip[-1] != grip[-2]: coarsen_segments() then starts a brand-new (size-1) node at the
# very last raw frame regardless of coarsen, so that node's phase hits exactly 1.0 -- the
# _tiny_table trick in test_nodes.py achieves the same NodeTable.validate() phase-convention
# requirement (table-wide phase.max() > 0.99) via a hand-picked T instead.
finger[-1] = 4 * schema.GRIP_OPEN_THR if closed[-2] else 0.01
gripper_states = np.stack([finger, -finger], axis=1)
demo = {
"joint_states": q,
"actions": actions,
"ee_pos": q[:, :3] * 0.1,
"ee_ori": q[:, 3:6] * 0.1,
"states": np.zeros((t, 110), dtype=np.float64),
}
if has_gripper_states:
demo["gripper_states"] = gripper_states
return demo
def _write_fixture(
root: Path, *, n_tasks: int = 2, n_demos: int = 3, base_t: int = 40,
missing_gripper: tuple[int, int] | None = None,
) -> Path:
"""Write n_tasks hdf5 files (task{i}_demo.hdf5), each holding n_demos demo groups.
missing_gripper=(task_idx, demo_idx) omits obs/gripper_states for exactly that one demo,
to exercise onf.graph.build.from_demos.measured_grip's command-channel fallback."""
import h5py
root.mkdir(parents=True, exist_ok=True)
for ti in range(n_tasks):
fp = root / f"task{ti}_demo.hdf5"
with h5py.File(fp, "w") as f:
grp = f.create_group("data")
for di in range(n_demos):
t = base_t + 3 * di + ti # varying lengths -- tests must not assume a fixed T
has_gs = (ti, di) != missing_gripper
d = _synth_demo(t, has_gripper_states=has_gs, seed=ti * 100 + di)
dgrp = grp.create_group(f"demo_{di}")
dgrp.create_dataset("actions", data=d["actions"])
dgrp.create_dataset("states", data=d["states"])
obs = dgrp.create_group("obs")
obs.create_dataset("joint_states", data=d["joint_states"])
obs.create_dataset("ee_pos", data=d["ee_pos"])
obs.create_dataset("ee_ori", data=d["ee_ori"])
if "gripper_states" in d:
obs.create_dataset("gripper_states", data=d["gripper_states"])
return root
# ==================================================================================================
# stage_labels — pure array unit tests (no HDF5). DIAGNOSTIC ONLY (see schema.py's note under
# RELATIONS): a plain, undebounced gripper-release count, with no debounce and no trailing fold —
# both were tried and retired because the label is a cumulative sum over a channel that really does
# flicker on real data, and no threshold can clean that without also erasing genuine short stages
# elsewhere. These tests lock in the current, deliberately simple behaviour.
# ==================================================================================================
def test_stage_labels_two_cycles_exact():
"""Two full close/open cycles (2 releases) -> stage == [0]*n + [1]*m + [2]*k."""
n, m, k = 12, 9, 7
# stage0: open(a) -> closed(b) -> [RELEASE] stage1: open(c) -> closed(d) -> [RELEASE] stage2: open(e)
a, b = n // 2, n - n // 2
c, d = m // 2, m - m // 2
gb = np.concatenate([np.zeros(a), np.ones(b), np.zeros(c), np.ones(d), np.zeros(k)])
cmd = np.where(gb > 0.5, 1.0, -1.0)
stage = stage_labels(cmd)
expected = np.array([0] * n + [1] * m + [2] * k, dtype=np.int16)
np.testing.assert_array_equal(stage, expected)
assert stage.dtype == np.int16
def test_stage_labels_no_release_is_all_zero():
cmd = np.concatenate([np.full(5, -1.0), np.full(20, 1.0)]) # grasps, never releases
stage = stage_labels(cmd)
np.testing.assert_array_equal(stage, np.zeros(25, dtype=np.int16))
def test_stage_labels_all_open_is_all_zero():
cmd = np.full(10, -1.0)
stage = stage_labels(cmd)
assert stage.dtype == np.int16
np.testing.assert_array_equal(stage, np.zeros(10, dtype=np.int16))
def test_stage_labels_no_debounce_short_flicker_still_counts():
"""The retired debounce would have erased a short release/regrasp blip; the current plain count
does NOT -- it counts every diff == -1 transition, flicker or genuine, by design (see the
module docstring's Decision 2 and schema.py's note under RELATIONS for why: no threshold can
separate flicker from genuine transitions across every suite, so the label stopped trying to)."""
hold = 40
flicker = 3 # short blip; would have been erased by the retired debounce
gb = np.concatenate([
np.zeros(10), np.ones(hold // 2), np.zeros(flicker), np.ones(hold // 2), np.zeros(5),
])
cmd = np.where(gb > 0.5, 1.0, -1.0)
stage = stage_labels(cmd)
assert int(stage.max()) == 2, "a plain release count must see BOTH releases, flicker or not"
def test_stage_labels_dtype_and_length():
cmd = np.array([-1.0, -1.0, 1.0, 1.0, -1.0, -1.0], dtype=np.float64)
stage = stage_labels(cmd)
assert stage.dtype == np.int16
assert stage.shape == (6,)
# ==================================================================================================
# measured_grip
# ==================================================================================================
def test_measured_grip_uses_gripper_states():
gs = np.array([[0.01, -0.01], [0.05, -0.05], [0.001, -0.001]])
cmd = np.array([1.0, 1.0, 1.0]) # command disagrees with measurement on row 1 -- must be ignored
grip = measured_grip(gs, cmd)
np.testing.assert_array_equal(grip, np.array([1, 0, 1], dtype=np.uint8))
assert grip.dtype == np.uint8
def test_measured_grip_falls_back_without_gripper_states():
cmd = np.array([-1.0, 1.0, 1.0, -1.0])
grip = measured_grip(None, cmd)
np.testing.assert_array_equal(grip, np.array([0, 1, 1, 0], dtype=np.uint8))
# ==================================================================================================
# load_demos
# ==================================================================================================
def test_load_demos_shapes_dtypes_and_task_names(tmp_path):
_write_fixture(tmp_path, n_tasks=2, n_demos=3, base_t=40)
demos, task_names = load_demos(tmp_path)
assert task_names == ["task0", "task1"]
assert len(demos) == 6
for d in demos:
t = d["q"].shape[0]
assert d["q"].shape == (t, D_JOINT) and d["q"].dtype == np.float32
assert d["qdot"].shape == (t, D_JOINT) and d["qdot"].dtype == np.float32
assert d["grip"].shape == (t,) and d["grip"].dtype == np.uint8
assert d["stage"].shape == (t,) and d["stage"].dtype == np.int16
assert d["ee"].shape == (t, 6) and d["ee"].dtype == np.float32
assert d["a"].shape == (t, 7) and d["a"].dtype == np.float32
assert set(np.unique(d["grip"]).tolist()) <= {0, 1}
assert d["task_id"] in (0, 1)
# first 3 demos (list order) belong to task0, next 3 to task1 (file-per-task, in-order)
assert [d["task_id"] for d in demos[:3]] == [0, 0, 0]
assert [d["task_id"] for d in demos[3:]] == [1, 1, 1]
def test_load_demos_qdot_matches_finite_difference(tmp_path):
_write_fixture(tmp_path, n_tasks=1, n_demos=1, base_t=37)
demos, _ = load_demos(tmp_path)
q = demos[0]["q"]
expected = np.zeros_like(q)
expected[:-1] = q[1:] - q[:-1]
expected[-1] = expected[-2]
np.testing.assert_allclose(demos[0]["qdot"], expected, atol=1e-6)
def test_load_demos_grip_transitions_exercised(tmp_path):
"""The fixture scripts a real close/open cycle -- grip must show BOTH 0 and 1 in every demo."""
_write_fixture(tmp_path, n_tasks=1, n_demos=2, base_t=45)
demos, _ = load_demos(tmp_path)
for d in demos:
vals = set(np.unique(d["grip"]).tolist())
assert vals == {0, 1}, f"expected both grip states, got {vals}"
def test_load_demos_grip_from_measured_channel_not_command(tmp_path):
"""The measured channel LAGS the command by lag frames (scripted in the fixture) -- grip must
follow the lagged measurement, not the instantaneous command."""
lag = 2
root = tmp_path
root.mkdir(parents=True, exist_ok=True)
import h5py
t = 50
d = _synth_demo(t, has_gripper_states=True, seed=0, lag=lag)
fp = root / "task0_demo.hdf5"
with h5py.File(fp, "w") as f:
grp = f.create_group("data")
dgrp = grp.create_group("demo_0")
dgrp.create_dataset("actions", data=d["actions"])
dgrp.create_dataset("states", data=d["states"])
obs = dgrp.create_group("obs")
obs.create_dataset("joint_states", data=d["joint_states"])
obs.create_dataset("ee_pos", data=d["ee_pos"])
obs.create_dataset("ee_ori", data=d["ee_ori"])
obs.create_dataset("gripper_states", data=d["gripper_states"])
demos, _ = load_demos(root)
grip = demos[0]["grip"]
cmd_closed = d["actions"][:, 6] > 0.5
# in the lag window right after the command rises, the command says "closed" but the measured
# channel (and hence grip) has not caught up yet
rise = t // 3
assert cmd_closed[rise] and grip[rise] == 0, "grip must lag the command, not track it instantly"
# matches the measured signal directly (recomputed independently here, not via measured_grip)
gs = d["gripper_states"]
expected = (np.abs(gs).mean(axis=1) < schema.GRIP_OPEN_THR).astype(np.uint8)
np.testing.assert_array_equal(grip, expected)
def test_load_demos_missing_gripper_states_falls_back(tmp_path):
_write_fixture(tmp_path, n_tasks=1, n_demos=2, base_t=40, missing_gripper=(0, 1))
demos, _ = load_demos(tmp_path)
# demo_0 (has gripper_states) and demo_1 (missing) are both present, list-ordered by h5py key
# iteration (not necessarily numeric) -- identify the fallback demo by re-deriving it directly.
import h5py
with h5py.File(tmp_path / "task0_demo.hdf5", "r") as f:
keys = list(f["data"].keys())
idx_missing = keys.index("demo_1")
act = np.asarray(f["data"]["demo_1"]["actions"])
expected = (act[:, 6] > 0.5).astype(np.uint8)
np.testing.assert_array_equal(demos[idx_missing]["grip"], expected)
def test_load_demos_limit_tasks_and_demos(tmp_path):
_write_fixture(tmp_path, n_tasks=3, n_demos=4, base_t=40)
demos, task_names = load_demos(tmp_path, limit_tasks=2, limit_demos=2)
assert task_names == ["task0", "task1"]
assert len(demos) == 4
def test_load_demos_missing_dir_raises():
with pytest.raises(FileNotFoundError):
load_demos("/nonexistent/definitely/not/a/real/path")
# ==================================================================================================
# build_nodes
# ==================================================================================================
def test_build_nodes_writes_valid_npz(tmp_path):
fixture = tmp_path / "hdf5"
_write_fixture(fixture, n_tasks=2, n_demos=3, base_t=40)
out_dir = tmp_path / "artifacts"
path = build_nodes(fixture, out_dir, coarsen=3)
assert path.endswith(schema.NODES_NPZ)
table = NodeTable.load(path)
table.validate()
assert table.n_demos == 6
assert table.n_tasks == 2
def test_build_nodes_coarsen_reduces_node_count(tmp_path):
fixture = tmp_path / "hdf5"
_write_fixture(fixture, n_tasks=1, n_demos=2, base_t=100)
out1 = build_nodes(fixture, tmp_path / "c1", coarsen=1)
out5 = build_nodes(fixture, tmp_path / "c5", coarsen=5)
t1, t5 = NodeTable.load(out1), NodeTable.load(out5)
assert len(t5) < len(t1)
# ==================================================================================================
# build_edges
# ==================================================================================================
def test_build_edges_accepts_table_or_path(tmp_path):
fixture = tmp_path / "hdf5"
_write_fixture(fixture, n_tasks=2, n_demos=3, base_t=40)
nodes_path = build_nodes(fixture, tmp_path / "artifacts", coarsen=3)
table = NodeTable.load(nodes_path)
p_from_table = build_edges(table, tmp_path / "from_table")
p_from_path = build_edges(nodes_path, tmp_path / "from_path")
es1 = EdgeSet.load(p_from_table)
es2 = EdgeSet.load(p_from_path)
es1.validate(table)
es2.validate(table)
assert es1.counts() == es2.counts()
# ==================================================================================================
# build_graph — full pipeline
# ==================================================================================================
def test_build_graph_roundtrip_validates(tmp_path):
fixture = tmp_path / "hdf5"
_write_fixture(fixture, n_tasks=2, n_demos=3, base_t=45)
out_dir = tmp_path / "artifacts"
metrics = build_graph("unit_test_suite", hdf5_dir=fixture, out_dir=out_dir, coarsen=3)
assert metrics["n_nodes"] > 0
assert metrics["n_demos"] == 6
assert metrics["n_tasks"] == 2
assert metrics["n_edges"] == sum(metrics["edge_counts"].values())
table = NodeTable.load(metrics["nodes_path"])
table.validate()
es = EdgeSet.load(metrics["edges_path"])
es.validate(table)
def test_build_graph_run_dir_has_provenance_files(tmp_path, monkeypatch):
monkeypatch.setenv("ONF_OUTPUTS", str(tmp_path / "outputs"))
fixture = tmp_path / "hdf5"
_write_fixture(fixture, n_tasks=2, n_demos=2, base_t=40)
metrics = build_graph("unit_test_suite2", hdf5_dir=fixture, coarsen=3)
assert metrics["n_nodes"] > 0
run_dirs = sorted((tmp_path / "outputs" / "unit_test_suite2").glob("graph_*"))
assert len(run_dirs) == 1, run_dirs
run_dir = run_dirs[0]
for fname in ("config.json", "manifest.json", "metrics.json", "log.txt"):
assert (run_dir / fname).exists(), fname
import json
manifest = json.loads((run_dir / "manifest.json").read_text())
assert manifest["status"] == "complete"
recorded = set(manifest["inputs"].keys())
expected_inputs = {str(p) for p in sorted(fixture.glob("*.hdf5"))}
assert recorded == expected_inputs
metrics_json = json.loads((run_dir / "metrics.json").read_text())
assert metrics_json["n_nodes"] == metrics["n_nodes"]
assert (run_dir / "artifacts" / schema.NODES_NPZ).exists()
assert (run_dir / "artifacts" / schema.EDGES_NPZ).exists()
def test_build_graph_deterministic(tmp_path):
fixture = tmp_path / "hdf5"
_write_fixture(fixture, n_tasks=2, n_demos=3, base_t=40)
m1 = build_graph("det_suite", hdf5_dir=fixture, out_dir=tmp_path / "run1", coarsen=3)
m2 = build_graph("det_suite", hdf5_dir=fixture, out_dir=tmp_path / "run2", coarsen=3)
b1_nodes = Path(m1["nodes_path"]).read_bytes()
b2_nodes = Path(m2["nodes_path"]).read_bytes()
assert b1_nodes == b2_nodes, "g_nodes.npz must be byte-identical across repeated builds"
b1_edges = Path(m1["edges_path"]).read_bytes()
b2_edges = Path(m2["edges_path"]).read_bytes()
assert b1_edges == b2_edges, "g_edges.npz must be byte-identical across repeated builds"
def test_build_graph_single_grasp_suite_has_no_boundary_relation(tmp_path):
"""A single-grasp suite (no genuine stage transitions) must build normally. There is no
boundary/stage-handoff relation at all any more to assert zero-edges on -- edge_counts simply
never has a stage_next/stage_prev key (schema.RELATIONS dropped both; see the long comment
there)."""
fixture = tmp_path / "hdf5"
_write_fixture(fixture, n_tasks=2, n_demos=3, base_t=40)
metrics = build_graph("single_grasp_suite", hdf5_dir=fixture, out_dir=tmp_path / "artifacts", coarsen=3)
assert set(metrics["edge_counts"]) == set(schema.RELATIONS)
assert "stage_next" not in metrics["edge_counts"]
assert "stage_prev" not in metrics["edge_counts"]
def test_build_graph_multi_release_stage_is_diagnostic_only(tmp_path):
"""A suite whose demos genuinely release the gripper twice must still report n_stages
(DIAGNOSTIC ONLY now, see schema.py) counting those releases -- the field still works end-to-end
through the real HDF5 pipeline -- but produces no boundary-type edges: that relation no longer
exists at all, regardless of how many stages a demo has."""
import h5py
root = tmp_path / "hdf5"
root.mkdir(parents=True, exist_ok=True)
fp = root / "task0_demo.hdf5"
n_demos = 3
with h5py.File(fp, "w") as f:
grp = f.create_group("data")
for di in range(n_demos):
rng = np.random.RandomState(di)
seg = 60 + di
gb = np.concatenate([
np.zeros(seg), np.ones(seg), # stage0 -> release 1
np.zeros(seg), np.ones(seg), # stage1 -> release 2
np.zeros(30), # stage2 tail
])
t = len(gb)
q = np.cumsum(rng.randn(t, D_JOINT).astype(np.float64) * 0.01, axis=0)
actions = np.zeros((t, 7), dtype=np.float64)
actions[:, :6] = rng.randn(t, 6).astype(np.float64) * 0.05
actions[:, 6] = np.where(gb > 0.5, 1.0, -1.0)
finger = np.where(gb > 0.5, 0.01, 4 * schema.GRIP_OPEN_THR)
# force a size-1 last coarse node (see _synth_demo's identical trick) so this demo's
# phase reaches exactly 1.0 -- satisfies NodeTable.validate()'s phase-convention check.
finger[-1] = 0.01 if finger[-2] > schema.GRIP_OPEN_THR else 4 * schema.GRIP_OPEN_THR
gripper_states = np.stack([finger, -finger], axis=1)
dgrp = grp.create_group(f"demo_{di}")
dgrp.create_dataset("actions", data=actions)
dgrp.create_dataset("states", data=np.zeros((t, 110)))
obs = dgrp.create_group("obs")
obs.create_dataset("joint_states", data=q)
obs.create_dataset("ee_pos", data=q[:, :3] * 0.1)
obs.create_dataset("ee_ori", data=q[:, 3:6] * 0.1)
obs.create_dataset("gripper_states", data=gripper_states)
metrics = build_graph("multistage_suite", hdf5_dir=root, out_dir=tmp_path / "artifacts", coarsen=5)
assert metrics["n_stages"] == 3 # diagnostic: 2 releases seen -> 3 distinct stage values
assert set(metrics["edge_counts"]) == set(schema.RELATIONS)
assert "stage_next" not in metrics["edge_counts"]
assert "stage_prev" not in metrics["edge_counts"]
table = NodeTable.load(metrics["nodes_path"])
table.validate()
es = EdgeSet.load(metrics["edges_path"])
es.validate(table)
def test_build_graph_given_logger_does_not_reenter(tmp_path, monkeypatch):
"""Passing an already-active logger must not create a second run directory."""
monkeypatch.setenv("ONF_OUTPUTS", str(tmp_path / "outputs"))
fixture = tmp_path / "hdf5"
_write_fixture(fixture, n_tasks=1, n_demos=2, base_t=40)
from onf.graph.report import StageLogger
with StageLogger("graph", "given_logger_suite", config={}) as log:
metrics = build_graph("given_logger_suite", hdf5_dir=fixture, coarsen=3, logger=log)
assert metrics["n_nodes"] > 0
run_dirs = sorted((tmp_path / "outputs" / "given_logger_suite").glob("graph_*"))
assert len(run_dirs) == 1
# ==================================================================================================
# real-data smoke test
# ==================================================================================================
@skip_no_data
def test_build_graph_real_object_suite_smoke(tmp_path):
"""A tiny, fast slice of the real object suite (1 task, 2 demos) -- just enough to prove the
pipeline runs end-to-end against real hdf5 without pulling in all 500 demos."""
metrics = build_graph(
"object", out_dir=tmp_path / "artifacts", coarsen=5, limit_tasks=1, limit_demos=2,
)
assert metrics["n_nodes"] > 0
table = NodeTable.load(metrics["nodes_path"])
table.validate()
es = EdgeSet.load(metrics["edges_path"])
es.validate(table)

Xet Storage Details

Size:
25.6 kB
·
Xet hash:
7f32b3faa8a7ed1da5d91f0a66940ed791cc8715b6118813b2fd9cd268ec0c9e

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.