Files
ios-location-track/core/noise.py
T
jbjf6666 85e9a53750 docs: 新增 Windows 移植交接文档
docs/WINDOWS_PORT.md 覆盖:
- 逐文件可移植性审计(核心逻辑层约 90% 可直接复用)
- 核心难点:iOS 17+ 的 RSD 隧道无法在 Windows 复用(macOS 实现依赖 XPC +
  ObjC runtime 私有框架),改用 pymobiledevice3 的 userspace 隧道
- 已实测确认的命令与 API(--userspace 挂在 lockdown start-tunnel / mounter
  auto-mount 上,remote start-tunnel 不支持;类名是 UserspaceRsdTunnel)
- 窗口启动、数据目录、打包与分发(DMG → Inno Setup)、图标(.icns → .ico)
- macOS 侧踩过的坑:镜像挂载用 Python API、必须打包包元数据、不要把 distutils
  放进 excludes、AlreadyMountedError 当成功处理
- 设计约束(DVT 长连接、只能改经纬度、last_point 与 last_reported 分离等)
- 验收基线(复用现有测试套件)与建议的工作顺序
2026-10-16 01:05:09 +08:00

209 lines
9.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""真实感定位模型:多尺度 GPS 误差 + 发送节奏抖动 + 运动学速度。
为什么不用 random():真实手机的 GPS 误差是**缓慢游走**的(受卫星几何、多径、电离层影响),
相邻两次定位通常只差几十厘米到一两米,而且有惯性。如果每个点独立随机跳,轨迹会变成毛刺,
速度和航向也会剧烈抖动——这类特征在验收测试里会掩盖真实问题(也有经验的风控一眼能看出)。
这里用**值噪声(value noise)+ 平滑插值 + 多尺度叠加**建模:
- **多尺度误差**:真实 GPS 误差是幂律分布(Allan 方差),不是单一尺度。
这里用 3 个八度叠加:慢漂(相关长度 ~120 米,多径/电离层)+ 中尺度(~30 米,卫星几何)
+ 快抖(~6 米,接收机噪声)。单尺度噪声的统计特征一眼可辨。
- **发送节奏抖动**:真实 GPS 回调不是精确 1.000 秒,而是 0.85~1.15 秒抖动,
偶尔丢一次采样。节奏完全均匀是最容易被识别的特征之一。
- **速度由位移/时间推算**:这与 iOS 从坐标序列推算 speed 的方式一致,
因此"上报速度"与"实际位移"天然自洽,不会出现自相矛盾的数据。
- 纯函数式、无状态、可复现:同样的距离轴/时间轴永远得到同样的噪声值,便于回归测试。
"""
from __future__ import annotations
import math
# 相关长度:多少米走完一个完整的"漂移周期"
DEFAULT_CORRELATION_M = 30.0
# 默认漂移半径(米):手机 GPS 在城市环境下的典型误差量级
DEFAULT_DRIFT_M = 4.0
DEFAULT_DRIFT_PERIOD_S = 18.0
# 默认速度浮动幅度:±8%
DEFAULT_SPEED_JITTER = 0.08
MAX_DRIFT_M = 50.0
MAX_SPEED_JITTER = 0.6
# 多尺度误差的三个八度:(相对权重, 相关长度倍数)
# 慢漂:中尺度:快抖 ≈ 0.5 : 0.33 : 0.17,接近 GPS 误差的幂律谱。
# 快抖的相关长度不能太短:取 0.5×30m = 15m。若取 6~10m,1Hz 下每秒误差变化可达
# 6~8 米,位移推算出的"瞬时速度"会明显超出步行/跑步范围(真实接收机内部对速度做过平滑)。
ERROR_OCTAVES = ((0.5, 4.0), (0.34, 1.0), (0.16, 0.5))
# 叠加后的幅值归一化系数:实测三个尺度加权叠加的 RMS 约为单尺度的 0.55 倍,
# 除以该系数即可让 drift_m 参数与"典型偏移半径"一致(数值由测试标定)。
OCTAVE_NORMALIZER = 1.0 / 0.55
# 单个采样的偏移上限(相对 radius 的倍数):真实 GPS 误差有长尾,但不会无限大
MAX_OFFSET_FACTOR = 1.8
# 发送间隔抖动:真实 1Hz GPS 的实际间隔分布
DEFAULT_TIMING_JITTER = 0.15 # ±15%
DROPOUT_PROBABILITY = 0.02 # 约 2% 的采样会丢一次(变成 2 秒间隔)
MIN_INTERVAL_FACTOR = 0.3 # 抖动下限保护(避免退化成高频)
def _hash01(seed: int, index: int) -> float:
"""把 (seed, index) 映射成 [0,1) 的确定性伪随机数(无需 numpy)。"""
x = (index * 374761393 + seed * 668265263) & 0xFFFFFFFF
x = (x ^ (x >> 13)) * 1274126177 & 0xFFFFFFFF
x = x ^ (x >> 16)
return (x & 0xFFFFFF) / float(0x1000000)
def _smooth(t: float) -> float:
"""smoothstep:让分段插值在节点处一阶连续,避免折角。"""
return t * t * (3.0 - 2.0 * t)
def value_noise(seed: int, x: float) -> float:
"""一维值噪声,返回 [-1, 1],在 x 的整数节点间平滑插值。"""
i0 = math.floor(x)
frac = x - i0
a = _hash01(seed, i0) * 2.0 - 1.0
b = _hash01(seed, i0 + 1) * 2.0 - 1.0
return a + (b - a) * _smooth(frac)
def fbm(seed: int, x: float, octaves: int = 2) -> float:
"""两个八度的分形噪声,归一化到 [-1, 1]。"""
total = 0.0
amplitude = 1.0
frequency = 1.0
norm = 0.0
for octave in range(max(1, octaves)):
total += value_noise(seed + octave * 7919, x * frequency) * amplitude
norm += amplitude
amplitude *= 0.5
frequency *= 2.3
return total / norm if norm else 0.0
class DriftGenerator:
"""按"沿轨迹行进的距离"生成平滑漂移偏移量与速度浮动系数。
- 位置漂移:沿行进方向与垂直方向各一份噪声,合成横向摆动(GPS 误差的常见形态);
- 速度浮动:乘性系数,围绕 1.0 平滑波动,让瞬时速度在设定值上下浮动。
"""
def __init__(self, seed: int = 20261014) -> None:
self.seed = int(seed)
# ---------------- 位置漂移 ----------------
def offset_m(self, distance_m: float, drift_m: float,
correlation_m: float = DEFAULT_CORRELATION_M,
octaves: bool = True) -> tuple[float, float]:
"""返回 (沿行进方向偏移, 垂直方向偏移),单位米。
octaves=True 时叠加 3 个尺度的误差(慢漂 + 中尺度 + 快抖),
更接近真实 GPS 误差的幂律谱;False 时退化为单一尺度(旧行为)。
"""
if drift_m <= 0:
return 0.0, 0.0
scale = max(5.0, correlation_m)
if not octaves:
x = distance_m / scale
along = fbm(self.seed, x) * drift_m
cross = fbm(self.seed + 104729, x + 0.5) * drift_m
return along, cross
along = 0.0
cross = 0.0
for index, (weight, factor) in enumerate(ERROR_OCTAVES):
x = distance_m / (scale * factor)
# 每个尺度用不同种子与相位偏移,避免各尺度同相叠加成规律波形
along += value_noise(self.seed + index * 1013, x) * weight
cross += value_noise(self.seed + 104729 + index * 7919, x + 0.37 * (index + 1)) * weight
# 多尺度叠加后幅值会明显小于单尺度(各尺度并不总是同相),
# 这里归一化,保证 drift_m 参数就是"典型偏移半径",与用户设定一致。
along *= OCTAVE_NORMALIZER
cross *= OCTAVE_NORMALIZER
# 合成偏移做上限裁剪:真实 GPS 误差有长尾,但不会无限大,
# 否则偶发的大偏移会被下游 App 当成"跳点"整段丢弃。
magnitude = math.hypot(along, cross)
if magnitude > MAX_OFFSET_FACTOR:
shrink = MAX_OFFSET_FACTOR / magnitude
along *= shrink
cross *= shrink
return along * drift_m, cross * drift_m
def offset_latlng(self, lat: float, lng: float, distance_m: float,
heading_deg: float, drift_m: float,
correlation_m: float = DEFAULT_CORRELATION_M) -> tuple[float, float]:
"""把米制偏移换算成经纬度偏移(按当前纬度修正经度比例)。"""
along, cross = self.offset_m(distance_m, drift_m, correlation_m)
if along == 0.0 and cross == 0.0:
return 0.0, 0.0
rad = math.radians(heading_deg)
# 行进方向单位向量(x=东, y=北)
fx, fy = math.sin(rad), math.cos(rad)
# 垂直方向(右手侧)
px, py = fy, -fx
east_m = along * fx + cross * px
north_m = along * fy + cross * py
dlat = north_m / 111320.0
cos_lat = math.cos(math.radians(max(-85.0, min(85.0, lat))))
dlng = east_m / (111320.0 * max(0.05, cos_lat))
return dlat, dlng
# ---------------- 速度浮动 ----------------
def speed_factor(self, elapsed_s: float, jitter: float,
period_s: float = DEFAULT_DRIFT_PERIOD_S) -> float:
"""返回速度乘性系数(1.0 表示不浮动)。
对应真实跑步中步频/步幅的自然波动与路况影响:
用 2 个八度的低频噪声(不是正弦,因此没有可识别的周期)。
"""
if jitter <= 0:
return 1.0
x = elapsed_s / max(2.0, period_s)
value = fbm(self.seed + 15485863, x + 3.25)
factor = 1.0 + value * jitter
# 防止极端情况下出现 0 或负速度
return max(0.2, min(2.0, factor))
# ---------------- 发送节奏(真实 GPS 不是精确等间隔) ----------------
def timing_factor(self, sample_index: int, jitter: float = DEFAULT_TIMING_JITTER,
dropouts: bool = True) -> float:
"""返回本次发送间隔的乘性系数。
真实 1Hz GPS 的实际间隔在 0.85~1.15 秒之间抖动,偶发丢采样时变成约 2 秒。
间隔完全均匀(精确 1.000s)在数据上非常"人工",部分验收用例甚至会据此告警。
"""
if jitter <= 0:
return 1.0
# 用噪声而不是 random:连续几次采样的间隔会小幅相关,像真实接收机
value = value_noise(self.seed + 777771, sample_index * 0.35)
factor = 1.0 + value * jitter
# 丢采样是独立随机事件(不能用平滑噪声表达),用哈希伪随机
if dropouts and _hash01(self.seed + 31337, sample_index) < DROPOUT_PROBABILITY:
factor += 1.0 # 丢一次采样 → 间隔约翻倍(真实 GPS 偶发情况)
return max(MIN_INTERVAL_FACTOR, min(2.5, factor))
# ---------------- 粗差(真实 GPS 偶发的坏点) ----------------
def gross_error_m(self, sample_index: int, probability: float = 0.0,
magnitude_m: float = 30.0) -> tuple[float, float]:
"""按概率生成一次"粗差"偏移(多径/城市峡谷导致的坏点)。
默认关闭(probability=0)。开启后可用来测你自己 App 的
`horizontalAccuracy` 过滤与跳点剔除逻辑是否生效。
"""
if probability <= 0:
return 0.0, 0.0
roll = _hash01(self.seed + 5150, sample_index)
if roll >= probability:
return 0.0, 0.0
# 坏点方向随机、幅度在 0.5~1.5 倍之间
angle = _hash01(self.seed + 6161, sample_index) * 2.0 * math.pi
scale = magnitude_m * (0.5 + _hash01(self.seed + 7171, sample_index))
return math.cos(angle) * scale, math.sin(angle) * scale
_generator = DriftGenerator()
def default_generator() -> DriftGenerator:
return _generator