22 KiB
规划操作预算与诊断收尾实施计划
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 反射回归脚本、Stopwatch、CancellationToken。
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.cs、EnvironmentMapBuildResult.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.md、CoarsePath/README.md |
分别说明地图构建状态,以及粗规划总超时和取消状态。 |
tests/verify_planning_map_factory.ps1、verify_planning_map_adapter.ps1、verify_coarse_path_search.ps1、verify_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.Stopwatch、System.Threading.CancellationToken。
Produces: PlanningOperationStopReason、PlanningOperationBudget、PlanningMapBuildStatus;下游 Map/CoarsePath 均只通过这些类型传递停止信息。
-
Step 1: 写失败测试,锁定新的地图状态与预算公开反射形状。
在现存的
verify_planning_map_factory.ps1断言存在内部MultiWheelC.TrajectoryPlanning.Utils.PlanningOperationBudget与三值PlanningOperationStopReason,并断言PlanningMapBuildResult.Status存在;已取消、已超时结果的Succeeded必须为false、Map为$null、CacheHit为None。$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.ps1Expected: 断言报告
PlanningOperationBudget或PlanningMapBuildStatus缺失。 -
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 的 PlanningOperationBudget 与 PlanningOperationStopReason。
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.ps1Expected: 新带预算
Create或TryCreate反射查找失败。 -
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);保留现有公开
Build、Rasterize、PlanningMapAdapter.Create、ObstacleDistanceField.Create与ComputeSquaredDistances,让它们以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.StaleOpenListEntryCount 和 PeakOpenListCount。
-
Step 1: 写失败测试,覆盖 Dijkstra 中取消、总超时和统计透传。
在搜索脚本上创建至少 500×500 格的已就绪空地图,在地图创建完成后启动
CancellationTokenSource.CancelAfter(1)并调用搜索。断言返回Cancelled、SuccessNodeIndex为$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.ps1Expected: 新属性或 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);删除搜索器内部新建的
Stopwatch与IsTimedOut,所有原有取消/超时位置改为读取budget.GetStopReason()并精确映射为PlanningStatus.Cancelled或PlanningStatus.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,断言地图结果和规划结果分别为TimedOut、SearchTimeout,且调试 sink 不会把已停止操作改写为成功。 -
Step 2: 运行集成脚本,确认当前门面把地图阶段停止误报为
InvalidMap或继续建图。Run:
powershell -NoProfile -ExecutionPolicy Bypass -File .\ClumsyPilot\tests\verify_coarse_path_integration.ps1Expected: 地图状态属性或正确的
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.md在PlanningMapBuildResult的说明处增加Status:Success、Failed、Cancelled、TimedOut;后两种不提供地图也不会进入缓存。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 1–4.
-
Step 1: 执行 Debug 构建。
Run:
dotnet build .\ClumsyPilot\ClumsyPilot.csproj --no-restoreExpected:
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.ps1Expected: 每个脚本退出码为 0 并输出
passed。 -
Step 3: 对照设计完成验收。
逐项检查:预先取消和中途 Dijkstra 取消均返回
Cancelled;零总超时返回SearchTimeout;地图停止不创建快照或缓存条目;正常输入的路径与缓存行为不变;诊断两项不再硬编码为零;README 已说明总预算语义。
自检
- 规格覆盖:Task 1 定义共享预算和显式地图状态;Task 2 覆盖地图、EDT、锁和缓存;Task 3 覆盖 Dijkstra、Hybrid A*、统计和 Planner;Task 4 覆盖门面映射与文档;Task 5 覆盖完整回归。
- 类型一致性:所有耗时组件仅接收
PlanningOperationBudget并输出PlanningOperationStopReason;Map 使用PlanningMapBuildStatus,粗规划使用既有PlanningStatus。 - 范围:不触及 UI、Painter、场景工厂、Release 基准、运动模型或旧 TrapMap 脚本。