diff --git a/MultiWheelC/Experiments/NewControllerTrackingTests.cs b/MultiWheelC/Experiments/NewControllerTrackingTests.cs index ff48c25..559f71d 100644 --- a/MultiWheelC/Experiments/NewControllerTrackingTests.cs +++ b/MultiWheelC/Experiments/NewControllerTrackingTests.cs @@ -47,7 +47,7 @@ namespace MultiWheelC /// /// 获取或设置参考速度减速度,单位为m/s²。 /// - public double DecelerationMetersPerSecondSquared = 0.20; + public double DecelerationMetersPerSecondSquared = 0.12; /// /// 获取或设置离散轨迹点间距,单位为m。 @@ -294,4 +294,314 @@ namespace MultiWheelC MillimetersPerMeter)); } } + + /// + /// 从当前Detour位姿开始执行“3m直线—左半圆—3m直线”新版控制器跟踪实验。 + /// + [MovementTest(name = "新版控制器:直线-左半圆-直线轨迹跟踪")] + public sealed class NewControllerStraightSemicircleStraightTest + : MovementTest + { + private const float MillimetersPerMeter = 1000f; + + private readonly Painter _painter = + UI.GetPainter( + "NewControllerStraightSemicircleStraight"); + + private DriveTask _task; + private TrackingExperimentRecorder _recorder; + private DetourVehicleStateProvider _stateProvider; + + /// + /// 获取或设置本次测试编号,用于区分重复实验CSV。 + /// + public int TrialNumber = 1; + + /// + /// 获取或设置半圆前后两段直线的长度,单位为m。 + /// + public double StraightLengthMeters = 3.0; + + /// + /// 获取或设置左转半圆的转弯半径,单位为m。 + /// + public double TurnRadiusMeters = 2.0; + + /// + /// 获取或设置直线与等曲率转弯之间的曲率过渡长度,单位为m。 + /// + public double CurvatureTransitionLengthMeters = 0.60; + + /// + /// 获取或设置两段直线的最大参考速度,单位为m/s。 + /// + public double StraightMaximumSpeedMetersPerSecond = 0.30; + + /// + /// 获取或设置半圆段的最大参考速度,单位为m/s。 + /// + public double SemicircleMaximumSpeedMetersPerSecond = 0.25; + + /// + /// 获取或设置参考速度加速度,单位为m/s²。 + /// + public double AccelerationMetersPerSecondSquared = 0.20; + + /// + /// 获取或设置参考速度减速度,单位为m/s²。 + /// + public double DecelerationMetersPerSecondSquared = 0.12; + + /// + /// 获取或设置离散轨迹点间距,单位为m。 + /// + public double PointSpacingMeters = 0.02; + + /// + /// 读取当前位姿、绘制组合轨迹并启动新版轨迹跟踪动作。 + /// + public override void Test() + { + if (_task != null) + { + Console.WriteLine( + "新版直线-左半圆-直线测试已经在运行,请先停止当前测试。"); + return; + } + + if (!MovementTestPreparation.AreWheelsForward()) + { + return; + } + + var chassis = + PilotDefinition.Chassis as MultiWheelChassis; + if (chassis == null) + { + Console.WriteLine( + "当前底盘不是MultiWheelChassis,无法执行新版轨迹测试。"); + return; + } + + _stateProvider = + new DetourVehicleStateProvider(); + if (!_stateProvider.TryGetState( + out var initialState)) + { + Console.WriteLine( + "无法读取有效Detour起点位姿:" + + _stateProvider.LastFailureReason); + _stateProvider = null; + return; + } + + var trajectory = + TestTrajectoryFactory + .CreateStraightLeftSemicircleStraight( + initialState.PoseInWorld, + StraightLengthMeters, + TurnRadiusMeters, + CurvatureTransitionLengthMeters, + StraightMaximumSpeedMetersPerSecond, + SemicircleMaximumSpeedMetersPerSecond, + AccelerationMetersPerSecondSquared, + DecelerationMetersPerSecondSquared, + PointSpacingMeters); + + DrawTrajectory(trajectory); + + var referenceStart = ToMillimeterVector( + trajectory.StartPoint.PoseInWorld); + var referenceEnd = ToMillimeterVector( + trajectory.EndPoint.PoseInWorld); + var recorder = + new TrackingExperimentRecorder( + controllerName: "NewStanleyPid", + trajectoryName: + "ProfiledStraightSmoothLeftTurnStraight", + trialNumber: TrialNumber, + referenceStart: referenceStart, + referenceEnd: referenceEnd, + referenceSpeed: + (float)StraightMaximumSpeedMetersPerSecond, + sampleIntervalMs: 50, + referenceAccelerationMetersPerSecondSquared: + (float)AccelerationMetersPerSecondSquared, + referenceDecelerationMetersPerSecondSquared: + (float)DecelerationMetersPerSecondSquared); + _recorder = recorder; + + var controlPointRadiusMeters = + chassis.ControlPointRadius / + MillimetersPerMeter; + var movement = + new TrajectoryTrackingMovement + { + Trajectory = trajectory, + StateProvider = _stateProvider, + MaximumCommandSpeedMetersPerSecond = + StraightMaximumSpeedMetersPerSecond, + CycleObserver = controller => + RecordControlCycle( + recorder, + controller, + controlPointRadiusMeters) + }; + + recorder.Start(); + + try + { + _task = new DriveTask(movement.Get()); + _task.Wait(); + + // 保留少量停车后原始Detour数据,并生成一帧处理后的静止状态。 + Thread.Sleep(400); + recorder.UpdateCommand(0f, 0f); + if (_stateProvider.TryGetState( + out var stoppedState)) + { + recorder.UpdateProcessedState( + stoppedState); + } + } + finally + { + _task?.Stop(); + recorder.UpdateCommand(0f, 0f); + recorder.StopAndSave(); + _painter.Clear(); + _task = null; + _recorder = null; + _stateProvider = null; + } + } + + /// + /// 停止组合轨迹测试、保存已有数据并清除Clumsy轨迹可视化。 + /// + public override void TestStop() + { + _task?.Stop(); + _recorder?.UpdateCommand(0f, 0f); + _recorder?.StopAndSave(); + _painter.Clear(); + _task = null; + _recorder = null; + _stateProvider = null; + } + + /// + /// 将组合轨迹的离散点和相邻线段转换为Clumsy毫米坐标后绘制。 + /// + private void DrawTrajectory( + Trajectory.Trajectory2D trajectory) + { + _painter.Clear(); + + for (var index = 0; + index < trajectory.Count; + index++) + { + var point = ToMillimeterVector( + trajectory[index].PoseInWorld); + _painter.DrawDot( + Color.Cyan, + point.X, + point.Y, + 3f); + + if (index == 0) + { + continue; + } + + var previousPoint = ToMillimeterVector( + trajectory[index - 1].PoseInWorld); + _painter.DrawLine( + Color.DeepSkyBlue, + previousPoint.X, + previousPoint.Y, + point.X, + point.Y, + width: 2); + } + + var start = ToMillimeterVector( + trajectory.StartPoint.PoseInWorld); + var end = ToMillimeterVector( + trajectory.EndPoint.PoseInWorld); + _painter.DrawDot( + Color.LimeGreen, + start.X, + start.Y, + 8f); + _painter.DrawDot( + Color.OrangeRed, + end.X, + end.Y, + 8f); + } + + /// + /// 将组合轨迹控制周期的状态、参考量和最终GCP命令同步给实验记录器。 + /// + private static void RecordControlCycle( + TrackingExperimentRecorder recorder, + ParkingGeometricController controller, + double controlPointRadiusMeters) + { + if (controller.LastVehicleState.HasValue) + { + recorder.UpdateProcessedState( + controller.LastVehicleState.Value); + } + + if (!controller.LastCommand.HasValue) + { + return; + } + + if (controller.LastProjection.HasValue && + controller.LastReferenceSpeedMetersPerSecond.HasValue) + { + var projection = + controller.LastProjection.Value; + recorder.UpdateControlReference( + projection.ArcLengthMeters, + controller.LastReferenceSpeedMetersPerSecond.Value, + projection.LateralErrorMeters, + projection.HeadingErrorRadians, + projection.DistanceToTrajectoryMeters, + projection.RemainingDistanceMeters); + } + + var command = controller.LastCommand.Value; + var curvaturePerMeter = Math.Tan( + command.FrontAngleRadians) / + controlPointRadiusMeters; + var angularSpeedRadiansPerSecond = + command.SpeedMetersPerSecond * + curvaturePerMeter; + + recorder.UpdateCommand( + (float)command.SpeedMetersPerSecond, + (float)angularSpeedRadiansPerSecond); + } + + /// + /// 将Shared世界坐标系米制位姿转换为Clumsy绘图和记录器使用的毫米坐标。 + /// + private static Vector2 ToMillimeterVector( + Pose2D poseInWorld) + { + return new Vector2( + (float)( + poseInWorld.XMeters * + MillimetersPerMeter), + (float)( + poseInWorld.YMeters * + MillimetersPerMeter)); + } + } } diff --git a/MultiWheelC/Experiments/TestTrajectoryFactory.cs b/MultiWheelC/Experiments/TestTrajectoryFactory.cs index 4e7ea70..b215a0a 100644 --- a/MultiWheelC/Experiments/TestTrajectoryFactory.cs +++ b/MultiWheelC/Experiments/TestTrajectoryFactory.cs @@ -92,6 +92,399 @@ namespace MultiWheelC return new Trajectory2D(points); } + /// + /// 从当前位姿生成“3m直线、平滑进入半径2m左转、平滑退出、3m直线”的180°转弯轨迹。 + /// + public static Trajectory2D CreateStraightLeftSemicircleStraight( + Pose2D startPoseInWorld, + double straightLengthMeters = 3.0, + double turnRadiusMeters = 2.0, + double curvatureTransitionLengthMeters = 0.60, + double straightMaximumSpeedMetersPerSecond = 0.30, + double semicircleMaximumSpeedMetersPerSecond = 0.25, + double accelerationMetersPerSecondSquared = 0.20, + double decelerationMetersPerSecondSquared = 0.12, + double pointSpacingMeters = 0.02) + { + EnsureFinitePose( + startPoseInWorld, + nameof(startPoseInWorld)); + EnsureFinitePositive( + straightLengthMeters, + nameof(straightLengthMeters)); + EnsureFinitePositive( + turnRadiusMeters, + nameof(turnRadiusMeters)); + EnsureFinitePositive( + curvatureTransitionLengthMeters, + nameof(curvatureTransitionLengthMeters)); + EnsureFinitePositive( + straightMaximumSpeedMetersPerSecond, + nameof(straightMaximumSpeedMetersPerSecond)); + EnsureFinitePositive( + semicircleMaximumSpeedMetersPerSecond, + nameof(semicircleMaximumSpeedMetersPerSecond)); + EnsureFinitePositive( + accelerationMetersPerSecondSquared, + nameof(accelerationMetersPerSecondSquared)); + EnsureFinitePositive( + decelerationMetersPerSecondSquared, + nameof(decelerationMetersPerSecondSquared)); + EnsureFinitePositive( + pointSpacingMeters, + nameof(pointSpacingMeters)); + + var originalSemicircleLengthMeters = + Math.PI * turnRadiusMeters; + var constantCurvatureLengthMeters = + originalSemicircleLengthMeters - + curvatureTransitionLengthMeters; + + if (constantCurvatureLengthMeters <= 0.0) + { + throw new ArgumentOutOfRangeException( + nameof(curvatureTransitionLengthMeters), + "曲率过渡段长度必须小于半径对应的原始半圆弧长。"); + } + + // 两段平滑过渡的平均曲率均为最大曲率的一半; + // 将等曲率段缩短一个过渡长度后,总曲率积分仍严格等于π。 + var turnLengthMeters = + 2.0 * curvatureTransitionLengthMeters + + constantCurvatureLengthMeters; + var turnStartArcLengthMeters = + straightLengthMeters; + var turnEndArcLengthMeters = + straightLengthMeters + + turnLengthMeters; + var maximumCurvaturePerMeter = + 1.0 / turnRadiusMeters; + + var sampleArcLengths = + BuildCompositeArcLengthSamples( + straightLengthMeters, + curvatureTransitionLengthMeters, + constantCurvatureLengthMeters, + pointSpacingMeters); + var speedLimits = new double[ + sampleArcLengths.Count]; + var referenceSpeeds = new double[ + sampleArcLengths.Count]; + + for (var index = 0; + index < sampleArcLengths.Count; + index++) + { + var arcLengthMeters = + sampleArcLengths[index]; + + // 整个转弯及两侧曲率过渡段采用转弯限速。 + speedLimits[index] = + arcLengthMeters >= + turnStartArcLengthMeters && + arcLengthMeters <= + turnEndArcLengthMeters + ? semicircleMaximumSpeedMetersPerSecond + : straightMaximumSpeedMetersPerSecond; + } + + ApplyAccelerationAndBrakingLimits( + sampleArcLengths, + speedLimits, + referenceSpeeds, + accelerationMetersPerSecondSquared, + decelerationMetersPerSecondSquared); + + var points = new List( + sampleArcLengths.Count); + var startCos = Math.Cos( + startPoseInWorld.YawRadians); + var startSin = Math.Sin( + startPoseInWorld.YawRadians); + var localX = 0.0; + var localY = 0.0; + var localYawRadians = 0.0; + var previousArcLengthMeters = 0.0; + + for (var index = 0; + index < sampleArcLengths.Count; + index++) + { + var arcLengthMeters = + sampleArcLengths[index]; + if (index > 0) + { + var segmentLengthMeters = + arcLengthMeters - + previousArcLengthMeters; + var segmentMiddleArcLengthMeters = + (arcLengthMeters + + previousArcLengthMeters) / + 2.0; + var segmentCurvaturePerMeter = + CalculateSmoothTurnCurvature( + segmentMiddleArcLengthMeters - + turnStartArcLengthMeters, + curvatureTransitionLengthMeters, + constantCurvatureLengthMeters, + maximumCurvaturePerMeter); + var segmentYawChangeRadians = + segmentCurvaturePerMeter * + segmentLengthMeters; + + if (Math.Abs(segmentCurvaturePerMeter) <= + 1e-12) + { + localX += + Math.Cos(localYawRadians) * + segmentLengthMeters; + localY += + Math.Sin(localYawRadians) * + segmentLengthMeters; + } + else + { + var nextYawRadians = + localYawRadians + + segmentYawChangeRadians; + localX += + (Math.Sin(nextYawRadians) - + Math.Sin(localYawRadians)) / + segmentCurvaturePerMeter; + localY += + (Math.Cos(localYawRadians) - + Math.Cos(nextYawRadians)) / + segmentCurvaturePerMeter; + } + + localYawRadians += + segmentYawChangeRadians; + } + + var curvaturePerMeter = + CalculateSmoothTurnCurvature( + arcLengthMeters - + turnStartArcLengthMeters, + curvatureTransitionLengthMeters, + constantCurvatureLengthMeters, + maximumCurvaturePerMeter); + + var worldX = + startPoseInWorld.XMeters + + startCos * localX - + startSin * localY; + var worldY = + startPoseInWorld.YMeters + + startSin * localX + + startCos * localY; + var worldYawRadians = + AngleMath.NormalizeRadians( + startPoseInWorld.YawRadians + + localYawRadians); + + points.Add( + new TrajectoryPoint( + arcLengthMeters, + new Pose2D( + worldX, + worldY, + worldYawRadians), + curvaturePerMeter, + referenceSpeeds[index])); + + previousArcLengthMeters = + arcLengthMeters; + } + + return new Trajectory2D(points); + } + + /// + /// 分别采样直线、入弯过渡、等曲率段和出弯过渡,保证所有边界均为精确轨迹点。 + /// + private static List BuildCompositeArcLengthSamples( + double straightLengthMeters, + double curvatureTransitionLengthMeters, + double constantCurvatureLengthMeters, + double pointSpacingMeters) + { + var samples = new List { 0.0 }; + var accumulatedArcLengthMeters = 0.0; + + AppendSectionArcLengthSamples( + samples, + ref accumulatedArcLengthMeters, + straightLengthMeters, + pointSpacingMeters); + AppendSectionArcLengthSamples( + samples, + ref accumulatedArcLengthMeters, + curvatureTransitionLengthMeters, + pointSpacingMeters); + AppendSectionArcLengthSamples( + samples, + ref accumulatedArcLengthMeters, + constantCurvatureLengthMeters, + pointSpacingMeters); + AppendSectionArcLengthSamples( + samples, + ref accumulatedArcLengthMeters, + curvatureTransitionLengthMeters, + pointSpacingMeters); + AppendSectionArcLengthSamples( + samples, + ref accumulatedArcLengthMeters, + straightLengthMeters, + pointSpacingMeters); + + return samples; + } + + /// + /// 计算180°左转中连续变化的参考曲率,过渡段两端的曲率变化率均为零。 + /// + private static double CalculateSmoothTurnCurvature( + double distanceInTurnMeters, + double transitionLengthMeters, + double constantCurvatureLengthMeters, + double maximumCurvaturePerMeter) + { + var totalTurnLengthMeters = + 2.0 * transitionLengthMeters + + constantCurvatureLengthMeters; + + if (distanceInTurnMeters <= 0.0 || + distanceInTurnMeters >= totalTurnLengthMeters) + { + return 0.0; + } + + if (distanceInTurnMeters < transitionLengthMeters) + { + return maximumCurvaturePerMeter * + SmoothStep01( + distanceInTurnMeters / + transitionLengthMeters); + } + + var exitTransitionStartMeters = + transitionLengthMeters + + constantCurvatureLengthMeters; + if (distanceInTurnMeters <= + exitTransitionStartMeters) + { + return maximumCurvaturePerMeter; + } + + var exitRatio = + (distanceInTurnMeters - + exitTransitionStartMeters) / + transitionLengthMeters; + return maximumCurvaturePerMeter * + (1.0 - SmoothStep01(exitRatio)); + } + + /// + /// 将零到一的比例转换为两端一阶导数均为零的三次平滑比例。 + /// + private static double SmoothStep01(double ratio) + { + var limitedRatio = Math.Max( + 0.0, + Math.Min(1.0, ratio)); + return limitedRatio * + limitedRatio * + (3.0 - 2.0 * limitedRatio); + } + + /// + /// 将一段指定长度的轨迹追加为均匀弧长采样,并使最后一个点严格落在该段终点。 + /// + private static void AppendSectionArcLengthSamples( + ICollection samples, + ref double accumulatedArcLengthMeters, + double sectionLengthMeters, + double pointSpacingMeters) + { + var sectionStartArcLengthMeters = + accumulatedArcLengthMeters; + var segmentCount = (int)Math.Ceiling( + sectionLengthMeters / + pointSpacingMeters); + + for (var index = 1; + index <= segmentCount; + index++) + { + samples.Add( + sectionStartArcLengthMeters + + sectionLengthMeters * + index / + segmentCount); + } + + accumulatedArcLengthMeters = + sectionStartArcLengthMeters + + sectionLengthMeters; + } + + /// + /// 对逐点速度上限执行前向加速约束和反向制动约束,生成连续可执行的空间速度曲线。 + /// + private static void ApplyAccelerationAndBrakingLimits( + IReadOnlyList arcLengthsMeters, + IReadOnlyList speedLimitsMetersPerSecond, + double[] referenceSpeedsMetersPerSecond, + double accelerationMetersPerSecondSquared, + double decelerationMetersPerSecondSquared) + { + referenceSpeedsMetersPerSecond[0] = 0.0; + + for (var index = 1; + index < arcLengthsMeters.Count; + index++) + { + var segmentLengthMeters = + arcLengthsMeters[index] - + arcLengthsMeters[index - 1]; + var accelerationLimitedSpeed = Math.Sqrt( + referenceSpeedsMetersPerSecond[index - 1] * + referenceSpeedsMetersPerSecond[index - 1] + + 2.0 * + accelerationMetersPerSecondSquared * + segmentLengthMeters); + + referenceSpeedsMetersPerSecond[index] = + Math.Min( + speedLimitsMetersPerSecond[index], + accelerationLimitedSpeed); + } + + var finalIndex = + referenceSpeedsMetersPerSecond.Length - 1; + referenceSpeedsMetersPerSecond[finalIndex] = 0.0; + + for (var index = finalIndex - 1; + index >= 0; + index--) + { + var segmentLengthMeters = + arcLengthsMeters[index + 1] - + arcLengthsMeters[index]; + var brakingLimitedSpeed = Math.Sqrt( + referenceSpeedsMetersPerSecond[index + 1] * + referenceSpeedsMetersPerSecond[index + 1] + + 2.0 * + decelerationMetersPerSecondSquared * + segmentLengthMeters); + + referenceSpeedsMetersPerSecond[index] = + Math.Min( + referenceSpeedsMetersPerSecond[index], + brakingLimitedSpeed); + } + } + /// /// 根据起步、巡航和制动能力计算指定弧长位置允许的参考速度。 /// diff --git a/MultiWheelC/Movements/TrajectoryTrackingMovement.cs b/MultiWheelC/Movements/TrajectoryTrackingMovement.cs index 5c7a140..dded045 100644 --- a/MultiWheelC/Movements/TrajectoryTrackingMovement.cs +++ b/MultiWheelC/Movements/TrajectoryTrackingMovement.cs @@ -38,7 +38,7 @@ namespace MultiWheelC /// /// Stanley横向误差增益,单位为1/s。 /// - public double StanleyCrossTrackGainPerSecond = 1.0; + public double StanleyCrossTrackGainPerSecond = 0.4; /// /// Stanley航向误差增益。 @@ -48,7 +48,7 @@ namespace MultiWheelC /// /// Stanley低速分母保护速度,单位为m/s。 /// - public double StanleyMinimumSpeedMetersPerSecond = 0.05; + public double StanleyMinimumSpeedMetersPerSecond = 0.15; /// /// 获取或设置Stanley是否优先使用Detour估算的实际速度。 diff --git a/MultiWheelC/PilotDefinition.cs b/MultiWheelC/PilotDefinition.cs index 188fa94..a0b4551 100644 --- a/MultiWheelC/PilotDefinition.cs +++ b/MultiWheelC/PilotDefinition.cs @@ -33,10 +33,10 @@ public class PilotDefinition : MultiWheelPilotDefinition