265 lines
15 KiB
C#
265 lines
15 KiB
C#
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/m;cancellationToken 会在搜索扩展检查点取消。
|
||
/// 返回:输入、边界、碰撞、搜索、回溯或最终复核失败均返回空路径;只有 <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));
|
||
}
|
||
}
|