using System; using System.Diagnostics; using System.Globalization; using System.Threading; using MultiWheelC.TrajectoryPlanning.CoarsePath.Output; using MultiWheelC.TrajectoryPlanning.CoarsePath.Search; using MultiWheelC.TrajectoryPlanning.CoarsePath.Vehicle; using MultiWheelC.TrajectoryPlanning.Mapping; using MultiWheelC.TrajectoryPlanning.Utils; namespace MultiWheelC.TrajectoryPlanning.CoarsePath; /// /// 已建 上 Hybrid A* 粗路径规划的下层门面。 /// 本类不建图、不读取 UI 或传感器;只有路径经回溯、装配和最终连续复核后才发布成功结果。 /// public sealed class HybridAStarPlanner { private readonly HybridAStarSearch _search; private readonly PathBacktracker _backtracker; private readonly CoarsePathAssembler _assembler; private readonly CoarsePathValidator _validator; private readonly FootprintCollisionChecker _collisionChecker; /// 创建使用默认搜索、回溯、装配和最终复核组件的规划器。 public HybridAStarPlanner() : this(new HybridAStarSearch(), new PathBacktracker(), new CoarsePathAssembler(), new CoarsePathValidator(), new FootprintCollisionChecker()) { } internal HybridAStarPlanner(HybridAStarSearch search, PathBacktracker backtracker, CoarsePathAssembler assembler, CoarsePathValidator validator, FootprintCollisionChecker collisionChecker) { _search = search ?? throw new ArgumentNullException(nameof(search)); _backtracker = backtracker ?? throw new ArgumentNullException(nameof(backtracker)); _assembler = assembler ?? throw new ArgumentNullException(nameof(assembler)); _validator = validator ?? throw new ArgumentNullException(nameof(validator)); _collisionChecker = collisionChecker ?? throw new ArgumentNullException(nameof(collisionChecker)); } /// /// 在请求提供的不可变规划地图上执行一次 Hybrid A* 粗路径规划。 /// 参数:request 的位置单位为 m、航向单位为 rad、曲率单位为 1/m;cancellationToken 会在搜索扩展检查点取消。 /// 返回:输入、边界、碰撞、搜索、回溯或最终复核失败均返回空路径;只有 携带完整路径和分段。 /// public PlanningResult Plan(PlanningRequest request, CancellationToken cancellationToken = default(CancellationToken)) { PlanningOperationBudget budget = request != null && request.Configuration != null && request.Configuration.SearchTimeout >= TimeSpan.Zero ? new PlanningOperationBudget(cancellationToken, request.Configuration.SearchTimeout) : PlanningOperationBudget.Unlimited(cancellationToken); return Plan(request, budget); } /// 使用门面传入的共享预算执行预检、搜索和最终路径复核。 internal PlanningResult Plan(PlanningRequest request, PlanningOperationBudget budget) { Stopwatch pathSearchStopwatch = null; try { if (budget == null) throw new ArgumentNullException(nameof(budget)); PlanningOperationStopReason stopReason = budget.GetStopReason(); if (stopReason != PlanningOperationStopReason.None) return Failure(ToPlanningStatus(stopReason), budget, "规划在开始前已停止。", null); PlanningStatus preflightStatus = ValidatePreflight(request, out string preflightReason); if (preflightStatus != PlanningStatus.Success) return Failure(preflightStatus, budget, preflightReason, null); if (!IsFootprintInsideMap(request.Start, request.Map, request.Vehicle)) return Failure(PlanningStatus.StartOutsideMap, budget, "起始扩大车体不完全位于地图边界内。", null); if (!_collisionChecker.IsPoseCollisionFree(request.Start, request.Map, request.Vehicle, 0d, out _)) return Failure(PlanningStatus.StartInCollision, budget, "起始扩大车体与障碍物相交或擦边。", null); if (!IsFootprintInsideMap(request.Goal, request.Map, request.Vehicle)) return Failure(PlanningStatus.GoalOutsideMap, budget, "目标扩大车体不完全位于地图边界内。", null); if (!_collisionChecker.IsPoseCollisionFree(request.Goal, request.Map, request.Vehicle, 0d, out _)) return Failure(PlanningStatus.GoalInCollision, budget, "目标扩大车体与障碍物相交或擦边。", null); stopReason = budget.GetStopReason(); if (stopReason != PlanningOperationStopReason.None) return Failure(ToPlanningStatus(stopReason), budget, "规划在搜索前已停止。", null); pathSearchStopwatch = Stopwatch.StartNew(); HybridAStarSearchResult searchResult = _search.Search(request, budget); if (searchResult == null) return Failure(PlanningStatus.InternalError, budget, "搜索器未返回结果。", null, pathSearchStopwatch); if (searchResult.Status != PlanningStatus.Success) return Failure(searchResult.Status, budget, BuildSearchFailureReason(searchResult, request.Configuration), searchResult, pathSearchStopwatch); if (!_backtracker.TryBacktrack(searchResult, request, out BacktrackedPath backtrackedPath, out string backtrackingReason)) return Failure(PlanningStatus.BacktrackingFailed, budget, backtrackingReason, searchResult, pathSearchStopwatch); if (!_assembler.TryAssemble(backtrackedPath, request, out var path, out var segments, out string assemblyReason)) return Failure(PlanningStatus.FinalValidationFailed, budget, assemblyReason, searchResult, pathSearchStopwatch); if (!_validator.TryValidate(path, segments, request, out double minimumClearanceMeters, out string validationReason)) return Failure(PlanningStatus.FinalValidationFailed, budget, validationReason, searchResult, pathSearchStopwatch); double pathLengthMeters = path[path.Count - 1].ArcLength; return PlanningResult.Success(path, segments, CreateDiagnostics(searchResult, budget.Elapsed, pathLengthMeters, minimumClearanceMeters, string.Empty, pathSearchStopwatch.Elapsed)); } catch (Exception exception) { string reason = "规划内部错误:" + exception.GetType().Name + (string.IsNullOrEmpty(exception.Message) ? "。" : "。" + exception.Message); return Failure(PlanningStatus.InternalError, budget ?? PlanningOperationBudget.Unlimited(CancellationToken.None), reason, null, pathSearchStopwatch); } } private static PlanningStatus ValidatePreflight(PlanningRequest request, out string failureReason) { failureReason = string.Empty; if (request == null || request.Map == null || request.Vehicle == null || request.Configuration == null || !IsFinitePose(request.Start) || !IsFinitePose(request.Goal) || !NumericGuard.IsFinite(request.StartVehicleCurvature) || !IsGoalDirection(request.GoalDirection) || (request.StartDirection.HasValue && !IsTravelDirection(request.StartDirection.Value))) { failureReason = "规划请求缺少必要对象或包含非法数值。"; return PlanningStatus.InvalidRequest; } PlanningGridMap map = request.Map; if (map.Bounds == null || map.Rows <= 0 || map.Cols <= 0 || !NumericGuard.IsPositiveFinite(map.ResolutionMeters)) { failureReason = "规划地图结构无效。"; return PlanningStatus.InvalidMap; } if (!map.PlanningReady) { failureReason = string.IsNullOrEmpty(map.PlanningBlockReason) ? "规划地图尚未就绪。" : map.PlanningBlockReason; return PlanningStatus.MapNotReady; } VehicleParameters vehicle = request.Vehicle; if (!NumericGuard.IsPositiveFinite(vehicle.LengthMeters) || !NumericGuard.IsPositiveFinite(vehicle.WidthMeters) || !NumericGuard.IsFinite(vehicle.SafetyMarginMeters) || vehicle.SafetyMarginMeters < 0d || !VehicleKinematics.TryGetMaximumCurvaturePerMeter(vehicle, out double maximumCurvaturePerMeter)) { failureReason = "车辆尺寸、安全余量或曲率限制无效。"; return PlanningStatus.InvalidVehicleParameters; } HybridAStarConfiguration configuration = request.Configuration; if (!IsValidConfiguration(configuration) || Math.Abs(request.StartVehicleCurvature) > maximumCurvaturePerMeter || (request.StartDirection == TravelDirection.Reverse && !configuration.AllowReverse)) { failureReason = "Hybrid A* 曲率、离散、代价或资源配置无效。"; return PlanningStatus.InvalidCurvatureConfiguration; } return PlanningStatus.Success; } private static bool IsValidConfiguration(HybridAStarConfiguration configuration) { return NumericGuard.IsPositiveFinite(configuration.PrimitiveLengthMeters) && NumericGuard.IsPositiveFinite(configuration.IntegrationStepMeters) && NumericGuard.IsPositiveFinite(configuration.MaximumCollisionCheckStepMeters) && NumericGuard.IsPositiveFinite(configuration.HeadingResolutionRadians) && configuration.HeadingResolutionRadians <= 2d * Math.PI && configuration.CurvatureLevelCount >= 3 && configuration.CurvatureLevelCount % 2 == 1 && NumericGuard.IsFinite(configuration.GoalPositionToleranceMeters) && configuration.GoalPositionToleranceMeters >= 0d && NumericGuard.IsFinite(configuration.GoalHeadingToleranceRadians) && configuration.GoalHeadingToleranceRadians >= 0d && configuration.MaximumExpandedNodes >= 0 && configuration.SearchTimeout >= TimeSpan.Zero && NumericGuard.IsFinite(configuration.HeuristicWeight) && configuration.HeuristicWeight >= 0d && NumericGuard.IsPositiveFinite(configuration.ReverseCostMultiplier) && NumericGuard.IsFinite(configuration.GearSwitchPenaltyMeters) && configuration.GearSwitchPenaltyMeters >= 0d && NumericGuard.IsFinite(configuration.CurvatureMagnitudeWeight) && configuration.CurvatureMagnitudeWeight >= 0d && NumericGuard.IsFinite(configuration.CurvatureChangePenaltyMetersPerLevel) && configuration.CurvatureChangePenaltyMetersPerLevel >= 0d && NumericGuard.IsFinite(configuration.ClearanceCostWeight) && configuration.ClearanceCostWeight >= 0d && NumericGuard.IsPositiveFinite(configuration.ClearanceCostDistanceMeters); } private static bool IsFootprintInsideMap(Pose2D pose, PlanningGridMap map, VehicleParameters vehicle) { if (!map.TryWorldToGrid(pose.X, pose.Y, out _, out _)) return false; double halfLengthMeters = vehicle.LengthMeters / 2d + vehicle.SafetyMarginMeters; double halfWidthMeters = vehicle.WidthMeters / 2d + vehicle.SafetyMarginMeters; double longitudinalX = Math.Cos(pose.Heading); double longitudinalY = Math.Sin(pose.Heading); double lateralX = -longitudinalY; double lateralY = longitudinalX; for (int longitudinalSign = -1; longitudinalSign <= 1; longitudinalSign += 2) for (int lateralSign = -1; lateralSign <= 1; lateralSign += 2) { double cornerX = pose.X + longitudinalSign * halfLengthMeters * longitudinalX + lateralSign * halfWidthMeters * lateralX; double cornerY = pose.Y + longitudinalSign * halfLengthMeters * longitudinalY + lateralSign * halfWidthMeters * lateralY; if (!map.TryWorldToGrid(cornerX, cornerY, out _, out _)) return false; } return true; } private static string BuildSearchFailureReason(HybridAStarSearchResult searchResult, HybridAStarConfiguration configuration) { string reason = string.IsNullOrEmpty(searchResult.TerminationReason) ? "Hybrid A* 搜索以 " + searchResult.Status + " 状态终止。" : searchResult.TerminationReason; string resourceLimit = string.Empty; if (searchResult.Status == PlanningStatus.SearchTimeout) { resourceLimit = "总预算=" + configuration.SearchTimeout.TotalSeconds.ToString( "F3", CultureInfo.InvariantCulture) + "s;"; } else if (searchResult.Status == PlanningStatus.SearchNodeLimitExceeded) { resourceLimit = "节点上限=" + configuration.MaximumExpandedNodes.ToString( CultureInfo.InvariantCulture) + ";"; } return reason + resourceLimit + "扩展=" + searchResult.ExpandedNodeCount.ToString(CultureInfo.InvariantCulture) + "," + "生成=" + searchResult.GeneratedNodeCount.ToString(CultureInfo.InvariantCulture) + "," + "重开=" + searchResult.ReopenedNodeCount.ToString(CultureInfo.InvariantCulture) + "," + "陈旧条目=" + searchResult.StaleOpenListEntryCount.ToString(CultureInfo.InvariantCulture) + "," + "Open List峰值=" + searchResult.PeakOpenListCount.ToString(CultureInfo.InvariantCulture) + "。"; } private static PlanningResult Failure(PlanningStatus status, PlanningOperationBudget budget, string reason, HybridAStarSearchResult searchResult, Stopwatch pathSearchStopwatch = null) { TimeSpan pathSearchElapsed = pathSearchStopwatch == null ? TimeSpan.Zero : pathSearchStopwatch.Elapsed; return PlanningResult.Failure(status, CreateDiagnostics(searchResult, budget.Elapsed, 0d, 0d, reason, pathSearchElapsed)); } private static PlanningDiagnostics CreateDiagnostics(HybridAStarSearchResult searchResult, TimeSpan elapsed, double pathLengthMeters, double minimumClearanceMeters, string reason, TimeSpan pathSearchElapsed) { return new PlanningDiagnostics( searchResult == null ? 0 : searchResult.ExpandedNodeCount, searchResult == null ? 0 : searchResult.GeneratedNodeCount, searchResult == null ? 0 : searchResult.ReopenedNodeCount, searchResult == null ? 0 : searchResult.StaleOpenListEntryCount, searchResult == null ? 0 : searchResult.PeakOpenListCount, pathLengthMeters, minimumClearanceMeters, elapsed, reason, pathSearchElapsed); } private static bool IsFinitePose(Pose2D pose) { return pose != null && NumericGuard.IsFinite(pose.X) && NumericGuard.IsFinite(pose.Y) && NumericGuard.IsFinite(pose.Heading); } private static bool IsTravelDirection(TravelDirection direction) { return direction == TravelDirection.Forward || direction == TravelDirection.Reverse; } private static bool IsGoalDirection(GoalDirectionConstraint direction) { return direction == GoalDirectionConstraint.Any || direction == GoalDirectionConstraint.Forward || direction == GoalDirectionConstraint.Reverse; } private static PlanningStatus ToPlanningStatus(PlanningOperationStopReason stopReason) { if (stopReason == PlanningOperationStopReason.Cancelled) return PlanningStatus.Cancelled; if (stopReason == PlanningOperationStopReason.TimedOut) return PlanningStatus.SearchTimeout; throw new ArgumentOutOfRangeException(nameof(stopReason)); } }