using System; using System.Collections.Generic; using System.Collections.ObjectModel; using MyParking.Shared; namespace MultiWheelC.Fleet { // 一辆成员车在主车统一时间轴上的状态样本。 public readonly struct FleetMemberStateSample { public FleetMemberStateSample( int vehicleId, double sampleTimestampSeconds, Pose2D poseInWorld, Twist2D twistAtVehicleOriginInWorld, bool isStateAvailable, bool hasValidVelocityEstimate) { if (vehicleId <= 0) { throw new ArgumentOutOfRangeException( nameof(vehicleId), "编队成员车号必须大于零。"); } NumericGuard.EnsureFiniteNonNegative( sampleTimestampSeconds, nameof(sampleTimestampSeconds)); NumericGuard.EnsureFinite( poseInWorld, nameof(poseInWorld)); NumericGuard.EnsureFinite( twistAtVehicleOriginInWorld, nameof(twistAtVehicleOriginInWorld)); VehicleId = vehicleId; SampleTimestampSeconds = sampleTimestampSeconds; PoseInWorld = new Pose2D( poseInWorld.XMeters, poseInWorld.YMeters, AngleMath.NormalizeRadians( poseInWorld.YawRadians)); TwistAtVehicleOriginInWorld = twistAtVehicleOriginInWorld; IsStateAvailable = isStateAvailable; HasValidVelocityEstimate = hasValidVelocityEstimate; } public int VehicleId { get; } // 该时间戳必须已经换算到主车/协调器的单调时间轴。 public double SampleTimestampSeconds { get; } public Pose2D PoseInWorld { get; } // 成员车体中心处的实际速度,在世界坐标系中表达。 public Twist2D TwistAtVehicleOriginInWorld { get; } public bool IsStateAvailable { get; } public bool HasValidVelocityEstimate { get; } } // 成员实际位姿相对固定布局目标位姿的误差。 public readonly struct FleetMemberLayoutError { public FleetMemberLayoutError( int vehicleId, Pose2D actualPoseInExpectedVehicleFrame) { if (vehicleId <= 0) { throw new ArgumentOutOfRangeException( nameof(vehicleId), "编队成员车号必须大于零。"); } NumericGuard.EnsureFinite( actualPoseInExpectedVehicleFrame, nameof(actualPoseInExpectedVehicleFrame)); VehicleId = vehicleId; ActualPoseInExpectedVehicleFrame = new Pose2D( actualPoseInExpectedVehicleFrame.XMeters, actualPoseInExpectedVehicleFrame.YMeters, AngleMath.NormalizeRadians( actualPoseInExpectedVehicleFrame .YawRadians)); } public int VehicleId { get; } // 期望成员车体系中表达的实际成员位姿;理想刚体布局时为Identity。 public Pose2D ActualPoseInExpectedVehicleFrame { get; } } // 一次车队状态估计的结果;不可用时不提供FleetState。 public sealed class FleetStateEstimateResult { private static readonly IReadOnlyList EmptyMemberErrors = Array.AsReadOnly( Array.Empty()); private FleetStateEstimateResult( bool isAvailable, FleetState? state, IReadOnlyList memberErrors, string unavailableReason) { IsAvailable = isAvailable; State = state; MemberErrors = memberErrors; UnavailableReason = unavailableReason; } public bool IsAvailable { get; } public FleetState? State { get; } public IReadOnlyList MemberErrors { get; } public string UnavailableReason { get; } internal static FleetStateEstimateResult Available( FleetState state, FleetMemberLayoutError[] memberErrors) { return new FleetStateEstimateResult( true, state, Array.AsReadOnly(memberErrors), string.Empty); } internal static FleetStateEstimateResult Unavailable( string reason) { return new FleetStateEstimateResult( false, null, EmptyMemberErrors, reason ?? string.Empty); } } // 从各成员状态反算并融合车队虚拟中心状态。 public sealed class FleetStateEstimator { private const double TimestampToleranceSeconds = 1e-9; private const double MinimumCircularMeanMagnitude = 1e-12; private readonly double _maximumMemberStateAgeSeconds; private readonly double _maximumPositionDisagreementMeters; private readonly double _maximumYawDisagreementRadians; public FleetStateEstimator( double maximumMemberStateAgeSeconds, double maximumPositionDisagreementMeters, double maximumYawDisagreementRadians) { NumericGuard.EnsureFinitePositive( maximumMemberStateAgeSeconds, nameof(maximumMemberStateAgeSeconds)); NumericGuard.EnsureFinitePositive( maximumPositionDisagreementMeters, nameof(maximumPositionDisagreementMeters)); NumericGuard.EnsureFinitePositive( maximumYawDisagreementRadians, nameof(maximumYawDisagreementRadians)); if (maximumYawDisagreementRadians > Math.PI) { throw new ArgumentOutOfRangeException( nameof(maximumYawDisagreementRadians), "车队候选航向差阈值不能大于π。"); } _maximumMemberStateAgeSeconds = maximumMemberStateAgeSeconds; _maximumPositionDisagreementMeters = maximumPositionDisagreementMeters; _maximumYawDisagreementRadians = maximumYawDisagreementRadians; } public FleetStateEstimateResult Estimate( FleetLayout layout, IReadOnlyList memberStates, double targetTimestampSeconds) { if (layout == null) { throw new ArgumentNullException(nameof(layout)); } if (memberStates == null) { throw new ArgumentNullException(nameof(memberStates)); } NumericGuard.EnsureFiniteNonNegative( targetTimestampSeconds, nameof(targetTimestampSeconds)); if (memberStates.Count != layout.VehicleCount) { return FleetStateEstimateResult.Unavailable( $"成员状态数量{memberStates.Count}与布局数量" + $"{layout.VehicleCount}不一致。"); } var statesByVehicleId = new Dictionary( memberStates.Count); for (var index = 0; index < memberStates.Count; index++) { var memberState = memberStates[index]; if (memberState.VehicleId <= 0) { return FleetStateEstimateResult.Unavailable( $"第{index}个成员状态的车号无效。"); } if (!layout.TryGetVehicle( memberState.VehicleId, out _)) { return FleetStateEstimateResult.Unavailable( $"成员状态包含布局外车辆" + $"{memberState.VehicleId}。"); } if (statesByVehicleId.ContainsKey( memberState.VehicleId)) { return FleetStateEstimateResult.Unavailable( $"成员状态包含重复车号" + $"{memberState.VehicleId}。"); } statesByVehicleId.Add( memberState.VehicleId, memberState); } var alignedMembers = new AlignedMemberState[layout.VehicleCount]; for (var index = 0; index < layout.Vehicles.Count; index++) { var vehicleLayout = layout.Vehicles[index]; if (!statesByVehicleId.TryGetValue( vehicleLayout.VehicleId, out var memberState)) { return FleetStateEstimateResult.Unavailable( $"缺少车辆{vehicleLayout.VehicleId}的状态。"); } var alignmentResult = AlignMemberState( memberState, targetTimestampSeconds, out var alignedPoseInWorld); if (alignmentResult != null) { return FleetStateEstimateResult.Unavailable( alignmentResult); } var candidateFleetPoseInWorld = FrameTransform2D.Compose( alignedPoseInWorld, FrameTransform2D.Inverse( vehicleLayout.PoseInFleet)); alignedMembers[index] = new AlignedMemberState( vehicleLayout, memberState, alignedPoseInWorld, candidateFleetPoseInWorld); } var disagreementReason = FindCandidateDisagreement(alignedMembers); if (disagreementReason != null) { return FleetStateEstimateResult.Unavailable( disagreementReason); } if (!TryAverageCandidateFleetPose( alignedMembers, out var fleetPoseInWorld)) { return FleetStateEstimateResult.Unavailable( "成员候选航向无法形成唯一的车队平均航向。"); } var hasValidVelocityEstimate = TryAverageFleetOriginTwist( alignedMembers, fleetPoseInWorld, out var twistAtFleetOriginInWorld); var fleetState = new FleetState( targetTimestampSeconds, fleetPoseInWorld, twistAtFleetOriginInWorld, hasValidVelocityEstimate); var memberErrors = CalculateMemberErrors( alignedMembers, fleetPoseInWorld); return FleetStateEstimateResult.Available( fleetState, memberErrors); } private string AlignMemberState( FleetMemberStateSample memberState, double targetTimestampSeconds, out Pose2D alignedPoseInWorld) { alignedPoseInWorld = memberState.PoseInWorld; if (!memberState.IsStateAvailable) { return $"车辆{memberState.VehicleId}状态不可用。"; } var ageSeconds = targetTimestampSeconds - memberState.SampleTimestampSeconds; if (ageSeconds < -TimestampToleranceSeconds) { return $"车辆{memberState.VehicleId}的状态时间晚于" + "本次估计目标时间。"; } if (ageSeconds > _maximumMemberStateAgeSeconds) { return $"车辆{memberState.VehicleId}的状态已过期:" + $"{ageSeconds:F3}s。"; } if (ageSeconds <= TimestampToleranceSeconds) { return null; } if (!memberState.HasValidVelocityEstimate) { return $"车辆{memberState.VehicleId}缺少时间对齐所需的" + "有效速度。"; } var twist = memberState.TwistAtVehicleOriginInWorld; alignedPoseInWorld = new Pose2D( memberState.PoseInWorld.XMeters + twist.VxMetersPerSecond * ageSeconds, memberState.PoseInWorld.YMeters + twist.VyMetersPerSecond * ageSeconds, AngleMath.NormalizeRadians( memberState.PoseInWorld.YawRadians + twist.OmegaRadiansPerSecond * ageSeconds)); return null; } private string FindCandidateDisagreement( IReadOnlyList alignedMembers) { for (var firstIndex = 0; firstIndex < alignedMembers.Count; firstIndex++) { var first = alignedMembers[firstIndex]; for (var secondIndex = firstIndex + 1; secondIndex < alignedMembers.Count; secondIndex++) { var second = alignedMembers[secondIndex]; var dx = first.CandidateFleetPoseInWorld.XMeters - second.CandidateFleetPoseInWorld.XMeters; var dy = first.CandidateFleetPoseInWorld.YMeters - second.CandidateFleetPoseInWorld.YMeters; var positionDifferenceMeters = Math.Sqrt(dx * dx + dy * dy); var yawDifferenceRadians = Math.Abs( AngleMath.ShortestDifferenceRadians( first.CandidateFleetPoseInWorld .YawRadians, second.CandidateFleetPoseInWorld .YawRadians)); if (positionDifferenceMeters > _maximumPositionDisagreementMeters) { return $"车辆{first.VehicleLayout.VehicleId}与" + $"车辆{second.VehicleLayout.VehicleId}反算的" + $"车队中心相差{positionDifferenceMeters:F3}m," + "超过允许值。"; } if (yawDifferenceRadians > _maximumYawDisagreementRadians) { return $"车辆{first.VehicleLayout.VehicleId}与" + $"车辆{second.VehicleLayout.VehicleId}反算的" + $"车队航向相差" + $"{AngleMath.RadiansToDegrees(yawDifferenceRadians):F2}°," + "超过允许值。"; } } } return null; } private static bool TryAverageCandidateFleetPose( IReadOnlyList alignedMembers, out Pose2D fleetPoseInWorld) { var xMeters = 0.0; var yMeters = 0.0; var yawCosineSum = 0.0; var yawSineSum = 0.0; for (var index = 0; index < alignedMembers.Count; index++) { var candidate = alignedMembers[index] .CandidateFleetPoseInWorld; xMeters += candidate.XMeters; yMeters += candidate.YMeters; yawCosineSum += Math.Cos(candidate.YawRadians); yawSineSum += Math.Sin(candidate.YawRadians); } var count = alignedMembers.Count; var circularMeanMagnitude = Math.Sqrt( yawCosineSum * yawCosineSum + yawSineSum * yawSineSum); if (circularMeanMagnitude < MinimumCircularMeanMagnitude) { fleetPoseInWorld = Pose2D.Identity; return false; } fleetPoseInWorld = new Pose2D( xMeters / count, yMeters / count, Math.Atan2(yawSineSum, yawCosineSum)); return true; } private static bool TryAverageFleetOriginTwist( IReadOnlyList alignedMembers, Pose2D fleetPoseInWorld, out Twist2D twistAtFleetOriginInWorld) { for (var index = 0; index < alignedMembers.Count; index++) { if (!alignedMembers[index] .MemberState .HasValidVelocityEstimate) { twistAtFleetOriginInWorld = Twist2D.Zero; return false; } } var vxMetersPerSecond = 0.0; var vyMetersPerSecond = 0.0; var omegaRadiansPerSecond = 0.0; for (var index = 0; index < alignedMembers.Count; index++) { var member = alignedMembers[index]; var twist = member.MemberState .TwistAtVehicleOriginInWorld; var memberXFromFleetOrigin = member.AlignedPoseInWorld.XMeters - fleetPoseInWorld.XMeters; var memberYFromFleetOrigin = member.AlignedPoseInWorld.YMeters - fleetPoseInWorld.YMeters; vxMetersPerSecond += twist.VxMetersPerSecond + twist.OmegaRadiansPerSecond * memberYFromFleetOrigin; vyMetersPerSecond += twist.VyMetersPerSecond - twist.OmegaRadiansPerSecond * memberXFromFleetOrigin; omegaRadiansPerSecond += twist.OmegaRadiansPerSecond; } var count = alignedMembers.Count; twistAtFleetOriginInWorld = new Twist2D( vxMetersPerSecond / count, vyMetersPerSecond / count, omegaRadiansPerSecond / count); return true; } private static FleetMemberLayoutError[] CalculateMemberErrors( IReadOnlyList alignedMembers, Pose2D fleetPoseInWorld) { var errors = new FleetMemberLayoutError[alignedMembers.Count]; for (var index = 0; index < alignedMembers.Count; index++) { var member = alignedMembers[index]; var expectedPoseInWorld = FrameTransform2D.Compose( fleetPoseInWorld, member.VehicleLayout.PoseInFleet); var actualPoseInExpectedVehicleFrame = FrameTransform2D.Compose( FrameTransform2D.Inverse( expectedPoseInWorld), member.AlignedPoseInWorld); errors[index] = new FleetMemberLayoutError( member.VehicleLayout.VehicleId, actualPoseInExpectedVehicleFrame); } return errors; } private readonly struct AlignedMemberState { public AlignedMemberState( VehicleLayout vehicleLayout, FleetMemberStateSample memberState, Pose2D alignedPoseInWorld, Pose2D candidateFleetPoseInWorld) { VehicleLayout = vehicleLayout; MemberState = memberState; AlignedPoseInWorld = alignedPoseInWorld; CandidateFleetPoseInWorld = candidateFleetPoseInWorld; } public VehicleLayout VehicleLayout { get; } public FleetMemberStateSample MemberState { get; } public Pose2D AlignedPoseInWorld { get; } public Pose2D CandidateFleetPoseInWorld { get; } } } }