Files
ParkingRobot/MultiWheelC.Tests/FleetStateEstimatorTests.cs

431 lines
14 KiB
C#

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}。");
}
}
}
}