完善状态估计、车队协调与舵轮辨识日志

This commit is contained in:
2026-08-24 18:39:41 +08:00
parent 0ab409cd2a
commit 95c0b19a26
40 changed files with 5983 additions and 95 deletions
+85 -6
View File
@@ -15,13 +15,15 @@ namespace MultiWheelC.Tests
{
VerifyInactiveControllerStops();
VerifyStraightCommand();
VerifyFortyFiveDegreeMotionDirection();
VerifyNinetyDegreeMotionDirection();
VerifyInvalidVelocityUsesReferenceSpeed();
VerifyCompletionStops();
VerifyExcessiveTrackingErrorFaults();
VerifyCancelStops();
Console.WriteLine(
"FleetController车队中心控制测试通过。共6个场景。");
"FleetController车队中心控制测试通过。共8个场景。");
}
private static void VerifyInactiveControllerStops()
@@ -91,6 +93,56 @@ namespace MultiWheelC.Tests
"速度尚未初始化");
}
private static void VerifyFortyFiveDegreeMotionDirection()
{
VerifyMotionDirection(
Math.PI / 4.0,
"45度运动方向");
}
private static void VerifyNinetyDegreeMotionDirection()
{
VerifyMotionDirection(
Math.PI / 2.0,
"90度运动方向");
}
private static void VerifyMotionDirection(
double motionDirectionInFleetRadians,
string scenario)
{
var controller = CreateController(
motionDirectionInFleetRadians);
controller.Start(
CreateStraightTrajectory(
motionDirectionInFleetRadians));
var directionX =
Math.Cos(motionDirectionInFleetRadians);
var directionY =
Math.Sin(motionDirectionInFleetRadians);
var result = controller.ComputeCommand(
CreateState(
0.5 * directionX,
0.5 * directionY,
0.4 * directionX,
0.4 * directionY,
true),
0.02,
out var command);
AssertResult(
result,
FleetControlCycleResult.CommandGenerated,
scenario);
AssertTwist(
command.TwistAtReferencePoint,
0.4 * directionX,
0.4 * directionY,
0.0,
scenario);
}
private static void VerifyCompletionStops()
{
var controller = CreateController();
@@ -156,18 +208,27 @@ namespace MultiWheelC.Tests
AssertStop(command, "取消控制");
}
private static FleetController CreateController()
private static FleetController CreateController(
double motionDirectionInFleetRadians = 0.0)
{
return new FleetController(
new StraightLateralController(),
new ReferenceLongitudinalController(),
new GcpCommandAllocator(
Math.PI / 4.0),
virtualControlPointRadiusMeters: 0.5);
virtualControlPointRadiusMeters: 0.5,
motionDirectionInFleetRadians:
motionDirectionInFleetRadians);
}
private static Trajectory2D CreateStraightTrajectory()
private static Trajectory2D CreateStraightTrajectory(
double motionDirectionInFleetRadians = 0.0)
{
var directionX =
Math.Cos(motionDirectionInFleetRadians);
var directionY =
Math.Sin(motionDirectionInFleetRadians);
return new Trajectory2D(
new[]
{
@@ -178,7 +239,10 @@ namespace MultiWheelC.Tests
0.4),
new TrajectoryPoint(
1.0,
new Pose2D(1.0, 0.0, 0.0),
new Pose2D(
directionX,
directionY,
0.0),
0.0,
0.4)
});
@@ -189,6 +253,21 @@ namespace MultiWheelC.Tests
double yMeters,
double vxMetersPerSecond,
bool hasValidVelocityEstimate)
{
return CreateState(
xMeters,
yMeters,
vxMetersPerSecond,
0.0,
hasValidVelocityEstimate);
}
private static FleetState CreateState(
double xMeters,
double yMeters,
double vxMetersPerSecond,
double vyMetersPerSecond,
bool hasValidVelocityEstimate)
{
return new FleetState(
sampleTimestampSeconds: 1.0,
@@ -198,7 +277,7 @@ namespace MultiWheelC.Tests
0.0),
twistAtFleetOriginInWorld: new Twist2D(
vxMetersPerSecond,
0.0,
vyMetersPerSecond,
0.0),
hasValidVelocityEstimate:
hasValidVelocityEstimate);
+654
View File
@@ -0,0 +1,654 @@
using System;
using System.Collections.Generic;
using MultiWheelC.Control.Abstractions;
using MultiWheelC.Control.Allocation;
using MultiWheelC.Fleet;
using MultiWheelC.Trajectory;
using MyParking.Shared;
namespace MultiWheelC.Tests
{
internal static class FleetCoordinatorTests
{
private const double Tolerance = 1e-9;
public static void Run()
{
VerifyCommandCycle();
VerifySmallLayoutErrorIsCorrected();
VerifyWarningRangeScalesAllCommands();
VerifyUnavailableStateStopsAndRecovers();
VerifyUnsafeLayoutFaultsAndLatches();
VerifyCompletionStops();
VerifyTrackingFaultStops();
VerifyCancelReturnsInactive();
Console.WriteLine(
"FleetCoordinator车队协调测试通过。共8个场景。");
}
private static void VerifyCommandCycle()
{
var layout = CreateLayout();
var coordinator = CreateCoordinator();
coordinator.Start(layout, CreateTrajectory());
var result = coordinator.ExecuteCycle(
CreateRigidMemberStates(
layout,
new Pose2D(0.5, 0.0, 0.0),
new Twist2D(0.4, 0.0, 0.0),
1.0),
targetTimestampSeconds: 1.0,
deltaTimeSeconds: 0.02,
out var output);
AssertResult(
result,
FleetCoordinationCycleResult.CommandGenerated,
"正常协调周期");
if (!output.State.HasValue)
{
throw new InvalidOperationException(
"正常协调周期没有返回车队状态。");
}
AssertNear(
output.State.Value.FleetPoseInWorld.XMeters,
0.5,
"正常协调周期中心X");
AssertNear(
output.SpeedScale,
1.0,
"正常协调周期速度比例");
AssertTwist(
output.FleetCommand.TwistAtReferencePoint,
0.4,
0.0,
0.0,
"正常协调周期车队命令");
AssertTwist(
FindCommand(output.MemberCommands, 1)
.TwistInVehicleBody,
0.4,
0.0,
0.0,
"正常协调周期车辆1");
AssertTwist(
FindCommand(output.MemberCommands, 2)
.TwistInVehicleBody,
-0.4,
0.0,
0.0,
"正常协调周期车辆2");
}
private static void VerifySmallLayoutErrorIsCorrected()
{
var layout = CreateLayout();
var coordinator = CreateCoordinator();
coordinator.Start(layout, CreateTrajectory());
var result = coordinator.ExecuteCycle(
new[]
{
CreateMemberState(
1,
new Pose2D(-0.99, 0.0, 0.0),
1.0),
CreateMemberState(
2,
new Pose2D(0.99, 0.0, Math.PI),
1.0)
},
targetTimestampSeconds: 1.0,
deltaTimeSeconds: 0.02,
out var output);
AssertResult(
result,
FleetCoordinationCycleResult.CommandGenerated,
"小范围布局误差纠偏");
AssertNear(
output.SpeedScale,
1.0,
"小范围布局误差速度比例");
AssertTwist(
FindCommand(output.BaseMemberCommands, 1)
.TwistInVehicleBody,
0.4,
0.0,
0.0,
"车辆1基础命令");
AssertTwist(
FindCommand(output.BaseMemberCommands, 2)
.TwistInVehicleBody,
-0.4,
0.0,
0.0,
"车辆2基础命令");
AssertTwist(
FindCommand(output.MemberCommands, 1)
.TwistInVehicleBody,
0.395,
0.0,
0.0,
"车辆1纠偏后命令");
AssertTwist(
FindCommand(output.MemberCommands, 2)
.TwistInVehicleBody,
-0.405,
0.0,
0.0,
"车辆2纠偏后命令");
}
private static void VerifyWarningRangeScalesAllCommands()
{
var layout = CreateLayout();
var coordinator = CreateCoordinator();
coordinator.Start(layout, CreateTrajectory());
var result = coordinator.ExecuteCycle(
new[]
{
CreateMemberState(
1,
new Pose2D(-0.965, 0.0, 0.0),
1.0),
CreateMemberState(
2,
new Pose2D(0.965, 0.0, Math.PI),
1.0)
},
targetTimestampSeconds: 1.0,
deltaTimeSeconds: 0.02,
out var output);
AssertResult(
result,
FleetCoordinationCycleResult.CommandGenerated,
"警告区间统一缩放");
AssertNear(
output.SpeedScale,
0.5,
"警告区间速度比例");
AssertTwist(
output.FleetCommand.TwistAtReferencePoint,
0.2,
0.0,
0.0,
"警告区间车队命令");
AssertTwist(
FindCommand(output.MemberCommands, 1)
.TwistInVehicleBody,
0.2,
0.0,
0.0,
"警告区间车辆1");
AssertTwist(
FindCommand(output.MemberCommands, 2)
.TwistInVehicleBody,
-0.2,
0.0,
0.0,
"警告区间车辆2");
if (string.IsNullOrWhiteSpace(output.Reason))
{
throw new InvalidOperationException(
"警告区间缩放没有返回限制原因。");
}
}
private static void VerifyUnavailableStateStopsAndRecovers()
{
var layout = CreateLayout();
var coordinator = CreateCoordinator();
coordinator.Start(layout, CreateTrajectory());
var unavailableStates = CreateRigidMemberStates(
layout,
new Pose2D(0.5, 0.0, 0.0),
new Twist2D(0.4, 0.0, 0.0),
1.0);
unavailableStates[1] = new FleetMemberStateSample(
unavailableStates[1].VehicleId,
unavailableStates[1].SampleTimestampSeconds,
unavailableStates[1].PoseInWorld,
unavailableStates[1].TwistAtVehicleOriginInWorld,
isStateAvailable: false,
hasValidVelocityEstimate: true);
var waitingResult = coordinator.ExecuteCycle(
unavailableStates,
targetTimestampSeconds: 1.0,
deltaTimeSeconds: 0.02,
out var waitingOutput);
AssertResult(
waitingResult,
FleetCoordinationCycleResult.WaitingForState,
"状态暂时不可用");
AssertStop(waitingOutput, layout.VehicleCount);
if (!coordinator.IsActive)
{
throw new InvalidOperationException(
"状态暂时不可用不应取消车队控制器。");
}
var recoveredResult = coordinator.ExecuteCycle(
CreateRigidMemberStates(
layout,
new Pose2D(0.5, 0.0, 0.0),
new Twist2D(0.4, 0.0, 0.0),
1.02),
targetTimestampSeconds: 1.02,
deltaTimeSeconds: 0.02,
out _);
AssertResult(
recoveredResult,
FleetCoordinationCycleResult.CommandGenerated,
"状态恢复");
}
private static void VerifyUnsafeLayoutFaultsAndLatches()
{
var layout = CreateLayout();
var coordinator = CreateCoordinator();
coordinator.Start(layout, CreateTrajectory());
var deformedStates = new[]
{
CreateMemberState(
1,
new Pose2D(-0.9, 0.0, 0.0),
1.0),
CreateMemberState(
2,
new Pose2D(0.9, 0.0, Math.PI),
1.0)
};
var result = coordinator.ExecuteCycle(
deformedStates,
targetTimestampSeconds: 1.0,
deltaTimeSeconds: 0.02,
out var output);
AssertResult(
result,
FleetCoordinationCycleResult.Faulted,
"布局误差超限");
AssertStop(output, layout.VehicleCount);
if (!coordinator.IsFaulted ||
string.IsNullOrWhiteSpace(
coordinator.LastFailureReason))
{
throw new InvalidOperationException(
"布局误差故障没有被锁存。");
}
var latchedResult = coordinator.ExecuteCycle(
CreateRigidMemberStates(
layout,
new Pose2D(0.5, 0.0, 0.0),
new Twist2D(0.4, 0.0, 0.0),
1.02),
targetTimestampSeconds: 1.02,
deltaTimeSeconds: 0.02,
out var latchedOutput);
AssertResult(
latchedResult,
FleetCoordinationCycleResult.Faulted,
"布局误差故障锁存");
AssertStop(latchedOutput, layout.VehicleCount);
}
private static void VerifyCompletionStops()
{
var layout = CreateLayout();
var coordinator = CreateCoordinator();
coordinator.Start(layout, CreateTrajectory());
var result = coordinator.ExecuteCycle(
CreateRigidMemberStates(
layout,
new Pose2D(1.0, 0.0, 0.0),
Twist2D.Zero,
1.0),
targetTimestampSeconds: 1.0,
deltaTimeSeconds: 0.02,
out var output);
AssertResult(
result,
FleetCoordinationCycleResult.Completed,
"车队轨迹完成");
AssertStop(output, layout.VehicleCount);
if (!coordinator.IsCompleted)
{
throw new InvalidOperationException(
"车队轨迹完成状态没有被保存。");
}
}
private static void VerifyTrackingFaultStops()
{
var layout = CreateLayout();
var coordinator = CreateCoordinator();
coordinator.Start(layout, CreateTrajectory());
var result = coordinator.ExecuteCycle(
CreateRigidMemberStates(
layout,
new Pose2D(0.5, 0.5, 0.0),
Twist2D.Zero,
1.0),
targetTimestampSeconds: 1.0,
deltaTimeSeconds: 0.02,
out var output);
AssertResult(
result,
FleetCoordinationCycleResult.Faulted,
"车队中心跟踪故障");
AssertStop(output, layout.VehicleCount);
if (!coordinator.IsFaulted)
{
throw new InvalidOperationException(
"车队中心跟踪故障没有传递到协调器。");
}
}
private static void VerifyCancelReturnsInactive()
{
var layout = CreateLayout();
var coordinator = CreateCoordinator();
coordinator.Start(layout, CreateTrajectory());
coordinator.Cancel();
var result = coordinator.ExecuteCycle(
CreateRigidMemberStates(
layout,
new Pose2D(0.5, 0.0, 0.0),
Twist2D.Zero,
1.0),
targetTimestampSeconds: 1.0,
deltaTimeSeconds: 0.02,
out var output);
AssertResult(
result,
FleetCoordinationCycleResult.Inactive,
"取消车队协调");
AssertStop(output, layout.VehicleCount);
}
private static FleetCoordinator CreateCoordinator()
{
var estimator = new FleetStateEstimator(
maximumMemberStateAgeSeconds: 0.25,
maximumPositionDisagreementMeters: 0.5,
maximumYawDisagreementRadians:
AngleMath.DegreesToRadians(10.0));
var controller = new FleetController(
new StraightLateralController(),
new ReferenceLongitudinalController(),
new GcpCommandAllocator(Math.PI / 4.0),
virtualControlPointRadiusMeters: 0.5);
var commandCorrector =
new FleetMemberCommandCorrector(
longitudinalPositionGainPerSecond: 1.0,
lateralPositionGainPerSecond: 1.0,
yawGainPerSecond: 1.0,
positionErrorDeadbandMeters: 0.005,
yawErrorDeadbandRadians:
AngleMath.DegreesToRadians(0.5),
maximumLinearCorrectionMetersPerSecond:
0.03,
maximumAngularCorrectionRadiansPerSecond:
AngleMath.DegreesToRadians(2.0));
return new FleetCoordinator(
estimator,
controller,
commandCorrector,
memberPositionErrorWarningMeters: 0.02,
maximumMemberPositionErrorMeters: 0.05,
memberYawErrorWarningRadians:
AngleMath.DegreesToRadians(1.0),
maximumMemberYawErrorRadians:
AngleMath.DegreesToRadians(3.0));
}
private static FleetLayout CreateLayout()
{
return new FleetLayout(
new[]
{
new VehicleLayout(
1,
new Pose2D(-1.0, 0.0, 0.0)),
new VehicleLayout(
2,
new Pose2D(1.0, 0.0, Math.PI))
});
}
private static Trajectory2D CreateTrajectory()
{
return new Trajectory2D(
new[]
{
new TrajectoryPoint(
0.0,
Pose2D.Identity,
0.0,
0.4),
new TrajectoryPoint(
1.0,
new Pose2D(1.0, 0.0, 0.0),
0.0,
0.4)
});
}
private static FleetMemberStateSample[]
CreateRigidMemberStates(
FleetLayout layout,
Pose2D fleetPoseInWorld,
Twist2D twistAtFleetOriginInWorld,
double timestampSeconds)
{
var states =
new FleetMemberStateSample[layout.VehicleCount];
for (var index = 0;
index < layout.Vehicles.Count;
index++)
{
var vehicle = layout.Vehicles[index];
var poseInWorld = FrameTransform2D.Compose(
fleetPoseInWorld,
vehicle.PoseInFleet);
var offsetX =
poseInWorld.XMeters -
fleetPoseInWorld.XMeters;
var offsetY =
poseInWorld.YMeters -
fleetPoseInWorld.YMeters;
var twistInWorld = new Twist2D(
twistAtFleetOriginInWorld.VxMetersPerSecond -
twistAtFleetOriginInWorld.OmegaRadiansPerSecond *
offsetY,
twistAtFleetOriginInWorld.VyMetersPerSecond +
twistAtFleetOriginInWorld.OmegaRadiansPerSecond *
offsetX,
twistAtFleetOriginInWorld.OmegaRadiansPerSecond);
states[index] = new FleetMemberStateSample(
vehicle.VehicleId,
timestampSeconds,
poseInWorld,
twistInWorld,
isStateAvailable: true,
hasValidVelocityEstimate: true);
}
return states;
}
private static FleetMemberStateSample CreateMemberState(
int vehicleId,
Pose2D poseInWorld,
double timestampSeconds)
{
return new FleetMemberStateSample(
vehicleId,
timestampSeconds,
poseInWorld,
Twist2D.Zero,
isStateAvailable: true,
hasValidVelocityEstimate: true);
}
private static FleetMemberCommand FindCommand(
IReadOnlyList<FleetMemberCommand> commands,
int vehicleId)
{
for (var index = 0;
index < commands.Count;
index++)
{
if (commands[index].VehicleId == vehicleId)
{
return commands[index];
}
}
throw new InvalidOperationException(
$"没有找到车辆{vehicleId}的成员命令。");
}
private static void AssertStop(
FleetCoordinationCycleOutput output,
int expectedMemberCount)
{
AssertTwist(
output.FleetCommand.TwistAtReferencePoint,
0.0,
0.0,
0.0,
"车队停止命令");
if (output.MemberCommands.Count != expectedMemberCount)
{
throw new InvalidOperationException(
"停止输出的成员命令数量错误。");
}
if (output.BaseMemberCommands.Count != expectedMemberCount)
{
throw new InvalidOperationException(
"停止输出的成员基础命令数量错误。");
}
for (var index = 0;
index < output.MemberCommands.Count;
index++)
{
AssertTwist(
output.BaseMemberCommands[index]
.TwistInVehicleBody,
0.0,
0.0,
0.0,
"成员基础停止命令");
AssertTwist(
output.MemberCommands[index].TwistInVehicleBody,
0.0,
0.0,
0.0,
"成员停止命令");
}
}
private static void AssertResult(
FleetCoordinationCycleResult actual,
FleetCoordinationCycleResult expected,
string scenario)
{
if (actual != expected)
{
throw new InvalidOperationException(
$"{scenario}结果错误:" +
$"actual={actual}, expected={expected}。");
}
}
private static void AssertTwist(
Twist2D actual,
double expectedVx,
double expectedVy,
double expectedOmega,
string scenario)
{
AssertNear(
actual.VxMetersPerSecond,
expectedVx,
scenario + " Vx");
AssertNear(
actual.VyMetersPerSecond,
expectedVy,
scenario + " Vy");
AssertNear(
actual.OmegaRadiansPerSecond,
expectedOmega,
scenario + " Omega");
}
private static void AssertNear(
double actual,
double expected,
string valueName)
{
if (Math.Abs(actual - expected) > Tolerance)
{
throw new InvalidOperationException(
$"{valueName}错误:" +
$"actual={actual:F9}, expected={expected:F9}。");
}
}
private sealed class StraightLateralController :
ILateralController
{
public LateralControlCommand Compute(
PathTrackingContext context)
{
return LateralControlCommand.Straight;
}
public void Reset()
{
}
}
private sealed class ReferenceLongitudinalController :
ILongitudinalController
{
public double ComputeSpeedMetersPerSecond(
PathTrackingContext context)
{
return context.ControlReferenceSpeedMetersPerSecond;
}
public void Reset()
{
}
}
}
}
@@ -0,0 +1,268 @@
using System;
using MultiWheelC.Fleet;
using MyParking.Shared;
namespace MultiWheelC.Tests
{
internal static class FleetLayoutCaptureTests
{
private const double Tolerance = 1e-9;
public static void Run()
{
VerifySymmetricTailToTailLayout();
VerifyAsymmetricLayoutAndPoseReconstruction();
VerifyInputOrderDoesNotChangeResult();
VerifyEmptyInputIsRejected();
VerifyDuplicateVehicleIdIsRejected();
VerifyMissingLeaderIsRejected();
Console.WriteLine(
"FleetLayoutCapture布局建立测试通过。共6个场景。");
}
private static void VerifySymmetricTailToTailLayout()
{
var result = FleetLayoutCapture.Capture(
new[]
{
new FleetMemberPose(
1,
new Pose2D(-1.2, 0.0, 0.0)),
new FleetMemberPose(
2,
new Pose2D(1.2, 0.0, Math.PI))
},
leaderVehicleId: 1);
AssertPose(
result.FleetPoseInWorld,
Pose2D.Identity,
"对称双车中心");
AssertVehicleLayout(
result.Layout,
1,
new Pose2D(-1.2, 0.0, 0.0),
"对称双车主车布局");
AssertVehicleLayout(
result.Layout,
2,
new Pose2D(1.2, 0.0, Math.PI),
"对称双车从车布局");
}
private static void VerifyAsymmetricLayoutAndPoseReconstruction()
{
var members = new[]
{
new FleetMemberPose(
3,
new Pose2D(1.0, 1.0, 0.4)),
new FleetMemberPose(
1,
new Pose2D(4.0, 1.0, 0.4)),
new FleetMemberPose(
2,
new Pose2D(1.0, 4.0, -0.8))
};
var result = FleetLayoutCapture.Capture(
members,
leaderVehicleId: 1);
AssertPose(
result.FleetPoseInWorld,
new Pose2D(2.0, 2.0, 0.4),
"非对称三车中心");
for (var index = 0;
index < members.Length;
index++)
{
if (!result.Layout.TryGetVehicle(
members[index].VehicleId,
out var vehicleLayout))
{
throw new InvalidOperationException(
"非对称布局缺少成员车。" +
members[index].VehicleId);
}
var reconstructedPoseInWorld =
FrameTransform2D.Compose(
result.FleetPoseInWorld,
vehicleLayout.PoseInFleet);
AssertPose(
reconstructedPoseInWorld,
members[index].PoseInWorld,
"非对称布局世界位姿还原");
}
}
private static void VerifyInputOrderDoesNotChangeResult()
{
var first = FleetLayoutCapture.Capture(
new[]
{
new FleetMemberPose(
1,
new Pose2D(2.0, 3.0, 0.6)),
new FleetMemberPose(
2,
new Pose2D(4.0, 5.0, -1.0))
},
leaderVehicleId: 1);
var second = FleetLayoutCapture.Capture(
new[]
{
new FleetMemberPose(
2,
new Pose2D(4.0, 5.0, -1.0)),
new FleetMemberPose(
1,
new Pose2D(2.0, 3.0, 0.6))
},
leaderVehicleId: 1);
AssertPose(
first.FleetPoseInWorld,
second.FleetPoseInWorld,
"输入顺序不变中心");
AssertSameLayout(first.Layout, second.Layout);
}
private static void VerifyEmptyInputIsRejected()
{
ExpectException<ArgumentException>(
() => FleetLayoutCapture.Capture(
Array.Empty<FleetMemberPose>(),
leaderVehicleId: 1),
"空成员集合");
}
private static void VerifyDuplicateVehicleIdIsRejected()
{
ExpectException<ArgumentException>(
() => FleetLayoutCapture.Capture(
new[]
{
new FleetMemberPose(
1,
Pose2D.Identity),
new FleetMemberPose(
1,
new Pose2D(1.0, 0.0, 0.0))
},
leaderVehicleId: 1),
"重复车号");
}
private static void VerifyMissingLeaderIsRejected()
{
ExpectException<ArgumentException>(
() => FleetLayoutCapture.Capture(
new[]
{
new FleetMemberPose(
2,
Pose2D.Identity)
},
leaderVehicleId: 1),
"缺少主车");
}
private static void AssertSameLayout(
FleetLayout first,
FleetLayout second)
{
if (first.VehicleCount != second.VehicleCount)
{
throw new InvalidOperationException(
"输入顺序变化后成员数量发生变化。");
}
for (var index = 0;
index < first.Vehicles.Count;
index++)
{
var vehicle = first.Vehicles[index];
AssertVehicleLayout(
second,
vehicle.VehicleId,
vehicle.PoseInFleet,
"输入顺序不变布局");
}
}
private static void AssertVehicleLayout(
FleetLayout layout,
int vehicleId,
Pose2D expectedPoseInFleet,
string scenario)
{
if (!layout.TryGetVehicle(
vehicleId,
out var vehicle))
{
throw new InvalidOperationException(
$"{scenario}缺少车辆{vehicleId}。");
}
AssertPose(
vehicle.PoseInFleet,
expectedPoseInFleet,
scenario);
}
private static void AssertPose(
Pose2D actual,
Pose2D expected,
string scenario)
{
AssertNear(
actual.XMeters,
expected.XMeters,
scenario + " X");
AssertNear(
actual.YMeters,
expected.YMeters,
scenario + " Y");
AssertNear(
AngleMath.NormalizeRadians(
actual.YawRadians -
expected.YawRadians),
0.0,
scenario + " Yaw");
}
private static void AssertNear(
double actual,
double expected,
string valueName)
{
if (Math.Abs(actual - expected) > Tolerance)
{
throw new InvalidOperationException(
$"{valueName}错误:" +
$"actual={actual:F9}, expected={expected:F9}。");
}
}
private static void ExpectException<TException>(
Action action,
string scenario)
where TException : Exception
{
try
{
action();
}
catch (TException)
{
return;
}
throw new InvalidOperationException(
$"{scenario}没有抛出{typeof(TException).Name}。");
}
}
}
@@ -0,0 +1,302 @@
using System;
using System.Collections.Generic;
using MultiWheelC.Fleet;
using MyParking.Shared;
namespace MultiWheelC.Tests
{
internal static class FleetMemberCommandCorrectorTests
{
private const double Tolerance = 1e-9;
public static void Run()
{
VerifyZeroErrorsPreserveBaseCommands();
VerifyRelativePositionErrorProducesOpposingCorrection();
VerifyCommonTranslationIsRemoved();
VerifyCommonRotationIsRemoved();
VerifyDeadbandSuppressesSmallErrors();
VerifyCorrectionLimits();
Console.WriteLine(
"FleetMemberCommandCorrector测试通过,共6个场景。");
}
private static void VerifyZeroErrorsPreserveBaseCommands()
{
var layout = CreateLayout();
var baseCommands = FleetKinematics.Decompose(
layout,
new FleetMotionCommand(
Point2D.Zero,
new Twist2D(0.4, 0.1, 0.05)));
var corrected = CreateCorrector().Correct(
layout,
baseCommands,
CreateErrors(Pose2D.Identity, Pose2D.Identity));
AssertCommandsEqual(
corrected,
baseCommands,
"零布局误差");
}
private static void
VerifyRelativePositionErrorProducesOpposingCorrection()
{
var layout = CreateLayout();
var corrected = CreateCorrector().Correct(
layout,
CreateStopCommands(layout),
CreateErrors(
new Pose2D(0.02, 0.0, 0.0),
new Pose2D(0.02, 0.0, 0.0)));
AssertTwist(
FindCommand(corrected, 1).TwistInVehicleBody,
-0.02,
0.0,
0.0,
"车辆1相对位置纠偏");
AssertTwist(
FindCommand(corrected, 2).TwistInVehicleBody,
-0.02,
0.0,
0.0,
"车辆2相对位置纠偏");
}
private static void VerifyCommonTranslationIsRemoved()
{
var layout = CreateLayout();
var corrected = CreateCorrector().Correct(
layout,
CreateStopCommands(layout),
CreateErrors(
new Pose2D(0.02, 0.0, 0.0),
new Pose2D(-0.02, 0.0, 0.0)));
AssertAllStopped(
corrected,
"共同平移不应成为成员相对纠偏");
}
private static void VerifyCommonRotationIsRemoved()
{
const double fleetYawErrorRadians = 0.02;
var layout = CreateLayout();
var commonRotationError = new Pose2D(
0.0,
-fleetYawErrorRadians,
fleetYawErrorRadians);
var corrected = CreateCorrector().Correct(
layout,
CreateStopCommands(layout),
CreateErrors(
commonRotationError,
commonRotationError));
AssertAllStopped(
corrected,
"共同旋转不应成为成员相对纠偏");
}
private static void VerifyDeadbandSuppressesSmallErrors()
{
var layout = CreateLayout();
var corrector = new FleetMemberCommandCorrector(
longitudinalPositionGainPerSecond: 1.0,
lateralPositionGainPerSecond: 1.0,
yawGainPerSecond: 1.0,
positionErrorDeadbandMeters: 0.005,
yawErrorDeadbandRadians: 0.02,
maximumLinearCorrectionMetersPerSecond: 1.0,
maximumAngularCorrectionRadiansPerSecond: 1.0);
var corrected = corrector.Correct(
layout,
CreateStopCommands(layout),
CreateErrors(
new Pose2D(0.004, 0.003, 0.01),
new Pose2D(0.004, -0.003, -0.01)));
AssertAllStopped(corrected, "布局误差死区");
}
private static void VerifyCorrectionLimits()
{
var layout = CreateLayout();
var corrector = new FleetMemberCommandCorrector(
longitudinalPositionGainPerSecond: 1.0,
lateralPositionGainPerSecond: 1.0,
yawGainPerSecond: 1.0,
positionErrorDeadbandMeters: 0.0,
yawErrorDeadbandRadians: 0.0,
maximumLinearCorrectionMetersPerSecond: 0.03,
maximumAngularCorrectionRadiansPerSecond: 0.05);
var corrected = corrector.Correct(
layout,
CreateStopCommands(layout),
CreateErrors(
new Pose2D(0.2, 0.0, 0.2),
new Pose2D(0.2, 0.0, -0.2)));
for (var index = 0; index < corrected.Count; index++)
{
var twist = corrected[index].TwistInVehicleBody;
var linearMagnitude = Math.Sqrt(
twist.VxMetersPerSecond *
twist.VxMetersPerSecond +
twist.VyMetersPerSecond *
twist.VyMetersPerSecond);
AssertNear(
linearMagnitude,
0.03,
"线速度纠偏限幅");
AssertNear(
Math.Abs(twist.OmegaRadiansPerSecond),
0.05,
"角速度纠偏限幅");
}
}
private static FleetMemberCommandCorrector CreateCorrector()
{
return new FleetMemberCommandCorrector(
longitudinalPositionGainPerSecond: 1.0,
lateralPositionGainPerSecond: 1.0,
yawGainPerSecond: 1.0,
positionErrorDeadbandMeters: 0.0,
yawErrorDeadbandRadians: 0.0,
maximumLinearCorrectionMetersPerSecond: 1.0,
maximumAngularCorrectionRadiansPerSecond: 1.0);
}
private static FleetLayout CreateLayout()
{
return new FleetLayout(
new[]
{
new VehicleLayout(
1,
new Pose2D(-1.0, 0.0, 0.0)),
new VehicleLayout(
2,
new Pose2D(1.0, 0.0, Math.PI))
});
}
private static IReadOnlyList<FleetMemberCommand>
CreateStopCommands(FleetLayout layout)
{
return FleetKinematics.Decompose(
layout,
FleetMotionCommand.Stop());
}
private static FleetMemberLayoutError[] CreateErrors(
Pose2D vehicle1Error,
Pose2D vehicle2Error)
{
return new[]
{
new FleetMemberLayoutError(1, vehicle1Error),
new FleetMemberLayoutError(2, vehicle2Error)
};
}
private static FleetMemberCommand FindCommand(
IReadOnlyList<FleetMemberCommand> commands,
int vehicleId)
{
for (var index = 0; index < commands.Count; index++)
{
if (commands[index].VehicleId == vehicleId)
{
return commands[index];
}
}
throw new InvalidOperationException(
$"没有找到车辆{vehicleId}的成员命令。");
}
private static void AssertCommandsEqual(
IReadOnlyList<FleetMemberCommand> actual,
IReadOnlyList<FleetMemberCommand> expected,
string scenario)
{
if (actual.Count != expected.Count)
{
throw new InvalidOperationException(
$"{scenario}的命令数量不一致。");
}
for (var index = 0; index < expected.Count; index++)
{
var expectedCommand = expected[index];
var actualCommand = FindCommand(
actual,
expectedCommand.VehicleId);
AssertTwist(
actualCommand.TwistInVehicleBody,
expectedCommand.TwistInVehicleBody
.VxMetersPerSecond,
expectedCommand.TwistInVehicleBody
.VyMetersPerSecond,
expectedCommand.TwistInVehicleBody
.OmegaRadiansPerSecond,
scenario);
}
}
private static void AssertAllStopped(
IReadOnlyList<FleetMemberCommand> commands,
string scenario)
{
for (var index = 0; index < commands.Count; index++)
{
AssertTwist(
commands[index].TwistInVehicleBody,
0.0,
0.0,
0.0,
scenario);
}
}
private static void AssertTwist(
Twist2D actual,
double expectedVx,
double expectedVy,
double expectedOmega,
string scenario)
{
AssertNear(
actual.VxMetersPerSecond,
expectedVx,
scenario + " Vx");
AssertNear(
actual.VyMetersPerSecond,
expectedVy,
scenario + " Vy");
AssertNear(
actual.OmegaRadiansPerSecond,
expectedOmega,
scenario + " Omega");
}
private static void AssertNear(
double actual,
double expected,
string valueName)
{
if (Math.Abs(actual - expected) > Tolerance)
{
throw new InvalidOperationException(
$"{valueName}错误:" +
$"actual={actual:F9}, expected={expected:F9}。");
}
}
}
}
@@ -0,0 +1,310 @@
using System;
using MultiWheelC.Fleet;
using MyParking.Shared;
namespace MultiWheelC.Tests
{
internal static class FleetPreparationCoordinatorTests
{
private const double Tolerance = 1e-9;
public static void Run()
{
VerifyLeaderYawExample();
VerifyTailToTailUsesEquivalentAxis();
VerifyAllMembersMustBeReady();
VerifyStaleStatusIsIgnored();
VerifyMemberFaultIsLatched();
VerifyFaultAfterAuthorizationIsLatched();
VerifyPrematureActiveIsRejected();
VerifyCancelClearsPlan();
Console.WriteLine(
"FleetPreparationCoordinator测试通过。共8个场景。");
}
private static void VerifyLeaderYawExample()
{
var capture = FleetLayoutCapture.Capture(
new[]
{
new FleetMemberPose(
1,
new Pose2D(
-1.0,
0.0,
AngleMath.DegreesToRadians(20.0))),
new FleetMemberPose(
2,
new Pose2D(1.0, 0.0, 0.0))
},
leaderVehicleId: 1);
var coordinator =
new FleetPreparationCoordinator();
coordinator.StartRollingPreparation(
planId: 1,
capture.Layout,
motionDirectionInFleetRadians: 0.0);
AssertTargetDegrees(coordinator, 1, 0.0);
AssertTargetDegrees(coordinator, 2, 20.0);
}
private static void VerifyTailToTailUsesEquivalentAxis()
{
var coordinator =
new FleetPreparationCoordinator();
coordinator.StartRollingPreparation(
planId: 2,
CreateTailToTailLayout(),
motionDirectionInFleetRadians: 0.0);
AssertTargetDegrees(coordinator, 1, 0.0);
AssertTargetDegrees(coordinator, 2, 0.0);
}
private static void VerifyAllMembersMustBeReady()
{
var coordinator = CreateStartedCoordinator(3);
AssertState(
coordinator.ReportMemberStatus(
CreateStatus(
3,
1,
FleetMemberAgentState.Ready)),
FleetPreparationCoordinatorState
.WaitingForMembers,
"仅一辆车Ready");
AssertState(
coordinator.ReportMemberStatus(
CreateStatus(
3,
2,
FleetMemberAgentState.Ready)),
FleetPreparationCoordinatorState
.ReadyToActivate,
"全部成员Ready");
AssertState(
coordinator.ReportMemberStatus(
CreateStatus(
3,
1,
FleetMemberAgentState.Preparing)),
FleetPreparationCoordinatorState
.WaitingForMembers,
"成员失去Ready");
AssertState(
coordinator.ReportMemberStatus(
CreateStatus(
3,
1,
FleetMemberAgentState.Ready)),
FleetPreparationCoordinatorState
.ReadyToActivate,
"成员重新Ready");
if (coordinator.TryAuthorizeActivation(4))
{
throw new InvalidOperationException(
"错误任务编号不应获得激活授权。");
}
if (!coordinator.TryAuthorizeActivation(3))
{
throw new InvalidOperationException(
"全部成员Ready后没有获得激活授权。");
}
AssertState(
coordinator.State,
FleetPreparationCoordinatorState
.ActivationAuthorized,
"统一激活授权");
}
private static void VerifyStaleStatusIsIgnored()
{
var coordinator = CreateStartedCoordinator(5);
var state = coordinator.ReportMemberStatus(
CreateStatus(
4,
1,
FleetMemberAgentState.Ready));
AssertState(
state,
FleetPreparationCoordinatorState
.WaitingForMembers,
"旧任务状态报告");
}
private static void VerifyMemberFaultIsLatched()
{
var coordinator = CreateStartedCoordinator(6);
var state = coordinator.ReportMemberStatus(
CreateStatus(
6,
2,
FleetMemberAgentState.Faulted,
"舵轮未到位"));
AssertState(
state,
FleetPreparationCoordinatorState.Faulted,
"成员准备故障");
if (string.IsNullOrWhiteSpace(
coordinator.LastFailureReason))
{
throw new InvalidOperationException(
"成员准备故障没有保存原因。");
}
}
private static void VerifyFaultAfterAuthorizationIsLatched()
{
var coordinator = CreateStartedCoordinator(9);
coordinator.ReportMemberStatus(
CreateStatus(
9,
1,
FleetMemberAgentState.Ready));
coordinator.ReportMemberStatus(
CreateStatus(
9,
2,
FleetMemberAgentState.Ready));
coordinator.TryAuthorizeActivation(9);
var state = coordinator.ReportMemberStatus(
CreateStatus(
9,
2,
FleetMemberAgentState.Faulted,
"激活失败"));
AssertState(
state,
FleetPreparationCoordinatorState.Faulted,
"授权后的成员故障");
}
private static void VerifyPrematureActiveIsRejected()
{
var coordinator = CreateStartedCoordinator(7);
var state = coordinator.ReportMemberStatus(
CreateStatus(
7,
1,
FleetMemberAgentState.Active));
AssertState(
state,
FleetPreparationCoordinatorState.Faulted,
"成员提前运动");
}
private static void VerifyCancelClearsPlan()
{
var coordinator = CreateStartedCoordinator(8);
coordinator.Cancel();
AssertState(
coordinator.State,
FleetPreparationCoordinatorState.Idle,
"取消准备任务");
if (coordinator.CurrentPlanId != 0 ||
coordinator.Targets.Count != 0)
{
throw new InvalidOperationException(
"取消后没有清除准备任务数据。");
}
}
private static FleetPreparationCoordinator
CreateStartedCoordinator(long planId)
{
var coordinator =
new FleetPreparationCoordinator();
coordinator.StartRollingPreparation(
planId,
CreateTailToTailLayout(),
motionDirectionInFleetRadians: 0.0);
return coordinator;
}
private static FleetLayout CreateTailToTailLayout()
{
return new FleetLayout(
new[]
{
new VehicleLayout(
1,
new Pose2D(-1.0, 0.0, 0.0)),
new VehicleLayout(
2,
new Pose2D(1.0, 0.0, Math.PI))
});
}
private static FleetMemberPreparationStatus CreateStatus(
long planId,
int vehicleId,
FleetMemberAgentState state,
string failureReason = "")
{
return new FleetMemberPreparationStatus(
planId,
vehicleId,
state,
failureReason);
}
private static void AssertTargetDegrees(
FleetPreparationCoordinator coordinator,
int vehicleId,
double expectedDegrees)
{
if (!coordinator.TryGetTarget(
vehicleId,
out var target))
{
throw new InvalidOperationException(
$"没有找到车辆{vehicleId}的准备目标。");
}
AssertNear(
AngleMath.RadiansToDegrees(
target.MotionDirectionInBodyRadians),
expectedDegrees,
$"车辆{vehicleId}本地β");
}
private static void AssertState(
FleetPreparationCoordinatorState actual,
FleetPreparationCoordinatorState expected,
string scenario)
{
if (actual != expected)
{
throw new InvalidOperationException(
$"{scenario}状态错误:" +
$"actual={actual}, expected={expected}。");
}
}
private static void AssertNear(
double actual,
double expected,
string valueName)
{
if (Math.Abs(actual - expected) > Tolerance)
{
throw new InvalidOperationException(
$"{valueName}错误:" +
$"actual={actual:F9}, expected={expected:F9}。");
}
}
}
}
@@ -0,0 +1,430 @@
using System;
using System.Collections.Generic;
using MultiWheelC.Fleet;
using MyParking.Shared;
namespace MultiWheelC.Tests
{
internal static class FleetStateEstimatorTests
{
private const double Tolerance = 1e-9;
public static void Run()
{
VerifyRigidStateIsRecovered();
VerifyOlderSamplesAreAligned();
VerifyYawWrapAroundIsAveraged();
VerifySmallLayoutErrorIsReported();
VerifyInconsistentCentersAreRejected();
VerifyMissingMemberIsRejected();
VerifyInvalidVelocityRemainsExplicit();
Console.WriteLine(
"FleetStateEstimator车队状态估计测试通过。共7个场景。");
}
private static void VerifyRigidStateIsRecovered()
{
var layout = CreateLayout();
var fleetPose = new Pose2D(4.0, -2.0, 0.4);
var fleetTwist = new Twist2D(0.3, -0.1, 0.2);
var result = CreateEstimator().Estimate(
layout,
CreateRigidMemberStates(
layout,
fleetPose,
fleetTwist,
sampleTimestampSeconds: 5.0,
hasValidVelocityEstimate: true),
targetTimestampSeconds: 5.0);
var state = RequireState(result, "刚体状态还原");
AssertPose(
state.FleetPoseInWorld,
fleetPose,
"刚体状态还原");
AssertTwist(
state.TwistAtFleetOriginInWorld,
fleetTwist,
"刚体速度还原");
for (var index = 0;
index < result.MemberErrors.Count;
index++)
{
AssertPose(
result.MemberErrors[index]
.ActualPoseInExpectedVehicleFrame,
Pose2D.Identity,
"刚体布局误差");
}
}
private static void VerifyOlderSamplesAreAligned()
{
var layout = CreateLayout();
var sampleFleetPose =
new Pose2D(1.0, 2.0, 0.3);
var fleetTwist =
new Twist2D(0.4, -0.2, 0.0);
var result = CreateEstimator().Estimate(
layout,
CreateRigidMemberStates(
layout,
sampleFleetPose,
fleetTwist,
sampleTimestampSeconds: 0.9,
hasValidVelocityEstimate: true),
targetTimestampSeconds: 1.0);
var state = RequireState(result, "成员时间对齐");
AssertPose(
state.FleetPoseInWorld,
new Pose2D(1.04, 1.98, 0.3),
"成员时间对齐");
AssertTwist(
state.TwistAtFleetOriginInWorld,
fleetTwist,
"时间对齐后速度");
}
private static void VerifySmallLayoutErrorIsReported()
{
var layout = CreateLayout();
var result = CreateEstimator().Estimate(
layout,
new[]
{
CreateMemberState(
1,
new Pose2D(-0.98, 0.0, 0.0),
Twist2D.Zero,
1.0,
true),
CreateMemberState(
2,
new Pose2D(0.99, 0.0, Math.PI),
Twist2D.Zero,
1.0,
true)
},
targetTimestampSeconds: 1.0);
var state = RequireState(result, "小范围布局误差");
AssertNear(
state.FleetPoseInWorld.XMeters,
0.005,
"小范围布局误差中心X");
AssertNear(
FindError(result.MemberErrors, 1)
.ActualPoseInExpectedVehicleFrame.XMeters,
0.015,
"车辆1布局误差X");
AssertNear(
FindError(result.MemberErrors, 2)
.ActualPoseInExpectedVehicleFrame.XMeters,
0.015,
"车辆2布局误差X");
}
private static void VerifyYawWrapAroundIsAveraged()
{
var layout = CreateLayout();
var firstCandidate = new Pose2D(
0.0,
0.0,
AngleMath.DegreesToRadians(179.0));
var secondCandidate = new Pose2D(
0.0,
0.0,
AngleMath.DegreesToRadians(-179.0));
var result = CreateEstimator().Estimate(
layout,
new[]
{
CreateMemberState(
1,
FrameTransform2D.Compose(
firstCandidate,
layout.Vehicles[0].PoseInFleet),
Twist2D.Zero,
1.0,
true),
CreateMemberState(
2,
FrameTransform2D.Compose(
secondCandidate,
layout.Vehicles[1].PoseInFleet),
Twist2D.Zero,
1.0,
true)
},
targetTimestampSeconds: 1.0);
var state = RequireState(result, "跨正负π航向平均");
AssertNear(
Math.Abs(state.FleetPoseInWorld.YawRadians),
Math.PI,
"跨正负π航向平均");
}
private static void VerifyInconsistentCentersAreRejected()
{
var result = CreateEstimator().Estimate(
CreateLayout(),
new[]
{
CreateMemberState(
1,
new Pose2D(-1.0, 0.0, 0.0),
Twist2D.Zero,
1.0,
true),
CreateMemberState(
2,
new Pose2D(1.3, 0.0, Math.PI),
Twist2D.Zero,
1.0,
true)
},
targetTimestampSeconds: 1.0);
AssertUnavailable(result, "候选中心冲突");
}
private static void VerifyMissingMemberIsRejected()
{
var result = CreateEstimator().Estimate(
CreateLayout(),
new[]
{
CreateMemberState(
1,
new Pose2D(-1.0, 0.0, 0.0),
Twist2D.Zero,
1.0,
true)
},
targetTimestampSeconds: 1.0);
AssertUnavailable(result, "成员缺失");
}
private static void VerifyInvalidVelocityRemainsExplicit()
{
var layout = CreateLayout();
var result = CreateEstimator().Estimate(
layout,
CreateRigidMemberStates(
layout,
Pose2D.Identity,
new Twist2D(0.4, 0.0, 0.0),
sampleTimestampSeconds: 1.0,
hasValidVelocityEstimate: false),
targetTimestampSeconds: 1.0);
var state = RequireState(result, "速度未初始化");
if (state.HasValidVelocityEstimate)
{
throw new InvalidOperationException(
"成员速度无效时车队速度不应标记为有效。");
}
AssertTwist(
state.TwistAtFleetOriginInWorld,
Twist2D.Zero,
"速度未初始化");
}
private static FleetStateEstimator CreateEstimator()
{
return new FleetStateEstimator(
maximumMemberStateAgeSeconds: 0.25,
maximumPositionDisagreementMeters: 0.1,
maximumYawDisagreementRadians:
AngleMath.DegreesToRadians(5.0));
}
private static FleetLayout CreateLayout()
{
return new FleetLayout(
new[]
{
new VehicleLayout(
1,
new Pose2D(-1.0, 0.0, 0.0)),
new VehicleLayout(
2,
new Pose2D(1.0, 0.0, Math.PI))
});
}
private static FleetMemberStateSample[]
CreateRigidMemberStates(
FleetLayout layout,
Pose2D fleetPoseInWorld,
Twist2D twistAtFleetOriginInWorld,
double sampleTimestampSeconds,
bool hasValidVelocityEstimate)
{
var states =
new FleetMemberStateSample[layout.VehicleCount];
for (var index = 0;
index < layout.Vehicles.Count;
index++)
{
var vehicle = layout.Vehicles[index];
var memberPoseInWorld =
FrameTransform2D.Compose(
fleetPoseInWorld,
vehicle.PoseInFleet);
var xFromFleetOrigin =
memberPoseInWorld.XMeters -
fleetPoseInWorld.XMeters;
var yFromFleetOrigin =
memberPoseInWorld.YMeters -
fleetPoseInWorld.YMeters;
var memberTwistInWorld = new Twist2D(
twistAtFleetOriginInWorld
.VxMetersPerSecond -
twistAtFleetOriginInWorld
.OmegaRadiansPerSecond *
yFromFleetOrigin,
twistAtFleetOriginInWorld
.VyMetersPerSecond +
twistAtFleetOriginInWorld
.OmegaRadiansPerSecond *
xFromFleetOrigin,
twistAtFleetOriginInWorld
.OmegaRadiansPerSecond);
states[index] = new FleetMemberStateSample(
vehicle.VehicleId,
sampleTimestampSeconds,
memberPoseInWorld,
memberTwistInWorld,
isStateAvailable: true,
hasValidVelocityEstimate:
hasValidVelocityEstimate);
}
return states;
}
private static FleetMemberStateSample CreateMemberState(
int vehicleId,
Pose2D poseInWorld,
Twist2D twistInWorld,
double timestampSeconds,
bool hasValidVelocityEstimate)
{
return new FleetMemberStateSample(
vehicleId,
timestampSeconds,
poseInWorld,
twistInWorld,
isStateAvailable: true,
hasValidVelocityEstimate:
hasValidVelocityEstimate);
}
private static FleetState RequireState(
FleetStateEstimateResult result,
string scenario)
{
if (!result.IsAvailable || !result.State.HasValue)
{
throw new InvalidOperationException(
$"{scenario}应产生可用状态:" +
result.UnavailableReason);
}
return result.State.Value;
}
private static void AssertUnavailable(
FleetStateEstimateResult result,
string scenario)
{
if (result.IsAvailable ||
string.IsNullOrWhiteSpace(
result.UnavailableReason))
{
throw new InvalidOperationException(
$"{scenario}应返回带原因的不可用结果。");
}
}
private static FleetMemberLayoutError FindError(
IReadOnlyList<FleetMemberLayoutError> errors,
int vehicleId)
{
for (var index = 0;
index < errors.Count;
index++)
{
if (errors[index].VehicleId == vehicleId)
{
return errors[index];
}
}
throw new InvalidOperationException(
$"没有找到车辆{vehicleId}的布局误差。");
}
private static void AssertPose(
Pose2D actual,
Pose2D expected,
string scenario)
{
AssertNear(
actual.XMeters,
expected.XMeters,
scenario + " X");
AssertNear(
actual.YMeters,
expected.YMeters,
scenario + " Y");
AssertNear(
AngleMath.ShortestDifferenceRadians(
actual.YawRadians,
expected.YawRadians),
0.0,
scenario + " Yaw");
}
private static void AssertTwist(
Twist2D actual,
Twist2D expected,
string scenario)
{
AssertNear(
actual.VxMetersPerSecond,
expected.VxMetersPerSecond,
scenario + " Vx");
AssertNear(
actual.VyMetersPerSecond,
expected.VyMetersPerSecond,
scenario + " Vy");
AssertNear(
actual.OmegaRadiansPerSecond,
expected.OmegaRadiansPerSecond,
scenario + " Omega");
}
private static void AssertNear(
double actual,
double expected,
string valueName)
{
if (Math.Abs(actual - expected) > Tolerance)
{
throw new InvalidOperationException(
$"{valueName}错误:" +
$"actual={actual:F9}, expected={expected:F9}。");
}
}
}
}
+5
View File
@@ -35,8 +35,13 @@ namespace MultiWheelC.Tests
Console.WriteLine(
"Stanley前进/倒车横向符号测试通过。共8个场景。");
FleetLayoutCaptureTests.Run();
FleetStateEstimatorTests.Run();
FleetKinematicsTests.Run();
FleetControllerTests.Run();
FleetMemberCommandCorrectorTests.Run();
FleetCoordinatorTests.Run();
FleetPreparationCoordinatorTests.Run();
}
/// <summary>