diff --git a/docs/experiment_round33_both.json b/docs/experiment_round33_both.json new file mode 100644 index 0000000..ac7f50d --- /dev/null +++ b/docs/experiment_round33_both.json @@ -0,0 +1,70 @@ +{ + "config": { + "ckpt": "ml/runs/base_dag2_dag1/best.pt", + "samples": "ml/data/samples", + "tasks": null, + "split": "val", + "n": 256, + "K": 8, + "commit": 2, + "fb": 2.2, + "ki": 0.0, + "fb_scale": 0.0, + "fb_near": 0.2, + "fb_fallback": 0.0, + "kdecay": 0.9, + "kimax": 0.5, + "fb_every": 1, + "safety_try": 0, + "sel_w": 0.1, + "dls_safety": 1, + "fb_clear": 0.0, + "fb_cmin": 0.3, + "sub_refresh": 0, + "fb_block_gain": 0.5, + "dump": "ml/runs/round33_both_rows.json", + "out": "docs/experiment_round33_both.json", + "trace": "ml/runs/round33_both.pt", + "select": "endpoint", + "replan": "empty", + "safety": 1, + "partial": 1, + "inc_cap": 1, + "gap_margin": 0.5, + "max_sec": null, + "seed": 0, + "dls": "analytic", + "device": "cuda" + }, + "batch_size": 256, + "batch_seed_rule": "same seed per batch", + "checkpoint_sha256": "f7f686fd8ee92fb6948fbcc4ee954636065ad87bb54f661c4f8f575bc16c2ce1", + "tasks_sha256": "249bd93457825cda98c59e64d9a096d3473c69aeb3553de1dea6d9e56be71772", + "kin_fp": "2ebcbf17", + "act_scale": 1.0, + "metrics": { + "n": 256, + "n_ref": 256, + "success": 0.51953125, + "collision_free_success": 0.51953125, + "time_ok_1p3": 0.26171875, + "collision_rate": 0.0, + "collision_steps_mean": 0.0, + "travel_ratio_median": 2.5444939136505127, + "time_mean_s": 2.4729323387145996, + "brakes_mean": 19.97265625, + "start_fail": 0.0, + "goal_ratio_2s": 0.2868920564651489, + "err_fail_med": 202.12567138671875, + "err_success_med": 0.22085000574588776, + "dls_share": 0.62109375, + "time_budget": "teacher×2(≥3 s)", + "speed_ratio_p50": 1.0000016689300537, + "speed_ratio_p95": 1.0000101327896118, + "speed_ratio_max": 1.0000101327896118, + "over_vmax_rate": 0.0, + "accel_ratio_p95": 1.0000016689300537, + "accel_ratio_max": 1.0000101327896118 + }, + "simulation_wall_s": 277.54961390000244 +} diff --git a/docs/experiment_round33_cap.json b/docs/experiment_round33_cap.json new file mode 100644 index 0000000..137a473 --- /dev/null +++ b/docs/experiment_round33_cap.json @@ -0,0 +1,70 @@ +{ + "config": { + "ckpt": "ml/runs/base_dag2_dag1/best.pt", + "samples": "ml/data/samples", + "tasks": null, + "split": "val", + "n": 256, + "K": 8, + "commit": 2, + "fb": 2.2, + "ki": 0.0, + "fb_scale": 0.0, + "fb_near": 0.2, + "fb_fallback": 0.0, + "kdecay": 0.9, + "kimax": 0.5, + "fb_every": 1, + "safety_try": 0, + "sel_w": 0.1, + "dls_safety": 1, + "fb_clear": 0.0, + "fb_cmin": 0.3, + "sub_refresh": 0, + "fb_block_gain": 0.5, + "dump": "ml/runs/round33_cap_rows.json", + "out": "docs/experiment_round33_cap.json", + "trace": "ml/runs/round33_cap.pt", + "select": "endpoint", + "replan": "empty", + "safety": 1, + "partial": 1, + "inc_cap": 1, + "gap_margin": 0.0, + "max_sec": null, + "seed": 0, + "dls": "analytic", + "device": "cuda" + }, + "batch_size": 256, + "batch_seed_rule": "same seed per batch", + "checkpoint_sha256": "f7f686fd8ee92fb6948fbcc4ee954636065ad87bb54f661c4f8f575bc16c2ce1", + "tasks_sha256": "249bd93457825cda98c59e64d9a096d3473c69aeb3553de1dea6d9e56be71772", + "kin_fp": "2ebcbf17", + "act_scale": 1.0, + "metrics": { + "n": 256, + "n_ref": 256, + "success": 0.5234375, + "collision_free_success": 0.5234375, + "time_ok_1p3": 0.265625, + "collision_rate": 0.0, + "collision_steps_mean": 0.0, + "travel_ratio_median": 2.615795850753784, + "time_mean_s": 2.486567258834839, + "brakes_mean": 19.5546875, + "start_fail": 0.0, + "goal_ratio_2s": 0.27920001745224, + "err_fail_med": 204.71360778808594, + "err_success_med": 0.21751396358013153, + "dls_share": 0.61328125, + "time_budget": "teacher×2(≥3 s)", + "speed_ratio_p50": 1.0000016689300537, + "speed_ratio_p95": 1.0000101327896118, + "speed_ratio_max": 1.0000101327896118, + "over_vmax_rate": 0.0, + "accel_ratio_p95": 1.0000016689300537, + "accel_ratio_max": 1.0000101327896118 + }, + "simulation_wall_s": 325.18856850000157 +} diff --git a/docs/experiment_round33_gpu_cycle.json b/docs/experiment_round33_gpu_cycle.json new file mode 100644 index 0000000..43c665e --- /dev/null +++ b/docs/experiment_round33_gpu_cycle.json @@ -0,0 +1,202 @@ +{ + "device": "NVIDIA GeForce RTX 3090", + "torch": "2.10.0+cu130", + "act_scale": 1.0, + "cfg": {}, + "B": 1, + "commit_n": 2, + "fp64_note": "3090 fp64 \u541e\u5410\u7ea6 fp32 \u7684 1/54\uff1b\u8ba4\u8bc1\u76ee\u524d\u8981\u6c42 fp64", + "inference": { + "sample_K1_steps1": { + "p50": 7.453, + "p95": 9.912, + "p99": 10.766, + "max": 11.109, + "reps": 120 + }, + "sample_K8_steps1": { + "p50": 8.208, + "p95": 10.685, + "p99": 11.22, + "max": 15.224, + "reps": 120 + }, + "sample_K1_steps2": { + "p50": 13.149, + "p95": 17.621, + "p99": 18.438, + "max": 19.296, + "reps": 120 + }, + "sample_K8_steps2": { + "p50": 14.167, + "p95": 19.241, + "p99": 24.77, + "max": 25.703, + "reps": 120 + }, + "sample_K1_steps4": { + "p50": 19.193, + "p95": 22.338, + "p99": 23.666, + "max": 25.787, + "reps": 120 + }, + "sample_K8_steps4": { + "p50": 21.509, + "p95": 24.732, + "p99": 35.655, + "max": 41.514, + "reps": 120 + }, + "sample_K1_steps8": { + "p50": 40.658, + "p95": 55.162, + "p99": 60.394, + "max": 60.398, + "reps": 120 + }, + "sample_K8_steps8": { + "p50": 40.416, + "p95": 47.997, + "p99": 62.49, + "max": 71.976, + "reps": 120 + }, + "sample_K1_steps16": { + "p50": 72.429, + "p95": 90.188, + "p99": 107.536, + "max": 112.674, + "reps": 120 + }, + "sample_K8_steps16": { + "p50": 79.893, + "p95": 99.933, + "p99": 105.746, + "max": 106.078, + "reps": 120 + } + }, + "encode_once": { + "p50": 2.265, + "p95": 4.001, + "p99": 6.086, + "max": 6.136, + "reps": 120 + }, + "tiny_op_ms": 0.028, + "sync_only_ms": 0.005, + "components": { + "features_f32": { + "p50": 12.443, + "p95": 14.227, + "p99": 19.678, + "max": 20.531, + "reps": 120 + }, + "endpoint_score_K8_f32": { + "p50": 4.278, + "p95": 5.841, + "p99": 6.322, + "max": 8.883, + "reps": 120 + }, + "dls_fb_1step_f64": { + "p50": 7.191, + "p95": 12.26, + "p99": 14.642, + "max": 15.384, + "reps": 120 + }, + "dls_fb_1step_f32": { + "p50": 7.619, + "p95": 10.142, + "p99": 11.597, + "max": 12.022, + "reps": 120 + }, + "discrete_cert_4pts_f32": { + "p50": 8.214, + "p95": 13.365, + "p99": 14.235, + "max": 14.991, + "reps": 120 + }, + "cert_n8_f64_2roots": { + "p50": 11.265, + "p95": 14.64, + "p99": 16.788, + "max": 17.761, + "reps": 120 + }, + "cert_n16_f64_2roots": { + "p50": 14.574, + "p95": 16.731, + "p99": 18.459, + "max": 22.346, + "reps": 120 + }, + "cert_n32_f64_2roots": { + "p50": 21.528, + "p95": 24.746, + "p99": 28.037, + "max": 28.066, + "reps": 120 + } + }, + "cycle_K8_steps16_discrete": { + "p50": 116.474, + "p95": 134.474, + "p99": 142.593, + "max": 152.768, + "reps": 120 + }, + "cycle_K8_steps4_discrete": { + "p50": 55.919, + "p95": 65.767, + "p99": 74.809, + "max": 78.059, + "reps": 120 + }, + "cycle_K1_steps4_discrete": { + "p50": 51.087, + "p95": 58.614, + "p99": 62.326, + "max": 63.919, + "reps": 120 + }, + "cycle_K1_steps1_discrete": { + "p50": 34.493, + "p95": 42.078, + "p99": 43.906, + "max": 50.268, + "reps": 120 + }, + "cycle_K8_steps16_cert_n8": { + "p50": 132.027, + "p95": 150.463, + "p99": 160.83, + "max": 192.586, + "reps": 120 + }, + "cycle_K8_steps4_cert_n8": { + "p50": 64.812, + "p95": 79.192, + "p99": 89.629, + "max": 95.837, + "reps": 120 + }, + "cycle_K1_steps4_cert_n16": { + "p50": 61.967, + "p95": 73.219, + "p99": 79.053, + "max": 83.18, + "reps": 120 + }, + "f32_vs_f64": { + "points": 3, + "max_abs_err_mm": 0.0, + "sign_flips": 0 + } +} \ No newline at end of file diff --git a/docs/experiment_round33_gpu_cycle_steps1.json b/docs/experiment_round33_gpu_cycle_steps1.json new file mode 100644 index 0000000..ad77aa1 --- /dev/null +++ b/docs/experiment_round33_gpu_cycle_steps1.json @@ -0,0 +1,143 @@ +{ + "device": "NVIDIA GeForce RTX 3090", + "B": 1, + "steps_used_by_eval": 1, + "note": "eval_cl \u4ece\u4e0d\u4f20 steps\uff0crollout \u9ed8\u8ba4 steps=1\uff08MeanFlow \u4e00\u6b65\u751f\u6210\uff09\uff1b\u6240\u4ee5\u771f\u5b9e\u5468\u671f\u6309 steps=1 \u6d4b", + "parts": { + "features_total": { + "p50": 12.589, + "p95": 18.334, + "p99": 20.217, + "max": 23.649, + "reps": 120 + }, + "features_gm_groups_only": { + "p50": 6.817, + "p95": 8.771, + "p99": 10.254, + "max": 17.169, + "reps": 120 + }, + "features_gaps40_all_groups": { + "p50": 7.245, + "p95": 10.501, + "p99": 13.794, + "max": 14.508, + "reps": 120 + }, + "features_fk_only": { + "p50": 3.708, + "p95": 5.024, + "p99": 5.991, + "max": 6.301, + "reps": 120 + }, + "sample_k_K8_steps1": { + "p50": 7.799, + "p95": 11.655, + "p99": 13.342, + "max": 16.212, + "reps": 120 + }, + "sample_k_K1_steps1": { + "p50": 7.205, + "p95": 8.935, + "p99": 9.615, + "max": 13.92, + "reps": 120 + }, + "sample_k_K4_steps1": { + "p50": 7.834, + "p95": 10.354, + "p99": 14.744, + "max": 15.241, + "reps": 120 + }, + "dls_fb_f64": { + "p50": 6.729, + "p95": 9.297, + "p99": 11.186, + "max": 12.064, + "reps": 120 + }, + "discrete_cert_4pts": { + "p50": 8.603, + "p95": 11.181, + "p99": 12.464, + "max": 15.799, + "reps": 120 + }, + "cert_uniform_n8_2roots_f64": { + "p50": 11.479, + "p95": 19.221, + "p99": 20.942, + "max": 21.183, + "reps": 120 + }, + "cert_uniform_n16_2roots_f64": { + "p50": 16.138, + "p95": 21.972, + "p99": 23.898, + "max": 25.793, + "reps": 120 + } + }, + "cycles": { + "cycle_K8_full_discrete": { + "p50": 40.676, + "p95": 51.742, + "p99": 59.782, + "max": 62.074, + "reps": 120 + }, + "cycle_K4_full_discrete": { + "p50": 43.997, + "p95": 56.145, + "p99": 59.905, + "max": 65.882, + "reps": 120 + }, + "cycle_K1_full_discrete": { + "p50": 37.87, + "p95": 45.754, + "p99": 49.394, + "max": 53.139, + "reps": 120 + }, + "cycle_K8_noscore_discrete": { + "p50": 37.427, + "p95": 49.602, + "p99": 60.777, + "max": 70.139, + "reps": 120 + }, + "cycle_K8_full_cert_n8": { + "p50": 43.984, + "p95": 65.795, + "p99": 75.53, + "max": 95.452, + "reps": 120 + }, + "cycle_K8_full_cert_n16": { + "p50": 51.054, + "p95": 74.862, + "p99": 95.768, + "max": 106.48, + "reps": 120 + }, + "cycle_K8_fused_estimate": { + "p50": 24.054, + "p95": 26.595, + "p99": 30.17, + "max": 33.284, + "reps": 120 + }, + "cycle_K8_fused_cert_n8": { + "p50": 34.274, + "p95": 46.329, + "p99": 72.737, + "max": 79.022, + "reps": 120 + } + } +} \ No newline at end of file diff --git a/docs/experiment_round33_margin.json b/docs/experiment_round33_margin.json new file mode 100644 index 0000000..f49486c --- /dev/null +++ b/docs/experiment_round33_margin.json @@ -0,0 +1,70 @@ +{ + "config": { + "ckpt": "ml/runs/base_dag2_dag1/best.pt", + "samples": "ml/data/samples", + "tasks": null, + "split": "val", + "n": 256, + "K": 8, + "commit": 2, + "fb": 2.2, + "ki": 0.0, + "fb_scale": 0.0, + "fb_near": 0.2, + "fb_fallback": 0.0, + "kdecay": 0.9, + "kimax": 0.5, + "fb_every": 1, + "safety_try": 0, + "sel_w": 0.1, + "dls_safety": 1, + "fb_clear": 0.0, + "fb_cmin": 0.3, + "sub_refresh": 0, + "fb_block_gain": 0.5, + "dump": "ml/runs/round33_margin_rows.json", + "out": "docs/experiment_round33_margin.json", + "trace": "ml/runs/round33_margin.pt", + "select": "endpoint", + "replan": "empty", + "safety": 1, + "partial": 1, + "inc_cap": 0, + "gap_margin": 0.5, + "max_sec": null, + "seed": 0, + "dls": "analytic", + "device": "cuda" + }, + "batch_size": 256, + "batch_seed_rule": "same seed per batch", + "checkpoint_sha256": "f7f686fd8ee92fb6948fbcc4ee954636065ad87bb54f661c4f8f575bc16c2ce1", + "tasks_sha256": "249bd93457825cda98c59e64d9a096d3473c69aeb3553de1dea6d9e56be71772", + "kin_fp": "2ebcbf17", + "act_scale": 1.0, + "metrics": { + "n": 256, + "n_ref": 256, + "success": 0.5625, + "collision_free_success": 0.5625, + "time_ok_1p3": 0.37890625, + "collision_rate": 0.0, + "collision_steps_mean": 0.0, + "travel_ratio_median": 2.448458433151245, + "time_mean_s": 2.2555553913116455, + "brakes_mean": 20.046875, + "start_fail": 0.0, + "goal_ratio_2s": 0.18239997327327728, + "err_fail_med": 225.54278564453125, + "err_success_med": 0.18544802069664001, + "dls_share": 0.62890625, + "time_budget": "teacher×2(≥3 s)", + "speed_ratio_p50": 1.6600004434585571, + "speed_ratio_p95": 1.660003662109375, + "speed_ratio_max": 1.660003662109375, + "over_vmax_rate": 1.0, + "accel_ratio_p95": 1.5889430046081543, + "accel_ratio_max": 1.6600035429000854 + }, + "simulation_wall_s": 298.24517190000915 +} diff --git a/docs/探索日志.md b/docs/探索日志.md index 4aa9ebf..f38458b 100644 --- a/docs/探索日志.md +++ b/docs/探索日志.md @@ -650,9 +650,50 @@ - **两个新开关**:`inc_cap=1` 在**反馈叠加之后**再按每轴 VMAX·DT 限幅(此前 `inc=clamp(act,±1)·astep` 之后又加 `gain×fb`,所以 1.66× 是结构性的);`gap_margin>0` 让安全层要求间隙 > 余量而非 >0,并把宽相位 cap 自动抬到 `margin+0.5`——不抬的话 `gaps(allq, 0.5)` 的截顶值 0.5 会让任何 >0.5 的余量门限恒假。`col`(真实碰撞计数)仍按 `gap≤0` 判,不随余量改变,所以两者不会互相顶替。 - **部署侧同病**(只读核查,未改 JS):`webapp/js/ik-policy.js:355` 也是 `inc[s][j] += fbGain*fbStep[j]` 在 ±1·STEP 之后叠加,所以浏览器控制器同样能超 VMAX;JS 的余量是 `o.margin=5` 的**投影式**护栏(`project()` 每轴 ≤projMax,只处理 gap1(灵敏度)、限幅后恰为 1.0、0.3 mm 间隙在余量 0 时通过而余量 0.5 时刹停、5 mm 间隙在余量 0.5 时仍通过(证明 cap 抬升生效,否则该测试会恒假)。 +- **GPU 上的一个控制周期(按 GPU 口径实测)**:诊断档与逐项分解见 `docs/experiment_round33_gpu_cycle.json`,真实配方(steps=1)见 `docs/experiment_round33_gpu_cycle_steps1.json`,RTX 3090、B=1、float32 推理 + float64 认证,每项 20 次预热 + 120 次计时并 `cuda.synchronize`。 + + - **前提更正(重要)**:`eval_cl` 从不传 `steps`,`rollout` 默认 `steps=1`,而 `ml/model/policy.py:156` 写明"steps=1 即一步生成,steps>1 是诊断用"。所以第27轮以来的所有评估成绩一直是**单步推理**;本节先用 steps=16 测出的"当前配方 116 ms ≈ 8.6 Hz"是口径错误,已作废并在下面用真实配方重测。 + - **真实配方(steps=1, K=8)的分解**: + + | 环节 | P50 | P95 | P99 | + |---|---|---|---| + | 推理 `sample_k` steps=1,K=1 / 4 / 8 | 7.11 / 7.08 / 7.19 | 9.45 / 10.17 / 9.95 | 11.91 / 14.00 / 11.34 | + | 特征 83 维(总 / 其中宽相位间隙 / 其中 FK) | 13.11 / 8.18 / 4.00 | 19.57 / 16.57 / 7.51 | 21.17 / 19.43 / 8.47 | + | 目标反馈 DLS 1 步 | 6.53 | 9.32 | 12.62 | + | 现有离散安全层 4 点(cap 1.0) | 7.41 | 11.34 | 13.80 | + | `certify_uniform` 2 根 n=8 / n=16 | 9.70 / 16.07 | 14.40 / 20.95 | 15.75 / 24.42 | + + | 组合周期(同一位姿,顺序测;跨行比较受漂移影响,只作量级) | P50 | P95 | P99 | + |---|---|---|---| + | K=8 + 离散安全层(真实配方) | **36.68 → 27.3 Hz** | 43.27 | 49.17 | + | K=4 / K=1 + 离散安全层 | 37.71 / 40.36 | 55.17 / 48.46 | 76.53 / 52.06 | + | K=8 + 连续认证 n=8 | 45.34 → 22.1 Hz | 49.68 | 52.20 | + | K=8 + 连续认证 n=16 | 49.55 → 20.2 Hz | 56.85 | 59.06 | + | (对照)诊断档 steps=16 的同一周期 | 116.5 | 134.5 | 142.6 | + + - **归因更正(先证仪表再下结论)**:空算子 P50 0.028 ms、空 `synchronize` 0.005 ms、一次 `encode` 2.27 ms,所以**不是启动/同步地板**。先前口算的"5–10 ms 是 GPU 启动开销"错了,在此更正:特征 13.1 ms、认证 7.4 ms、DLS 6.5 ms 是 B=1 下真实的小算子工作量——即 **GPU 在单臂批量=1 时利用率极低**,而不是被框架拖慢。 + - **推理不是瓶颈,几何才是**:单步推理 7.2 ms,且候选数几乎免费(K 1→8 不涨);一个周期里几何类工作合计约 27 ms(特征宽相位 8.2 + 特征 FK 4.0 + 认证 7.4 + DLS 6.5),并且这几次调用各自重复做 `cum_xf` 与宽相位筛选。所以 50 Hz 的路径是**合并几何查询**(`features()` 已支持 `gm=`/`xf=` 复用,`gm_groups` 文档保证与整表 `gaps` 数值一致),而不是继续压推理。 + - **连续认证的增量成本可接受**:在同一位姿上 n=8 只比现有离散层贵约 8.7 ms(整周期 36.7→45.3 ms),n=16 贵约 12.9 ms。也就是说"段内连续证明"在 GPU 上不是奢侈项,真正卡住 20 ms 的是那 ~27 ms 的重复几何。合并后再看是否进得了 20 ms——这一步正在做交错复测(跨行顺序测的数字受漂移影响,不作最终结论)。 + - **50 Hz 结论(当前状态)**:真实配方一个规划周期 **36.7 ms(P99 49.2)≈ 27 Hz**,仍不满足"P99<20 ms";且必须记住这是在**不限幅**(可超速 1.66×)的策略分布下测的——限幅后的到达率代价见上表,两者不能混着谈。 + - f32 与 f64 的间隙在 3 个点上完全相同(最大偏差 0.0 mm、无符号翻转),但 **3 个点不足以支持"认证可以改用 float32"**,需按真实轨迹批量对比才能下结论。 + ## 分支清理(2026-10-20) - `feat/fb-mix`(8e5431c):反馈混合方案的中间分支,**已验证是 `exp/dagger-deploy` 的祖先**(所有提交都在后者历史里),本地/远端分支删除,内容保留在 exp/dagger-deploy。 diff --git a/ml/common/kin_torch.py b/ml/common/kin_torch.py index b894ecc..205a6a9 100644 --- a/ml/common/kin_torch.py +++ b/ml/common/kin_torch.py @@ -280,6 +280,8 @@ class Kin: """ if self.dtype != torch.float64 or a.dtype != torch.float64 or b.dtype != torch.float64: raise ValueError('segment certification requires float64') + if a.shape != b.shape or a.ndim != 2 or a.shape[1] != 6: + raise ValueError('a and b must be [count,6] with identical shape') # 预算给成张量时 max_depth + 1 会在 uint8 这类窄类型上回绕成 0,循环整个跳过,把碰撞段当安全返回。 if type(max_queries) is not int or type(max_depth) is not int: raise ValueError('certification budgets must be plain ints') @@ -336,6 +338,8 @@ class Kin: raise ValueError('segment certification requires float64') if type(n) is not int or n < 1 or eps <= 0 or (cap is not None and cap <= eps): raise ValueError('invalid subdivision count or tolerance') + if a.shape != b.shape or a.ndim != 2 or a.shape[1] != 6: + raise ValueError('a and b must be [count,6] with identical shape') count = a.shape[0] inside = (((a >= self.lo) & (a <= self.hi) & (b >= self.lo) & (b <= self.hi)) | self.cont).all(-1) bound = (b - a).abs() @ self.cert_lever.T / n # [count,15] 子段界 diff --git a/tests/test_eval_cl.py b/tests/test_eval_cl.py index 46b685a..0469170 100644 --- a/tests/test_eval_cl.py +++ b/tests/test_eval_cl.py @@ -437,6 +437,15 @@ class SegmentCertificateTest(unittest.TestCase): self.assertTrue(result['unresolved'].item()) self.assertEqual(result['queries'].item(), 2) + def test_shape_mismatch_between_endpoints_is_rejected(self): + a = torch.zeros(1, 6, dtype=torch.float64) + b = torch.zeros(2, 6, dtype=torch.float64) + kin = self.ThinObstacle() + for method in (kin.certify_segments, kin.certify_uniform): + with self.subTest(method=method.__name__): + with self.assertRaisesRegex(ValueError, 'identical shape'): + method(a, b) + def test_budget_exhaustion_scales_with_gap_and_bound(self): class ConstantGap(self.ThinObstacle): def gaps(self, q, cap):