Files
ParkingRobot/ClumsyPilot/ParkrobTrajplanner/CoarsePath/HybridAStarPlanner.cs
T

265 lines
15 KiB
C#
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.
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;
/// <summary>
/// 已建 <see cref="PlanningGridMap"/> 上 Hybrid A* 粗路径规划的下层门面。
/// 本类不建图、不读取 UI 或传感器;只有路径经回溯、装配和最终连续复核后才发布成功结果。
/// </summary>
public sealed class HybridAStarPlanner
{
private readonly HybridAStarSearch _search;
private readonly PathBacktracker _backtracker;
private readonly CoarsePathAssembler _assembler;
private readonly CoarsePathValidator _validator;
private readonly FootprintCollisionChecker _collisionChecker;
/// <summary>创建使用默认搜索、回溯、装配和最终复核组件的规划器。</summary>
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));
}
/// <summary>
/// 在请求提供的不可变规划地图上执行一次 Hybrid A* 粗路径规划。
/// 参数:request 的位置单位为 m、航向单位为 rad、曲率单位为 1/mcancellationToken 会在搜索扩展检查点取消。
/// 返回:输入、边界、碰撞、搜索、回溯或最终复核失败均返回空路径;只有 <see cref="PlanningStatus.Success"/> 携带完整路径和分段。
/// </summary>
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);
}
/// <summary>使用门面传入的共享预算执行预检、搜索和最终路径复核。</summary>
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));
}
}