From 349814db7e44767864ab8424226dfea4b1d8dbba Mon Sep 17 00:00:00 2001 From: cbb <2625274797@qq.com> Date: Tue, 20 Oct 2026 21:25:47 +0800 Subject: [PATCH] =?UTF-8?q?exp+feat:=20=E8=BD=AE=E6=AC=A136(=E7=BB=AD)?= =?UTF-8?q?=E2=80=94=E2=80=94=E6=97=B6=E9=97=B4=E8=B4=A6=E8=AF=81=E6=98=8E?= =?UTF-8?q?=E9=99=90=E5=B9=85=E5=90=8E=E7=9A=84=E6=8D=9F=E5=A4=B1=E6=98=AF?= =?UTF-8?q?"=E7=AB=99=E4=BD=8F"=E4=B8=8D=E6=98=AF=E5=B7=A1=E8=88=AA?= =?UTF-8?q?=EF=BC=9B=E5=85=A8=E6=8B=92=E7=BC=A9=E6=AD=A5=E9=A1=B6=E4=B8=8A?= =?UTF-8?q?(backoff=EF=BC=8C=E9=BB=98=E8=AE=A4=E5=85=B3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/experiment_round36_time_audit.json | 106 ++++++++++++++++++++++++ docs/探索日志.md | 16 ++++ ml/eval/eval_cl.py | 3 +- ml/sim/closed_loop.py | 43 +++++++--- tests/test_eval_cl.py | 68 +++++++++++++++ 5 files changed, 224 insertions(+), 12 deletions(-) create mode 100644 docs/experiment_round36_time_audit.json diff --git a/docs/experiment_round36_time_audit.json b/docs/experiment_round36_time_audit.json new file mode 100644 index 0000000..6148ecd --- /dev/null +++ b/docs/experiment_round36_time_audit.json @@ -0,0 +1,106 @@ +[ + { + "file": "ml/runs/round33_cap.pt", + "n": 256, + "all": { + "n": 256, + "mean_window_s": 3.73765625, + "zero_share": 0.20881234062121148, + "low_share": 0.0812884076752644, + "mid_share": 0.27427783119434807, + "hi_share": 0.435621420509176, + "zero_s_per_task": 0.78046875, + "runs_per_task": 18.5078125, + "run_len_mean": 2.1084845926551288, + "run_len_max": 41 + }, + "arrived_time_ok": { + "n": 68, + "mean_window_s": 1.9370588235294117, + "zero_share": 0.013817187974491345, + "low_share": 0.033404190707561496, + "mid_share": 0.17901609474643185, + "hi_share": 0.7737625265715153, + "zero_s_per_task": 0.02676470588235294, + "runs_per_task": 0.7647058823529411, + "run_len_mean": 1.75, + "run_len_max": 4 + }, + "arrived_slow": { + "n": 66, + "mean_window_s": 3.052727272727273, + "zero_share": 0.049632717887631524, + "low_share": 0.06293428628151677, + "mid_share": 0.22553107008139764, + "hi_share": 0.6619019257494541, + "zero_s_per_task": 0.15151515151515152, + "runs_per_task": 4.590909090909091, + "run_len_mean": 1.6501650165016502, + "run_len_max": 7 + }, + "not_arrived": { + "n": 122, + "mean_window_s": 5.111803278688525, + "zero_share": 0.3014238984029248, + "low_share": 0.09733179398370856, + "mid_share": 0.31014687961003146, + "hi_share": 0.29109742800333527, + "zero_s_per_task": 1.5408196721311478, + "runs_per_task": 35.92622950819672, + "run_len_mean": 2.144421629021218, + "run_len_max": 41 + } + }, + { + "file": "ml/runs/round33_base.pt", + "n": 256, + "all": { + "n": 256, + "mean_window_s": 3.482578125, + "zero_share": 0.22394957040626332, + "low_share": 0.08179105816901093, + "mid_share": 0.2871435942302084, + "hi_share": 0.4071157771945174, + "zero_s_per_task": 0.779921875, + "runs_per_task": 18.7421875, + "run_len_mean": 2.0806586077532305, + "run_len_max": 21 + }, + "arrived_time_ok": { + "n": 100, + "mean_window_s": 1.8818000000000001, + "zero_share": 0.019236900839621637, + "low_share": 0.039642895100435754, + "mid_share": 0.19353810181740885, + "hi_share": 0.7475821022425337, + "zero_s_per_task": 0.0362, + "runs_per_task": 1.2, + "run_len_mean": 1.5083333333333333, + "run_len_max": 5 + }, + "arrived_slow": { + "n": 47, + "mean_window_s": 3.2136170212765958, + "zero_share": 0.10474046610169492, + "low_share": 0.07216631355932204, + "mid_share": 0.25529661016949157, + "hi_share": 0.5677966101694916, + "zero_s_per_task": 0.33659574468085107, + "runs_per_task": 10.063829787234043, + "run_len_mean": 1.6723044397463003, + "run_len_max": 7 + }, + "not_arrived": { + "n": 109, + "mean_window_s": 5.067155963302753, + "zero_share": 0.3262963499420625, + "low_share": 0.09878331402085747, + "mid_share": 0.32774478563151793, + "hi_share": 0.247175550405562, + "zero_s_per_task": 1.6533944954128441, + "runs_per_task": 38.57798165137615, + "run_len_mean": 2.142925089179548, + "run_len_max": 21 + } + } +] \ No newline at end of file diff --git a/docs/探索日志.md b/docs/探索日志.md index 7400303..00d4520 100644 --- a/docs/探索日志.md +++ b/docs/探索日志.md @@ -754,6 +754,22 @@ - **顺带记下的实测**:同一批 32 题上 empty/commit2 到达 0.469、r3/commit3 0.438、加 inc_cap 后 0.406(n=32,只作方向参考,不足以定论;256 题口径见轮次 33)。规划次数差 1.5 倍(175 vs 118)——r3 每次只新增 commit 步却已含 3 步前缀,所以同样墙钟时间内规划得更少,这正是它更贴近部署的原因。 - 测试 35 项仍全过(`plans['r']` 只增不改)。 +## 时间账:限幅之后 time_ok 的损失在"站住"上,不在巡航速度上(2026-10-20,轮次 36 续) + +- **口径**:直接吃轮次 33 的 256 题轨迹转储(`ml/runs/round33_cap.pt`、`round33_base.pt`,离线分析 `ml/runs/_time_audit.py`,产物 `docs/experiment_round36_time_audit.json`),按每题的活动窗把每一步按「该步最大关节位移 / VMAX·DT」分四档:零运动、<0.5、0.5–0.95、≥0.95。切片时踩了一个自己的坑:`per['traj']` 是 `torch.stack(traj, 1)` 出来的 **[B,T,6]** 而不是 [T,B,6],按列切成了"同一时刻不同题",比值全是 70~230 的 nonsense;加了 `traj[i][0] == q0` 的断言之后才挡住这类错。 +- **实测(占活动窗的比例)** + + | 组 | 题数 | 零运动 | <0.5 | 0.5–0.95 | ≥0.95 | 刹停段/题 | 平均段长 | + |---|---|---|---|---|---|---|---| + | base 到达且按时 | 100 | 1.9% | 4.0% | 19.4% | 74.8% | 1.2 | 1.5 步 | + | base 到达但超时 | 47 | 10.5% | 7.2% | 25.5% | 56.8% | 10.1 | 1.7 步 | + | base 未到达 | 109 | 32.6% | 9.9% | 32.8% | 24.7% | 38.6 | 2.1 步 | + | cap(限幅) 未到达 | 122 | 30.1% | 9.7% | 31.0% | 29.1% | 35.9 | 2.1 步 | + +- **判读**:限幅并没有把动作整体拖慢——未到达组的 ≥0.95 档反而从 24.7% 升到 29.1%;真正吃掉时限的是**零运动**(占活动窗 30%,平均每题 1.54 s)和**长刹停段**(最长 41 步 = 0.82 s 原地不动)。所以 #17 不该往"打分里加剩余时限"这种间接方向做,直接打"全拒⇒站住"这一条。 +- **改动(默认关,`--backoff`)**:全拒时不再站住,把首选候选按 1/2、1/4、1/8…(档数 = backoff)**并进同一次批量认证**(零额外几何调用),只在原候选连第 1 步都不安全时顶上;缩步候选不参与"前缀更长者优先"的比较,所以正常路径的选择逐位不变。新增 `backoff_mean` 指标记录缩步次数。测试 35→38 项(全拒→慢爬、有安全前缀时插手无效、未开批量认证时报错)。 +- **待测**:256 题 A/B(`--inc_cap 1 --cert_batch 1 --backoff 0/1/2/3`)——GPU 先让给 K-MF E1,这轮排在其后。 + ## 分支清理(2026-10-20) - `feat/fb-mix`(8e5431c):反馈混合方案的中间分支,**已验证是 `exp/dagger-deploy` 的祖先**(所有提交都在后者历史里),本地/远端分支删除,内容保留在 exp/dagger-deploy。 diff --git a/ml/eval/eval_cl.py b/ml/eval/eval_cl.py index 0815845..f2388f6 100644 --- a/ml/eval/eval_cl.py +++ b/ml/eval/eval_cl.py @@ -93,6 +93,7 @@ def main(): ap.add_argument('--inc_cap', type=int, default=0, help='1=反馈叠加后再按每轴 VMAX·DT 限幅') ap.add_argument('--gap_margin', type=float, default=0.0, help='安全层要求的间隙余量 mm(>0 时宽相位 cap 自动抬到 margin+0.5)') ap.add_argument('--cert_batch', type=int, default=0, help='1=一次 gaps 调用覆盖全部候选(压最坏情况,典型路径略贵)') + ap.add_argument('--backoff', type=int, default=0, help='全拒时把首选候选按 1/2、1/4…缩步重试的档数(需 --cert_batch 1)') ap.add_argument('--max_sec', type=float, default=None) ap.add_argument('--seed', type=int, default=0) ap.add_argument('--dls', default='analytic', choices=['analytic', 'fd']) @@ -119,7 +120,7 @@ def main(): safety_partial=(a.commit if (a.safety and a.partial) else 0), commit_n=a.commit, select=a.select, replan=a.replan, fb_gain=a.fb, fb_scale=a.fb_scale, fb_near=a.fb_near, fb_fallback=a.fb_fallback, fb_ki=a.ki, fb_decay=a.kdecay, fb_imax=a.kimax, fb_every=a.fb_every, safety_try=a.safety_try, sel_w=a.sel_w, dls_safety=a.dls_safety, fb_clear=a.fb_clear, fb_cmin=a.fb_cmin, sub_refresh=a.sub_refresh, fb_block_gain=a.fb_block_gain, - inc_cap=a.inc_cap, gap_margin=a.gap_margin, cert_batch=a.cert_batch, return_traj=bool(a.trace)) + inc_cap=a.inc_cap, gap_margin=a.gap_margin, cert_batch=a.cert_batch, backoff=a.backoff, return_traj=bool(a.trace)) wall = time.perf_counter() - t0 result = {**meta, 'metrics': r, 'simulation_wall_s': wall} print(json.dumps(json_finite(result), ensure_ascii=False, allow_nan=False), flush=True) diff --git a/ml/sim/closed_loop.py b/ml/sim/closed_loop.py index af2d0ac..a691ea7 100644 --- a/ml/sim/closed_loop.py +++ b/ml/sim/closed_loop.py @@ -163,7 +163,7 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None, qp0=None, steps=1, z_scale=1.0, select='consist', oracle=None, replan='r3', sub_rule='cross', fb_gain=0.0, fb_scale=0.0, fb_near=0.2, fb_fallback=0.0, safety_partial=0, commit_n=3, collect=None, fb_exec=0.0, exec_safety=0, fb_ki=0.0, fb_decay=0.9, fb_imax=0.5, fb_every=1, safety_try=0, sel_w=0.1, dls_safety=0, fb_clear=0.0, fb_cmin=0.3, sub_refresh=0, fb_block_gain=1.0, - inc_cap=0, gap_margin=0.0, cert_batch=0, progress=None): + inc_cap=0, gap_margin=0.0, cert_batch=0, backoff=0, progress=None): """use_sub=False:不用子目标(sf=0);sub_override=(subs [B,M,6] 绝对关节角, n [B] 个数 或 need [B] bool):冷启动时用给定途经点代替子目标头; qp0 [B,6]:起点的上一帧(默认 = q0,即静止起步)。steps:MeanFlow 采样段数;z_scale:噪声缩放; select:consist(部署口径:冷启动价值前瞻、之后与上一条已提交计划的一致性)/ value(每次都价值前瞻)/ first(不选,取第 0 条)。 @@ -172,7 +172,10 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None, sub_rule:子目标推进规则,见 sub_advance。除默认值外均为诊断用。 inc_cap=1:把「策略动作 + 目标反馈」的合计增量再按每轴 VMAX·DT 限一次幅(默认关;关时反馈是在 ±1 限幅之后叠加的,第30轮实测最大 1.66×VMAX)。 gap_margin>0:安全层要求间隙大于该毫米余量而不是 >0;宽相位 cap 自动抬到 margin+0.5,否则 0.5 的截顶值会让余量门限恒假。 + backoff>0:全拒(连第 1 步都不安全)时不再站住,把首选候选按 1/2、1/4…(共 backoff 档)缩步一起送进同一次认证, + 取能通过的第 1 档执行;缩步候选不参与"前缀更长者优先"的比较,只在原候选全被拒时顶上。需 cert_batch=1。 两者默认值下行为与第32轮逐位相同。""" + if backoff and not cert_batch: raise ValueError('backoff 目前只实现在 cert_batch=1 口径') g = torch.Generator(device=device); g.manual_seed(seed) q0, T, R, ex, tr = tasks_to_tensors(tasks, device) B = q0.shape[0] @@ -193,6 +196,7 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None, sub_t = torch.zeros(B, device=device); sub_prev = q0.clone() done = torch.zeros(B, dtype=torch.bool, device=device); t_done = torch.full((B,), float('nan'), device=device) col = torch.zeros(B, device=device); travel = torch.zeros(B, device=device); brakes = torch.zeros(B, device=device) + bo = torch.zeros(B, device=device) # 缩步顶上的次数(backoff>0 才会计) first_hit = torch.full((B,), float('nan'), device=device); dls_it = torch.full((B,), -1, dtype=torch.long, device=device) # 2026-10-20(轮次33):速度/加速度按「实际执行位移」算,不看指令——第30轮证明反馈是在限幅之后叠加的 vratio = torch.zeros(B, device=device); aratio = torch.zeros(B, device=device); prev_exec = torch.zeros(B, 6, device=device) @@ -330,18 +334,34 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None, chosen = torch.zeros(B, commit_n, 6, device=device); chosen_k = pick.clone() ntry = min(K_eff, safety_try) if safety_try > 0 else K_eff if cert_batch: # 一次 gaps 覆盖全部候选:典型路径略贵,但把「前面候选被拒→串行调用 K 次」的最坏情况封成 1 次 - cks = inc[ar[:, None], order[:, :ntry], r:r + commit_n].reshape(B * ntry, commit_n, 6) - pts, mids = execution_points(kin, qe.repeat_interleave(ntry, 0), cks) - gaps6 = kin.gaps(torch.cat([pts, mids], 1).reshape(-1, 6), gcap)['gm'].min(-1).values.view(B, ntry, 2 * commit_n) - plen = torch.cumprod(((gaps6 > gap_margin)[:, :, :commit_n] & - (gaps6 > gap_margin)[:, :, commit_n:]).long(), -1).sum(-1) + cks = inc[ar[:, None], order[:, :ntry], r:r + commit_n] # [B,ntry,cn,6] + if backoff: # 缩步档与正常候选并进同一次认证,不额外调用几何 + sc = (0.5 ** torch.arange(1, backoff + 1, device=device, dtype=inc.dtype))[None, :, None, None] + cands = torch.cat([cks, inc[ar, order[:, 0], r:r + commit_n][:, None] * sc], 1) + else: + cands = cks + nc = cands.shape[1] + pts, mids = execution_points(kin, qe.repeat_interleave(nc, 0), cands.reshape(B * nc, commit_n, 6)) + gaps6 = kin.gaps(torch.cat([pts, mids], 1).reshape(-1, 6), gcap)['gm'].min(-1).values.view(B, nc, 2 * commit_n) + stepok = (gaps6 > gap_margin)[:, :, :commit_n] & (gaps6 > gap_margin)[:, :, commit_n:] + pl_all = torch.cumprod(stepok.long(), -1).sum(-1) # [B,nc] 每条候选的安全前缀长度 + plen = pl_all[:, :ntry] maxlen = plen.max(-1).values best = plen.argmax(-1) # 与串行同口径:严格更大者胜 ⇒ 首个最大值 - chosen = cks.view(B, ntry, commit_n, 6)[ar, best] - chosen_k = order[ar, best] + if backoff: # 缩步候选只在全拒时顶上:不参与「前缀更长者优先」的比较,所以正常路径逐位不变 + full0 = maxlen == 0 + bb = pl_all[:, ntry:].argmax(-1) # 平手取缩得最少的(档 0 = 1/2) + mv = pl_all[ar, ntry + bb] + bo += (full0 & (mv > 0)) + best = torch.where(full0, ntry + bb, best) + maxlen = torch.where(full0, mv, maxlen) + chosen_k = torch.where(full0, order[:, 0], order[ar, best.clamp(max=ntry - 1)]) + else: + chosen_k = order[ar, best] + chosen = cands[ar, best] if return_traj: - cert_pts = pts.view(B, ntry, commit_n, 6)[ar, best] - cert_mids = mids.view(B, ntry, commit_n, 6)[ar, best] + cert_pts = pts.view(B, nc, commit_n, 6)[ar, best] + cert_mids = mids.view(B, nc, commit_n, 6)[ar, best] cert_gaps = gaps6[ar, best] else: for kk in range(ntry): @@ -466,6 +486,7 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None, 'collision_rate': (col > 0).float().mean().item(), 'collision_steps_mean': col.mean().item(), 'travel_ratio_median': (travel[okr] / tr[okr].clamp(min=1)).median().item() if okr.any() else None, 'time_mean_s': t_done[ok].mean().item() if ok.any() else None, 'brakes_mean': brakes.mean().item(), + 'backoff_mean': bo.mean().item(), 'start_fail': start_fail.float().mean().item(), 'goal_ratio_2s': ratio2.median().item(), 'speed_ratio_mean': vratio.mean().item(), 'over_vmax_rate': (vratio > 1.001).float().mean().item(), 'time_budget': 'teacher×2(≥3 s)' if bool(has_ref.all()) else ('fixed %.0f s' % (max_sec or NOREF_SEC) if not has_ref.any() else 'mixed')} @@ -473,7 +494,7 @@ def rollout(model, kin, tasks, device, K=8, safety=False, seed=0, max_sec=None, # 首次方向;全程没偏离到 FIRST_DEG 的(基本不绕 J1)退回净方向 side = torch.where(j1_first != 0, j1_first, torch.where(j1_max >= -j1_min, torch.ones_like(j1_max), -torch.ones_like(j1_max))) per = {'done': done.cpu(), 'ok': ok.cpu(), 't_done': t_done.cpu(), 'limit': limit.cpu(), 'col': col.cpu(), 'travel': travel.cpu(), 'err': e_end.cpu(), - 'side': side.cpu(), 'start_fail': start_fail.cpu(), 'brakes': brakes.cpu(), 'first_hit': first_hit.cpu(), 'dls_it': dls_it.cpu(), + 'side': side.cpu(), 'start_fail': start_fail.cpu(), 'brakes': brakes.cpu(), 'backoff': bo.cpu(), 'first_hit': first_hit.cpu(), 'dls_it': dls_it.cpu(), 'goal_ratio_2s': ratio2.cpu(), 'speed_ratio': vratio.cpu(), 'accel_ratio': aratio.cpu()} if return_traj: per['traj'] = torch.stack(traj, 1).cpu() diff --git a/tests/test_eval_cl.py b/tests/test_eval_cl.py index 8ab17cd..8bdb0b2 100644 --- a/tests/test_eval_cl.py +++ b/tests/test_eval_cl.py @@ -713,5 +713,73 @@ class CertBatchEquivalenceTest(unittest.TestCase): self.assertEqual(kin.gm_groups(q5, [(0, 2)], 40.0, xf=xf5).shape, (5, 1)) +class BackoffTest(unittest.TestCase): + """#17:全拒时把首选候选缩步顶上——不许改变「还有安全前缀」的题,但要把站住变成慢爬。""" + LIM = 0.5 # 桩里 J1 超过它就是碰撞(一步满速 = VMAX 60°/s × DT 0.02 = 1.2°) + + class StubModel: + act_scale = 1.0 + + def __init__(self, act): + self.act = act + + def sample_k(self, feat, K, z, steps=1): + B = feat.shape[0] + return (self.act.expand(B, K, 16, 6).clone(), torch.zeros(B, device=feat.device), + torch.zeros(B, 13, device=feat.device)) + + def run1(self, acts, backoff, cert_batch=1): + class StepKin: + def clamp_box(self, q): + return q + + def gaps(self, q, cap): + bad = q[:, 0] > BackoffTest.LIM + return {'gm': torch.where(bad[:, None], -torch.ones(len(q), 49, device=q.device), + torch.full((len(q), 49), 1.0, device=q.device))} + + model = self.StubModel(torch.tensor(acts, dtype=torch.float32)) + tasks = [{'q0': [0] * 6, 'T': [10, 0, 0]}] + with patch('ml.sim.closed_loop.features', return_value=torch.zeros(1, 83)), \ + patch('ml.sim.closed_loop.goal_err', + side_effect=lambda kin, q, T, R: (torch.full((q.shape[0],), 10.0), torch.zeros(q.shape[0]))): + res, per = rollout(model, StepKin(), tasks, 'cpu', K=len(acts), safety=True, max_sec=0.06, + return_traj=True, use_sub=False, select='first', replan='empty', commit_n=2, + safety_partial=1, cert_batch=cert_batch, backoff=backoff) + return res, per + + def test_full_rejection_creeps_instead_of_standing_still(self): + acts = [[[1.0, 0, 0, 0, 0, 0], [1.0, 0, 0, 0, 0, 0]] + [[0, 0, 0, 0, 0, 0]] * 14] + _, p0 = self.run1(acts, 0) + self.assertEqual(p0['traj'][0, :, 0].tolist(), [0.0] * 4) # 0.06 s 内规划 2 次,两次都全拒 + self.assertEqual(float(p0['brakes'][0]), 2.0) + self.assertEqual(float(p0['backoff'][0]), 0.0) + _, p1 = self.run1(acts, 1) # 1/2 档 = 0.6° 仍然越界 ⇒ 与不开缩步逐位相同 + torch.testing.assert_close(p1['traj'], p0['traj'], rtol=0, atol=0) + torch.testing.assert_close(p1['brakes'], p0['brakes'], rtol=0, atol=0) + _, p2 = self.run1(acts, 2) # 1/4 档 = 0.3° 安全 ⇒ 先爬一步,再规划时原地缩步也被拒 + self.assertEqual([round(v, 5) for v in p2['traj'][0, :, 0].tolist()], [0.0, 0.3, 0.3, 0.3]) + self.assertEqual(float(p2['backoff'][0]), 1.0) + self.assertEqual(float(p2['brakes'][0]), 1.0) + + def test_backoff_never_overrides_a_candidate_with_a_safe_prefix(self): + # 第 1 步安全(0.24°)、第 2 步越界(0.84°)⇒ 前缀 1 步就够,缩步档不该插手 + acts = [[[0.2, 0, 0, 0, 0, 0], [0.5, 0, 0, 0, 0, 0]] + [[0, 0, 0, 0, 0, 0]] * 14] + _, p0 = self.run1(acts, 0) + _, p2 = self.run1(acts, 2) + for key in ('traj', 'brakes', 'backoff'): + with self.subTest(key=key): + torch.testing.assert_close(p2[key], p0[key], rtol=0, atol=0) + for key in ('commit', 'certified', 'points', 'gaps'): + with self.subTest(key=key): + torch.testing.assert_close(p2['plans'][key], p0['plans'][key], rtol=0, atol=0) + self.assertGreater(float(p2['traj'][0, 1, 0]), 0.0) + + def test_backoff_requires_batched_certification(self): + acts = [[[1.0, 0, 0, 0, 0, 0], [1.0, 0, 0, 0, 0, 0]] + [[0, 0, 0, 0, 0, 0]] * 14] + with self.assertRaisesRegex(ValueError, 'cert_batch'): + self.run1(acts, 1, cert_batch=0) + + if __name__ == '__main__': unittest.main()