Files
ParkingRobot/docs/superpowers/plans/2026-07-27-planning-operation-budget.md
T

22 KiB
Raw Blame History

规划操作预算与诊断收尾实施计划

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 让一次粗规划调用在建图、距离场、Dijkstra 和 Hybrid A* 阶段共用可取消的总超时预算,并发布真实的 Open List 诊断计数。

Architecture:Utils 新增内部 PlanningOperationBudget,它不依赖 Map 或 CoarsePath,只报告继续、取消、超时。Map 与搜索分别将该中立结果映射为自己的结果;CoarsePathPlanningService 创建唯一预算并传递给下层。原有公开的无预算入口保持兼容,门面使用内部带预算入口。

Tech Stack: C# 10、.NET Standard 2.0、PowerShell 反射回归脚本、StopwatchCancellationToken

Global Constraints

  • 位置使用 m、地图输入使用 mm、航向使用 rad、曲率使用 1/m;不得改变现有单位边界。
  • 取消或超时必须返回空路径和空方向分段,绝不发布部分路径或部分 PlanningGridMap
  • 地图不得依赖 CoarsePath;共享预算只能放在 ParkrobTrajplanner/Utils
  • 每 256 个或更少循环工作单元检查一次预算;外部 IMapObstacleSource.ProjectToWorld() 是调用方提供的同步快照接口,只能在调用前后检查,不能强制抢占其内部执行。
  • 不改变碰撞保守性、运动原语、代价公式、目标候选保护或 Open List 排序。
  • 保留 PlanningMapFactory.Create(request)HybridAStarPlanner.Plan(request, token)HybridAStarSearch.Search(request, token)GridDijkstraHeuristic(map,row,col) 的兼容入口。
  • 不执行 Git 添加、提交、重置或工作区清理。

文件结构

文件 职责
Utils/PlanningOperationBudget.cs 内部单调计时、取消检查和统一停止原因。
Map/PlanningMapBuildResult.cs 地图构建状态:成功、失败、取消、超时。
Map/Core/EnvironmentMapBuilder.csEnvironmentMapBuildResult.cs 将预算传入来源处理与栅格化,并保留停止原因。
Map/Obstacles/MapObstacleRasterizer.cs 在圆形/矩形逐格写入期间定期停止。
Map/Planning/{PlanningMapAdapter,ObstacleDistanceField,EuclideanDistanceTransform}.cs 在占据复制、EDT 和距离换算期间定期停止且不产出快照。
Map/PlanningMapFactory.cs 可取消地等待创建锁、检查缓存、建图、哈希和写缓存。
CoarsePath/Search/{GridDijkstraHeuristic,HybridAStarSearch}.cs 为 Dijkstra 和 Open List 使用共享预算,记录陈旧条目和峰值。
CoarsePath/{HybridAStarPlanner,Contracts/PlanningDiagnostics}.cs 将搜索统计传给最终结果,并让公开 Planner 包装兼容预算。
CoarsePath/Facade/CoarsePathPlanningService.cs 创建一次调用唯一预算,映射地图阶段停止状态。
Map/README.mdCoarsePath/README.md 分别说明地图构建状态,以及粗规划总超时和取消状态。
tests/verify_planning_map_factory.ps1verify_planning_map_adapter.ps1verify_coarse_path_search.ps1verify_coarse_path_integration.ps1 预算与诊断的反射回归覆盖。

Task 1: 建立独立的操作预算与地图终止契约

Files:

  • Create: ClumsyPilot/ParkrobTrajplanner/Utils/PlanningOperationBudget.cs
  • Modify: ClumsyPilot/ParkrobTrajplanner/Map/PlanningMapBuildResult.cs
  • Modify: ClumsyPilot/tests/verify_planning_map_factory.ps1

Consumes: System.Diagnostics.StopwatchSystem.Threading.CancellationToken

Produces: PlanningOperationStopReasonPlanningOperationBudgetPlanningMapBuildStatus;下游 Map/CoarsePath 均只通过这些类型传递停止信息。

  • Step 1: 写失败测试,锁定新的地图状态与预算公开反射形状。

    在现存的 verify_planning_map_factory.ps1 断言存在内部 MultiWheelC.TrajectoryPlanning.Utils.PlanningOperationBudget 与三值 PlanningOperationStopReason,并断言 PlanningMapBuildResult.Status 存在;已取消、已超时结果的 Succeeded 必须为 falseMap$nullCacheHitNone

    $statusType = $assembly.GetType($ns + 'PlanningMapBuildStatus', $true)
    Assert-True ($statusType.GetEnumNames() -contains 'Cancelled') 'Map build status must expose cancellation.'
    Assert-True ($statusType.GetEnumNames() -contains 'TimedOut') 'Map build status must expose timeout.'
    Assert-True ($resultType.GetProperty('Status') -ne $null) 'Map result must expose an explicit status.'
    
  • Step 2: 运行两个脚本,确认因类型或属性不存在而失败。

    Run:

    powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_factory.ps1
    

    Expected: 断言报告 PlanningOperationBudgetPlanningMapBuildStatus 缺失。

  • Step 3: 实现最小共享预算和显式地图状态。

    PlanningOperationBudget 的核心接口固定如下;超时使用构造时启动的单调 Stopwatch,取消优先于超时:

    internal enum PlanningOperationStopReason { None, Cancelled, TimedOut }
    
    internal sealed class PlanningOperationBudget
    {
        internal PlanningOperationBudget(CancellationToken cancellationToken, TimeSpan timeout);
        internal static PlanningOperationBudget Unlimited(CancellationToken cancellationToken);
        internal TimeSpan Elapsed { get; }
        internal PlanningOperationStopReason GetStopReason();
        internal PlanningOperationStopReason CheckEvery(ref int workItemCount);
    }
    

    CheckEvery 在第一次工作单元以及每 256 个工作单元检查;Unlimited 不启用时间限制但仍响应取消。将地图结果从布尔构造改为状态构造:

    public enum PlanningMapBuildStatus { Success, Failed, Cancelled, TimedOut }
    
    public PlanningMapBuildStatus Status { get; }
    public bool Succeeded { get { return Status == PlanningMapBuildStatus.Success; } }
    
    internal static PlanningMapBuildResult Stopped(PlanningOperationStopReason reason,
        IReadOnlyList<ObstacleProjectionResult> sourceResults)
    {
        return new PlanningMapBuildResult(
            reason == PlanningOperationStopReason.Cancelled
                ? PlanningMapBuildStatus.Cancelled
                : PlanningMapBuildStatus.TimedOut,
            reason == PlanningOperationStopReason.Cancelled ? "地图创建已取消。" : "地图创建已超时。",
            sourceResults, PlanningMapCacheHit.None, null);
    }
    
  • Step 4: 重跑两个脚本,确认新契约通过且旧地图缓存断言未回归。

    Run: 与 Step 2 相同。

    Expected: 地图工厂脚本输出 Planning map factory checks passed.

Task 2: 让地图创建、栅格化和距离场遵守预算

Files:

  • Modify: ClumsyPilot/ParkrobTrajplanner/Map/Core/EnvironmentMapBuildResult.cs
  • Modify: ClumsyPilot/ParkrobTrajplanner/Map/Core/EnvironmentMapBuilder.cs
  • Modify: ClumsyPilot/ParkrobTrajplanner/Map/Obstacles/MapObstacleRasterizer.cs
  • Modify: ClumsyPilot/ParkrobTrajplanner/Map/Planning/PlanningMapAdapter.cs
  • Modify: ClumsyPilot/ParkrobTrajplanner/Map/Planning/ObstacleDistanceField.cs
  • Modify: ClumsyPilot/ParkrobTrajplanner/Map/Planning/EuclideanDistanceTransform.cs
  • Modify: ClumsyPilot/ParkrobTrajplanner/Map/PlanningMapFactory.cs
  • Modify: ClumsyPilot/tests/verify_planning_map_factory.ps1
  • Modify: ClumsyPilot/tests/verify_planning_map_adapter.ps1

Consumes: Task 1 的 PlanningOperationBudgetPlanningOperationStopReason

Produces: PlanningMapFactory 的内部 Create(PlanningMapRequest, PlanningOperationBudget);它在取消/超时时返回 PlanningMapBuildResult.Stopped,不会写入 LRU 缓存。

  • Step 1: 写失败测试,覆盖预先取消、EDT 中超时与缓存不污染。

    在工厂脚本创建一个已取消的 CancellationTokenSource,通过反射调用新的内部带预算 Create,断言:

    Assert-Equal 'Cancelled' $cancelledMapResult.Status.ToString() 'Cancelled map construction must report cancellation.'
    Assert-False $cancelledMapResult.Succeeded 'Cancelled map construction must not succeed.'
    Assert-Null $cancelledMapResult.Map 'Cancelled map construction must not publish a map.'
    Assert-Equal 'None' $cancelledMapResult.CacheHit.ToString() 'Stopped construction must not publish a cache hit.'
    

    在适配器脚本为含障碍的大栅格创建 PlanningOperationBudget,使用零超时调用内部 TryCreate,断言返回 TimedOut 且输出 PlanningGridMap$null。随后用无预算入口再次创建相同地图,断言距离场仍可用,以证明停止时没有污染输入或缓存。

  • Step 2: 运行地图工厂和适配器脚本,确认新增反射入口缺失而失败。

    Run:

    powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_factory.ps1
    powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_adapter.ps1
    

    Expected: 新带预算 CreateTryCreate 反射查找失败。

  • Step 3: 在地图管线的全部长循环中传递并检查预算。

    实现以下内部接口,所有 Try* 在停止时返回 false 并把 stopReason 设为非 None;普通参数错误仍按现有失败原因或异常处理。

    internal EnvironmentMapBuildResult Build(MapBuildRequest request, PlanningOperationBudget budget);
    internal static bool TryRasterize(EnvironmentGridMap map, IMapObstacle obstacle,
        PlanningOperationBudget budget, out PlanningOperationStopReason stopReason);
    internal static bool TryCreate(EnvironmentGridMap environmentMap, PlanningOperationBudget budget,
        out PlanningGridMap map, out PlanningOperationStopReason stopReason);
    internal static bool TryCreate(byte[] occupied, int rows, int cols, double resolutionMeters,
        PlanningOperationBudget budget, out ObstacleDistanceField field,
        out PlanningOperationStopReason stopReason);
    internal static bool TryComputeSquaredDistances(byte[] occupied, int rows, int cols,
        PlanningOperationBudget budget, out double[] squared,
        out PlanningOperationStopReason stopReason);
    internal PlanningMapBuildResult Create(PlanningMapRequest request, PlanningOperationBudget budget);
    

    保留现有公开 BuildRasterizePlanningMapAdapter.CreateObstacleDistanceField.CreateComputeSquaredDistances,让它们以 PlanningOperationBudget.Unlimited(CancellationToken.None) 包装新入口。对圆形/矩形逐格循环、EDT 两遍扫描、Transform1D 两个 q 循环、距离场扫描均调用 budget.CheckEvery(ref workItemCount)

    工厂以 Monitor.TryEnter(_createGate, 16) 循环等待创建锁;每次失败后检查预算。拿到锁后立刻再检查预算,随后在缓存读、来源处理、适配、占据哈希和每次缓存写入前检查。将 SHA-256 改为每 4096 字节调用 TransformBlock 的增量哈希,并在块间检查预算。任一非 None 停止原因直接返回 PlanningMapBuildResult.Stopped,且不会执行 _cache.AddOccupancy_cache.AddInput

  • Step 4: 重跑地图脚本,确认普通建图、两级缓存和新停止结果同时通过。

    Run: 与 Step 2 相同。

    Expected: Planning map factory checks passed.Planning map adapter checks passed.

Task 3: 为 Dijkstra 与 Hybrid A* 使用同一预算并记录 Open List 统计

Files:

  • Modify: ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/GridDijkstraHeuristic.cs
  • Modify: ClumsyPilot/ParkrobTrajplanner/CoarsePath/Search/HybridAStarSearch.cs
  • Modify: ClumsyPilot/ParkrobTrajplanner/CoarsePath/HybridAStarPlanner.cs
  • Modify: ClumsyPilot/tests/verify_coarse_path_search.ps1
  • Modify: ClumsyPilot/tests/verify_coarse_path_integration.ps1

Consumes: Task 1 的预算和 Task 2 不可变地图;现有 BinaryMinHeap<int>

Produces: GridDijkstraHeuristic.TryCreate、带共享预算的内部搜索/规划入口,以及 HybridAStarSearchResult.StaleOpenListEntryCountPeakOpenListCount

  • Step 1: 写失败测试,覆盖 Dijkstra 中取消、总超时和统计透传。

    在搜索脚本上创建至少 500×500 格的已就绪空地图,在地图创建完成后启动 CancellationTokenSource.CancelAfter(1) 并调用搜索。断言返回 CancelledSuccessNodeIndex$null、运行时间小于 2 秒。再用同一地图和 SearchTimeout = TimeSpan.Zero 断言 SearchTimeout,以证明启发式创建前即尊重总预算。

    反射断言搜索结果的新属性存在,并让直接路径搜索至少满足:

    Assert-True ($searchResultType.GetProperty('StaleOpenListEntryCount') -ne $null) 'Search result must expose stale Open List entries.'
    Assert-True ($searchResultType.GetProperty('PeakOpenListCount') -ne $null) 'Search result must expose Open List peak size.'
    Assert-True ($searchResult.PeakOpenListCount -ge 1) 'A successful search must retain at least one Open List entry.'
    Assert-True ($searchResult.StaleOpenListEntryCount -ge 0) 'Stale Open List entry count must never be negative.'
    

    在集成脚本断言 PlanningResult.Diagnostics 中的两项值与搜索结果一致;对产生重开/失效条目的固定障碍场景断言陈旧条目数大于零。

  • Step 2: 运行搜索和集成脚本,确认新增属性、带预算入口或中途取消断言失败。

    Run:

    powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_search.ps1
    powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1
    

    Expected: 新属性或 Dijkstra 中止行为缺失导致失败;现有“搜索前取消”检查不应被视为通过中途取消测试。

  • Step 3: 实现预算感知的 Dijkstra、搜索与真实诊断。

    GridDijkstraHeuristic 保留当前公开构造函数,并增加内部可失败构建:

    internal static bool TryCreate(PlanningGridMap map, int goalRow, int goalCol,
        PlanningOperationBudget budget, out GridDijkstraHeuristic heuristic,
        out PlanningOperationStopReason stopReason);
    

    Build 的每次出堆与每 256 个邻居检查预算;停止时不返回部分启发式。HybridAStarSearch.Search(request, token)request.Configuration.SearchTimeout 创建预算作为兼容包装,新增内部:

    internal HybridAStarSearchResult Search(PlanningRequest request, PlanningOperationBudget budget);
    

    删除搜索器内部新建的 StopwatchIsTimedOut,所有原有取消/超时位置改为读取 budget.GetStopReason() 并精确映射为 PlanningStatus.CancelledPlanningStatus.SearchTimeout。Dijkstra 返回停止原因时立即返回对应搜索状态;节点上限仍只在预算检查之后、真正扩展之前检查。

    扩展搜索结果构造函数与只读属性:

    public int StaleOpenListEntryCount { get; }
    public int PeakOpenListCount { get; }
    

    每次 openList.Push 后执行 peakOpenListCount = Math.Max(peakOpenListCount, openList.Count)。普通节点出堆后因 best-G 已更新、节点索引不匹配或已关闭而跳过时递增 staleOpenListEntryCount;目标候选的出队复核失败不算陈旧条目。所有 CreateResult 调用传递两项计数。

    HybridAStarPlanner 的公开 Plan 保持签名并建立自己的预算;新增内部 Plan(request, budget) 供门面调用。诊断使用 budget.Elapsed,并把两个搜索统计填入原本为零的构造参数:

    searchResult == null ? 0 : searchResult.StaleOpenListEntryCount,
    searchResult == null ? 0 : searchResult.PeakOpenListCount,
    
  • Step 4: 重跑搜索和集成脚本,确认取消、超时、目标候选与统计均通过。

    Run: 与 Step 2 相同。

    Expected: Coarse path search primitive checks passed.Coarse path Hybrid A star search checks passed.Coarse path integration checks passed.Coarse path facade checks passed.

Task 4: 让业务门面映射地图阶段终止状态并更新调用文档

Files:

  • Modify: ClumsyPilot/ParkrobTrajplanner/CoarsePath/Facade/CoarsePathPlanningService.cs
  • Modify: ClumsyPilot/ParkrobTrajplanner/Map/README.md
  • Modify: ClumsyPilot/ParkrobTrajplanner/CoarsePath/README.md
  • Modify: ClumsyPilot/tests/verify_coarse_path_integration.ps1

Consumes: Task 1 的地图状态、Task 2 的带预算工厂入口、Task 3 的内部 Planner 入口。

Produces: 一次 CoarsePathPlanningService.Plan 的统一预算和对调用方稳定的 PlanningStatus 映射。

  • Step 1: 写失败测试,锁定门面状态映射和空结果。

    在集成脚本使用已取消 Token 调用门面,断言:

    Assert-Equal 'Cancelled' $facadeResult.MapResult.Status.ToString() 'Facade must retain a cancelled map result.'
    Assert-Equal 'Cancelled' $facadeResult.PlanningResult.Status.ToString() 'Facade must map map-stage cancellation to planning cancellation.'
    Assert-Equal 0 $facadeResult.PlanningResult.Path.Count 'Cancelled facade planning must publish no path.'
    Assert-Equal 0 $facadeResult.PlanningResult.Segments.Count 'Cancelled facade planning must publish no segments.'
    

    SearchTimeout = TimeSpan.Zero 的有效 job,断言地图结果和规划结果分别为 TimedOutSearchTimeout,且调试 sink 不会把已停止操作改写为成功。

  • Step 2: 运行集成脚本,确认当前门面把地图阶段停止误报为 InvalidMap 或继续建图。

    Run:

    powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1
    

    Expected: 地图状态属性或正确的 Cancelled/SearchTimeout 映射不存在。

  • Step 3: 让门面创建并传递唯一预算,随后更新 README。

    门面从有效 job.Configuration.SearchTimeout 创建 PlanningOperationBudget;配置为空或时间值非法时使用无超时预算,让既有 Planner 预检继续返回原有无效配置状态。依次调用:

    PlanningMapBuildResult mapResult = _mapFactory.Create(job == null ? null : job.MapRequest, budget);
    if (!mapResult.Succeeded)
    {
        PlanningStatus status = mapResult.Status == PlanningMapBuildStatus.Cancelled
            ? PlanningStatus.Cancelled
            : mapResult.Status == PlanningMapBuildStatus.TimedOut
                ? PlanningStatus.SearchTimeout
                : PlanningStatus.InvalidMap;
        return PublishDebug(job, mapResult, PlanningResult.Failure(status, diagnostics));
    }
    PlanningResult planningResult = _planner.Plan(request, budget);
    

    Map/README.mdPlanningMapBuildResult 的说明处增加 StatusSuccessFailedCancelledTimedOut;后两种不提供地图也不会进入缓存。CoarsePath/README.md 增加“总预算与取消”小节:SearchTimeout 是从门面开始的总预算,覆盖建图、距离场、Dijkstra 与 Hybrid A*Cancelled/SearchTimeout 一律无路径;不应以 InvalidMap 重试用户主动取消。

  • Step 4: 重跑集成脚本,确认门面状态映射、缓存复用和 debug 旁路隔离均通过。

    Run: 与 Step 2 相同。

    Expected: Coarse path integration checks passed.Coarse path facade checks passed.

Task 5: 全量回归与验收记录

Files:

  • Modify only if a command reveals a concrete regression: the exact responsible source or test file from Tasks 14.

  • Step 1: 执行 Debug 构建。

    Run:

    dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restore
    

    Expected: 0 个警告0 个错误

  • Step 2: 执行全部现行 P0 地图与粗规划回归。

    Run:

    powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_factory.ps1
    powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_adapter.ps1
    powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_image.ps1
    powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_documentation.ps1
    powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_planning_map_test_config.ps1
    powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_collision.ps1
    powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_search.ps1
    powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1
    

    Expected: 每个脚本退出码为 0 并输出 passed

  • Step 3: 对照设计完成验收。

    逐项检查:预先取消和中途 Dijkstra 取消均返回 Cancelled;零总超时返回 SearchTimeout;地图停止不创建快照或缓存条目;正常输入的路径与缓存行为不变;诊断两项不再硬编码为零;README 已说明总预算语义。

自检

  • 规格覆盖:Task 1 定义共享预算和显式地图状态;Task 2 覆盖地图、EDT、锁和缓存;Task 3 覆盖 Dijkstra、Hybrid A*、统计和 PlannerTask 4 覆盖门面映射与文档;Task 5 覆盖完整回归。
  • 类型一致性:所有耗时组件仅接收 PlanningOperationBudget 并输出 PlanningOperationStopReasonMap 使用 PlanningMapBuildStatus,粗规划使用既有 PlanningStatus
  • 范围:不触及 UI、Painter、场景工厂、Release 基准、运动模型或旧 TrapMap 脚本。