fix: publish distinct EM observation semantics

This commit is contained in:
梁薄云
2026-08-07 09:28:55 +08:00
parent 5d1c875584
commit d33257cc46
8 changed files with 313 additions and 30 deletions
@@ -321,7 +321,8 @@ internal static class TrajectoryObservationMovementTestRunner
DrawIfCurrent(sessionId, bootstrap, observation, charts, runtimeState, diagnostic);
webPublisher?.TryPublish(now, () => dynamicSnapshotBuilder.Build(
Interlocked.Increment(ref visualizationSequence), tick, controller.ActiveSegment,
controller.PreviousTrajectoryForVisualization, controller.CreateEffectiveConfigurationSnapshot()));
controller.PreviousTrajectoryForVisualization, bootstrap.Vehicle,
controller.CreateEffectiveConfigurationSnapshot()));
if (runtimeState.WaitingAtGearSwitch && !gearSwitchWaitingPrinted)
{
LogIfCurrent(sessionId, runtimeState.WorldNotice);
@@ -13,10 +13,12 @@ public sealed class TrajectoryObservationDynamicSnapshotBuilder
private readonly TrajectoryObservationHandoffAnalyzer handoff = new TrajectoryObservationHandoffAnalyzer();
public PlanningVisualizationDynamicSnapshot Build(long sequence, TrajectoryObservationLoopTick tick,
DirectionSegmentView activeSegment, EmTrajectory previousTrajectoryForVisualization, EmPlannerConfiguration configuration)
DirectionSegmentView activeSegment, EmTrajectory previousTrajectoryForVisualization,
VehicleParameters vehicle, EmPlannerConfiguration configuration)
{
if (tick == null) throw new ArgumentNullException(nameof(tick));
if (activeSegment == null) throw new ArgumentNullException(nameof(activeSegment));
if (vehicle == null) throw new ArgumentNullException(nameof(vehicle));
if (configuration == null) throw new ArgumentNullException(nameof(configuration));
EmTrajectory current = tick.Observation.PublishedTrajectory;
@@ -24,14 +26,17 @@ public sealed class TrajectoryObservationDynamicSnapshotBuilder
AddSegment(polylines, activeSegment);
AddTrajectory(polylines, "previous", "上一轨迹", previousTrajectoryForVisualization, VisualizationLineStyle.Dashed);
AddTrajectory(polylines, "current", "当前轨迹", current, VisualizationLineStyle.Solid);
AddTrajectory(polylines, "current-horizon", "当前规划视界", current, VisualizationLineStyle.Solid);
IReadOnlyList<VisualizationChart> chartList = current == null
? Array.Empty<VisualizationChart>()
: charts.Build(current, activeSegment, configuration);
: charts.Build(current, activeSegment, vehicle, configuration);
int projectionFailures = current == null ? 0 : charts.CountProjectionFailures(current, activeSegment, configuration);
TrajectoryObservationHandoffMetrics metrics = handoff.Analyze(current, previousTrajectoryForVisualization, activeSegment);
EmTrajectoryPoint terminal = current == null ? null : current.Points[current.Points.Count - 1];
var markers = new List<VisualizationMarker>();
AddVehicleMarker(markers, tick);
AddPlanStartMarker(markers, current);
AddBoundaryMarker(markers, activeSegment);
var status = new List<VisualizationValue>
{
Value("活动段", "ActiveSegmentIndex", activeSegment.SegmentIndex.ToString(CultureInfo.InvariantCulture), "", "normal"),
@@ -57,7 +62,7 @@ public sealed class TrajectoryObservationDynamicSnapshotBuilder
return new PlanningVisualizationDynamicSnapshot(sequence, tick.Observation.ObservedAtUtc, "OBSERVE_ONLY",
activeSegment.SegmentIndex, activeSegment.Direction.ToString(), new VisualizationPose(
tick.Observation.VehicleState.Pose.X, tick.Observation.VehicleState.Pose.Y,
tick.Observation.VehicleState.Pose.Heading), polylines, Array.Empty<VisualizationMarker>(), chartList, status,
tick.Observation.VehicleState.Pose.Heading), polylines, markers, chartList, status,
new VisualizationCycleSummary(cycle == null ? 0L : cycle.Version, tick.Observation.ObservedAtUtc,
cycle == null ? "pending" : cycle.Result.Status.ToString(), cycle != null && cycle.Published,
Math.Max(0d, tick.LatestPlanningElapsed.TotalMilliseconds), activeSegment.SegmentIndex,
@@ -84,6 +89,39 @@ public sealed class TrajectoryObservationDynamicSnapshotBuilder
lines.Add(new VisualizationPolyline(id, legend, id, style, points));
}
private static void AddVehicleMarker(ICollection<VisualizationMarker> markers,
TrajectoryObservationLoopTick tick)
{
markers.Add(new VisualizationMarker("vehicle", "vehicle", "车辆",
new VisualizationPoint(tick.Observation.VehicleState.Pose.X,
tick.Observation.VehicleState.Pose.Y)));
}
private static void AddPlanStartMarker(ICollection<VisualizationMarker> markers, EmTrajectory trajectory)
{
if (trajectory == null || trajectory.Points.Count == 0) return;
EmTrajectoryPoint start = trajectory.Points[0];
markers.Add(new VisualizationMarker("plan-start", "plan-start", "计划起点",
new VisualizationPoint(start.X, start.Y)));
}
private static void AddBoundaryMarker(ICollection<VisualizationMarker> markers,
DirectionSegmentView segment)
{
var point = segment.Points[segment.Points.Count - 1];
if (segment.EndBoundary.BoundaryType == EmBoundaryType.GearSwitchApproach)
{
markers.Add(new VisualizationMarker("gear-switch-end", "gear-switch-end",
"换向点 " + (segment.SegmentIndex + 1) + " / s_end",
new VisualizationPoint(point.X, point.Y)));
}
else if (segment.EndBoundary.BoundaryType == EmBoundaryType.Goal)
{
markers.Add(new VisualizationMarker("final-goal", "final-goal", "终点 / s_end",
new VisualizationPoint(point.X, point.Y)));
}
}
private static string RollingTerminalText(EmTrajectory trajectory)
{
if (trajectory == null) return "不可用";
@@ -10,26 +10,32 @@ namespace MultiWheelC.TrajectoryPlanning.TrajectoryObservation;
public sealed class TrajectoryObservationKinematicChartBuilder
{
public IReadOnlyList<VisualizationChart> Build(EmTrajectory trajectory, DirectionSegmentView segment,
EmPlannerConfiguration configuration)
VehicleParameters vehicle, EmPlannerConfiguration configuration)
{
if (trajectory == null) throw new ArgumentNullException(nameof(trajectory));
if (segment == null) throw new ArgumentNullException(nameof(segment));
if (vehicle == null) throw new ArgumentNullException(nameof(vehicle));
if (configuration == null) throw new ArgumentNullException(nameof(configuration));
LsBuildResult ls = BuildLs(trajectory, segment, configuration.Frenet.MaximumProjectionDistanceMeters);
double curvatureLimit = CurvatureLimit(vehicle);
double terminalTime = trajectory.Points.Count == 0 ? 0d : trajectory.Points[trajectory.Points.Count - 1].TimeFromStart;
return new[]
{
Chart("ls", "横向偏移", "ReferenceS (m)", "l (m)", ls.Points,
"完整活动方向段投影;投影失败=" + ls.ProjectionFailureCount.ToString(CultureInfo.InvariantCulture)),
Chart("st", "时空轨迹", "t (s)", "PathS (m)", Points(trajectory, p => p.PathS), ""),
"完整活动方向段投影;投影失败=" + ls.ProjectionFailureCount.ToString(CultureInfo.InvariantCulture),
new[] { new VisualizationChartAnnotation("ls-s-end", "s-end", "s_end",
segment.SourceStartArcLength + segment.LengthMeters) }),
Chart("st", "时空轨迹", "t (s)", "PathS (m)", Points(trajectory, p => p.PathS), "",
new[] { new VisualizationChartAnnotation("st-s-end", "s-end", "s_end", terminalTime) }),
WithSymmetricLimit("curvature-s", "曲率-距离", "s (m)", "κ (m⁻¹)",
PointsBySegmentS(trajectory), configuration.Longitudinal.MaximumCurvatureRatePerMeterPerSecond),
PointsBySegmentS(trajectory), curvatureLimit),
WithSymmetricLimit("curvature-t", "曲率-时间", "t (s)", "κ (m⁻¹)",
Points(trajectory, p => p.VehicleCurvature), configuration.Longitudinal.MaximumCurvatureRatePerMeterPerSecond),
Points(trajectory, p => p.VehicleCurvature), curvatureLimit),
Velocity(trajectory, configuration),
Acceleration(trajectory, configuration),
Jerk(trajectory, configuration),
Chart("yaw-rate-t", "横摆角速度", "t (s)", "yaw rate (rad/s)",
Chart("yaw-rate-t", "横摆角速度", "t (s)", "ω (rad/s)",
Points(trajectory, p => p.YawRate), "生效配置没有独立横摆角速度上限"),
};
}
@@ -98,10 +104,12 @@ public sealed class TrajectoryObservationKinematicChartBuilder
}
private static VisualizationChart Chart(string id, string title, string xAxis, string yAxis,
IReadOnlyList<VisualizationPoint> points, string note)
IReadOnlyList<VisualizationPoint> points, string note,
IReadOnlyList<VisualizationChartAnnotation> annotations = null)
{
return new VisualizationChart(id, title, xAxis, yAxis,
new[] { new VisualizationSeries(id + "-current", "当前轨迹", VisualizationLineStyle.Solid, points) }, note);
new[] { new VisualizationSeries(id + "-current", "当前轨迹", VisualizationLineStyle.Solid, points) },
note, annotations ?? Array.Empty<VisualizationChartAnnotation>());
}
private static IReadOnlyList<VisualizationPoint> Points(EmTrajectory trajectory, Func<EmTrajectoryPoint, double> y)
@@ -120,6 +128,21 @@ public sealed class TrajectoryObservationKinematicChartBuilder
return points;
}
private static double CurvatureLimit(VehicleParameters vehicle)
{
if (vehicle.MaximumCurvaturePerMeter.HasValue)
{
return Math.Abs(vehicle.MaximumCurvaturePerMeter.Value);
}
if (vehicle.MinimumTurningRadiusMeters.HasValue && vehicle.MinimumTurningRadiusMeters.Value > 0d)
{
return 1d / vehicle.MinimumTurningRadiusMeters.Value;
}
throw new ArgumentException("Vehicle curvature limit is required.", nameof(vehicle));
}
private static LsBuildResult BuildLs(EmTrajectory trajectory, DirectionSegmentView segment,
double maximumProjectionDistanceMeters)
{
@@ -57,11 +57,20 @@ public sealed class TrajectoryObservationStaticSnapshotBuilder
{
var markers = new List<VisualizationMarker>();
foreach (DirectionSegmentView segment in bootstrap.Segments)
{
if (segment.EndBoundary.BoundaryType == EmBoundaryType.GearSwitchApproach)
{
var point = segment.Points[segment.Points.Count - 1];
markers.Add(new VisualizationMarker("gear-switch-" + segment.SegmentIndex, "gear-switch",
"换向 " + (segment.SegmentIndex + 1), new VisualizationPoint(point.X, point.Y)));
markers.Add(new VisualizationMarker("gear-switch-end-" + segment.SegmentIndex, "gear-switch-end",
"换向 " + (segment.SegmentIndex + 1) + " / s_end",
new VisualizationPoint(point.X, point.Y)));
}
else if (segment.EndBoundary.BoundaryType == EmBoundaryType.Goal)
{
var point = segment.Points[segment.Points.Count - 1];
markers.Add(new VisualizationMarker("final-goal-" + segment.SegmentIndex, "final-goal",
"终点 / s_end", new VisualizationPoint(point.X, point.Y)));
}
}
return markers;
}
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
namespace TrajectoryPlanningVisualization;
@@ -26,10 +27,39 @@ public sealed class VisualizationSeries
public IReadOnlyList<VisualizationPoint> Points { get; }
}
public sealed class VisualizationChartAnnotation
{
public VisualizationChartAnnotation(string id, string kind, string labelChinese,
double x, double? y = null)
{
Id = VisualizationContract.RequireText(id, nameof(id));
Kind = VisualizationContract.RequireText(kind, nameof(kind));
LabelChinese = VisualizationContract.RequireText(labelChinese, nameof(labelChinese));
VisualizationContract.RequireFinite(x, nameof(x));
VisualizationContract.RequireFinite(y, nameof(y));
X = x;
Y = y;
}
public string Id { get; }
public string Kind { get; }
public string LabelChinese { get; }
public double X { get; }
public double? Y { get; }
}
public sealed class VisualizationChart
{
public VisualizationChart(string id, string chineseTitle, string xAxisLabel, string yAxisLabel,
IReadOnlyList<VisualizationSeries> series, string noteChinese = "")
: this(id, chineseTitle, xAxisLabel, yAxisLabel, series, noteChinese,
Array.Empty<VisualizationChartAnnotation>())
{
}
public VisualizationChart(string id, string chineseTitle, string xAxisLabel, string yAxisLabel,
IReadOnlyList<VisualizationSeries> series, string noteChinese,
IReadOnlyList<VisualizationChartAnnotation> annotations)
{
Id = VisualizationContract.RequireText(id, nameof(id));
ChineseTitle = VisualizationContract.RequireText(chineseTitle, nameof(chineseTitle));
@@ -37,6 +67,7 @@ public sealed class VisualizationChart
YAxisLabel = VisualizationContract.RequireText(yAxisLabel, nameof(yAxisLabel));
Series = VisualizationContract.Copy(series, nameof(series));
NoteChinese = VisualizationContract.RequireText(noteChinese, nameof(noteChinese));
Annotations = VisualizationContract.Copy(annotations, nameof(annotations));
}
public string Id { get; }
@@ -45,4 +76,5 @@ public sealed class VisualizationChart
public string YAxisLabel { get; }
public IReadOnlyList<VisualizationSeries> Series { get; }
public string NoteChinese { get; }
public IReadOnlyList<VisualizationChartAnnotation> Annotations { get; }
}
@@ -21,6 +21,7 @@ internal static class TrajectoryObservationVisualizationChecks
VerifiesKinematicChartsKeepKnotAndIntervalSemantics();
VerifiesHandoffUsesSharedReferenceSAndSurvivesProjectionFailure();
VerifiesDynamicSnapshotContainsObservationEvidenceWithoutCyclePoints();
VerifiesFullModeSnapshotPublishesSingleCurrentTrajectoryAndBoundaryMarkers();
VerifiesWebPublisherFusesFaultWithoutStoppingObserverTicks();
VerifiesWebPublisherGatesAtConfiguredCadenceAndSkipsDisabledOutput();
}
@@ -94,6 +95,17 @@ internal static class TrajectoryObservationVisualizationChecks
break;
}
Verification.True(verifiedOccupiedCell, "static fixture has an occupied cell");
EmPlannerConfiguration defaults = EmPlannerConfiguration.CreateDefault();
TrajectoryObservationBootstrapResult gearBootstrap = TrajectoryObservationBootstrapResult.Success(
bootstrap.Job, bootstrap.CoarseResult, bootstrap.SmoothedPath,
new[] { CreateGearSwitchSegment(0, 0d) });
PlanningVisualizationStaticSnapshot gearStatic = new TrajectoryObservationStaticSnapshotBuilder()
.Build(gearBootstrap, defaults, settings.CreateValidatedSnapshot(), 0L);
Verification.True(gearStatic.StaticMarkers.Any(x => x.Kind == "gear-switch-end"),
"static snapshot marks gear-switch s_end");
Verification.True(snapshot.StaticMarkers.Any(x => x.Kind == "final-goal"),
"static snapshot marks final goal s_end");
}
private static void VerifiesKinematicChartsKeepKnotAndIntervalSemantics()
@@ -103,8 +115,16 @@ internal static class TrajectoryObservationVisualizationChecks
EmTrajectory trajectory = CreateTrajectory("rolling", DateTimeOffset.UnixEpoch, 21,
EmTerminalType.Goal, EmLongitudinalMode.RollingContinuation, TravelDirection.Forward);
var vehicle = new VehicleParameters
{
LengthMeters = 0.8d,
WidthMeters = 0.6d,
SafetyMarginMeters = 0.05d,
MaximumCurvaturePerMeter = 0.4d,
MinimumTurningRadiusMeters = 2.5d,
};
IReadOnlyList<VisualizationChart> charts = new TrajectoryObservationKinematicChartBuilder()
.Build(trajectory, segment, configuration);
.Build(trajectory, segment, vehicle, configuration);
Verification.Equal(8, charts.Count, "all eight kinematic charts exported");
Verification.Equal(20, FindChart(charts, "jerk-t").Series[0].Points.Count, "jerk has N-1 intervals");
@@ -118,9 +138,49 @@ internal static class TrajectoryObservationVisualizationChecks
id + " has an effective limit line");
Verification.True(!FindChart(charts, "yaw-rate-t").Series.Any(x => x.LineStyle == VisualizationLineStyle.Limit),
"yaw rate has no invented limit");
configuration.Longitudinal.MaximumCurvatureRatePerMeterPerSecond = 1.25d;
charts = new TrajectoryObservationKinematicChartBuilder().Build(trajectory, segment, vehicle, configuration);
Verification.Equal("ω (rad/s)", FindChart(charts, "yaw-rate-t").YAxisLabel,
"yaw-rate axis uses omega unit");
Verification.NearlyEqual(0.4d, FindChart(charts, "curvature-s")
.Series.Single(x => x.Id == "curvature-s-positive-limit").Points[0].Y,
"curvature hard limit uses vehicle curvature");
Verification.True(FindChart(charts, "curvature-s").Series[0].Points.Any(p => Math.Abs(p.Y) > 1e-6),
"curvature-distance uses vehicle curvature data");
PropertyInfo? annotationsProperty = typeof(VisualizationChart).GetProperty("Annotations");
Verification.True(annotationsProperty != null, "chart exposes annotation collection");
if (annotationsProperty != null)
{
object annotationList = annotationsProperty!.GetValue(FindChart(charts, "st"))!;
Verification.True(annotationList is System.Collections.IEnumerable,
"ST annotations are enumerable");
var annotations = new List<object>();
foreach (object annotation in (System.Collections.IEnumerable)annotationList!)
{
annotations.Add(annotation);
}
Verification.True(annotations.Count > 0, "ST chart marks s_end");
Verification.True(annotations.Any(annotation =>
(string)annotation.GetType().GetProperty("Kind")!.GetValue(annotation)! == "s-end"),
"ST s_end annotation kind is explicit");
object lsAnnotationList = annotationsProperty!.GetValue(FindChart(charts, "ls"))!;
var lsAnnotations = new List<object>();
foreach (object annotation in (System.Collections.IEnumerable)lsAnnotationList!)
{
lsAnnotations.Add(annotation);
}
Verification.True(lsAnnotations.Count > 0, "LS chart marks s_end");
Verification.True(lsAnnotations.Any(annotation =>
(string)annotation.GetType().GetProperty("Kind")!.GetValue(annotation)! == "s-end"),
"LS s_end annotation kind is explicit");
}
Verification.True(typeof(TrajectoryObservationDynamicSnapshotBuilder).GetMethods().Any(method =>
method.Name == "Build" &&
method.GetParameters().Any(parameter => parameter.ParameterType == typeof(VehicleParameters))),
"dynamic builder accepts vehicle parameters");
EmTrajectory unprojectable = CreateTrajectory("unprojectable", DateTimeOffset.UnixEpoch, 21,
EmTerminalType.Goal, EmLongitudinalMode.RollingContinuation, TravelDirection.Forward, 100d);
Verification.True(FindChart(new TrajectoryObservationKinematicChartBuilder().Build(unprojectable, segment, configuration), "ls")
Verification.True(FindChart(new TrajectoryObservationKinematicChartBuilder().Build(unprojectable, segment, vehicle, configuration), "ls")
.NoteChinese.Contains("投影失败=21"), "LS chart reports every failed full-segment projection");
}
@@ -166,12 +226,15 @@ internal static class TrajectoryObservationVisualizationChecks
EmLongitudinalMode.RollingContinuation, TravelDirection.Forward);
PlanningVisualizationDynamicSnapshot snapshot = new TrajectoryObservationDynamicSnapshotBuilder()
.Build(5L, tick, controller.ActiveSegment, previous, configuration);
.Build(5L, tick, controller.ActiveSegment, previous, bootstrap.Vehicle, configuration);
Verification.True(snapshot.DynamicPolylines.Any(x => x.Id == "current"), "dynamic snapshot contains current trajectory");
Verification.True(snapshot.DynamicPolylines.Any(x => x.Id == "previous"), "dynamic snapshot contains previous trajectory");
Verification.True(snapshot.DynamicPolylines.Any(x => x.Id == "active-segment"), "dynamic snapshot contains active segment highlight");
Verification.True(snapshot.DynamicPolylines.Any(x => x.Id == "current-horizon"), "dynamic snapshot contains projected horizon");
Verification.Equal(1, snapshot.DynamicPolylines.Count(x => x.Id == "current"),
"same current trajectory is published once");
Verification.True(!snapshot.DynamicPolylines.Any(x => x.Id == "current-horizon"),
"identical current trajectory is not duplicated as current horizon");
foreach (string rawName in new[] { "ActiveSegmentIndex", "ActiveDirection", "PlanningStatus", "LongitudinalMode",
"TerminalType", "PlanningElapsedMilliseconds", "TrajectoryAgeSeconds", "LsProjectionFailures",
"RollingTerminalConstraint", "TerminalVelocity", "TerminalAcceleration", "DeltaPositionMeters",
@@ -197,7 +260,8 @@ internal static class TrajectoryObservationVisualizationChecks
TrajectoryObservationLoopTick exactTick = new TrajectoryObservationLoop(exactController)
.Tick(now.AddSeconds(settings.ObserverPeriodSeconds), state, CancellationToken.None);
PlanningVisualizationDynamicSnapshot exactSnapshot = new TrajectoryObservationDynamicSnapshotBuilder().Build(6L,
exactTick, exactController.ActiveSegment, null, ReadEffectiveConfiguration(exactController));
exactTick, exactController.ActiveSegment, null, bootstrap.Vehicle,
ReadEffectiveConfiguration(exactController));
Verification.True(exactSnapshot.StatusValues.Single(x => x.RawName == "RollingTerminalConstraint").Value.Contains("精确停车锚点"),
"exact-stop status describes the true stop anchor");
Verification.NearlyEqual(exactStop.Points[exactStop.Points.Count - 1].SignedLongitudinalVelocity,
@@ -213,11 +277,50 @@ internal static class TrajectoryObservationVisualizationChecks
TrajectoryObservationLoopTick offSegmentTick = new TrajectoryObservationLoop(offSegmentController)
.Tick(now.AddSeconds(settings.ObserverPeriodSeconds), state, CancellationToken.None);
PlanningVisualizationDynamicSnapshot recovered = new TrajectoryObservationDynamicSnapshotBuilder().Build(7L,
offSegmentTick, offSegmentController.ActiveSegment, null, ReadEffectiveConfiguration(offSegmentController));
offSegmentTick, offSegmentController.ActiveSegment, null, bootstrap.Vehicle,
ReadEffectiveConfiguration(offSegmentController));
Verification.Equal("21", recovered.StatusValues.Single(x => x.RawName == "LsProjectionFailures").Value,
"projection failure is reported while dynamic snapshot construction remains available");
}
private static void VerifiesFullModeSnapshotPublishesSingleCurrentTrajectoryAndBoundaryMarkers()
{
var settings = new TrajectoryObservationSettings();
TrajectoryObservationBootstrapResult goalBootstrap = Bootstrap(settings);
TrajectoryObservationBootstrapResult gearBootstrap = TrajectoryObservationBootstrapResult.Success(
goalBootstrap.Job, goalBootstrap.CoarseResult, goalBootstrap.SmoothedPath,
new[] { CreateGearSwitchSegment(0, 0d) });
EmPlannerConfiguration configuration = EmPlannerConfiguration.CreateDefault();
PlanningVisualizationDynamicSnapshot gearSnapshot = BuildDynamicSnapshot(gearBootstrap, settings, configuration,
CreateTrajectory("full-gear", DateTimeOffset.UnixEpoch.AddSeconds(1d), 21, EmTerminalType.GearSwitch,
EmLongitudinalMode.ExactStopAtBoundary, TravelDirection.Forward, 0d, 0d,
EmPlanningScope.FullDirectionSegment));
Verification.Equal(1, gearSnapshot.DynamicPolylines.Count(x => x.Kind == "current"),
"full mode publishes one current trajectory");
Verification.True(!gearSnapshot.DynamicPolylines.Any(x => x.Id == "current-horizon"),
"full mode does not duplicate current horizon");
Verification.True(gearSnapshot.DynamicMarkers.Any(x => x.Kind == "vehicle"),
"dynamic snapshot marks the vehicle");
Verification.True(gearSnapshot.DynamicMarkers.Any(x => x.Kind == "plan-start"),
"dynamic snapshot marks the plan start");
VisualizationMarker gearEnd = gearSnapshot.DynamicMarkers.Single(x => x.Kind == "gear-switch-end");
Verification.True(gearEnd.LabelChinese.Contains("换向点") && gearEnd.LabelChinese.Contains("s_end"),
"gear switch boundary uses the explicit s_end label");
PlanningVisualizationDynamicSnapshot goalSnapshot = BuildDynamicSnapshot(goalBootstrap, settings, configuration,
CreateTrajectory("full-goal", DateTimeOffset.UnixEpoch.AddSeconds(2d), 21, EmTerminalType.Goal,
EmLongitudinalMode.ExactStopAtBoundary, TravelDirection.Forward, 0d, 0d,
EmPlanningScope.FullDirectionSegment));
Verification.Equal(1, goalSnapshot.DynamicPolylines.Count(x => x.Kind == "current"),
"goal full mode publishes one current trajectory");
Verification.True(!goalSnapshot.DynamicPolylines.Any(x => x.Id == "current-horizon"),
"goal full mode does not duplicate current horizon");
VisualizationMarker finalGoal = goalSnapshot.DynamicMarkers.Single(x => x.Kind == "final-goal");
Verification.True(finalGoal.LabelChinese.Contains("终点") && finalGoal.LabelChinese.Contains("s_end"),
"final goal boundary uses the explicit s_end label");
}
private static void VerifiesWebPublisherFusesFaultWithoutStoppingObserverTicks()
{
var settings = new TrajectoryObservationSettings { EnableWebVisualization = true };
@@ -444,6 +547,22 @@ internal static class TrajectoryObservationVisualizationChecks
return charts.Single(x => x.Id == id);
}
private static PlanningVisualizationDynamicSnapshot BuildDynamicSnapshot(
TrajectoryObservationBootstrapResult bootstrap, TrajectoryObservationSettings settings,
EmPlannerConfiguration configuration, EmTrajectory trajectory)
{
DateTimeOffset now = DateTimeOffset.UnixEpoch.AddMinutes(1);
var controller = new TrajectoryObservationController(bootstrap, settings,
new FixedPlanningService(trajectory), "snapshot-" + trajectory.Metadata.TrajectoryId);
var loop = new TrajectoryObservationLoop(controller);
var state = new VehicleMotionState(new Pose2D(0d, 0d, 0d), 0.1d, null, now, 1L);
controller.StartCycle(now, state, CancellationToken.None).GetAwaiter().GetResult();
TrajectoryObservationLoopTick tick = loop.Tick(now.AddSeconds(settings.ObserverPeriodSeconds),
state, CancellationToken.None);
return new TrajectoryObservationDynamicSnapshotBuilder().Build(
1L, tick, controller.ActiveSegment, null, bootstrap.Vehicle, configuration);
}
private static DirectionSegmentView CreateStraightSegment(int index, double sourceStart)
{
var points = new List<MultiWheelC.TrajectoryPlanning.PathSmoothing.SmoothedPathPoint>
@@ -458,8 +577,23 @@ internal static class TrajectoryObservationVisualizationChecks
new ReferenceBoundary(index, 10d, EmBoundaryType.Goal, sourceStart + 10d), sourceStart);
}
private static DirectionSegmentView CreateGearSwitchSegment(int index, double sourceStart)
{
var points = new List<MultiWheelC.TrajectoryPlanning.PathSmoothing.SmoothedPathPoint>
{
new MultiWheelC.TrajectoryPlanning.PathSmoothing.SmoothedPathPoint(0d, 0d, 0d, 0d, 0d,
TravelDirection.Forward, 0d, 0d, 1d, false, MultiWheelC.TrajectoryPlanning.PathSmoothing.SmoothedPathPointSource.Anchor),
new MultiWheelC.TrajectoryPlanning.PathSmoothing.SmoothedPathPoint(10d, 0d, 0d, 0d, 10d,
TravelDirection.Forward, 0d, 0d, 1d, false, MultiWheelC.TrajectoryPlanning.PathSmoothing.SmoothedPathPointSource.Anchor),
};
return new DirectionSegmentView(index, TravelDirection.Forward, points,
new ReferenceBoundary(index, 0d, EmBoundaryType.None, sourceStart),
new ReferenceBoundary(index, 10d, EmBoundaryType.GearSwitchApproach, sourceStart + 10d), sourceStart);
}
private static EmTrajectory CreateTrajectory(string id, DateTimeOffset effectiveAt, int count, EmTerminalType terminal,
EmLongitudinalMode mode, TravelDirection direction, double startX = 0d, double startPathS = 1000d)
EmLongitudinalMode mode, TravelDirection direction, double startX = 0d, double startPathS = 1000d,
EmPlanningScope planningScope = EmPlanningScope.RollingHorizon)
{
var points = new List<EmTrajectoryPoint>();
for (int index = 0; index < count; index++)
@@ -470,7 +604,7 @@ internal static class TrajectoryObservationVisualizationChecks
0.2d + t, 0.3d + t));
}
return new EmTrajectory(new EmTrajectoryMetadata(id, effectiveAt, effectiveAt, 1L, "visualization-reference", 1L,
string.Empty, 0, direction, terminal, mode, EmPlanningScope.RollingHorizon), points);
string.Empty, 0, direction, terminal, mode, planningScope), points);
}
private sealed class FixedPlanningService : IEmPlanningService
@@ -13,6 +13,7 @@ internal static class ContractChecks
CopiesCollectionsAndRejectsNonFiniteNumbers();
StoresOccupancyAsDefensiveCompactBits();
ExposesImmutableSnapshotContracts();
ExposesChartAnnotationContract();
DocumentsPublicSafetyAndLifecycleBoundary();
}
@@ -87,6 +88,36 @@ internal static class ContractChecks
}
}
private static void ExposesChartAnnotationContract()
{
Type chartType = typeof(VisualizationChart);
PropertyInfo annotationsProperty = chartType.GetProperty("Annotations");
Verification.True(annotationsProperty != null, "chart exposes immutable annotations");
Type annotationType = chartType.Assembly.GetType("TrajectoryPlanningVisualization.VisualizationChartAnnotation");
Verification.True(annotationType != null, "chart annotation contract type exists");
var chart = new VisualizationChart("annotation-legacy", "标题", "x (m)", "y (m)",
new[]
{
new VisualizationSeries("s", "轨迹", VisualizationLineStyle.Solid,
new[] { new VisualizationPoint(0d, 0d) })
});
if (annotationsProperty == null)
{
return;
}
object value = annotationsProperty.GetValue(chart);
Verification.True(value is System.Collections.IEnumerable, "chart annotations are enumerable");
int count = 0;
foreach (object annotation in (System.Collections.IEnumerable)value)
{
count++;
}
Verification.Equal(0, count, "legacy chart constructor keeps annotations empty");
AssertNoSetters(chartType);
}
private static void DocumentsPublicSafetyAndLifecycleBoundary()
{
string readmePath = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory,
@@ -20,7 +20,12 @@ internal static class SampleSnapshotFactory
new VisualizationPolyline("global", "全局参考路径", "global", VisualizationLineStyle.Solid,
Points((0d, 1d), (3d, 1d), (6d, 2d), (8d, 4d), (11d, 6d)))
},
new[] { new VisualizationMarker("gear-1", "gear-switch", "换向 1", new VisualizationPoint(6d, 2d)) },
new[]
{
new VisualizationMarker("gear-1", "gear-switch", "换向 1", new VisualizationPoint(6d, 2d)),
new VisualizationMarker("gear-1-end", "gear-switch-end", "换向点 1 / s_end", new VisualizationPoint(6d, 2d)),
new VisualizationMarker("final-goal", "final-goal", "终点 / s_end", new VisualizationPoint(11d, 6d))
},
new[]
{
new VisualizationDirectionSegment(0, "forward", false, true, Points((0d, 1d), (3d, 1d), (6d, 2d))),
@@ -55,7 +60,12 @@ internal static class SampleSnapshotFactory
new VisualizationPolyline("previous", "上一轮轨迹", "previous", VisualizationLineStyle.Dashed, Points((1d, 1d), (2.5d, 1d), (4d, 1.3d))),
new VisualizationPolyline("current", "当前轨迹", "current", VisualizationLineStyle.Solid, Points((2d + offset, 1d), (3.8d + offset, 1.2d), (5.8d + offset, 1.9d)))
},
new[] { new VisualizationMarker("handoff", "handoff", "交接", new VisualizationPoint(2d + offset, 1d)) },
new[]
{
new VisualizationMarker("vehicle", "vehicle", "车辆", new VisualizationPoint(2d + offset, 1d)),
new VisualizationMarker("plan-start", "plan-start", "计划起点", new VisualizationPoint(2d + offset, 1d)),
new VisualizationMarker("gear-switch-end", "gear-switch-end", "换向点 1 / s_end", new VisualizationPoint(6d, 2d))
},
CreateCharts(offset),
new[]
{
@@ -70,21 +80,26 @@ internal static class SampleSnapshotFactory
{
return new[]
{
Chart("ls", "横向偏移", "ReferenceS (m)", "l (m)", Points((0d, .05d), (2d, .08d), (4d, .02d))),
Chart("st", "时空轨迹", "t (s)", "PathS (m)", Points((0d, 0d), (1d, 1.2d), (2d, 3.0d))),
Chart("ls", "横向偏移", "ReferenceS (m)", "l (m)", Points((0d, .05d), (2d, .08d), (4d, .02d)),
new[] { new VisualizationChartAnnotation("ls-s-end", "s-end", "s_end", 4d) }),
Chart("st", "时空轨迹", "t (s)", "PathS (m)", Points((0d, 0d), (1d, 1.2d), (2d, 3.0d)),
new[] { new VisualizationChartAnnotation("st-s-end", "s-end", "s_end", 2d) }),
Chart("curvature-s", "曲率—距离", "s (m)", "κ (m⁻¹)", Points((0d, .01d), (2d, .04d), (4d, .02d))),
Chart("curvature-t", "曲率—时间", "t (s)", "κ (m⁻¹)", Points((0d, .01d), (1d, .04d), (2d, .02d))),
Chart("velocity-t", "速度", "t (s)", "v (m/s)", Points((0d, 0d), (1d, 1d), (2d, 1.2d + offset))),
Chart("acceleration-t", "加速度", "t (s)", "a (m/s²)", Points((0d, .8d), (1d, .2d), (2d, 0d))),
Chart("jerk-t", "加加速度", "t (s)", "j (m/s³)", Points((0d, -.6d), (1d, -.2d))),
Chart("yaw-rate-t", "横摆角速度", "t (s)", "yaw rate (rad/s)", Points((0d, 0d), (1d, .08d), (2d, .04d)))
Chart("yaw-rate-t", "横摆角速度", "t (s)", "ω (rad/s)", Points((0d, 0d), (1d, .08d), (2d, .04d)))
};
}
private static VisualizationChart Chart(string id, string title, string xAxis, string yAxis, IReadOnlyList<VisualizationPoint> points)
private static VisualizationChart Chart(string id, string title, string xAxis, string yAxis,
IReadOnlyList<VisualizationPoint> points,
IReadOnlyList<VisualizationChartAnnotation> annotations = null)
{
return new VisualizationChart(id, title, xAxis, yAxis,
new[] { new VisualizationSeries(id + "-current", "当前轨迹", VisualizationLineStyle.Solid, points) });
new[] { new VisualizationSeries(id + "-current", "当前轨迹", VisualizationLineStyle.Solid, points) },
"", annotations ?? Array.Empty<VisualizationChartAnnotation>());
}
private static IReadOnlyList<VisualizationPoint> Points(params (double x, double y)[] values)