mtc 新模块:命令式动作脚本(Python DSL)→ .mt 矩阵编译器,非阻塞时间轴 IR(重叠截断/rest 静止值)+ 10ms 栅格化,产物可由 pybot/bot4cj 直接执行;34 测试全绿(含 pybot parser e2e)。 pybot speaker.py:frequency<=0 改为静音节拍(仅 sleep 保时序,不再钳到 min_f 低鸣),对齐仓颉版 sleep 占位语义;mtc Beep 通道 rest=0,段前/段间/段后回落静音,两侧语义闭环。 修复 test_actpred_compatibility 过时断言(99c1bc1 首行分解为重复微步 19→20 帧时漏改,14→15)。 文档三处同步:根 README 回填 memdb/pybot/actpred 并新增 mtc(模块表+端口一览+章节),MODULES/ARCHITECTURE 同步 mtc,新建 mtc/README.md。
92 lines
3.2 KiB
Python
92 lines
3.2 KiB
Python
"""时间轴 IR 测试:Segment 插值 / Channel 重叠截断 / 采样保持语义"""
|
||
|
||
import pytest
|
||
|
||
from mtc.timeline import Channel, Segment, total_ms
|
||
|
||
|
||
# ---------------- Segment ----------------
|
||
|
||
def test_segment_value_at_endpoints_and_midpoint():
|
||
s = Segment(t0=50, dur=100, v0=10, v1=30)
|
||
assert s.end == 150
|
||
assert s.value_at(50) == 10 # 段起点取 v0
|
||
assert s.value_at(100) == 20 # 中点线性
|
||
assert s.value_at(150) == 30 # 段尾取 v1
|
||
assert s.value_at(0) == 10 # 段前保持 v0
|
||
assert s.value_at(999) == 30 # 段后保持 v1
|
||
|
||
|
||
# ---------------- Channel ----------------
|
||
|
||
def test_channel_target_defaults_to_initial():
|
||
ch = Channel("s1.angle")
|
||
assert ch.target == 0.0 and ch.busy_until == 0.0
|
||
ch.set_initial(45)
|
||
assert ch.target == 45.0
|
||
|
||
|
||
def test_channel_set_initial_after_segment_rejected():
|
||
ch = Channel("s1.angle")
|
||
ch.add(0, 100, 0, 10)
|
||
with pytest.raises(ValueError):
|
||
ch.set_initial(45)
|
||
|
||
|
||
def test_channel_overlap_truncates_prev_and_warns():
|
||
"""重叠:前段截断至新段 t0,基准取前段目标值(由调用方传 v0)"""
|
||
ch = Channel("s1.angle")
|
||
ch.add(0, 100, 0, 10) # [0,100) 0→10
|
||
v0 = ch.target # 10(前段目标值)
|
||
ch.add(50, 100, v0, v0 + 10) # [50,150) 10→20
|
||
assert len(ch.segments) == 2
|
||
assert ch.segments[0].dur == 50 # 前段截断至 t0=50
|
||
assert ch.segments[0].end == 50
|
||
assert ch.overlap_warns and "截断" in ch.overlap_warns[0]
|
||
# 截断点连续:前段尾值 == 新段起点值
|
||
assert ch.value_at(50) == 10
|
||
assert ch.value_at(100) == 15
|
||
|
||
|
||
def test_channel_value_at_no_segment_uses_initial():
|
||
ch = Channel("s1.angle")
|
||
ch.set_initial(30)
|
||
assert ch.value_at(10) == 30
|
||
assert ch.value_at(9999) == 30
|
||
|
||
|
||
def test_channel_value_at_holds_last_target_after_all_segments():
|
||
ch = Channel("s1.angle")
|
||
ch.add(0, 100, 0, 10)
|
||
ch.add(200, 100, 10, 5)
|
||
assert ch.value_at(150) == 10 # 段间空隙保持前段目标值
|
||
assert ch.value_at(100000) == 5 # 全部完结后保持最后目标值
|
||
|
||
|
||
def test_channel_rest_value_falls_back_in_gaps():
|
||
"""frequency 型通道(rest=0):段前/段间空隙/段后统一回落静止值"""
|
||
ch = Channel("spk.frequency", rest=0.0)
|
||
ch.add(20, 30, 1000, 1000) # [20,50) 阶跃段
|
||
ch.add(70, 20, 2000, 2000) # [70,90)
|
||
assert ch.value_at(10) == 0 # 首段之前
|
||
assert ch.value_at(30) == 1000 # 段内
|
||
assert ch.value_at(50) == 1000 # 段尾端点仍属段内
|
||
assert ch.value_at(60) == 0 # 段间空隙回落 rest
|
||
assert ch.value_at(80) == 2000
|
||
assert ch.value_at(999) == 0 # 全部段后回落 rest
|
||
|
||
|
||
def test_channel_rest_does_not_affect_in_segment_value():
|
||
"""rest 仅作用段外:舵机通道设 rest 时段内插值不受影响"""
|
||
ch = Channel("s1.angle", rest=0.0)
|
||
ch.add(0, 100, 10, 20)
|
||
assert ch.value_at(50) == 15
|
||
|
||
|
||
def test_total_ms_takes_max_of_channels_and_cursor():
|
||
a, b = Channel("a"), Channel("b")
|
||
a.add(0, 100, 0, 1)
|
||
b.add(50, 100, 0, 1)
|
||
assert total_ms({"a": a, "b": b}, cursor=0) == 150
|
||
assert total_ms({"a": a, "b": b}, cursor=500) == 500
|