From 885c4fbc9ef86dccb865e11d69a8613ec561d3a3 Mon Sep 17 00:00:00 2001 From: Joe Rivera Date: Sun, 27 Sep 2026 02:50:46 -0500 Subject: [PATCH] test: CLI-train + export + reload the deployable risk/lam coef_mlp (.cvcnav) Integration test for the coef-train --rollout bicycle --w-risk --learned-lam deployable path (the grl-snam side of libcvc's NavCoefTrain.TrainThenSaveReloadIntegration): CLI-train a lam net to a .cvcnav, assert the SHIPPED 6-in risk-only + 4-out lam layout survives export (CVNV, risk flag set, mu flag clear), then reload it torch-free via nav_native.coef_mlp_forward and confirm it forwards to (N,4) with a finite, softplus->=0 learned-lam column. The load+DRIVE contract is pinned in libcvc (NavMaterialDeploy.ShippedRiskOnlyLamNetRoundTripsAndDrives); this closes the gap that the CLI lam path had no train->export->reload test. Verified: passes in 6s (torch train + torch-free reload). --- tests/test_coef_train_lam_deploy.py | 78 +++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 tests/test_coef_train_lam_deploy.py diff --git a/tests/test_coef_train_lam_deploy.py b/tests/test_coef_train_lam_deploy.py new file mode 100644 index 0000000..aa5a3ae --- /dev/null +++ b/tests/test_coef_train_lam_deploy.py @@ -0,0 +1,78 @@ +"""Deployable lam-net integration: CLI-train a risk/lam coef_mlp (the ``coef-train --rollout +bicycle --w-risk --learned-lam`` path) to a ``.cvcnav``, confirm the shipped 6-in-risk-only+lam +layout survives export, and reload it torch-free — the Python train -> export -> reload half of the +deployable path. The C++ twin (libcvc ``NavMaterialDeploy.ShippedRiskOnlyLamNetRoundTripsAndDrives`` ++ ``NavCoefTrain.TrainThenSaveReloadIntegration``) covers the load+DRIVE contract; this pins that the +CLI actually produces a loadable risk/lam artifact. Mirrors libcvc's train->save->reload integration +test on the grl-snam side. +""" + +from __future__ import annotations + +import struct + +import numpy as np +import pytest + +torch = pytest.importorskip("torch") # the CLI training path is torch + +import grl_snam.tools.coef_export as CE # noqa: E402 +from grl_snam import nav_native # noqa: E402 +from grl_snam.tools import coef_train # noqa: E402 + + +def _read_header(path): + with open(path, "rb") as f: + assert f.read(4) == b"CVNV", "not a CVNV coef_mlp blob" + _ver, flags, in_f, out_f, _nl = struct.unpack(" 0 + + # (1) The SHIPPED risk-only+lam layout survives export: 6-in (base5 + risk, NO grip), 4-out + # (learned lam head), risk flag set + mu flag clear. Same contract libcvc's shipped-blob deploy + # test pins on the C++ side. + flags, in_f, out_f = _read_header(out) + assert in_f == 6, f"expected 6-in risk net, got {in_f}" + assert out_f == 4, f"expected 4-out lam head, got {out_f}" + assert flags & CE.FLAG_FEAT_RISK, "risk feature flag not set" + assert not (flags & CE.FLAG_FEAT_MU), "grip (mu) flag set — expected risk-only" + + # (2) Torch-free reload: the trained 4-output net loads via the C++ coef_mlp and forwards to + # (N,4) with a finite learned-lam (4th) column — the reload half of the deployable contract, + # on the actual CLI-produced artifact (not a hand-built net). + if not nav_native.HAS_COEF_MLP: + pytest.skip("pycvc lacks nav_coef_mlp_forward — cannot reload .cvcnav torch-free") + feats = np.random.default_rng(0).uniform(-1.0, 1.0, (16, 6)).astype(np.float32) + coef = nav_native.coef_mlp_forward(str(out), feats) + assert coef.shape == (16, 4), f"reloaded lam net forwarded to {coef.shape}, expected (16, 4)" + assert np.all(np.isfinite(coef)), "reloaded net produced non-finite coefficients" + assert np.all(coef[:, 3] >= 0.0), "learned lam (4th output, softplus) must be >= 0"