#!/usr/bin/env python3 """test_trim_hold.py — 着いたあとの止まりの詰め方 (build_correction_episodes --trim-hold. 依頼 48) の確かめ. python3 pi05/test_trim_hold.py -v ファイルを読まない部分だけ (KIOXIA にもアームにも触らない). """ import sys import unittest from pathlib import Path import numpy as np _HERE = Path(__file__).resolve().parent sys.path.insert(0, str(_HERE)) import build_correction_episodes as bce # noqa: E402 import build_demo_episodes as bde # noqa: E402 def episode(pattern): """pattern の 1 文字 = 1 コマ. '#' = j1 が 2° 動く / '.' = 止まる / 'g' = 爪を閉じる指令のコマ (止まったまま). 戻り値: state, actions (= 次のコマの state. 最後は自分自身. 爪は次の指令).""" n = len(pattern) + 1 s = np.zeros((n, 7)) for i, c in enumerate(pattern): s[i + 1] = s[i] if c == "#": s[i + 1, 1] += 2.0 if c == "g": s[i + 1, 6] = 1.0 a = np.concatenate([s[1:], s[-1:]]) return s, a def idle_of(s, a): return (np.max(np.abs(a[:, :6] - s[:, :6]), axis=1) < bde.MID_IDLE_EPS_DEG) & (a[:, 6] == s[:, 6]) class TestHoldKeep(unittest.TestCase): def test_same_as_trim2_when_all_two(self): """残す数が全部 MID_IDLE_KEEP_STEPS なら trim2 の mid_idle_keep_mask と同じ.""" rng = np.random.default_rng(0) k = bde.MID_IDLE_KEEP_STEPS for _ in range(300): n = 80 s = np.cumsum(rng.choice([0, 0, 0, 0.1, 0.2, 1.5], size=(n, 7)), axis=0) s[:, 6] = np.arange(n) > rng.integers(5, 75) a = np.concatenate([s[1:], s[-1:]]) modes = list(rng.choice([bce.MODE_MODEL, bce.MODE_CORRECT], size=n)) np.testing.assert_array_equal(bce.hold_keep_mask(s, a, modes, k, k, k), bde.mid_idle_keep_mask(s, a)) def test_model_holds_removed_correct_keeps_one(self): s, a = episode("##....##....##....#....#....##") n = len(s) modes = [bce.MODE_MODEL] * 14 + [bce.MODE_CORRECT] * (n - 14) keep = bce.hold_keep_mask(s, a, modes) idle = idle_of(s, a) regions, i = [], 0 while i < n: if idle[i]: j = i while j < n and idle[j]: j += 1 regions.append((i, j)) i = j else: i += 1 for i, j in regions[:-1]: # 最後の区間はエピソードの終わり (最後のコマは必ず残す) want = 1 if modes[i] == bce.MODE_CORRECT else 0 self.assertEqual(int(keep[i:j].sum()), want, (i, j, modes[i])) self.assertTrue(keep[~idle].all()) # 動いているコマは消さない self.assertTrue(keep[0] and keep[-1]) def test_gripper_neighbors_keep_like_trim2(self): """爪の切り替えのすぐ前・すぐ後の止まりは trim2 と同じ先頭 2 コマ (+ 切り替えの前後 1 コマ).""" s, a = episode("##......g......##......##") modes = [bce.MODE_MODEL] * len(s) keep = bce.hold_keep_mask(s, a, modes) trim2 = bde.mid_idle_keep_mask(s, a) g = int(np.flatnonzero(a[:, 6] != s[:, 6])[0]) # 指令のコマ. 切り替わるのは g + 1 np.testing.assert_array_equal(keep[:g + 9], trim2[:g + 9]) # 爪の前後は trim2 と同じ self.assertEqual(int(keep[2:g].sum()), bde.MID_IDLE_KEEP_STEPS) # 直前の区間: 先頭 2 self.assertTrue(keep[g:g + 3].all()) # 切り替え (g + 1) の前後 1 コマ # 直後の区間 (g + 1 〜 g + 6) は先頭 2 コマ (g + 1, g + 2. 前後の保護と重なる) だけ self.assertEqual(int(keep[g + 1:g + 7].sum()), bde.MID_IDLE_KEEP_STEPS) # 爪から離れた止まり (model) は残さない far = idle_of(s, a) far[:g + 9] = False far[-1] = False self.assertFalse(keep[far].any()) def test_slow_drift_not_trimmed(self): """0.2° ずつ動き続ける区間はずれで区切られ, model (残す数 0) でも 0.5° ごとの姿勢が残る (codex 必須).""" n = 30 s = np.zeros((n, 7)) s[:, 1] = np.arange(n) * 0.2 a = np.concatenate([s[1:], s[-1:]]) for mode in (bce.MODE_MODEL, bce.MODE_CORRECT): keep = bce.hold_keep_mask(s, a, [mode] * n) jump = np.abs(np.diff(s[keep, 1])).max() self.assertLessEqual(jump, bde.MID_IDLE_DRIFT_DEG + 0.2 + 1e-9, mode) # 詰めても 1 コマ 0.7° まで self.assertGreaterEqual(int(keep.sum()), n // 3, mode) self.assertGreaterEqual(int(bce.hold_keep_mask(s, a, [bce.MODE_CORRECT] * n).sum()), int(bce.hold_keep_mask(s, a, [bce.MODE_MODEL] * n).sum())) def test_select_steps_trim_hold(self): """select_steps(trim_hold=True) で止まりが減り, actions が「次のコマの state」のまま.""" t = np.arange(0, 6.0, 0.1) q = np.zeros((len(t), 6)) for k in range(len(t)): # 0.5 秒ごとに 1 コマで 2° 動いて止まる q[k, 1] = 2.0 * (k // 5) g = np.where(t >= 3.0, 1.0, 0.0) mode_ts, modes = np.array([0.0]), [bce.MODE_MODEL] old = bce.select_steps(t, q, g, mode_ts, modes, [], trim_mid_idle=True) new = bce.select_steps(t, q, g, mode_ts, modes, [], trim_mid_idle=True, trim_hold=True) self.assertLess(len(new["idx"]), len(old["idx"])) self.assertLess(idle_of(new["state"], new["actions"]).mean(), idle_of(old["state"], old["actions"]).mean()) np.testing.assert_array_equal(new["actions"][:-1, :6], new["state"][1:, :6]) self.assertEqual(new["state"][-1, 1], q[-1, 1]) # 終わりの姿勢は変わらない self.assertEqual(int(np.sum(np.diff(new["state"][:, 6]) != 0)), 1) # 爪の切り替えは残る if __name__ == "__main__": unittest.main()