"""对比原始Detour差分与C#在线车辆状态估计结果。"""

from __future__ import annotations

import argparse
from collections import Counter
from dataclasses import dataclass
from pathlib import Path

import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd


REQUIRED_COLUMNS = {
    "ElapsedSeconds",
    "DetourX",
    "DetourY",
    "DetourTheta",
}


def wrap_radians(angle: float | np.ndarray) -> float | np.ndarray:
    """将弧度归一化到[-π, π)区间。"""
    return (angle + np.pi) % (2.0 * np.pi) - np.pi


def angle_difference(target: float, current: float) -> float:
    """计算从当前角到目标角的最短有符号弧度差。"""
    return float(wrap_radians(target - current))


@dataclass(frozen=True)
class Pose:
    """保存世界坐标系中的二维位姿，单位为m和rad。"""

    x: float
    y: float
    yaw: float


@dataclass(frozen=True)
class State:
    """保存脚本复现得到的世界位姿和世界速度。"""

    timestamp: float
    pose: Pose
    vx: float
    vy: float
    omega: float
    velocity_valid: bool


class LowPassFilter:
    """复现FirstOrderLowPassFilter的一阶低通计算。"""

    def __init__(self, time_constant_seconds: float) -> None:
        if not np.isfinite(time_constant_seconds) or time_constant_seconds <= 0:
            raise ValueError("滤波时间常数必须是正有限值。")
        self.time_constant_seconds = float(time_constant_seconds)
        self.initialized = False
        self.value = 0.0

    def update(self, value: float, delta_time_seconds: float) -> float:
        """按照真实采样间隔更新滤波输出。"""
        if not self.initialized:
            self.value = float(value)
            self.initialized = True
            return self.value
        alpha = delta_time_seconds / (
            self.time_constant_seconds + delta_time_seconds
        )
        self.value += alpha * (float(value) - self.value)
        return self.value

    def reset(self) -> None:
        """清除滤波历史。"""
        self.initialized = False
        self.value = 0.0


class VelocityEstimator:
    """复现VelocityEstimator2D的世界速度差分与低通处理。"""

    def __init__(self, linear_tau: float, angular_tau: float) -> None:
        self.vx_filter = LowPassFilter(linear_tau)
        self.vy_filter = LowPassFilter(linear_tau)
        self.omega_filter = LowPassFilter(angular_tau)
        self.previous_pose: Pose | None = None
        self.previous_timestamp = 0.0

    def reset(self, pose: Pose | None = None, timestamp: float = 0.0) -> State | None:
        """清除历史，并可使用当前位姿建立新的零速差分基准。"""
        self.vx_filter.reset()
        self.vy_filter.reset()
        self.omega_filter.reset()
        self.previous_pose = pose
        self.previous_timestamp = float(timestamp)
        if pose is None:
            return None
        return State(timestamp, pose, 0.0, 0.0, 0.0, False)

    def update(self, pose: Pose, timestamp: float) -> State:
        """使用一个新的有效位姿更新速度估计。"""
        if self.previous_pose is None:
            state = self.reset(pose, timestamp)
            assert state is not None
            return state
        delta_time = timestamp - self.previous_timestamp
        if delta_time <= 0.0:
            raise ValueError("新样本时间戳必须严格递增。")
        raw_vx = (pose.x - self.previous_pose.x) / delta_time
        raw_vy = (pose.y - self.previous_pose.y) / delta_time
        raw_omega = angle_difference(
            pose.yaw,
            self.previous_pose.yaw,
        ) / delta_time
        state = State(
            timestamp,
            pose,
            self.vx_filter.update(raw_vx, delta_time),
            self.vy_filter.update(raw_vy, delta_time),
            self.omega_filter.update(raw_omega, delta_time),
            True,
        )
        self.previous_pose = pose
        self.previous_timestamp = timestamp
        return state

    def rebase_preserving_velocity(
        self,
        pose: Pose,
        timestamp: float,
    ) -> State:
        """更新差分基准但保留三个低通滤波器的当前输出。"""
        self.previous_pose = pose
        self.previous_timestamp = timestamp
        velocity_valid = (
            self.vx_filter.initialized
            and self.vy_filter.initialized
            and self.omega_filter.initialized
        )
        return State(
            timestamp,
            pose,
            self.vx_filter.value if velocity_valid else 0.0,
            self.vy_filter.value if velocity_valid else 0.0,
            self.omega_filter.value if velocity_valid else 0.0,
            velocity_valid,
        )


class DetourProviderSimulator:
    """按当前简化版DetourVehicleStateProvider处理离线CSV样本。"""

    def __init__(
        self,
        linear_tau: float = 0.15,
        angular_tau: float = 0.20,
        maximum_linear_speed: float = 1.20,
        maximum_angular_speed: float = np.pi / 4.0,
        position_jump_margin: float = 0.03,
        heading_jump_margin: float = np.deg2rad(5.0),
        stationary_seconds: float = 0.35,
    ) -> None:
        self.estimator = VelocityEstimator(linear_tau, angular_tau)
        self.maximum_linear_speed = maximum_linear_speed
        self.maximum_angular_speed = maximum_angular_speed
        self.position_jump_margin = position_jump_margin
        self.heading_jump_margin = heading_jump_margin
        self.stationary_seconds = stationary_seconds

        self.accepted_pose: Pose | None = None
        self.accepted_timestamp = 0.0
        self.latest_state: State | None = None
        self.stationary_hold = False

    @staticmethod
    def poses_equal(first: Pose, second: Pose) -> bool:
        """判断两次读取是否为Detour保持输出的同一数值帧。"""
        return (
            abs(first.x - second.x) <= 1e-9
            and abs(first.y - second.y) <= 1e-9
            and abs(angle_difference(first.yaw, second.yaw)) <= 1e-8
        )

    def motion_plausible(self, start: Pose, end: Pose, delta_time: float) -> bool:
        """按照车辆绝对运动能力判断两帧是否连续。"""
        if not np.isfinite(delta_time) or delta_time <= 0.0:
            return False
        displacement = np.hypot(end.x - start.x, end.y - start.y)
        heading_change = abs(angle_difference(end.yaw, start.yaw))
        return (
            displacement
            <= self.maximum_linear_speed * delta_time
            + self.position_jump_margin
            and heading_change
            <= self.maximum_angular_speed * delta_time
            + self.heading_jump_margin
        )

    def accept_after_reset(self, pose: Pose, timestamp: float) -> State:
        """接受首帧或确认后的重定位并清除速度历史。"""
        state = self.estimator.reset(pose, timestamp)
        assert state is not None
        self.latest_state = state
        self.accepted_pose = pose
        self.accepted_timestamp = timestamp
        self.stationary_hold = False
        return state

    def accept_continuous(self, pose: Pose, timestamp: float) -> State:
        """接受连续正常位姿并更新速度估计。"""
        state = self.estimator.update(pose, timestamp)
        self.latest_state = state
        self.accepted_pose = pose
        self.accepted_timestamp = timestamp
        self.stationary_hold = False
        return state

    def handle_repeated(self, timestamp: float) -> tuple[State, str]:
        """保留重复帧，并在长期不变后将估计速度归零。"""
        assert self.accepted_pose is not None
        assert self.latest_state is not None
        unchanged = timestamp - self.accepted_timestamp
        if not self.stationary_hold and unchanged >= self.stationary_seconds:
            self.latest_state = State(
                timestamp,
                self.accepted_pose,
                0.0,
                0.0,
                0.0,
                True,
            )
            self.stationary_hold = True
            return self.latest_state, "stationary_zero"
        return self.latest_state, "duplicate"

    def process(
        self,
        pose: Pose,
        timestamp: float,
        velocity_innovation_abnormal: bool = False,
    ) -> tuple[State | None, str]:
        """处理一帧CSV中的Detour读取结果。"""
        if self.accepted_pose is None:
            return self.accept_after_reset(pose, timestamp), "initialized"
        if self.poses_equal(pose, self.accepted_pose):
            return self.handle_repeated(timestamp)
        if self.stationary_hold:
            return self.accept_after_reset(pose, timestamp), "restart_after_stationary"

        elapsed = timestamp - self.accepted_timestamp
        if not self.motion_plausible(self.accepted_pose, pose, elapsed):
            assert self.latest_state is not None
            return self.latest_state, "physical_anomaly"
        if velocity_innovation_abnormal:
            self.latest_state = self.estimator.rebase_preserving_velocity(
                pose,
                timestamp,
            )
            self.accepted_pose = pose
            self.accepted_timestamp = timestamp
            self.stationary_hold = False
            return self.latest_state, "velocity_rebase"
        return self.accept_continuous(pose, timestamp), "accepted"


def segmented_unwrap_degrees(values_radians: np.ndarray) -> np.ndarray:
    """分别展开由NaN分隔的有效航向区间。"""
    result = np.full(values_radians.shape, np.nan, dtype=float)
    finite = np.isfinite(values_radians)
    indices = np.flatnonzero(finite)
    if not indices.size:
        return result
    starts = np.r_[0, np.flatnonzero(np.diff(indices) > 1) + 1]
    ends = np.r_[starts[1:], indices.size]
    for start, end in zip(starts, ends):
        segment_indices = indices[start:end]
        result[segment_indices] = np.rad2deg(
            np.unwrap(values_radians[segment_indices])
        )
    return result


def calculate_naive_derivatives(
    time: np.ndarray,
    x: np.ndarray,
    y: np.ndarray,
    yaw: np.ndarray,
) -> tuple[np.ndarray, np.ndarray]:
    """直接逐记录帧差分，保留重复帧造成的零值和更新尖峰。"""
    speed = np.full(time.shape, np.nan, dtype=float)
    omega = np.full(time.shape, np.nan, dtype=float)
    delta_time = np.diff(time)
    valid = np.isfinite(delta_time) & (delta_time > 0.0)
    delta_x = np.diff(x)
    delta_y = np.diff(y)
    delta_yaw = wrap_radians(np.diff(yaw))
    speed_values = np.full(delta_time.shape, np.nan, dtype=float)
    omega_values = np.full(delta_time.shape, np.nan, dtype=float)
    speed_values[valid] = (
        np.hypot(delta_x[valid], delta_y[valid])
        / delta_time[valid]
    )
    omega_values[valid] = delta_yaw[valid] / delta_time[valid]
    speed[1:] = speed_values
    omega[1:] = omega_values
    return speed, omega


def configure_matplotlib() -> None:
    """配置常见中文字体和负号显示。"""
    plt.rcParams["font.sans-serif"] = [
        "Microsoft YaHei",
        "SimHei",
        "Arial Unicode MS",
        "DejaVu Sans",
    ]
    plt.rcParams["axes.unicode_minus"] = False


def load_csv(csv_path: Path) -> pd.DataFrame:
    """读取并校验状态估计对比所需的CSV字段。"""
    frame = pd.read_csv(csv_path)
    missing = REQUIRED_COLUMNS.difference(frame.columns)
    if missing:
        raise ValueError(
            f"{csv_path.name}缺少字段：{', '.join(sorted(missing))}"
        )
    for column in REQUIRED_COLUMNS:
        frame[column] = pd.to_numeric(frame[column], errors="coerce")
    frame = (
        frame.dropna(subset=list(REQUIRED_COLUMNS))
        .sort_values("ElapsedSeconds")
        .drop_duplicates("ElapsedSeconds", keep="last")
        .reset_index(drop=True)
    )
    if len(frame) < 3:
        raise ValueError(f"{csv_path.name}有效数据不足3行。")
    frame["ElapsedSeconds"] -= frame["ElapsedSeconds"].iloc[0]
    return frame


def detect_visual_anomalies(
    time: np.ndarray,
    x: np.ndarray,
    y: np.ndarray,
    yaw: np.ndarray,
    linear_tau: float,
    angular_tau: float,
    position_residual_meters: float,
    heading_residual_radians: float,
    stationary_seconds: float,
) -> np.ndarray:
    """用恒速预测残差标注可疑跳变，不修改任何状态估计数据。"""
    anomalies = np.zeros(time.shape, dtype=bool)
    estimator = VelocityEstimator(linear_tau, angular_tau)
    previous_pose: Pose | None = None
    previous_update_time = 0.0
    latest_state: State | None = None
    stationary = False

    for index, timestamp in enumerate(time):
        pose = Pose(
            float(x[index]),
            float(y[index]),
            float(wrap_radians(yaw[index])),
        )

        if previous_pose is None:
            latest_state = estimator.reset(pose, float(timestamp))
            previous_pose = pose
            previous_update_time = float(timestamp)
            continue

        if DetourProviderSimulator.poses_equal(pose, previous_pose):
            if (
                not stationary
                and timestamp - previous_update_time >= stationary_seconds
            ):
                stationary = True
            continue

        # 静止后的第一个新定位只重新建立差分基准，避免把起步误标为跳变。
        if stationary:
            latest_state = estimator.reset(pose, float(timestamp))
            previous_pose = pose
            previous_update_time = float(timestamp)
            stationary = False
            continue

        delta_time = float(timestamp) - previous_update_time
        if (
            latest_state is not None
            and latest_state.velocity_valid
            and delta_time > 0.0
        ):
            predicted_x = previous_pose.x + latest_state.vx * delta_time
            predicted_y = previous_pose.y + latest_state.vy * delta_time
            predicted_yaw = float(
                wrap_radians(
                    previous_pose.yaw + latest_state.omega * delta_time
                )
            )
            position_residual = np.hypot(
                pose.x - predicted_x,
                pose.y - predicted_y,
            )
            heading_residual = abs(
                angle_difference(pose.yaw, predicted_yaw)
            )

            if (
                position_residual > position_residual_meters
                or heading_residual > heading_residual_radians
            ):
                anomalies[index] = True
                # 标注后从当前观测重新开始，避免一个跳变引发连续误标。
                latest_state = estimator.rebase_preserving_velocity(
                    pose,
                    float(timestamp),
                )
                previous_pose = pose
                previous_update_time = float(timestamp)
                continue

        latest_state = estimator.update(pose, float(timestamp))
        previous_pose = pose
        previous_update_time = float(timestamp)

    return anomalies


def simulate(
    frame: pd.DataFrame,
    args: argparse.Namespace,
) -> tuple[pd.DataFrame, Counter]:
    """使用当前C#参数处理整份Detour记录。"""
    time = frame["ElapsedSeconds"].to_numpy(float)
    raw_x = frame["DetourX"].to_numpy(float) / 1000.0
    raw_y = frame["DetourY"].to_numpy(float) / 1000.0
    raw_yaw = np.deg2rad(frame["DetourTheta"].to_numpy(float))
    raw_speed, raw_omega = calculate_naive_derivatives(
        time,
        raw_x,
        raw_y,
        raw_yaw,
    )
    visual_anomalies = detect_visual_anomalies(
        time,
        raw_x,
        raw_y,
        raw_yaw,
        args.linear_tau,
        args.angular_tau,
        args.annotation_position_residual_mm / 1000.0,
        np.deg2rad(args.annotation_heading_residual_deg),
        args.stationary_seconds,
    )

    simulator = DetourProviderSimulator(
        linear_tau=args.linear_tau,
        angular_tau=args.angular_tau,
        maximum_linear_speed=args.maximum_linear_speed,
        maximum_angular_speed=np.deg2rad(args.maximum_angular_speed_deg),
        position_jump_margin=args.position_jump_margin_mm / 1000.0,
        heading_jump_margin=np.deg2rad(args.heading_jump_margin_deg),
        stationary_seconds=args.stationary_seconds,
    )

    processed_x = np.full(time.shape, np.nan)
    processed_y = np.full(time.shape, np.nan)
    processed_yaw = np.full(time.shape, np.nan)
    processed_speed = np.full(time.shape, np.nan)
    processed_omega = np.full(time.shape, np.nan)
    events: list[str] = []

    for index, timestamp in enumerate(time):
        pose = Pose(
            raw_x[index],
            raw_y[index],
            float(wrap_radians(raw_yaw[index])),
        )
        state, event = simulator.process(
            pose,
            float(timestamp),
            bool(visual_anomalies[index]),
        )
        events.append(event)
        if state is None:
            continue
        processed_x[index] = state.pose.x
        processed_y[index] = state.pose.y
        processed_yaw[index] = state.pose.yaw
        if state.velocity_valid:
            processed_speed[index] = np.hypot(state.vx, state.vy)
            processed_omega[index] = state.omega

    result = pd.DataFrame(
        {
            "TimeSeconds": time,
            "RawX": raw_x,
            "RawY": raw_y,
            "RawYawRadians": raw_yaw,
            "RawSpeed": raw_speed,
            "RawOmegaRadiansPerSecond": raw_omega,
            "ProcessedX": processed_x,
            "ProcessedY": processed_y,
            "ProcessedYawRadians": processed_yaw,
            "ProcessedSpeed": processed_speed,
            "ProcessedOmegaRadiansPerSecond": processed_omega,
            "VisualAnomaly": visual_anomalies,
            "Event": events,
        }
    )
    counts = Counter(events)
    counts["visual_anomaly"] = int(visual_anomalies.sum())
    return result, counts


def plot_comparison(
    csv_path: Path,
    frame: pd.DataFrame,
    result: pd.DataFrame,
    event_counts: Counter,
    output_directory: str | None,
    show: bool,
) -> Path:
    """生成位置、航向、线速度和角速度处理前后对比图。"""
    time = result["TimeSeconds"].to_numpy(float)
    anomalous = result["VisualAnomaly"].to_numpy(bool)
    raw_yaw_degrees = segmented_unwrap_degrees(
        result["RawYawRadians"].to_numpy(float)
    )
    processed_yaw_degrees = segmented_unwrap_degrees(
        result["ProcessedYawRadians"].to_numpy(float)
    )

    fig, axes = plt.subplots(
        5,
        1,
        figsize=(13.0, 16.0),
        sharex=True,
    )

    series = [
        ("RawX", "ProcessedX", "世界坐标X / m"),
        ("RawY", "ProcessedY", "世界坐标Y / m"),
    ]
    for axis, (raw_name, processed_name, ylabel) in zip(axes[:2], series):
        axis.plot(time, result[raw_name], color="0.65", linewidth=1.0, label="原始Detour")
        axis.plot(time, result[processed_name], color="tab:blue", linewidth=1.5, label="在线处理后")
        axis.scatter(
            time[anomalous],
            result.loc[anomalous, raw_name],
            color="tab:red",
            marker="x",
            s=26,
            label="异常位置",
            zorder=3,
        )
        axis.set_ylabel(ylabel)
        axis.grid(True, alpha=0.3)
        axis.legend(loc="best")

    axes[2].plot(time, raw_yaw_degrees, color="0.65", linewidth=1.0, label="原始Detour")
    axes[2].plot(time, processed_yaw_degrees, color="tab:blue", linewidth=1.5, label="在线处理后")
    axes[2].scatter(
        time[anomalous],
        raw_yaw_degrees[anomalous],
        color="tab:red",
        marker="x",
        s=26,
        label="异常位置",
        zorder=3,
    )
    axes[2].set_ylabel("展开航向角 / deg")
    axes[2].grid(True, alpha=0.3)
    axes[2].legend(loc="best")

    axes[3].plot(time, result["RawSpeed"], color="0.65", linewidth=1.0, label="逐记录帧直接差分")
    axes[3].plot(time, result["ProcessedSpeed"], color="tab:green", linewidth=1.5, label="去重、跳变保护和低通后")
    if "CommandSpeed" in frame.columns:
        command_speed = pd.to_numeric(
            frame["CommandSpeed"], errors="coerce"
        ).to_numpy(float)
        axes[3].plot(time, command_speed, linestyle="--", linewidth=1.0, color="tab:orange", label="记录的命令线速度")
    axes[3].set_ylabel("合线速度 / (m/s)")
    axes[3].grid(True, alpha=0.3)
    axes[3].legend(loc="best")

    axes[4].plot(
        time,
        np.rad2deg(result["RawOmegaRadiansPerSecond"]),
        color="0.65",
        linewidth=1.0,
        label="逐记录帧最短角差",
    )
    axes[4].plot(
        time,
        np.rad2deg(result["ProcessedOmegaRadiansPerSecond"]),
        color="tab:purple",
        linewidth=1.5,
        label="去重、跳变保护和低通后",
    )
    if "CommandAngularSpeedRadPerSecond" in frame.columns:
        command_omega = np.rad2deg(
            pd.to_numeric(
                frame["CommandAngularSpeedRadPerSecond"],
                errors="coerce",
            ).to_numpy(float)
        )
        axes[4].plot(time, command_omega, linestyle="--", linewidth=1.0, color="tab:orange", label="记录的命令角速度")
    elif "CommandAngularSpeed" in frame.columns:
        # 旧版CSV只有CommandAngularSpeed列，该列历史单位是deg/s；
        # 新版CSV另增RadPerSecond列，不能把旧列再次按rad/s换算。
        command_omega = pd.to_numeric(
            frame["CommandAngularSpeed"],
            errors="coerce",
        ).to_numpy(float)
        axes[4].plot(time, command_omega, linestyle="--", linewidth=1.0, color="tab:orange", label="记录的命令角速度")
    axes[4].set_ylabel("角速度 / (deg/s)")
    axes[4].set_xlabel("时间 / s")
    axes[4].grid(True, alpha=0.3)
    axes[4].legend(loc="best")

    controller = (
        str(frame["ControllerName"].iloc[0])
        if "ControllerName" in frame.columns
        else "UnknownController"
    )
    trajectory = (
        str(frame["TrajectoryName"].iloc[0])
        if "TrajectoryName" in frame.columns
        else csv_path.stem
    )
    anomaly_count = int(anomalous.sum())
    fig.suptitle(
        "Detour状态估计处理前后对比\n"
        f"{controller} - {trajectory}，"
        f"标注异常位置{anomaly_count}帧",
        fontsize=14,
    )
    fig.tight_layout(rect=(0.0, 0.0, 1.0, 0.965))

    if output_directory:
        destination_directory = Path(output_directory)
    else:
        destination_directory = csv_path.parent / "state_estimation_plots"
    destination_directory.mkdir(parents=True, exist_ok=True)
    destination = destination_directory / (
        csv_path.stem + "_state_estimation_comparison.png"
    )
    fig.savefig(destination, dpi=220, bbox_inches="tight")
    if show:
        plt.show()
    plt.close(fig)
    return destination


def discover_csv_files(arguments: list[str]) -> list[Path]:
    """解析文件或目录；目录会被递归展开为全部轨迹CSV。"""
    input_paths = (
        [Path(argument).resolve() for argument in arguments]
        if arguments
        else [Path(__file__).resolve().parent]
    )
    csv_files: set[Path] = set()

    for input_path in input_paths:
        if input_path.is_file():
            if input_path.suffix.lower() == ".csv":
                csv_files.add(input_path)
            continue

        if input_path.is_dir():
            csv_files.update(
                path.resolve()
                for path in input_path.rglob("*.csv")
                if not any(
                    part.startswith("state_estimation_plots")
                    for part in path.parts
                )
            )
            continue

        raise FileNotFoundError(
            f"输入文件或目录不存在：{input_path}"
        )

    return sorted(csv_files)


def main() -> None:
    """解析命令行并批量生成Detour状态估计对比图。"""
    configure_matplotlib()
    parser = argparse.ArgumentParser(
        description="比较原始Detour与当前C#在线状态估计算法。"
    )
    parser.add_argument(
        "files",
        nargs="*",
        help="一个或多个轨迹实验CSV或包含CSV的目录",
    )
    parser.add_argument("--output-dir")
    parser.add_argument("--show", action="store_true")
    parser.add_argument("--linear-tau", type=float, default=0.15)
    parser.add_argument("--angular-tau", type=float, default=0.20)
    parser.add_argument("--maximum-linear-speed", type=float, default=1.20)
    parser.add_argument("--maximum-angular-speed-deg", type=float, default=45.0)
    parser.add_argument("--position-jump-margin-mm", type=float, default=30.0)
    parser.add_argument("--heading-jump-margin-deg", type=float, default=5.0)
    parser.add_argument(
        "--annotation-position-residual-mm",
        type=float,
        default=40.0,
        help="只用于图中红色异常位置标注的预测位置残差阈值",
    )
    parser.add_argument(
        "--annotation-heading-residual-deg",
        type=float,
        default=5.0,
        help="只用于图中红色异常位置标注的预测航向残差阈值",
    )
    parser.add_argument("--stationary-seconds", type=float, default=0.35)
    args = parser.parse_args()

    processed_count = 0
    for csv_path in discover_csv_files(args.files):
        try:
            frame = load_csv(csv_path)
            result, counts = simulate(frame, args)
            destination = plot_comparison(
                csv_path,
                frame,
                result,
                counts,
                args.output_dir,
                args.show,
            )
            print(
                f"{csv_path.name}: "
                f"重复帧={counts['duplicate']}，"
                f"异常位置={counts['visual_anomaly']}"
            )
            print(f"已生成：{destination}")
            processed_count += 1
        except Exception as exception:
            print(f"跳过{csv_path.name}：{exception}")

    if processed_count == 0:
        raise SystemExit("没有找到包含有效Detour字段的轨迹实验CSV。")


if __name__ == "__main__":
    main()
