fix: stabilize stopped-start EM planning and add txt diagnostics
This commit is contained in:
@@ -96,9 +96,9 @@ public sealed partial class EmPlannerConfiguration
|
|||||||
Validation = new ValidationConfiguration
|
Validation = new ValidationConfiguration
|
||||||
{
|
{
|
||||||
SpatialToleranceMeters = 1e-8d,
|
SpatialToleranceMeters = 1e-8d,
|
||||||
KinematicTolerance = 1e-8d,
|
KinematicTolerance = 1e-5d,
|
||||||
TerminalPositionToleranceMeters = 0.03d,
|
TerminalPositionToleranceMeters = 0d,
|
||||||
TerminalYawToleranceRadians = 5d * System.Math.PI / 180d,
|
TerminalYawToleranceRadians = 0d,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+58
-20
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Globalization;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using MultiWheelC.TrajectoryPlanning.CoarsePath;
|
using MultiWheelC.TrajectoryPlanning.CoarsePath;
|
||||||
|
|
||||||
@@ -38,6 +39,21 @@ public sealed class SequentialLongitudinalOptimizer
|
|||||||
return Failed(EmPlanningStatus.InvalidInput, "Longitudinal planning input is required.");
|
return Failed(EmPlanningStatus.InvalidInput, "Longitudinal planning input is required.");
|
||||||
if (cancellationToken.IsCancellationRequested)
|
if (cancellationToken.IsCancellationRequested)
|
||||||
return Failed(EmPlanningStatus.Cancelled, "Longitudinal optimization was cancelled.");
|
return Failed(EmPlanningStatus.Cancelled, "Longitudinal optimization was cancelled.");
|
||||||
|
if (input.PlanningScope == EmPlanningScope.FullDirectionSegment &&
|
||||||
|
input.Mode == EmLongitudinalMode.ExactStopAtBoundary)
|
||||||
|
{
|
||||||
|
double staticStartSpeedTolerance = Math.Max(input.Configuration.Validation.SpatialToleranceMeters,
|
||||||
|
input.Configuration.Longitudinal.StopSpeedToleranceMetersPerSecond);
|
||||||
|
if (input.InitialProgressSpeedMetersPerSecond <= staticStartSpeedTolerance &&
|
||||||
|
Math.Abs(input.InitialAccelerationMetersPerSecondSquared) <=
|
||||||
|
input.Configuration.Validation.KinematicTolerance)
|
||||||
|
{
|
||||||
|
input = new LongitudinalPlanningInput(input.Path, input.Direction, 0d,
|
||||||
|
input.InitialAccelerationMetersPerSecondSquared, input.TerminalType, input.Mode,
|
||||||
|
input.Configuration, input.PlanningScope, input.KnotSchedule,
|
||||||
|
input.PreviousPathS, input.PreviousProgressSpeedMetersPerSecond);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!TryCreateSettings(input, out QpSolverSettings settings, out TimeSpan totalBudget, out double convergenceTolerance,
|
if (!TryCreateSettings(input, out QpSolverSettings settings, out TimeSpan totalBudget, out double convergenceTolerance,
|
||||||
out int iterationLimit, out string configurationFailure))
|
out int iterationLimit, out string configurationFailure))
|
||||||
{
|
{
|
||||||
@@ -223,14 +239,27 @@ public sealed class SequentialLongitudinalOptimizer
|
|||||||
projectionSolveCount = 0;
|
projectionSolveCount = 0;
|
||||||
failureStatus = EmPlanningStatus.LongitudinalInfeasible;
|
failureStatus = EmPlanningStatus.LongitudinalInfeasible;
|
||||||
failureReason = string.Empty;
|
failureReason = string.Empty;
|
||||||
if (input.InitialProgressSpeedMetersPerSecond <= input.Configuration.Validation.SpatialToleranceMeters &&
|
double staticStartSpeedTolerance = Math.Max(input.Configuration.Validation.SpatialToleranceMeters,
|
||||||
|
input.Configuration.Longitudinal.StopSpeedToleranceMetersPerSecond);
|
||||||
|
bool staticStartEligible = input.InitialProgressSpeedMetersPerSecond <= staticStartSpeedTolerance &&
|
||||||
Math.Abs(input.InitialAccelerationMetersPerSecondSquared) <=
|
Math.Abs(input.InitialAccelerationMetersPerSecondSquared) <=
|
||||||
input.Configuration.Validation.KinematicTolerance &&
|
input.Configuration.Validation.KinematicTolerance;
|
||||||
TryCreateStaticStartSeed(input, speedLimit, out LongitudinalCandidate staticStartSeed))
|
bool staticStartSeedUsed = false;
|
||||||
|
string staticStartSeedFailure = string.Empty;
|
||||||
|
if (staticStartEligible && TryCreateStaticStartSeed(input, speedLimit, out LongitudinalCandidate staticStartSeed,
|
||||||
|
out staticStartSeedFailure))
|
||||||
{
|
{
|
||||||
|
staticStartSeedUsed = true;
|
||||||
candidate = staticStartSeed;
|
candidate = staticStartSeed;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
string WithStaticSeedDiagnostic(string reason)
|
||||||
|
{
|
||||||
|
return staticStartEligible && !staticStartSeedUsed
|
||||||
|
? "staticStartSeed=failed (" + staticStartSeedFailure + "); " + reason
|
||||||
|
: reason;
|
||||||
|
}
|
||||||
|
|
||||||
LongitudinalCandidate linearizationIterate = CreateScheduleReferenceIterate(input);
|
LongitudinalCandidate linearizationIterate = CreateScheduleReferenceIterate(input);
|
||||||
string lastRejection = string.Empty;
|
string lastRejection = string.Empty;
|
||||||
for (int iteration = 0; iteration < iterationLimit; iteration++)
|
for (int iteration = 0; iteration < iterationLimit; iteration++)
|
||||||
@@ -238,21 +267,21 @@ public sealed class SequentialLongitudinalOptimizer
|
|||||||
if (cancellationToken.IsCancellationRequested)
|
if (cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
failureStatus = EmPlanningStatus.Cancelled;
|
failureStatus = EmPlanningStatus.Cancelled;
|
||||||
failureReason = "Initial full-direction feasibility projection was cancelled.";
|
failureReason = WithStaticSeedDiagnostic("Initial full-direction feasibility projection was cancelled.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
TimeSpan remainingBudget = totalBudget - stopwatch.Elapsed;
|
TimeSpan remainingBudget = totalBudget - stopwatch.Elapsed;
|
||||||
if (remainingBudget <= TimeSpan.Zero)
|
if (remainingBudget <= TimeSpan.Zero)
|
||||||
{
|
{
|
||||||
failureStatus = EmPlanningStatus.SolverTimedOut;
|
failureStatus = EmPlanningStatus.SolverTimedOut;
|
||||||
failureReason = "Initial full-direction feasibility projection exhausted the shared solve budget.";
|
failureReason = WithStaticSeedDiagnostic("Initial full-direction feasibility projection exhausted the shared solve budget.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!_constraintBuilder.TryBuildInitialFeasibilityProjection(input, speedLimit, linearizationIterate,
|
if (!_constraintBuilder.TryBuildInitialFeasibilityProjection(input, speedLimit, linearizationIterate,
|
||||||
out QuadraticProgram problem, out string buildFailure))
|
out QuadraticProgram problem, out string buildFailure))
|
||||||
{
|
{
|
||||||
failureStatus = EmPlanningStatus.LongitudinalInfeasible;
|
failureStatus = EmPlanningStatus.LongitudinalInfeasible;
|
||||||
failureReason = "Initial full-direction feasibility constraints are infeasible: " + buildFailure;
|
failureReason = WithStaticSeedDiagnostic("Initial full-direction feasibility constraints are infeasible: " + buildFailure);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,45 +296,45 @@ public sealed class SequentialLongitudinalOptimizer
|
|||||||
if (solved == null)
|
if (solved == null)
|
||||||
{
|
{
|
||||||
failureStatus = EmPlanningStatus.Failed;
|
failureStatus = EmPlanningStatus.Failed;
|
||||||
failureReason = "The initial full-direction feasibility solver returned no result.";
|
failureReason = WithStaticSeedDiagnostic("The initial full-direction feasibility solver returned no result.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (solved.Status == QpSolveStatus.TimeLimit || solved.Status == QpSolveStatus.MaximumIterations)
|
if (solved.Status == QpSolveStatus.TimeLimit || solved.Status == QpSolveStatus.MaximumIterations)
|
||||||
{
|
{
|
||||||
failureStatus = EmPlanningStatus.SolverTimedOut;
|
failureStatus = EmPlanningStatus.SolverTimedOut;
|
||||||
failureReason = "Initial full-direction feasibility projection timed out (status=" + solved.NativeStatus +
|
failureReason = WithStaticSeedDiagnostic("Initial full-direction feasibility projection timed out (status=" + solved.NativeStatus +
|
||||||
", iterations=" + solved.Iterations + ", primal=" + solved.PrimalResidual + ", dual=" +
|
", iterations=" + solved.Iterations + ", primal=" + solved.PrimalResidual + ", dual=" +
|
||||||
solved.DualResidual + "): " + solved.Diagnostic;
|
solved.DualResidual + "): " + solved.Diagnostic);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (solved.Status == QpSolveStatus.Cancelled)
|
if (solved.Status == QpSolveStatus.Cancelled)
|
||||||
{
|
{
|
||||||
failureStatus = EmPlanningStatus.Cancelled;
|
failureStatus = EmPlanningStatus.Cancelled;
|
||||||
failureReason = "Initial full-direction feasibility projection was cancelled: " + solved.Diagnostic;
|
failureReason = WithStaticSeedDiagnostic("Initial full-direction feasibility projection was cancelled: " + solved.Diagnostic);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (solved.Status == QpSolveStatus.PrimalInfeasible || solved.Status == QpSolveStatus.DualInfeasible)
|
if (solved.Status == QpSolveStatus.PrimalInfeasible || solved.Status == QpSolveStatus.DualInfeasible)
|
||||||
{
|
{
|
||||||
failureStatus = EmPlanningStatus.LongitudinalInfeasible;
|
failureStatus = EmPlanningStatus.LongitudinalInfeasible;
|
||||||
failureReason = "Initial full-direction feasibility projection is infeasible: " + solved.Diagnostic;
|
failureReason = WithStaticSeedDiagnostic("Initial full-direction feasibility projection is infeasible: " + solved.Diagnostic);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (solved.Status == QpSolveStatus.SolverUnavailable)
|
if (solved.Status == QpSolveStatus.SolverUnavailable)
|
||||||
{
|
{
|
||||||
failureStatus = EmPlanningStatus.SolverUnavailable;
|
failureStatus = EmPlanningStatus.SolverUnavailable;
|
||||||
failureReason = "Initial full-direction feasibility solver is unavailable: " + solved.Diagnostic;
|
failureReason = WithStaticSeedDiagnostic("Initial full-direction feasibility solver is unavailable: " + solved.Diagnostic);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (solved.Status != QpSolveStatus.Solved && solved.Status != QpSolveStatus.SolvedInaccurate)
|
if (solved.Status != QpSolveStatus.Solved && solved.Status != QpSolveStatus.SolvedInaccurate)
|
||||||
{
|
{
|
||||||
failureStatus = EmPlanningStatus.Failed;
|
failureStatus = EmPlanningStatus.Failed;
|
||||||
failureReason = "Initial full-direction feasibility solver failed: " + solved.Diagnostic;
|
failureReason = WithStaticSeedDiagnostic("Initial full-direction feasibility solver failed: " + solved.Diagnostic);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!TryCreateCandidate(input.KnotSchedule.KnotTimes, solved.Primal, out LongitudinalCandidate projected))
|
if (!TryCreateCandidate(input.KnotSchedule.KnotTimes, solved.Primal, out LongitudinalCandidate projected))
|
||||||
{
|
{
|
||||||
failureStatus = EmPlanningStatus.LongitudinalInfeasible;
|
failureStatus = EmPlanningStatus.LongitudinalInfeasible;
|
||||||
failureReason = "Initial full-direction feasibility solver primal does not match the ST layout.";
|
failureReason = WithStaticSeedDiagnostic("Initial full-direction feasibility solver primal does not match the ST layout.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (solved.Status == QpSolveStatus.Solved || HasStrictResiduals(solved, convergenceTolerance))
|
if (solved.Status == QpSolveStatus.Solved || HasStrictResiduals(solved, convergenceTolerance))
|
||||||
@@ -319,7 +348,7 @@ public sealed class SequentialLongitudinalOptimizer
|
|||||||
if (validationStatus == EmPlanningStatus.NoProgress)
|
if (validationStatus == EmPlanningStatus.NoProgress)
|
||||||
{
|
{
|
||||||
failureStatus = validationStatus;
|
failureStatus = validationStatus;
|
||||||
failureReason = validationFailure;
|
failureReason = WithStaticSeedDiagnostic(validationFailure);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
lastRejection = validationFailure;
|
lastRejection = validationFailure;
|
||||||
@@ -329,7 +358,7 @@ public sealed class SequentialLongitudinalOptimizer
|
|||||||
out LongitudinalCandidate nextLinearization))
|
out LongitudinalCandidate nextLinearization))
|
||||||
{
|
{
|
||||||
failureStatus = EmPlanningStatus.LongitudinalInfeasible;
|
failureStatus = EmPlanningStatus.LongitudinalInfeasible;
|
||||||
failureReason = "Initial full-direction feasibility candidate could not be relinearized against the PathS envelope.";
|
failureReason = WithStaticSeedDiagnostic("Initial full-direction feasibility candidate could not be relinearized against the PathS envelope.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
linearizationIterate = nextLinearization;
|
linearizationIterate = nextLinearization;
|
||||||
@@ -339,8 +368,8 @@ public sealed class SequentialLongitudinalOptimizer
|
|||||||
lastRejection = "Initial feasibility projection violated the strict physical validator.";
|
lastRejection = "Initial feasibility projection violated the strict physical validator.";
|
||||||
}
|
}
|
||||||
failureStatus = EmPlanningStatus.LongitudinalInfeasible;
|
failureStatus = EmPlanningStatus.LongitudinalInfeasible;
|
||||||
failureReason = "Initial full-direction feasibility projection exhausted the configured outer iterations. " +
|
failureReason = WithStaticSeedDiagnostic("Initial full-direction feasibility projection exhausted the configured outer iterations. " +
|
||||||
lastRejection;
|
lastRejection);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -466,24 +495,31 @@ public sealed class SequentialLongitudinalOptimizer
|
|||||||
}
|
}
|
||||||
|
|
||||||
private bool TryCreateStaticStartSeed(LongitudinalPlanningInput input, PathSpeedLimit speedLimit,
|
private bool TryCreateStaticStartSeed(LongitudinalPlanningInput input, PathSpeedLimit speedLimit,
|
||||||
out LongitudinalCandidate candidate)
|
out LongitudinalCandidate candidate, out string failureReason)
|
||||||
{
|
{
|
||||||
candidate = null;
|
candidate = null;
|
||||||
|
failureReason = "unknown";
|
||||||
int stabilizationStart = input.KnotSchedule.TerminalHoldStartIndex;
|
int stabilizationStart = input.KnotSchedule.TerminalHoldStartIndex;
|
||||||
if (stabilizationStart < 5)
|
if (stabilizationStart < 5)
|
||||||
|
{
|
||||||
|
failureReason = "terminalHoldStartIndex=" + stabilizationStart.ToString(CultureInfo.InvariantCulture);
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
IReadOnlyList<double> times = input.KnotSchedule.KnotTimes;
|
IReadOnlyList<double> times = input.KnotSchedule.KnotTimes;
|
||||||
if (TryCreateExactJerkSeed(input, times, stabilizationStart, speedLimit, out candidate))
|
if (TryCreateExactJerkSeed(input, times, stabilizationStart, speedLimit, out candidate))
|
||||||
return true;
|
return true;
|
||||||
|
failureReason = "exactJerkSeed=failed";
|
||||||
double firstDuration = times[1] - times[0];
|
double firstDuration = times[1] - times[0];
|
||||||
double secondDuration = times[2] - times[1];
|
double secondDuration = times[2] - times[1];
|
||||||
LongitudinalConfiguration configuration = input.Configuration.Longitudinal;
|
LongitudinalConfiguration configuration = input.Configuration.Longitudinal;
|
||||||
double maximumFirstJerk = Math.Min(configuration.MaximumJerkMetersPerSecondCubed,
|
double maximumFirstJerk = Math.Min(configuration.MaximumJerkMetersPerSecondCubed,
|
||||||
Math.Min(configuration.MaximumAccelerationMetersPerSecondSquared / firstDuration,
|
Math.Min(configuration.MaximumAccelerationMetersPerSecondSquared / firstDuration,
|
||||||
configuration.MaximumJerkMetersPerSecondCubed * secondDuration / firstDuration));
|
configuration.MaximumJerkMetersPerSecondCubed * secondDuration / firstDuration));
|
||||||
for (int sample = 1; sample <= 256; sample++)
|
for (int sample = -256; sample <= 256; sample++)
|
||||||
{
|
{
|
||||||
|
if (sample == 0)
|
||||||
|
continue;
|
||||||
double firstJerk = maximumFirstJerk * sample / 256d;
|
double firstJerk = maximumFirstJerk * sample / 256d;
|
||||||
var jerk = new double[stabilizationStart];
|
var jerk = new double[stabilizationStart];
|
||||||
jerk[0] = firstJerk;
|
jerk[0] = firstJerk;
|
||||||
@@ -497,9 +533,11 @@ public sealed class SequentialLongitudinalOptimizer
|
|||||||
out LongitudinalCandidate strict, out _))
|
out LongitudinalCandidate strict, out _))
|
||||||
{
|
{
|
||||||
candidate = strict;
|
candidate = strict;
|
||||||
|
failureReason = string.Empty;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
failureReason = "exactJerkSeed=failed; sampledSeeds=failed";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -94,8 +94,8 @@ LS 的变量是 `l`、`dl`、`ddl` 和区间 `dddl`;它对静态走廊、导
|
|||||||
| 静态走廊、LS、ST 或停车条件不可行 | `CorridorInfeasible`、`LateralInfeasible`、`LongitudinalInfeasible`、`StoppingDistanceInsufficient` | 空 | 不发布;由上层决定重试、停车或重新选路 |
|
| 静态走廊、LS、ST 或停车条件不可行 | `CorridorInfeasible`、`LateralInfeasible`、`LongitudinalInfeasible`、`StoppingDistanceInsufficient` | 空 | 不发布;由上层决定重试、停车或重新选路 |
|
||||||
| 求解器不可用、超时、取消或发布复核失败 | `SolverUnavailable`、`SolverTimedOut`、`Cancelled`、`ValidationFailed`、`Failed` | 空 | 读取 `FailureReason`;不得使用中间轨迹 |
|
| 求解器不可用、超时、取消或发布复核失败 | `SolverUnavailable`、`SolverTimedOut`、`Cancelled`、`ValidationFailed`、`Failed` | 空 | 读取 `FailureReason`;不得使用中间轨迹 |
|
||||||
| 已被更新周期替代 | `Superseded` | 空 | 由滚动协调器忽略旧结果 |
|
| 已被更新周期替代 | `Superseded` | 空 | 由滚动协调器忽略旧结果 |
|
||||||
| 完整方向段无有效进度 | `NoProgress` | 空 | 不发布;仅真实终点容差内或受监督停车保持观察可合法零进度 |
|
| 完整方向段无有效进度 | `NoProgress` | 空 | 不发布;仅真实终点精确匹配或受监督停车保持观察可合法零进度 |
|
||||||
| 完整方向段终端位姿超差 | `TerminalPoseMismatch` | 空 | 不发布;位置误差超过 `0.03 m` 或归一化 yaw 误差超过 `5°` |
|
| 完整方向段终端位姿超差 | `TerminalPoseMismatch` | 空 | 不发布;位置误差超过 `0 m` 或归一化 yaw 误差超过 `0 rad` |
|
||||||
| 自适应 knots 或发布样本超限 | `FullSegmentResourceLimitExceeded` | 空 | 不发布;不得截断轨迹或回退滚动模式 |
|
| 自适应 knots 或发布样本超限 | `FullSegmentResourceLimitExceeded` | 空 | 不发布;不得截断轨迹或回退滚动模式 |
|
||||||
|
|
||||||
`FailureReason` 始终以 `map=...;reference=...;state=...;previous=...;segment=...` 开头,供调用方追踪地图、参考路径、状态、上一轨迹和方向段绑定。
|
`FailureReason` 始终以 `map=...;reference=...;state=...;previous=...;segment=...` 开头,供调用方追踪地图、参考路径、状态、上一轨迹和方向段绑定。
|
||||||
@@ -165,7 +165,7 @@ EmTrajectory trajectory = result.Trajectory;
|
|||||||
|
|
||||||
- 对完整活动方向段做一次优化;`s_end` 来自已选 Local G2 的实际 `PathS`,`T_end` 由可行加速度、巡航、jerk 限速停止和零速 hold 推导,不依赖旧的距离/时间截断窗口。
|
- 对完整活动方向段做一次优化;`s_end` 来自已选 Local G2 的实际 `PathS`,`T_end` 由可行加速度、巡航、jerk 限速停止和零速 hold 推导,不依赖旧的距离/时间截断窗口。
|
||||||
- 前进期望/硬上限为 `1.0 m/s`,倒车期望/硬上限为 `0.5 m/s`。
|
- 前进期望/硬上限为 `1.0 m/s`,倒车期望/硬上限为 `0.5 m/s`。
|
||||||
- 成功终端必须停止,世界位置误差不超过 `0.03 m`,归一化 yaw 误差不超过 `5°`(约 `0.0873 rad`)。
|
- 成功终端必须停止,世界位置误差和归一化 yaw 误差均为 `0`(精确到达目标)。
|
||||||
- `NoProgress`、`TerminalPoseMismatch`、`FullSegmentResourceLimitExceeded` 失败均返回空轨迹,不降级为滚动或截断发布。
|
- `NoProgress`、`TerminalPoseMismatch`、`FullSegmentResourceLimitExceeded` 失败均返回空轨迹,不降级为滚动或截断发布。
|
||||||
- `DistanceHorizonMeters` 与 `TimeHorizonSeconds` 作为兼容字段保留,但不用于截断 full 模式。
|
- `DistanceHorizonMeters` 与 `TimeHorizonSeconds` 作为兼容字段保留,但不用于截断 full 模式。
|
||||||
|
|
||||||
|
|||||||
@@ -150,8 +150,8 @@ public static class EmPlanningRequestValidator
|
|||||||
!Positive(scheduling.MaximumOptimizationTimeStepSeconds) ||
|
!Positive(scheduling.MaximumOptimizationTimeStepSeconds) ||
|
||||||
!Positive(scheduling.MaximumOptimizationSpatialStepMeters) ||
|
!Positive(scheduling.MaximumOptimizationSpatialStepMeters) ||
|
||||||
scheduling.MaximumOptimizationKnotCount < 3 || scheduling.MaximumPublishedSampleCount < 2 ||
|
scheduling.MaximumOptimizationKnotCount < 3 || scheduling.MaximumPublishedSampleCount < 2 ||
|
||||||
!Positive(validation.TerminalPositionToleranceMeters) ||
|
validation.TerminalPositionToleranceMeters < 0d ||
|
||||||
!Positive(validation.TerminalYawToleranceRadians) || validation.TerminalYawToleranceRadians > Math.PI ||
|
validation.TerminalYawToleranceRadians < 0d || validation.TerminalYawToleranceRadians > Math.PI ||
|
||||||
!WeightsAreValid(configuration.Lateral.Weights) || !WeightsAreValid(configuration.Longitudinal.Weights))
|
!WeightsAreValid(configuration.Lateral.Weights) || !WeightsAreValid(configuration.Longitudinal.Weights))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
|||||||
+62
-2
@@ -204,6 +204,7 @@ internal static class TrajectoryObservationMovementTestRunner
|
|||||||
private static Task activeTask;
|
private static Task activeTask;
|
||||||
private static TrajectoryObservationVisualizationPublisher activeWebPublisher;
|
private static TrajectoryObservationVisualizationPublisher activeWebPublisher;
|
||||||
private static TrajectoryObservationPresentation activePresentation;
|
private static TrajectoryObservationPresentation activePresentation;
|
||||||
|
private static TrajectoryObservationReportWriter activeReportWriter;
|
||||||
private static long nextSessionId;
|
private static long nextSessionId;
|
||||||
private static long activeSessionId;
|
private static long activeSessionId;
|
||||||
private static long stateSequence;
|
private static long stateSequence;
|
||||||
@@ -218,6 +219,7 @@ internal static class TrajectoryObservationMovementTestRunner
|
|||||||
var cancellation = new CancellationTokenSource();
|
var cancellation = new CancellationTokenSource();
|
||||||
CancellationTokenSource previousCancellation;
|
CancellationTokenSource previousCancellation;
|
||||||
long sessionId;
|
long sessionId;
|
||||||
|
TrajectoryObservationReportWriter reportWriter = null;
|
||||||
lock (SessionSync)
|
lock (SessionSync)
|
||||||
{
|
{
|
||||||
previousCancellation = activeCancellation;
|
previousCancellation = activeCancellation;
|
||||||
@@ -227,6 +229,8 @@ internal static class TrajectoryObservationMovementTestRunner
|
|||||||
activeSessionId = sessionId;
|
activeSessionId = sessionId;
|
||||||
StopActiveVisualizationNoLock();
|
StopActiveVisualizationNoLock();
|
||||||
activePresentation = settings.EnableNativePainterVisualization ? new TrajectoryObservationPresentation() : null;
|
activePresentation = settings.EnableNativePainterVisualization ? new TrajectoryObservationPresentation() : null;
|
||||||
|
reportWriter = new TrajectoryObservationReportWriter(sessionId);
|
||||||
|
activeReportWriter = reportWriter;
|
||||||
PrintStatus("Starting frozen observation session " +
|
PrintStatus("Starting frozen observation session " +
|
||||||
sessionId.ToString(CultureInfo.InvariantCulture) + ".");
|
sessionId.ToString(CultureInfo.InvariantCulture) + ".");
|
||||||
}
|
}
|
||||||
@@ -239,7 +243,7 @@ internal static class TrajectoryObservationMovementTestRunner
|
|||||||
{
|
{
|
||||||
if (activeSessionId == sessionId) activeTask = task;
|
if (activeSessionId == sessionId) activeTask = task;
|
||||||
}
|
}
|
||||||
_ = task.ContinueWith(completed => Finish(sessionId, cancellation, completed), TaskScheduler.Default);
|
_ = task.ContinueWith(completed => Finish(sessionId, cancellation, completed, reportWriter), TaskScheduler.Default);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static void Stop()
|
internal static void Stop()
|
||||||
@@ -271,6 +275,13 @@ internal static class TrajectoryObservationMovementTestRunner
|
|||||||
{
|
{
|
||||||
token.ThrowIfCancellationRequested();
|
token.ThrowIfCancellationRequested();
|
||||||
VehicleMotionState initialState = ReadVehicleState();
|
VehicleMotionState initialState = ReadVehicleState();
|
||||||
|
LogIfCurrent(sessionId, "initial state: pose=(" + Format(initialState.Pose.X) + ", " +
|
||||||
|
Format(initialState.Pose.Y) + ", " + Format(initialState.Pose.Heading) + ") m/rad, signed speed=" +
|
||||||
|
initialState.SignedLongitudinalSpeedMetersPerSecond.ToString("R", CultureInfo.InvariantCulture) + " m/s, acceleration=" +
|
||||||
|
(initialState.LongitudinalAccelerationMetersPerSecondSquared.HasValue
|
||||||
|
? Format(initialState.LongitudinalAccelerationMetersPerSecondSquared.Value)
|
||||||
|
: "unavailable") + " m/s2, stateSequence=" +
|
||||||
|
initialState.SequenceId.ToString(CultureInfo.InvariantCulture));
|
||||||
CoarsePathPlanningJob job = TrajectoryObservationSetupFactory.CreateBootstrapJob(
|
CoarsePathPlanningJob job = TrajectoryObservationSetupFactory.CreateBootstrapJob(
|
||||||
initialState.Pose, goal, settings, obstacles, obstacleSnapshotVersion);
|
initialState.Pose, goal, settings, obstacles, obstacleSnapshotVersion);
|
||||||
|
|
||||||
@@ -294,6 +305,15 @@ internal static class TrajectoryObservationMovementTestRunner
|
|||||||
new EmPlanningService(new OsqpNativeSolver()),
|
new EmPlanningService(new OsqpNativeSolver()),
|
||||||
"trajectory-observer-" + sessionId.ToString(CultureInfo.InvariantCulture));
|
"trajectory-observer-" + sessionId.ToString(CultureInfo.InvariantCulture));
|
||||||
LogIfCurrent(sessionId, controller.CreateConfigurationDiagnostic().Text);
|
LogIfCurrent(sessionId, controller.CreateConfigurationDiagnostic().Text);
|
||||||
|
LogIfCurrent(sessionId, CreateSettingsDiagnostic(settings));
|
||||||
|
EmPlannerConfiguration effectiveConfiguration = controller.CreateEffectiveConfigurationSnapshot();
|
||||||
|
double stopSpeedTolerance = effectiveConfiguration.Longitudinal.StopSpeedToleranceMetersPerSecond;
|
||||||
|
LogIfCurrent(sessionId, "static start check: initialSignedSpeed=" +
|
||||||
|
initialState.SignedLongitudinalSpeedMetersPerSecond.ToString("R", CultureInfo.InvariantCulture) +
|
||||||
|
", stopSpeedTolerance=" + stopSpeedTolerance.ToString("R", CultureInfo.InvariantCulture) +
|
||||||
|
", treatedAsStopped=" +
|
||||||
|
(Math.Abs(initialState.SignedLongitudinalSpeedMetersPerSecond) <= stopSpeedTolerance)
|
||||||
|
.ToString(CultureInfo.InvariantCulture));
|
||||||
var observationLoop = new TrajectoryObservationLoop(controller);
|
var observationLoop = new TrajectoryObservationLoop(controller);
|
||||||
TrajectoryObservationVisualizationPublisher webPublisher = StartWebVisualizationIfEnabled(
|
TrajectoryObservationVisualizationPublisher webPublisher = StartWebVisualizationIfEnabled(
|
||||||
sessionId, bootstrap, controller, settings, obstacleSnapshotVersion);
|
sessionId, bootstrap, controller, settings, obstacleSnapshotVersion);
|
||||||
@@ -332,7 +352,14 @@ internal static class TrajectoryObservationMovementTestRunner
|
|||||||
if (tick.PlanningStarted)
|
if (tick.PlanningStarted)
|
||||||
LogIfCurrent(sessionId, "planning status=pending");
|
LogIfCurrent(sessionId, "planning status=pending");
|
||||||
if (tick.PlanningCompleted)
|
if (tick.PlanningCompleted)
|
||||||
|
{
|
||||||
LogIfCurrent(sessionId, diagnostic.Text);
|
LogIfCurrent(sessionId, diagnostic.Text);
|
||||||
|
if (observation.PublishedTrajectory != null && charts != null)
|
||||||
|
{
|
||||||
|
AppendReportIfCurrent(sessionId,
|
||||||
|
TrajectoryObservationDiagnostics.CreateCurveReport(charts, observation.PublishedTrajectory).Text);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -490,14 +517,25 @@ internal static class TrajectoryObservationMovementTestRunner
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void AppendReportIfCurrent(long sessionId, string text)
|
||||||
|
{
|
||||||
|
lock (SessionSync)
|
||||||
|
{
|
||||||
|
if (activeSessionId != sessionId) return;
|
||||||
|
activeReportWriter?.Append(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void PrintStatus(string message)
|
private static void PrintStatus(string message)
|
||||||
{
|
{
|
||||||
string text = ObserveOnlyNotice + "\n" + message;
|
string text = ObserveOnlyNotice + "\n" + message;
|
||||||
Hedingben.ToastText(text, StatusChannel);
|
Hedingben.ToastText(text, StatusChannel);
|
||||||
Console.WriteLine("[TrajectoryObserver] " + text);
|
Console.WriteLine("[TrajectoryObserver] " + text);
|
||||||
|
activeReportWriter?.Append(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void Finish(long sessionId, CancellationTokenSource cancellation, Task completed)
|
private static void Finish(long sessionId, CancellationTokenSource cancellation, Task completed,
|
||||||
|
TrajectoryObservationReportWriter reportWriter)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -514,16 +552,38 @@ internal static class TrajectoryObservationMovementTestRunner
|
|||||||
{
|
{
|
||||||
lock (SessionSync)
|
lock (SessionSync)
|
||||||
{
|
{
|
||||||
|
if (ReferenceEquals(activeReportWriter, reportWriter))
|
||||||
|
activeReportWriter = null;
|
||||||
if (activeSessionId == sessionId)
|
if (activeSessionId == sessionId)
|
||||||
{
|
{
|
||||||
activeTask = null;
|
activeTask = null;
|
||||||
activeCancellation = null;
|
activeCancellation = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string reportPath = reportWriter.Save();
|
||||||
|
Console.WriteLine("[TrajectoryObserver] report=" + reportPath);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[TrajectoryObserver] report write failed: " + exception.Message);
|
||||||
|
}
|
||||||
cancellation.Dispose();
|
cancellation.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string CreateSettingsDiagnostic(TrajectoryObservationSettings settings)
|
||||||
|
{
|
||||||
|
return "observation settings: planningScope=" + settings.PlanningScope +
|
||||||
|
", webVisualization=" + settings.EnableWebVisualization +
|
||||||
|
", nativePainter=" + settings.EnableNativePainterVisualization +
|
||||||
|
", outputTimeStep=" + Format(settings.OutputTimeStepSeconds) + "s" +
|
||||||
|
", observerPeriod=" + Format(settings.ObserverPeriodSeconds) + "s" +
|
||||||
|
", solverTimeout=" + Format(settings.SolverTimeoutSeconds) + "s" +
|
||||||
|
", maximumOsqpIterations=" + settings.MaximumOsqpIterations.ToString(CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
private static void ClearAndLogRuntimeFaultIfCurrent(long sessionId, Exception exception)
|
private static void ClearAndLogRuntimeFaultIfCurrent(long sessionId, Exception exception)
|
||||||
{
|
{
|
||||||
lock (SessionSync)
|
lock (SessionSync)
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ diagnostic prediction only. Goal and rolling-safety-stop commands are logged, ne
|
|||||||
gear-switch trajectory, the observer remains on the current direction segment and waits for real direction confirmation;
|
gear-switch trajectory, the observer remains on the current direction segment and waits for real direction confirmation;
|
||||||
it does not create or dispatch a direction-change action.
|
it does not create or dispatch a direction-change action.
|
||||||
|
|
||||||
|
Every frozen observation session writes a text report to
|
||||||
|
`TrajectoryObservationReports/trajectory-observation-session-<id>.txt` in the process working directory when the
|
||||||
|
session stops; the report path is printed to the console.
|
||||||
|
|
||||||
## FullDirectionSegment observation (default)
|
## FullDirectionSegment observation (default)
|
||||||
|
|
||||||
`UseFullDirectionSegmentPlanning=true` maps to `EmPlanningScope.FullDirectionSegment`. MovementTest performs one frozen
|
`UseFullDirectionSegmentPlanning=true` maps to `EmPlanningScope.FullDirectionSegment`. MovementTest performs one frozen
|
||||||
@@ -20,8 +24,7 @@ and zero-speed hold behavior instead of being forced by a legacy time horizon.
|
|||||||
|
|
||||||
`DistanceHorizonMeters` and `TimeHorizonSeconds` are rolling-compatible fields and do not truncate full mode. Forward
|
`DistanceHorizonMeters` and `TimeHorizonSeconds` are rolling-compatible fields and do not truncate full mode. Forward
|
||||||
desired/hard maximum speed is `1.0 m/s`; reverse desired/hard maximum speed is `0.5 m/s`. A successful real boundary
|
desired/hard maximum speed is `1.0 m/s`; reverse desired/hard maximum speed is `0.5 m/s`. A successful real boundary
|
||||||
requires a terminal stop plus world-position error no greater than `0.03 m` and normalized yaw error no greater than
|
requires a terminal stop plus exact world-position and normalized yaw matching (`0 m` / `0 rad`).
|
||||||
`5 deg` (`5 * PI / 180 rad`).
|
|
||||||
|
|
||||||
## Rolling trajectory observation
|
## Rolling trajectory observation
|
||||||
|
|
||||||
|
|||||||
+74
-1
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Text;
|
||||||
using MultiWheelC.TrajectoryPlanning.CoarsePath;
|
using MultiWheelC.TrajectoryPlanning.CoarsePath;
|
||||||
using MultiWheelC.TrajectoryPlanning.EMPlanner;
|
using MultiWheelC.TrajectoryPlanning.EMPlanner;
|
||||||
|
|
||||||
@@ -18,10 +19,69 @@ public sealed class TrajectoryObservationDiagnostic
|
|||||||
|
|
||||||
public static class TrajectoryObservationDiagnostics
|
public static class TrajectoryObservationDiagnostics
|
||||||
{
|
{
|
||||||
|
public static TrajectoryObservationDiagnostic CreateCurveReport(TrajectoryObservationCharts charts,
|
||||||
|
EmTrajectory trajectory)
|
||||||
|
{
|
||||||
|
if (charts == null || trajectory == null)
|
||||||
|
return new TrajectoryObservationDiagnostic("curve report unavailable");
|
||||||
|
|
||||||
|
IReadOnlyList<EmTrajectoryPoint> points = trajectory.Points;
|
||||||
|
var text = new StringBuilder();
|
||||||
|
text.AppendLine("LS_CURVE");
|
||||||
|
text.AppendLine("index,referenceS(m),lateralOffset(m)");
|
||||||
|
for (int index = 0; index < charts.LsSamples.Count; index++)
|
||||||
|
{
|
||||||
|
TrajectoryObservationLsSample sample = charts.LsSamples[index];
|
||||||
|
text.AppendLine(index.ToString(CultureInfo.InvariantCulture) + "," +
|
||||||
|
sample.PathS.ToString("R", CultureInfo.InvariantCulture) + "," +
|
||||||
|
sample.LateralOffset.ToString("R", CultureInfo.InvariantCulture));
|
||||||
|
}
|
||||||
|
|
||||||
|
text.AppendLine("ST_CURVE");
|
||||||
|
text.AppendLine("index,time(s),pathS(m)");
|
||||||
|
for (int index = 0; index < charts.StSamples.Count; index++)
|
||||||
|
{
|
||||||
|
TrajectoryObservationStSample sample = charts.StSamples[index];
|
||||||
|
text.AppendLine(index.ToString(CultureInfo.InvariantCulture) + "," +
|
||||||
|
sample.TimeFromStart.ToString("R", CultureInfo.InvariantCulture) + "," +
|
||||||
|
sample.PathS.ToString("R", CultureInfo.InvariantCulture));
|
||||||
|
}
|
||||||
|
|
||||||
|
text.AppendLine("SPEED_CURVE");
|
||||||
|
text.AppendLine("index,time(s),signedSpeed(m/s)");
|
||||||
|
for (int index = 0; index < charts.SpeedSamples.Count; index++)
|
||||||
|
{
|
||||||
|
TrajectoryObservationSpeedSample sample = charts.SpeedSamples[index];
|
||||||
|
text.AppendLine(index.ToString(CultureInfo.InvariantCulture) + "," +
|
||||||
|
sample.TimeFromStart.ToString("R", CultureInfo.InvariantCulture) + "," +
|
||||||
|
sample.SignedLongitudinalVelocity.ToString("R", CultureInfo.InvariantCulture));
|
||||||
|
}
|
||||||
|
|
||||||
|
text.AppendLine("ACCELERATION_CURVE");
|
||||||
|
text.AppendLine("index,time(s),acceleration(m/s2)");
|
||||||
|
for (int index = 0; index < points.Count; index++)
|
||||||
|
{
|
||||||
|
text.AppendLine(index.ToString(CultureInfo.InvariantCulture) + "," +
|
||||||
|
points[index].TimeFromStart.ToString("R", CultureInfo.InvariantCulture) + "," +
|
||||||
|
points[index].LongitudinalAcceleration.ToString("R", CultureInfo.InvariantCulture));
|
||||||
|
}
|
||||||
|
|
||||||
|
text.AppendLine("JERK_CURVE");
|
||||||
|
text.AppendLine("index,time(s),jerk(m/s3)");
|
||||||
|
for (int index = 0; index + 1 < points.Count; index++)
|
||||||
|
{
|
||||||
|
text.AppendLine(index.ToString(CultureInfo.InvariantCulture) + "," +
|
||||||
|
points[index].TimeFromStart.ToString("R", CultureInfo.InvariantCulture) + "," +
|
||||||
|
points[index].LongitudinalJerk.ToString("R", CultureInfo.InvariantCulture));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TrajectoryObservationDiagnostic(text.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
public static TrajectoryObservationDiagnostic CreateConfiguration(EmPlannerConfiguration configuration)
|
public static TrajectoryObservationDiagnostic CreateConfiguration(EmPlannerConfiguration configuration)
|
||||||
{
|
{
|
||||||
if (configuration == null || configuration.Scheduling == null || configuration.Solver == null ||
|
if (configuration == null || configuration.Scheduling == null || configuration.Solver == null ||
|
||||||
configuration.Longitudinal == null)
|
configuration.Longitudinal == null || configuration.Validation == null)
|
||||||
throw new ArgumentNullException(nameof(configuration));
|
throw new ArgumentNullException(nameof(configuration));
|
||||||
|
|
||||||
double horizon = configuration.Scheduling.TimeHorizonSeconds;
|
double horizon = configuration.Scheduling.TimeHorizonSeconds;
|
||||||
@@ -57,6 +117,19 @@ public static class TrajectoryObservationDiagnostics
|
|||||||
"maximumOsqpIterations=" + configuration.Solver.MaximumOsqpIterations.ToString(CultureInfo.InvariantCulture) + "\n" +
|
"maximumOsqpIterations=" + configuration.Solver.MaximumOsqpIterations.ToString(CultureInfo.InvariantCulture) + "\n" +
|
||||||
"solverTimeout=" + Format(configuration.Scheduling.SolverTimeoutSeconds, "F2") + "s\n" +
|
"solverTimeout=" + Format(configuration.Scheduling.SolverTimeoutSeconds, "F2") + "s\n" +
|
||||||
"replanPeriod=" + Format(configuration.Scheduling.ReplanPeriodSeconds, "F2") + "s\n" +
|
"replanPeriod=" + Format(configuration.Scheduling.ReplanPeriodSeconds, "F2") + "s\n" +
|
||||||
|
"kinematicTolerance=" + Format(configuration.Validation.KinematicTolerance, "G17") + "\n" +
|
||||||
|
"stopSpeedToleranceMetersPerSecond=" + Format(longitudinal.StopSpeedToleranceMetersPerSecond, "G17") + "\n" +
|
||||||
|
"terminalPositionToleranceMeters=" + Format(configuration.Validation.TerminalPositionToleranceMeters, "G17") + "\n" +
|
||||||
|
"terminalYawToleranceRadians=" + Format(configuration.Validation.TerminalYawToleranceRadians, "G17") + "\n" +
|
||||||
|
"maximumOptimizationTimeStepSeconds=" + Format(configuration.Scheduling.MaximumOptimizationTimeStepSeconds, "F3") + "\n" +
|
||||||
|
"maximumOptimizationSpatialStepMeters=" + Format(configuration.Scheduling.MaximumOptimizationSpatialStepMeters, "F3") + "\n" +
|
||||||
|
"maximumOptimizationKnotCount=" + configuration.Scheduling.MaximumOptimizationKnotCount.ToString(CultureInfo.InvariantCulture) + "\n" +
|
||||||
|
"maximumPublishedSampleCount=" + configuration.Scheduling.MaximumPublishedSampleCount.ToString(CultureInfo.InvariantCulture) + "\n" +
|
||||||
|
"maximumForwardSpeedMetersPerSecond=" + Format(longitudinal.MaximumForwardSpeedMetersPerSecond, "F3") + "\n" +
|
||||||
|
"maximumReverseSpeedMetersPerSecond=" + Format(longitudinal.MaximumReverseSpeedMetersPerSecond, "F3") + "\n" +
|
||||||
|
"maximumAccelerationMetersPerSecondSquared=" + Format(longitudinal.MaximumAccelerationMetersPerSecondSquared, "F3") + "\n" +
|
||||||
|
"maximumDecelerationMetersPerSecondSquared=" + Format(longitudinal.MaximumDecelerationMetersPerSecondSquared, "F3") + "\n" +
|
||||||
|
"maximumJerkMetersPerSecondCubed=" + Format(longitudinal.MaximumJerkMetersPerSecondCubed, "F3") + "\n" +
|
||||||
"maximumJerkLimitedStopDistance=" + Format(worstStop.DistanceMeters, "F3") + "m\n" +
|
"maximumJerkLimitedStopDistance=" + Format(worstStop.DistanceMeters, "F3") + "m\n" +
|
||||||
"maximumJerkLimitedStopDuration=" + Format(worstStop.DurationSeconds, "F3") + "s\n" +
|
"maximumJerkLimitedStopDuration=" + Format(worstStop.DurationSeconds, "F3") + "s\n" +
|
||||||
"requiredDistanceHorizon=" + Format(requiredDistanceHorizon, "F3") + "m");
|
"requiredDistanceHorizon=" + Format(requiredDistanceHorizon, "F3") + "m");
|
||||||
|
|||||||
+67
@@ -0,0 +1,67 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace MultiWheelC.TrajectoryPlanning.TrajectoryObservation;
|
||||||
|
|
||||||
|
/// <summary>Buffers a readable text report for one trajectory observation session.</summary>
|
||||||
|
public sealed class TrajectoryObservationReportWriter
|
||||||
|
{
|
||||||
|
private readonly long _sessionId;
|
||||||
|
private readonly string _reportDirectory;
|
||||||
|
private readonly StringBuilder _content = new StringBuilder();
|
||||||
|
private readonly object _sync = new object();
|
||||||
|
|
||||||
|
public TrajectoryObservationReportWriter(long sessionId, string reportDirectory = null)
|
||||||
|
{
|
||||||
|
if (sessionId <= 0)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(sessionId));
|
||||||
|
|
||||||
|
_sessionId = sessionId;
|
||||||
|
_reportDirectory = reportDirectory ??
|
||||||
|
Path.Combine(Environment.CurrentDirectory, "TrajectoryObservationReports");
|
||||||
|
AppendHeader();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string FilePath
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return Path.Combine(_reportDirectory, "trajectory-observation-session-" +
|
||||||
|
_sessionId.ToString(CultureInfo.InvariantCulture) + ".txt");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Append(string text)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(text))
|
||||||
|
return;
|
||||||
|
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
_content.Append(DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture));
|
||||||
|
_content.Append(" | ");
|
||||||
|
_content.AppendLine(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Save()
|
||||||
|
{
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(_reportDirectory);
|
||||||
|
File.WriteAllText(FilePath, _content.ToString(), new UTF8Encoding(false));
|
||||||
|
return FilePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AppendHeader()
|
||||||
|
{
|
||||||
|
_content.AppendLine("EM trajectory observation report");
|
||||||
|
_content.AppendLine("session=" + _sessionId.ToString(CultureInfo.InvariantCulture));
|
||||||
|
_content.AppendLine("OBSERVE_ONLY: no chassis command is sent.");
|
||||||
|
_content.AppendLine("reportGeneratedUtc=" + DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture));
|
||||||
|
_content.AppendLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -101,6 +101,8 @@ internal static class FoundationChecks
|
|||||||
EMPlannerVerificationHost.Verification.NearlyEqual(1e-5d, configuration.Solver.AbsoluteTolerance, "absolute tolerance");
|
EMPlannerVerificationHost.Verification.NearlyEqual(1e-5d, configuration.Solver.AbsoluteTolerance, "absolute tolerance");
|
||||||
EMPlannerVerificationHost.Verification.NearlyEqual(1e-5d, configuration.Solver.RelativeTolerance, "relative tolerance");
|
EMPlannerVerificationHost.Verification.NearlyEqual(1e-5d, configuration.Solver.RelativeTolerance, "relative tolerance");
|
||||||
EMPlannerVerificationHost.Verification.NearlyEqual(1e-5d, configuration.Solver.StrictResidualTolerance, "strict residual tolerance");
|
EMPlannerVerificationHost.Verification.NearlyEqual(1e-5d, configuration.Solver.StrictResidualTolerance, "strict residual tolerance");
|
||||||
|
EMPlannerVerificationHost.Verification.NearlyEqual(1e-5d, configuration.Validation.KinematicTolerance,
|
||||||
|
"kinematic tolerance");
|
||||||
EMPlannerVerificationHost.Verification.Equal(true, configuration.Solver.WarmStart, "warm start");
|
EMPlannerVerificationHost.Verification.Equal(true, configuration.Solver.WarmStart, "warm start");
|
||||||
EMPlannerVerificationHost.Verification.Equal(true, configuration.Solver.Polish, "polish");
|
EMPlannerVerificationHost.Verification.Equal(true, configuration.Solver.Polish, "polish");
|
||||||
EMPlannerVerificationHost.Verification.Equal(false, configuration.Solver.NativeVerbose, "native verbose");
|
EMPlannerVerificationHost.Verification.Equal(false, configuration.Solver.NativeVerbose, "native verbose");
|
||||||
@@ -179,9 +181,9 @@ internal static class FoundationChecks
|
|||||||
"optimization knot cap");
|
"optimization knot cap");
|
||||||
EMPlannerVerificationHost.Verification.Equal(5001, defaults.Scheduling.MaximumPublishedSampleCount,
|
EMPlannerVerificationHost.Verification.Equal(5001, defaults.Scheduling.MaximumPublishedSampleCount,
|
||||||
"publication sample cap");
|
"publication sample cap");
|
||||||
EMPlannerVerificationHost.Verification.NearlyEqual(0.03d,
|
EMPlannerVerificationHost.Verification.NearlyEqual(0d,
|
||||||
defaults.Validation.TerminalPositionToleranceMeters, "terminal position tolerance");
|
defaults.Validation.TerminalPositionToleranceMeters, "terminal position tolerance");
|
||||||
EMPlannerVerificationHost.Verification.NearlyEqual(5d * Math.PI / 180d,
|
EMPlannerVerificationHost.Verification.NearlyEqual(0d,
|
||||||
defaults.Validation.TerminalYawToleranceRadians, "terminal yaw tolerance");
|
defaults.Validation.TerminalYawToleranceRadians, "terminal yaw tolerance");
|
||||||
EMPlannerVerificationHost.Verification.Equal(EmPlanningStatus.NoProgress, EmPlanningStatus.NoProgress,
|
EMPlannerVerificationHost.Verification.Equal(EmPlanningStatus.NoProgress, EmPlanningStatus.NoProgress,
|
||||||
"no-progress status exists");
|
"no-progress status exists");
|
||||||
@@ -200,7 +202,7 @@ internal static class FoundationChecks
|
|||||||
copied.Longitudinal.DesiredForwardSpeedMetersPerSecond, "copy keeps desired forward speed");
|
copied.Longitudinal.DesiredForwardSpeedMetersPerSecond, "copy keeps desired forward speed");
|
||||||
EMPlannerVerificationHost.Verification.Equal(401, copied.Scheduling.MaximumOptimizationKnotCount,
|
EMPlannerVerificationHost.Verification.Equal(401, copied.Scheduling.MaximumOptimizationKnotCount,
|
||||||
"copy keeps knot cap");
|
"copy keeps knot cap");
|
||||||
EMPlannerVerificationHost.Verification.NearlyEqual(0.03d,
|
EMPlannerVerificationHost.Verification.NearlyEqual(0d,
|
||||||
copied.Validation.TerminalPositionToleranceMeters, "copy keeps terminal tolerance");
|
copied.Validation.TerminalPositionToleranceMeters, "copy keeps terminal tolerance");
|
||||||
|
|
||||||
EmPlannerConfiguration invalidKnotCap = EmPlannerConfiguration.CreateDefault();
|
EmPlannerConfiguration invalidKnotCap = EmPlannerConfiguration.CreateDefault();
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ internal static class LongitudinalIntegrationChecks
|
|||||||
VerifiesRollingOptimizationKeepsANonzeroTerminalSpeed();
|
VerifiesRollingOptimizationKeepsANonzeroTerminalSpeed();
|
||||||
VerifiesFullDirectionScheduleIsIndependentFromPublicationCadence();
|
VerifiesFullDirectionScheduleIsIndependentFromPublicationCadence();
|
||||||
VerifiesFullDirectionStaticStartMakesProgress();
|
VerifiesFullDirectionStaticStartMakesProgress();
|
||||||
|
VerifiesFullDirectionNearZeroSpeedUsesStaticStartSeed();
|
||||||
VerifiesFullDirectionPublicationDoesNotDuplicateItsTerminalHold();
|
VerifiesFullDirectionPublicationDoesNotDuplicateItsTerminalHold();
|
||||||
VerifiesExactStopIncludesAStabilizationTail();
|
VerifiesExactStopIncludesAStabilizationTail();
|
||||||
VerifiesLastStrictCandidateSurvivesLaterTimeout();
|
VerifiesLastStrictCandidateSurvivesLaterTimeout();
|
||||||
@@ -150,6 +151,42 @@ internal static class LongitudinalIntegrationChecks
|
|||||||
"full segment stops at the terminal boundary");
|
"full segment stops at the terminal boundary");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void VerifiesFullDirectionNearZeroSpeedUsesStaticStartSeed()
|
||||||
|
{
|
||||||
|
EmPlannerConfiguration configuration = EmPlannerConfiguration.CreateDefault();
|
||||||
|
configuration.Scheduling.SolverTimeoutSeconds = 5d;
|
||||||
|
configuration.Solver.MaximumOsqpIterations = 100000;
|
||||||
|
LateralPath path = new LateralPath(new[]
|
||||||
|
{
|
||||||
|
Point(0d, 0d, 0d),
|
||||||
|
Point(2.5d, 2.5d, 0d),
|
||||||
|
Point(5d, 5d, 0d),
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
EmPlanningStatus status = new PathSpeedLimitBuilder().Build(path, TravelDirection.Forward, 0.01d,
|
||||||
|
EmTerminalType.Goal, configuration, out PathSpeedLimit speedLimit, out string failureReason);
|
||||||
|
Verification.Equal(EmPlanningStatus.Success, status, "near-zero envelope: " + failureReason);
|
||||||
|
status = new FullDirectionSegmentScheduleBuilder().TryBuild(path, speedLimit, 0.01d, 0d,
|
||||||
|
configuration.Longitudinal.DesiredForwardSpeedMetersPerSecond, configuration,
|
||||||
|
out LongitudinalKnotSchedule schedule, out failureReason);
|
||||||
|
Verification.Equal(EmPlanningStatus.Success, status, "near-zero schedule: " + failureReason);
|
||||||
|
|
||||||
|
var input = new LongitudinalPlanningInput(path, TravelDirection.Forward, 0.01d, 0d,
|
||||||
|
EmTerminalType.Goal, EmLongitudinalMode.ExactStopAtBoundary, configuration,
|
||||||
|
EmPlanningScope.FullDirectionSegment, schedule, Array.Empty<double>(), Array.Empty<double>());
|
||||||
|
LongitudinalPlanningResult result = new LongitudinalPlanner(new OsqpNativeSolver()).Plan(input,
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
Verification.True(result.Status == EmPlanningStatus.Success || result.Status == EmPlanningStatus.SuccessWithFallback,
|
||||||
|
"near-zero full-direction start succeeds: " + result.FailureReason);
|
||||||
|
LongitudinalCandidate candidate = result.Candidate ??
|
||||||
|
throw new InvalidOperationException("Near-zero full-direction candidate was missing.");
|
||||||
|
Verification.NearlyEqual(path.Points[path.Points.Count - 1].PathS, candidate.S[candidate.S.Count - 1],
|
||||||
|
"near-zero start reaches the terminal boundary");
|
||||||
|
Verification.NearlyEqual(0d, candidate.U[candidate.U.Count - 1],
|
||||||
|
"near-zero start stops at the terminal boundary");
|
||||||
|
}
|
||||||
|
|
||||||
private static void VerifiesFullDirectionPublicationDoesNotDuplicateItsTerminalHold()
|
private static void VerifiesFullDirectionPublicationDoesNotDuplicateItsTerminalHold()
|
||||||
{
|
{
|
||||||
EmPlannerConfiguration configuration = EmPlannerConfiguration.CreateDefault();
|
EmPlannerConfiguration configuration = EmPlannerConfiguration.CreateDefault();
|
||||||
|
|||||||
@@ -769,9 +769,9 @@ internal static class LongitudinalModelChecks
|
|||||||
out speedFailure);
|
out speedFailure);
|
||||||
Verification.Equal(EmPlanningStatus.Success, speedStatus, "near-terminal no-progress envelope: " + speedFailure);
|
Verification.Equal(EmPlanningStatus.Success, speedStatus, "near-terminal no-progress envelope: " + speedFailure);
|
||||||
Verification.True(!validator.TryValidate(nearTerminalInput, nearTerminalLimit, stationary, out _,
|
Verification.True(!validator.TryValidate(nearTerminalInput, nearTerminalLimit, stationary, out _,
|
||||||
out failureStatus, out _), "near-terminal fixture remains subject to its stop-tail contract");
|
out failureStatus, out _), "near-terminal stationary full segment is rejected");
|
||||||
Verification.True(failureStatus != EmPlanningStatus.NoProgress,
|
Verification.Equal(EmPlanningStatus.NoProgress, failureStatus,
|
||||||
"near-terminal gear-switch stop is not classified as no progress");
|
"near-terminal gear-switch stop is classified as no progress");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void VerifiesPreviousTrajectorySeedResamplesAndProjectsMonotonically()
|
private static void VerifiesPreviousTrajectorySeedResamplesAndProjectsMonotonically()
|
||||||
|
|||||||
@@ -194,31 +194,27 @@ internal static class TrajectoryChecks
|
|||||||
EmTrajectoryPoint terminal = valid.Points[terminalIndex];
|
EmTrajectoryPoint terminal = valid.Points[terminalIndex];
|
||||||
var validator = new EmTrajectoryValidator();
|
var validator = new EmTrajectoryValidator();
|
||||||
|
|
||||||
EmTrajectoryValidationResult insidePosition = validator.Validate(valid, context.EmptyMap, context.Vehicle,
|
EmTrajectoryValidationResult exactPosition = validator.Validate(valid, context.EmptyMap, context.Vehicle,
|
||||||
context.Configuration, 2, 0.0055d, new Pose2D(terminal.X + 0.029d, terminal.Y, terminal.Yaw),
|
context.Configuration, 2, 0.0055d, new Pose2D(terminal.X, terminal.Y, terminal.Yaw),
|
||||||
EmBoundaryType.Goal);
|
EmBoundaryType.Goal);
|
||||||
Verification.True(insidePosition.IsValid, "2.9-centimetre terminal position error is accepted");
|
Verification.True(exactPosition.IsValid, "exact terminal position is accepted");
|
||||||
EmTrajectoryValidationResult outsidePosition = validator.Validate(valid, context.EmptyMap, context.Vehicle,
|
EmTrajectoryValidationResult outsidePosition = validator.Validate(valid, context.EmptyMap, context.Vehicle,
|
||||||
context.Configuration, 2, 0.0055d, new Pose2D(terminal.X + 0.031d, terminal.Y, terminal.Yaw),
|
context.Configuration, 2, 0.0055d, new Pose2D(terminal.X + 0.001d, terminal.Y, terminal.Yaw),
|
||||||
EmBoundaryType.Goal);
|
EmBoundaryType.Goal);
|
||||||
Verification.Equal(EmTrajectoryValidationFailure.TerminalPoseMismatch, outsidePosition.Failure,
|
Verification.Equal(EmTrajectoryValidationFailure.TerminalPoseMismatch, outsidePosition.Failure,
|
||||||
"3.1-centimetre terminal position error is rejected");
|
"1-millimetre terminal position error is rejected");
|
||||||
|
|
||||||
double degrees = Math.PI / 180d;
|
double degrees = Math.PI / 180d;
|
||||||
EmTrajectory wrappedYaw = Replace(valid, terminalIndex, Clone(terminal, yaw: 179d * degrees));
|
EmTrajectory wrappedYaw = Replace(valid, terminalIndex, Clone(terminal, yaw: Math.PI));
|
||||||
EmTrajectoryValidationResult wrapped = validator.Validate(wrappedYaw, context.EmptyMap, context.Vehicle,
|
EmTrajectoryValidationResult wrapped = validator.Validate(wrappedYaw, context.EmptyMap, context.Vehicle,
|
||||||
context.Configuration, 2, 0.0055d, new Pose2D(terminal.X, terminal.Y, -179d * degrees),
|
context.Configuration, 2, 0.0055d, new Pose2D(terminal.X, terminal.Y, -Math.PI),
|
||||||
EmBoundaryType.Goal);
|
EmBoundaryType.Goal);
|
||||||
Verification.True(wrapped.IsValid, "179 and -179 degrees use normalized yaw error");
|
Verification.True(wrapped.IsValid, "PI and -PI use normalized yaw error");
|
||||||
EmTrajectoryValidationResult insideYaw = validator.Validate(valid, context.EmptyMap, context.Vehicle,
|
|
||||||
context.Configuration, 2, 0.0055d, new Pose2D(terminal.X, terminal.Y, -4.9d * degrees),
|
|
||||||
EmBoundaryType.Goal);
|
|
||||||
Verification.True(insideYaw.IsValid, "4.9-degree terminal yaw error is accepted");
|
|
||||||
EmTrajectoryValidationResult outsideYaw = validator.Validate(valid, context.EmptyMap, context.Vehicle,
|
EmTrajectoryValidationResult outsideYaw = validator.Validate(valid, context.EmptyMap, context.Vehicle,
|
||||||
context.Configuration, 2, 0.0055d, new Pose2D(terminal.X, terminal.Y, -5.1d * degrees),
|
context.Configuration, 2, 0.0055d, new Pose2D(terminal.X, terminal.Y, -0.1d * degrees),
|
||||||
EmBoundaryType.Goal);
|
EmBoundaryType.Goal);
|
||||||
Verification.Equal(EmTrajectoryValidationFailure.TerminalPoseMismatch, outsideYaw.Failure,
|
Verification.Equal(EmTrajectoryValidationFailure.TerminalPoseMismatch, outsideYaw.Failure,
|
||||||
"5.1-degree terminal yaw error is rejected");
|
"0.1-degree terminal yaw error is rejected");
|
||||||
|
|
||||||
EmTrajectory rolling = new EmTrajectoryAssembler().Assemble(
|
EmTrajectory rolling = new EmTrajectoryAssembler().Assemble(
|
||||||
CreatePath(TravelDirection.Forward, 0d, 0d), CreateRollingLongitudinalResult(),
|
CreatePath(TravelDirection.Forward, 0d, 0d), CreateRollingLongitudinalResult(),
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ internal static class TrajectoryObservationChecks
|
|||||||
{
|
{
|
||||||
TrajectoryObservationSettingsChecks.Run();
|
TrajectoryObservationSettingsChecks.Run();
|
||||||
TrajectoryObservationSegmentChecks.Run();
|
TrajectoryObservationSegmentChecks.Run();
|
||||||
|
VerifiesTrajectoryObservationReportWriterCreatesTxt();
|
||||||
VerifiesPainterPoseGeometryReplacesLargeArrow();
|
VerifiesPainterPoseGeometryReplacesLargeArrow();
|
||||||
TrajectoryObservationVisualizationChecks.Run();
|
TrajectoryObservationVisualizationChecks.Run();
|
||||||
VerifiesObservationSourceHasNoActuatorCalls();
|
VerifiesObservationSourceHasNoActuatorCalls();
|
||||||
@@ -47,6 +48,7 @@ internal static class TrajectoryObservationChecks
|
|||||||
VerifiesPresentationTextDescribesObservationWithoutSendingCommand();
|
VerifiesPresentationTextDescribesObservationWithoutSendingCommand();
|
||||||
VerifiesPlanningDiagnosticsKeepRawFailureReason();
|
VerifiesPlanningDiagnosticsKeepRawFailureReason();
|
||||||
VerifiesPublishedPlanningDiagnosticsIncludeTrajectorySummary();
|
VerifiesPublishedPlanningDiagnosticsIncludeTrajectorySummary();
|
||||||
|
VerifiesCurveReportIncludesLsStSpeedAccelerationAndJerk();
|
||||||
VerifiesEmptyChartsReceivePersistentPlanningDiagnostic();
|
VerifiesEmptyChartsReceivePersistentPlanningDiagnostic();
|
||||||
VerifiesLsPresentationUsesPathSOnHorizontalAxis();
|
VerifiesLsPresentationUsesPathSOnHorizontalAxis();
|
||||||
VerifiesPainterStAxesUsePathSAndTimeUnits();
|
VerifiesPainterStAxesUsePathSAndTimeUnits();
|
||||||
@@ -81,6 +83,29 @@ internal static class TrajectoryObservationChecks
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void VerifiesTrajectoryObservationReportWriterCreatesTxt()
|
||||||
|
{
|
||||||
|
string reportDirectory = Path.Combine(Path.GetTempPath(),
|
||||||
|
"em-trajectory-report-" + Guid.NewGuid().ToString("N"));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var writer = new TrajectoryObservationReportWriter(17L, reportDirectory);
|
||||||
|
writer.Append("cycle=1 status=SolverTimedOut primal=140.918");
|
||||||
|
string reportPath = writer.Save();
|
||||||
|
|
||||||
|
Verification.True(File.Exists(reportPath), "trajectory observation txt report exists");
|
||||||
|
string text = File.ReadAllText(reportPath);
|
||||||
|
Verification.True(text.Contains("session=17"), "report includes session identity");
|
||||||
|
Verification.True(text.Contains("OBSERVE_ONLY"), "report records observation-only boundary");
|
||||||
|
Verification.True(text.Contains("SolverTimedOut"), "report includes cycle failure");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (Directory.Exists(reportDirectory))
|
||||||
|
Directory.Delete(reportDirectory, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void VerifiesObservationSourceUsesRequiredOperatorText()
|
private static void VerifiesObservationSourceUsesRequiredOperatorText()
|
||||||
{
|
{
|
||||||
string movementTestSource = Path.Combine(Directory.GetCurrentDirectory(), "ClumsyPilot",
|
string movementTestSource = Path.Combine(Directory.GetCurrentDirectory(), "ClumsyPilot",
|
||||||
@@ -960,6 +985,25 @@ internal static class TrajectoryObservationChecks
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void VerifiesCurveReportIncludesLsStSpeedAccelerationAndJerk()
|
||||||
|
{
|
||||||
|
DateTimeOffset effectiveAt = new DateTimeOffset(2026, 8, 5, 0, 0, 0, TimeSpan.Zero);
|
||||||
|
EmTrajectory trajectory = CreatePublishedTrajectory(effectiveAt);
|
||||||
|
DirectionSegmentView segment = CreateStraightSegment();
|
||||||
|
TrajectoryObservationCharts charts = TrajectoryObservationCharts.Build(trajectory, segment, 0.5d);
|
||||||
|
|
||||||
|
string text = TrajectoryObservationDiagnostics.CreateCurveReport(charts, trajectory).Text;
|
||||||
|
|
||||||
|
foreach (string section in new[]
|
||||||
|
{
|
||||||
|
"LS_CURVE", "ST_CURVE", "SPEED_CURVE", "ACCELERATION_CURVE", "JERK_CURVE",
|
||||||
|
"referenceS(m)", "time(s)", "signedSpeed(m/s)", "acceleration(m/s2)", "jerk(m/s3)",
|
||||||
|
})
|
||||||
|
{
|
||||||
|
Verification.True(text.Contains(section), "curve report includes " + section);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void VerifiesEmptyChartsReceivePersistentPlanningDiagnostic()
|
private static void VerifiesEmptyChartsReceivePersistentPlanningDiagnostic()
|
||||||
{
|
{
|
||||||
string presentationPath = Path.Combine(Directory.GetCurrentDirectory(), "ClumsyPilot",
|
string presentationPath = Path.Combine(Directory.GetCurrentDirectory(), "ClumsyPilot",
|
||||||
|
|||||||
Reference in New Issue
Block a user