feat: 发布 EM 轨迹规划首个版本
This commit is contained in:
+150
-9
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MultiWheelC.TrajectoryPlanning.CoarsePath;
|
||||
@@ -12,6 +13,7 @@ using MultiWheelC.TrajectoryPlanning.PathSmoothing.Facade;
|
||||
|
||||
namespace MultiWheelC.TrajectoryPlanning.TrajectoryObservation;
|
||||
|
||||
/// <summary>一次观察会话启动的不可变结果,成功时同时携带冻结的粗路径、平滑路径、地图和方向段。</summary>
|
||||
public sealed class TrajectoryObservationBootstrapResult
|
||||
{
|
||||
private readonly VehicleParameters vehicle;
|
||||
@@ -87,6 +89,7 @@ public sealed class TrajectoryObservationBootstrapResult
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>只在会话开始阶段构造并冻结规划输入;后续观察 tick 不会重新读取或改写该快照。</summary>
|
||||
public sealed class TrajectoryObservationBootstrapper
|
||||
{
|
||||
private readonly CoarsePathPlanningService coarseService;
|
||||
@@ -109,22 +112,110 @@ public sealed class TrajectoryObservationBootstrapper
|
||||
{
|
||||
if (job == null) throw new ArgumentNullException(nameof(job));
|
||||
|
||||
CoarsePathPlanningJobResult coarse = coarseService.Plan(job, cancellationToken);
|
||||
TimeSpan configuredLimit = job.Configuration == null
|
||||
? TimeSpan.FromSeconds(5d)
|
||||
: job.Configuration.SearchTimeout;
|
||||
using var deadline = new TrajectoryObservationPlanningDeadline(configuredLimit, cancellationToken);
|
||||
return Bootstrap(job, deadline, cancellationToken);
|
||||
}
|
||||
|
||||
public TrajectoryObservationBootstrapResult Bootstrap(CoarsePathPlanningJob job,
|
||||
TrajectoryObservationPlanningDeadline deadline, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (job == null) throw new ArgumentNullException(nameof(job));
|
||||
if (deadline == null) throw new ArgumentNullException(nameof(deadline));
|
||||
if (DeadlineExpired(deadline))
|
||||
return DeadlineFailure(job, null, null, deadline, "coarse");
|
||||
|
||||
CoarsePathPlanningJob effectiveJob = CopyJobWithDeadline(job, deadline);
|
||||
bool coarseUsesCycleRemainder = job.Configuration != null &&
|
||||
effectiveJob.Configuration.SearchTimeout < job.Configuration.SearchTimeout;
|
||||
using var linkedCancellation = CancellationTokenSource.CreateLinkedTokenSource(
|
||||
deadline.Token, cancellationToken);
|
||||
|
||||
CoarsePathPlanningJobResult coarse = coarseService.Plan(effectiveJob, linkedCancellation.Token);
|
||||
if (DeadlineExpired(deadline) ||
|
||||
(coarseUsesCycleRemainder && coarse.PlanningResult.Status == PlanningStatus.SearchTimeout))
|
||||
{
|
||||
return DeadlineFailure(job, coarse, null, deadline, "coarse");
|
||||
}
|
||||
if (coarse.PlanningResult.Status != PlanningStatus.Success)
|
||||
return TrajectoryObservationBootstrapResult.FromFailure(
|
||||
job, coarse, null, "Coarse planning status: " + coarse.PlanningResult.Status);
|
||||
|
||||
if (DeadlineExpired(deadline))
|
||||
return DeadlineFailure(job, coarse, null, deadline, "smoothing");
|
||||
|
||||
var smoothingRequest = new PathSmoothingRequest(
|
||||
CopyFiniteClearance(coarse.PlanningResult.Path, coarse.MapResult.Map),
|
||||
coarse.PlanningResult.Segments, coarse.MapResult.Map, job.Vehicle,
|
||||
new PathSmoothingConfiguration());
|
||||
PathSmoothingResult smooth = smoothingService.Smooth(smoothingRequest, cancellationToken);
|
||||
PathSmoothingResult smooth = smoothingService.Smooth(smoothingRequest, linkedCancellation.Token);
|
||||
if (DeadlineExpired(deadline))
|
||||
return DeadlineFailure(job, coarse, smooth, deadline, "smoothing");
|
||||
if (!IsPublishedSmoothingStatus(smooth.Status))
|
||||
return TrajectoryObservationBootstrapResult.FromFailure(
|
||||
job, coarse, smooth, smooth.Diagnostics.TerminationReason);
|
||||
return TrajectoryObservationBootstrapResult.Success(job, coarse, smooth, ReferencePathSegmenter.Create(smooth));
|
||||
}
|
||||
|
||||
private static bool DeadlineExpired(TrajectoryObservationPlanningDeadline deadline)
|
||||
{
|
||||
return deadline.IsExpired && !deadline.CallerCancellationRequested;
|
||||
}
|
||||
|
||||
private static TrajectoryObservationBootstrapResult DeadlineFailure(CoarsePathPlanningJob job,
|
||||
CoarsePathPlanningJobResult coarse, PathSmoothingResult smooth,
|
||||
TrajectoryObservationPlanningDeadline deadline, string phase)
|
||||
{
|
||||
string diagnostic = "cycleDeadlineExpired=true;phase=" + phase + ";remainingMs=" +
|
||||
deadline.Remaining.TotalMilliseconds.ToString("F3", CultureInfo.InvariantCulture);
|
||||
return TrajectoryObservationBootstrapResult.FromFailure(job, coarse, smooth, diagnostic);
|
||||
}
|
||||
|
||||
private static CoarsePathPlanningJob CopyJobWithDeadline(CoarsePathPlanningJob source,
|
||||
TrajectoryObservationPlanningDeadline deadline)
|
||||
{
|
||||
return new CoarsePathPlanningJob
|
||||
{
|
||||
MapRequest = source.MapRequest,
|
||||
Start = source.Start,
|
||||
Goal = source.Goal,
|
||||
Vehicle = source.Vehicle,
|
||||
Configuration = CopyConfigurationWithDeadline(source.Configuration, deadline),
|
||||
StartVehicleCurvature = source.StartVehicleCurvature,
|
||||
StartDirection = source.StartDirection,
|
||||
GoalDirection = source.GoalDirection,
|
||||
DebugOptions = source.DebugOptions,
|
||||
};
|
||||
}
|
||||
|
||||
private static HybridAStarConfiguration CopyConfigurationWithDeadline(HybridAStarConfiguration source,
|
||||
TrajectoryObservationPlanningDeadline deadline)
|
||||
{
|
||||
if (source == null) return null;
|
||||
return new HybridAStarConfiguration
|
||||
{
|
||||
PrimitiveLengthMeters = source.PrimitiveLengthMeters,
|
||||
IntegrationStepMeters = source.IntegrationStepMeters,
|
||||
MaximumCollisionCheckStepMeters = source.MaximumCollisionCheckStepMeters,
|
||||
HeadingResolutionRadians = source.HeadingResolutionRadians,
|
||||
CurvatureLevelCount = source.CurvatureLevelCount,
|
||||
GoalPositionToleranceMeters = source.GoalPositionToleranceMeters,
|
||||
GoalHeadingToleranceRadians = source.GoalHeadingToleranceRadians,
|
||||
MaximumExpandedNodes = source.MaximumExpandedNodes,
|
||||
SearchTimeout = deadline.Clamp(source.SearchTimeout),
|
||||
HeuristicWeight = source.HeuristicWeight,
|
||||
ReverseCostMultiplier = source.ReverseCostMultiplier,
|
||||
GearSwitchPenaltyMeters = source.GearSwitchPenaltyMeters,
|
||||
CurvatureMagnitudeWeight = source.CurvatureMagnitudeWeight,
|
||||
CurvatureChangePenaltyMetersPerLevel = source.CurvatureChangePenaltyMetersPerLevel,
|
||||
ClearanceCostWeight = source.ClearanceCostWeight,
|
||||
ClearanceCostDistanceMeters = source.ClearanceCostDistanceMeters,
|
||||
AllowReverse = source.AllowReverse,
|
||||
};
|
||||
}
|
||||
|
||||
private static IReadOnlyList<CoarsePathPoint> CopyFiniteClearance(IReadOnlyList<CoarsePathPoint> path,
|
||||
PlanningGridMap map)
|
||||
{
|
||||
@@ -158,6 +249,7 @@ public sealed class TrajectoryObservationBootstrapper
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>单个观察 tick 的只读结果,供状态文本、报告和可视化共同消费。</summary>
|
||||
public sealed class TrajectoryObservationObservation
|
||||
{
|
||||
internal TrajectoryObservationObservation(DateTimeOffset observedAtUtc, VehicleMotionState vehicleState,
|
||||
@@ -185,6 +277,7 @@ public sealed class TrajectoryObservationObservation
|
||||
public TrajectoryExecutionState ExecutorState { get; }
|
||||
}
|
||||
|
||||
/// <summary>协调一次冻结会话中的 EM 规划、轨迹采样和诊断;从不将预测命令发送给车辆。</summary>
|
||||
public sealed class TrajectoryObservationController
|
||||
{
|
||||
private readonly TrajectoryObservationBootstrapResult bootstrap;
|
||||
@@ -272,8 +365,19 @@ public sealed class TrajectoryObservationController
|
||||
|
||||
public async Task<PlanningCycleResult> StartCycle(DateTimeOffset now, VehicleMotionState state,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using var deadline = new TrajectoryObservationPlanningDeadline(
|
||||
TimeSpan.FromSeconds(settings.SolverTimeoutSeconds), cancellationToken);
|
||||
return await StartCycle(now, state, deadline, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<PlanningCycleResult> StartCycle(DateTimeOffset now, VehicleMotionState state,
|
||||
TrajectoryObservationPlanningDeadline deadline, CancellationToken cancellationToken)
|
||||
{
|
||||
if (state == null) throw new ArgumentNullException(nameof(state));
|
||||
if (deadline == null) throw new ArgumentNullException(nameof(deadline));
|
||||
using var callerCancellation = CancellationTokenSource.CreateLinkedTokenSource(
|
||||
deadline.CallerToken, cancellationToken);
|
||||
|
||||
int targetSegmentIndex = ActiveSegment.SegmentIndex;
|
||||
int pendingSegmentIndex = -1;
|
||||
@@ -303,18 +407,53 @@ public sealed class TrajectoryObservationController
|
||||
}
|
||||
|
||||
long currentCycleId = Interlocked.Increment(ref cycleId);
|
||||
EmPlannerConfiguration effectiveConfiguration = configuration.Copy();
|
||||
TimeSpan cycleRemaining = deadline.Remaining;
|
||||
TimeSpan configuredSolverLimit = TimeSpan.FromSeconds(
|
||||
configuration.Scheduling.SolverTimeoutSeconds);
|
||||
if (cycleRemaining > TimeSpan.Zero &&
|
||||
cycleRemaining < configuredSolverLimit - TimeSpan.FromMilliseconds(1d))
|
||||
{
|
||||
effectiveConfiguration.Scheduling.SolverTimeoutSeconds = cycleRemaining.TotalSeconds;
|
||||
}
|
||||
var request = new EmPlanningRequest(
|
||||
bootstrap.SmoothedPath, bootstrap.Map, bootstrap.Vehicle, state, configuration,
|
||||
bootstrap.SmoothedPath, bootstrap.Map, bootstrap.Vehicle, state, effectiveConfiguration,
|
||||
targetSegmentIndex, previousTrajectory, now, now,
|
||||
sessionId + "-trajectory-" + currentCycleId, sessionId + "-reference",
|
||||
previousTrajectory?.Metadata.TrajectoryId ?? string.Empty,
|
||||
EmMotionModel.NonholonomicForwardReverse, settings.PlanningScope);
|
||||
PlanningCycleResult result = await targetCoordinator.PlanLatestAsync(new PlanningCycleInput(request, now), cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (settings.PlanningScope == EmPlanningScope.FullDirectionSegment && result.Published &&
|
||||
result.Result.Trajectory != null && !planningPendingFullDirection)
|
||||
EmMotionModel.NonholonomicForwardReverse, settings.PlanningScope, cycleRemaining,
|
||||
callerCancellation.Token, deadline.DeadlineToken,
|
||||
deadline.PublicationAuthorization);
|
||||
if (callerCancellation.IsCancellationRequested)
|
||||
{
|
||||
activeFullDirectionTrajectory = result.Result.Trajectory;
|
||||
var cancelledResult = new EmPlanningResult(EmPlanningStatus.Cancelled, null,
|
||||
"callerCancellation=true;phase=request");
|
||||
return new PlanningCycleResult(currentCycleId, PlanningCycleIdentity.FromRequest(request),
|
||||
cancelledResult, false, cancelledResult.FailureReason);
|
||||
}
|
||||
if (deadline.DeadlineToken.IsCancellationRequested)
|
||||
{
|
||||
var expiredResult = new EmPlanningResult(EmPlanningStatus.CycleDeadlineExpired, null,
|
||||
"cycleDeadlineExpired=true;phase=request;remainingMs=" +
|
||||
cycleRemaining.TotalMilliseconds.ToString("F3", CultureInfo.InvariantCulture));
|
||||
return new PlanningCycleResult(currentCycleId, PlanningCycleIdentity.FromRequest(request),
|
||||
expiredResult, false, expiredResult.FailureReason);
|
||||
}
|
||||
Action<EmTrajectory> fullDirectionCommit =
|
||||
settings.PlanningScope == EmPlanningScope.FullDirectionSegment && !planningPendingFullDirection
|
||||
? trajectory => activeFullDirectionTrajectory = trajectory
|
||||
: null;
|
||||
PlanningCycleResult result = await targetCoordinator.PlanLatestAsync(
|
||||
new PlanningCycleInput(request, now), callerCancellation.Token,
|
||||
fullDirectionCommit).ConfigureAwait(false);
|
||||
if (!result.Published && deadline.IsExpired && !callerCancellation.IsCancellationRequested &&
|
||||
result.Result.Status == EmPlanningStatus.Cancelled)
|
||||
{
|
||||
var expiredResult = new EmPlanningResult(EmPlanningStatus.CycleDeadlineExpired, null,
|
||||
"cycleDeadlineExpired=true;phase=planning;remainingMs=" +
|
||||
deadline.Remaining.TotalMilliseconds.ToString("F3", CultureInfo.InvariantCulture));
|
||||
result = new PlanningCycleResult(result.Version, result.Identity, expiredResult, false,
|
||||
expiredResult.FailureReason);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -454,6 +593,7 @@ public sealed class TrajectoryObservationLoopTick
|
||||
public bool ShouldLog => true;
|
||||
}
|
||||
|
||||
/// <summary>按观察周期捕获新鲜只读状态并发布快照的循环;规划耗时不能改变观察安全边界。</summary>
|
||||
public sealed class TrajectoryObservationLoop
|
||||
{
|
||||
private readonly TrajectoryObservationController controller;
|
||||
@@ -511,6 +651,7 @@ public enum TrajectoryObservationSessionEndReason
|
||||
Cancellation,
|
||||
}
|
||||
|
||||
/// <summary>集中定义观察会话的开始、停止和故障清理语义,避免留下旧会话可视化。</summary>
|
||||
public static class TrajectoryObservationSessionLifecycle
|
||||
{
|
||||
public static bool ShouldClearLayers(TrajectoryObservationSessionEndReason reason)
|
||||
|
||||
Reference in New Issue
Block a user