Skip to content

信道建模与适配技术分析

概述

水声信道是已知最复杂的无线通信信道之一,其特征包括极低的传播速度(约 1500 m/s)、严重的多径效应、时变的多普勒频移以及频率依赖的传播损耗。本章首先综述水声信道的物理特性,然后分析系统的适配器层设计、三种后端实现,以及 Mock 模式的信道模拟方法。

水声信道物理特性综述

传播速度与延迟

水声信号的传播速度约为 1500 m/s,比电磁波(3x10^8 m/s)慢五个数量级。这意味着:

  • 1 km 距离的单程传播延迟约 0.67 s
  • 10 km 距离的单程传播延迟约 6.7 s
  • 端到端往返延迟可达数十秒

这一特性直接影响系统设计:runner.py 中的步长 dt_seconds 通常设置为 10-30 秒,以确保在一个时间步内有足够时间完成帧的发送-传播-接收周期。步间的 rx_settle_s 等待(runner.py:754)也是为了容纳声学传播延迟。

多径效应

水声信道中,信号从发射端到接收端可能经过多条路径:

  • 直达路径:发射端到接收端的直线路径
  • 海面反射路径:经海水-空气界面反射
  • 海底反射路径:经海水-海底界面反射
  • 体积散射路径:被水体中的气泡、生物等散射

多径效应导致接收端观测到同一信号的多个时延副本,产生时延扩展(delay spread)。典型浅海信道的时延扩展可达 10-100 ms,深海信道则可能更长。多径干扰是水声通信误码率的主要来源之一。

多普勒频移

水声通信中的多普勒效应来源于:

  • 平台运动:发射端或接收端(如 AUV、浮标)的相对运动
  • 海面波动:海表面的反射路径长度随波浪周期性变化
  • 水流运动:水体内部的流动导致介质本身的速度变化

由于声波传播速度低,即使较小的相对速度也会产生显著的多普勒频移。例如,1 m/s 的相对运动速度在 10 kHz 载波频率下产生约 6.7 Hz 的频移(相对频移 ~6.7x10^-4),远大于射频通信中的典型值。

频率依赖的传播损耗

水声信号的传播损耗由两部分组成:

  1. 几何扩展损耗:随距离增大的能量扩散,通常为球面扩展(~20 log r)或柱面扩展(~10 log r)
  2. 吸收损耗:水分子和化学物质(主要是硼酸和硫酸镁)对声能的吸收,强烈依赖于频率

吸收系数 α(f) 随频率近似二次增长,导致水声通信的可用带宽与通信距离呈反比关系。短距离(<1 km)可使用数百 kHz 带宽,长距离(>10 km)则仅有数 kHz 可用带宽。这是水声通信带宽极为受限的根本物理原因。

时变特性

水声信道不是静态的,其特征随时间变化:

  • 短时变化(秒级):海面波浪引起的反射路径变化
  • 中时变化(分钟-小时级):潮汐、温跃层深度变化
  • 长时变化(季节级):水温垂直分布的季节性变化影响声速剖面

这种时变特性是系统需要"动态"调度的根本原因——静态的固定策略无法适应信道质量的持续波动。

UnetAdapter 抽象层设计

接口定义

系统通过适配器模式(Adapter Pattern)屏蔽底层仿真引擎的 API 差异。抽象基类 UnetAdapter 定义于 unet_adapter/base.py:16-61

python
class UnetAdapter(ABC):
    @abstractmethod
    def connect(self, inst: UnetInstance) -> None: ...

    @abstractmethod
    def close(self, inst: UnetInstance) -> None: ...

    @abstractmethod
    def probe(self, inst: UnetInstance) -> Dict[str, Any]: ...

    @abstractmethod
    def check_phy(self, inst: UnetInstance,
                  probe: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: ...

    @abstractmethod
    def subscribe_rx(self, inst: UnetInstance,
                     on_msg: Callable[[Dict[str, Any]], None],
                     stop_event=None, timeout_s=1.0,
                     ready_event=None) -> None: ...

    @abstractmethod
    def send_frame(self, inst: UnetInstance, to: int,
                   payload: bytes, meta=None) -> Dict[str, Any]: ...

    @abstractmethod
    def get_time(self, inst: UnetInstance) -> float: ...

    def query_node_address(self, inst: UnetInstance) -> Optional[int]:
        return None

UnetInstance 数据类(base.py:8-13)封装单个仿真节点的连接信息:

python
@dataclass
class UnetInstance:
    name: str                    # 节点名称 (如 "node-1")
    host: str                    # Gateway 主机地址
    port: int                    # Gateway 端口
    meta: Dict[str, Any]         # 运行时元数据 (node_id, unet_addr 等)

接口语义

方法职责调用时机
connect建立与仿真节点的 Gateway 连接运行启动前
close关闭连接,释放资源运行结束或异常退出
probe探测节点状态,获取 agent/service 列表PHY preflight 阶段
check_phy验证 PHYSICAL 层服务是否可用PHY preflight 阶段
subscribe_rx订阅接收通知,循环轮询 RX 消息步循环开始前,独立线程
send_frame发送一个带 payload 的水声帧步循环内的 TX 阶段
get_time获取仿真器的当前时间保留接口,未在主循环使用
query_node_address查询节点的 UNet 地址PHY preflight 阶段

三种后端实现

后端选择机制

unet_adapter/factory.py:22-53 实现自动后端检测,优先级为 arlpy > fjagepy > logonly:

python
# src/unet_dt/unet_adapter/factory.py:22-40
def detect_backend() -> str:
    try:
        import arlpy
        if _arlpy_gateway_available():
            return 'arlpy'
    except Exception:
        pass
    try:
        import fjagepy
        return 'fjagepy'
    except Exception:
        pass
    return 'logonly'

fjagepy 后端 (GatewayAdapterFjagepy)

unet_adapter/fjagepy_impl.py 是功能最完整的后端实现,通过 fjagepy 库与 UNetStack 仿真器交互。

连接管理fjagepy_impl.py:25-51):

  • 使用 fjagepy.Gateway(host, port) 建立 JSON-over-TCP 连接
  • 每个 (host, port) 对维护一个连接实例,避免重复创建
  • RX 订阅使用独立的 Gateway 连接(fjagepy_impl.py:117-118),防止 RX 轮询消费 TX 路径的响应

帧发送策略fjagepy_impl.py:159-281):

系统采用分层 fallback 策略发送帧:

发送策略优先级:
1. DatagramReq -> PHYSICAL 服务代理
   (有意排除 DATAGRAM 服务,因其返回 transport agent 会导致分片重传)
2. DatagramReq -> LINK 服务代理
3. TxFrameReq -> phy agent (物理层直发)

关键设计决策(fjagepy_impl.py:193-214):排除 DATAGRAM 服务代理是因为 agentForService(DATAGRAM) 通常返回 UNetStack 的 transport agent,该 agent 会对大数据报进行可靠传输分片(ARQ),在慢速水声信道上导致拥塞崩溃。系统选择 PHYSICAL/LINK 服务代理,直接发送单帧不分片。

RX 订阅fjagepy_impl.py:105-157):

RX 线程使用 best_effort_subscribe() 注册消息通知,然后进入轮询循环。ready_event 机制确保 RX 订阅完成后才开始 TX(runner.py:609-627)。

arlpy 后端 (GatewayAdapterArlpy)

unet_adapter/arlpy_impl.py 与 fjagepy 后端结构相似,通过 arlpy.unet.Gateway 连接 UNetStack。主要差异:

  • 支持两种 Gateway 构造方式:Gateway()gateway()arlpy_impl.py:31-36
  • send_frame 使用 arlpy.unet.TxFrameReq 类(arlpy_impl.py:178
  • check_phy 增加了基于 probe 结果的 fallback 路径(arlpy_impl.py:99-126
  • query_node_address 复用 fjagepy_helpers 模块(arlpy_impl.py:213-217

logonly 后端 (LogOnlyAdapter)

unet_adapter/logonly_impl.py 是最小化的空实现,所有网络操作均抛出异常:

  • connect/close:空操作
  • probe:返回 {"status": "log_only"}
  • subscribe_rx/send_frame:直接抛出 RuntimeError
  • get_time:返回系统墙钟时间

该后端仅用于无 UNetStack 环境下的日志分析和开发测试。在 gateway 模式中,若检测到 logonly 后端,会立即抛出错误(runner.py:368-369)。

PHY 预检与地址解析

Gateway 预检流程

在 gateway 模式启动前,orchestrator/gateway_preflight.py 执行 PHY 预检,确保足够的健康节点可用:

算法: PHY 预检 (run_phy_preflight)
输入: adapter, candidates (候选节点列表), required_count, preferred_node_ids
输出: PreflightSelection

FOR each inst IN candidates:
    1. adapter.connect(inst)
    2. probe_data = adapter.probe(inst)
    3. phy_result = adapter.check_phy(inst, probe_data)
    4. IF phy_result.ok THEN
         address = adapter.query_node_address(inst)
         inst.meta["unet_addr"] = address
         标记为 healthy
    5. adapter.close(inst)

选择策略:
    1. 优先选取 preferred_node_ids 中的 healthy 节点
    2. 用其他 healthy 节点填充剩余名额
    3. IF len(selected) < required_count THEN 抛出 GatewayPreflightError

地址解析层次

runner.py:495-520 实现多层次地址解析:

  1. Preflight NODE_INFO 查询:连接时通过 adapter.query_node_address() 获取运行时分配地址
  2. Scenario YAML 静态覆盖node.unet_addr 字段可手动指定地址,优先级高于运行时查询
  3. 强制校验:所有参与流量的节点必须有已解析地址,否则终止运行

Mock 模式:确定性随机 trace 生成

当无法连接 UNetStack 时,系统回退到 Mock 模式(runner.py:85-126),生成确定性的仿真 trace:

python
# src/unet_dt/orchestrator/runner.py:85-126
def _generate_mock_traces(
    seed: int, steps: int, dt_seconds: float,
    flows: List[Dict[str, Any]],
    loss_rate: float = 0.05,         # 固定 5% 丢包率
    min_delay_ms: int = 50,          # 最小延迟 50ms
    max_delay_ms: int = 200,         # 最大延迟 200ms
    *, source: str = "mock",
) -> List[TraceRecord]:
    rng = random.Random(seed)        # 种子确定性
    ...
    for flow in flows:
        for t_ms in range(0, total_ms, period_ms):
            received = rng.random() >= loss_rate    # 伯努利丢包
            delay_ms = rng.randint(min_delay_ms, max_delay_ms) if received else None
            ...

Mock 信道模型特征

参数模型
丢包率5% (固定)独立同分布 Bernoulli(0.95)
延迟分布[50, 200] ms均匀分布 U(50, 200)
随机种子可配置保证实验可重复性
信道时变性各帧间统计独立
多径效应不模拟时延扩展

RX 消息解析

unet_adapter/rx_parse.py 实现了一个鲁棒的 RX 消息解析器,能够处理 UNetStack 不同版本的消息格式差异:

  • payload 提取rx_parse.py:193-239):在消息字典中搜索 datapayloadbytes 等候选字段,按评分(_score_key)选择最佳匹配
  • 地址提取rx_parse.py:95-112):搜索 src/source/from/sender 等同义字段
  • 嵌套搜索rx_parse.py:222-233):支持递归进入 rxframepacket 等嵌套容器

这种"最佳努力"的解析策略使系统能够适配 UNetStack 的不同版本和消息格式变体,而无需硬编码特定的消息类名。

局限性分析

L-1: 系统自身无声波传播模型

系统依赖外部 UNetStack 仿真器提供信道模型。当使用 Mock 模式时,信道行为退化为简单的伯努利丢包 + 均匀延迟,不包含任何水声物理特性(多径、多普勒、频率选择性衰落)。

L-2: Mock 模式参数固定

Mock 模式的丢包率(5%)和延迟范围(50-200ms)均为硬编码常量,无法通过场景配置文件自定义。

L-3: 无信道状态估计

系统不维护信道状态信息(CSI),策略层无法获知当前 SNR 或信道容量估计,只能通过事后统计的丢包率间接推断信道质量。

改进方向

短期:参数化 Mock 模型

将 Mock 模式的 loss_ratemin_delay_msmax_delay_ms 提取为场景 YAML 的可配置项,支持不同信道条件的对比实验。

中期:统计信道模型

引入更真实的统计信道模型替代简单伯努利模型:

  • Markov 信道:用两状态 Gilbert-Elliott 模型模拟突发丢包
  • Rayleigh/Ricean 衰落:模拟多径信道的接收信号幅度分布
  • 指数衰减延迟:用指数分布替代均匀分布,更贴近实际的多径功率时延谱

长期:Bellhop 声线追踪模型集成

Bellhop 是海洋声学领域的标准声线追踪工具,可根据海洋环境参数(声速剖面、海底地形、海面状态)精确计算声传播损耗和多径结构。集成路径:

  1. 将 Bellhop 作为子进程调用,输入环境参数,输出传播损耗矩阵
  2. 根据损耗矩阵动态计算每对节点间的丢包概率和延迟分布
  3. 支持时变声速剖面,模拟信道的昼夜和季节性变化