Files
ParkingRobot/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/SmoothingScenarioFixtureLoader.cs
T

245 lines
14 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using MultiWheelC.TrajectoryPlanning.CoarsePath;
using MultiWheelC.TrajectoryPlanning.Mapping;
using MultiWheelC.TrajectoryPlanning.PathSmoothing.Processing;
using Newtonsoft.Json;
namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Test;
/// <summary>加载并验证版本化 JSON 快速夹具;此类不运行 Hybrid A*。</summary>
public static class SmoothingScenarioFixtureLoader
{
/// <summary>读取、校验并冻结指定的快速夹具文件。</summary>
public static IReadOnlyList<SmoothingScenarioFixture> LoadAndVerify(string fixturePath)
{
if (string.IsNullOrWhiteSpace(fixturePath)) throw new ArgumentException("夹具路径不能为空。", nameof(fixturePath));
if (!File.Exists(fixturePath)) throw new FileNotFoundException("找不到路径平滑夹具文件。", fixturePath);
SmoothingFixtureDocument document = JsonConvert.DeserializeObject<SmoothingFixtureDocument>(
File.ReadAllText(fixturePath, Encoding.UTF8));
if (document == null || document.SchemaVersion != 1 || document.Scenarios == null)
throw new InvalidDataException("路径平滑夹具架构版本无效。");
var fixtures = new List<SmoothingScenarioFixture>(document.Scenarios.Count);
var ids = new HashSet<string>(StringComparer.Ordinal);
for (int index = 0; index < document.Scenarios.Count; index++)
{
SmoothingFixtureRecord record = document.Scenarios[index];
Validate(record, ids);
string fingerprint = ComputeFingerprint(record);
if (!string.Equals(record.ConfigurationFingerprint, fingerprint, StringComparison.Ordinal))
throw new InvalidDataException("路径平滑夹具指纹已过期:" + record.Id + "。");
fixtures.Add(new SmoothingScenarioFixture(record, true));
}
return fixtures.AsReadOnly();
}
internal static string ComputeFingerprint(SmoothingFixtureRecord record)
{
string material = BuildFingerprintMaterial(record);
using (SHA256 sha256 = SHA256.Create())
{
byte[] hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(material));
var builder = new StringBuilder(hash.Length * 2 + 7);
builder.Append("sha256:");
for (int index = 0; index < hash.Length; index++) builder.Append(hash[index].ToString("x2", CultureInfo.InvariantCulture));
return builder.ToString();
}
}
internal static PlanningGridMap BuildMap(SmoothingScenarioFixture fixture)
{
SmoothingFixtureRecord record = fixture.Record;
var obstacles = new List<IMapObstacle>(record.Map.Obstacles.Count);
for (int index = 0; index < record.Map.Obstacles.Count; index++)
{
SmoothingFixtureObstacle obstacle = record.Map.Obstacles[index];
if (string.Equals(obstacle.Kind, "circle", StringComparison.Ordinal))
obstacles.Add(new CircleObstacle(obstacle.CenterXMm, obstacle.CenterYMm, obstacle.RadiusMm));
else if (string.Equals(obstacle.Kind, "axis-aligned-rectangle", StringComparison.Ordinal))
obstacles.Add(new AxisAlignedRectangleObstacle(obstacle.XMinMm, obstacle.XMaxMm, obstacle.YMinMm, obstacle.YMaxMm));
else
throw new InvalidDataException("夹具包含未知障碍物类型。");
}
var mapRequest = new PlanningMapRequest
{
Bounds = new MapBoundsMm(record.Map.XMinMm, record.Map.XMaxMm, record.Map.YMinMm, record.Map.YMaxMm),
ResolutionMm = record.Map.ResolutionMm,
AllowExplicitEmptyMap = obstacles.Count == 0,
ObstacleSources = obstacles.Count == 0
? Array.Empty<IMapObstacleSource>()
: new IMapObstacleSource[] { new ManualObstacleSource("fixture-" + record.Id, record.FixtureVersion, true, obstacles) },
};
PlanningMapBuildResult build = new PlanningMapFactory().Create(mapRequest);
if (!build.Succeeded || build.Map == null) throw new InvalidDataException("夹具地图无法重建:" + record.Id + "。");
return build.Map;
}
internal static VehicleParameters BuildVehicle(SmoothingScenarioFixture fixture)
{
SmoothingFixtureVehicle vehicle = fixture.Record.Vehicle;
return new VehicleParameters
{
LengthMeters = vehicle.LengthMeters,
WidthMeters = vehicle.WidthMeters,
SafetyMarginMeters = vehicle.SafetyMarginMeters,
MaximumCurvaturePerMeter = vehicle.MaximumCurvaturePerMeter,
};
}
private static void Validate(SmoothingFixtureRecord record, ISet<string> ids)
{
if (record == null || string.IsNullOrWhiteSpace(record.Id) || record.FixtureVersion <= 0 ||
record.Map == null || record.Vehicle == null || record.PlanningConfiguration == null || record.Path == null || record.Path.Count < 2 ||
record.Segments == null || record.Segments.Count == 0 || !ids.Add(record.Id))
throw new InvalidDataException("路径平滑夹具缺少必需字段、版本或唯一 ID。");
ValidateMap(record);
ValidateVehicle(record);
ValidatePlanningConfiguration(record);
ValidatePathContract(record);
}
private static void ValidateMap(SmoothingFixtureRecord record)
{
SmoothingFixtureMap map = record.Map;
if (!IsFinite(map.XMinMm) || !IsFinite(map.XMaxMm) || !IsFinite(map.YMinMm) || !IsFinite(map.YMaxMm) ||
!IsFinite(map.ResolutionMm) || map.XMaxMm <= map.XMinMm || map.YMaxMm <= map.YMinMm || map.ResolutionMm <= 0f ||
map.Obstacles == null)
{
throw new InvalidDataException("Fixture map contract is invalid: " + record.Id + ".");
}
for (int index = 0; index < map.Obstacles.Count; index++)
{
SmoothingFixtureObstacle obstacle = map.Obstacles[index];
bool circle = obstacle != null && string.Equals(obstacle.Kind, "circle", StringComparison.Ordinal) &&
IsFinite(obstacle.CenterXMm) && IsFinite(obstacle.CenterYMm) && IsFinite(obstacle.RadiusMm) && obstacle.RadiusMm >= 0f;
bool rectangle = obstacle != null && string.Equals(obstacle.Kind, "axis-aligned-rectangle", StringComparison.Ordinal) &&
IsFinite(obstacle.XMinMm) && IsFinite(obstacle.XMaxMm) && IsFinite(obstacle.YMinMm) && IsFinite(obstacle.YMaxMm) &&
obstacle.XMaxMm >= obstacle.XMinMm && obstacle.YMaxMm >= obstacle.YMinMm;
if (!circle && !rectangle)
throw new InvalidDataException("Fixture map contract is invalid: " + record.Id + ".");
}
}
private static void ValidateVehicle(SmoothingFixtureRecord record)
{
SmoothingFixtureVehicle vehicle = record.Vehicle;
if (!IsFinite(vehicle.LengthMeters) || !IsFinite(vehicle.WidthMeters) || !IsFinite(vehicle.SafetyMarginMeters) ||
!IsFinite(vehicle.MaximumCurvaturePerMeter) || vehicle.LengthMeters <= 0d || vehicle.WidthMeters <= 0d ||
vehicle.SafetyMarginMeters < 0d || vehicle.MaximumCurvaturePerMeter <= 0d)
{
throw new InvalidDataException("Fixture vehicle contract is invalid: " + record.Id + ".");
}
}
private static void ValidatePlanningConfiguration(SmoothingFixtureRecord record)
{
SmoothingFixturePlanningConfiguration configuration = record.PlanningConfiguration;
if (!IsPositiveFinite(configuration.PrimitiveLengthMeters) || !IsPositiveFinite(configuration.IntegrationStepMeters) ||
!IsPositiveFinite(configuration.MaximumCollisionCheckStepMeters) || !IsPositiveFinite(configuration.HeadingResolutionRadians) ||
configuration.CurvatureLevelCount < 2 || !IsPositiveFinite(configuration.GoalPositionToleranceMeters) ||
!IsPositiveFinite(configuration.GoalHeadingToleranceRadians) || configuration.MaximumExpandedNodes <= 0 ||
!IsPositiveFinite(configuration.SearchTimeoutSeconds) || !IsFinite(configuration.HeuristicWeight) ||
!IsFinite(configuration.ReverseCostMultiplier) || !IsFinite(configuration.GearSwitchPenaltyMeters) ||
!IsFinite(configuration.CurvatureMagnitudeWeight) || !IsFinite(configuration.CurvatureChangePenaltyMetersPerLevel) ||
!IsFinite(configuration.ClearanceCostWeight) || !IsPositiveFinite(configuration.ClearanceCostDistanceMeters))
{
throw new InvalidDataException("Fixture planning configuration contract is invalid: " + record.Id + ".");
}
}
private static void ValidatePathContract(SmoothingFixtureRecord record)
{
var fixture = new SmoothingScenarioFixture(record, true);
try
{
var request = new PathSmoothingRequest(
fixture.Path,
fixture.Segments,
BuildMap(fixture),
BuildVehicle(fixture),
new PathSmoothingConfiguration());
if (!new PathSmoothingPreprocessor().TryPrepare(request, out _, out string reason))
throw new InvalidDataException("Fixture path contract is invalid: " + record.Id + "; " + reason);
}
catch (InvalidDataException)
{
throw;
}
catch (Exception exception)
{
throw new InvalidDataException("Fixture path contract is invalid: " + record.Id + "; " + exception.Message, exception);
}
}
private static bool IsFinite(float value)
{
return !float.IsNaN(value) && !float.IsInfinity(value);
}
private static bool IsFinite(double value)
{
return !double.IsNaN(value) && !double.IsInfinity(value);
}
private static bool IsPositiveFinite(double value)
{
return IsFinite(value) && value > 0d;
}
private static string BuildFingerprintMaterial(SmoothingFixtureRecord record)
{
var builder = new StringBuilder();
Append(builder, record.Id); Append(builder, record.FixtureVersion);
Append(builder, record.Map.XMinMm); Append(builder, record.Map.XMaxMm); Append(builder, record.Map.YMinMm);
Append(builder, record.Map.YMaxMm); Append(builder, record.Map.ResolutionMm);
for (int index = 0; index < record.Map.Obstacles.Count; index++)
{
SmoothingFixtureObstacle obstacle = record.Map.Obstacles[index];
Append(builder, obstacle.Kind); Append(builder, obstacle.XMinMm); Append(builder, obstacle.XMaxMm);
Append(builder, obstacle.YMinMm); Append(builder, obstacle.YMaxMm); Append(builder, obstacle.CenterXMm);
Append(builder, obstacle.CenterYMm); Append(builder, obstacle.RadiusMm);
}
Append(builder, record.Vehicle.LengthMeters); Append(builder, record.Vehicle.WidthMeters);
Append(builder, record.Vehicle.SafetyMarginMeters); Append(builder, record.Vehicle.MaximumCurvaturePerMeter);
SmoothingFixturePlanningConfiguration configuration = record.PlanningConfiguration;
Append(builder, configuration.PrimitiveLengthMeters); Append(builder, configuration.IntegrationStepMeters);
Append(builder, configuration.MaximumCollisionCheckStepMeters); Append(builder, configuration.HeadingResolutionRadians);
Append(builder, configuration.CurvatureLevelCount); Append(builder, configuration.GoalPositionToleranceMeters);
Append(builder, configuration.GoalHeadingToleranceRadians); Append(builder, configuration.MaximumExpandedNodes);
Append(builder, configuration.SearchTimeoutSeconds); Append(builder, configuration.HeuristicWeight);
Append(builder, configuration.ReverseCostMultiplier); Append(builder, configuration.GearSwitchPenaltyMeters);
Append(builder, configuration.CurvatureMagnitudeWeight); Append(builder, configuration.CurvatureChangePenaltyMetersPerLevel);
Append(builder, configuration.ClearanceCostWeight); Append(builder, configuration.ClearanceCostDistanceMeters);
Append(builder, configuration.AllowReverse ? 1 : 0);
for (int index = 0; index < record.Path.Count; index++)
{
SmoothingFixturePathPoint point = record.Path[index];
Append(builder, point.XMeters); Append(builder, point.YMeters); Append(builder, point.HeadingRadians);
Append(builder, point.UnwrappedHeadingRadians); Append(builder, point.ArcLengthMeters); Append(builder, (int)point.Direction);
Append(builder, point.VehicleCurvaturePerMeter); Append(builder, point.BodyClearanceMeters);
Append(builder, point.IsGearSwitchPoint ? 1 : 0); Append(builder, (int)point.Source);
}
for (int index = 0; index < record.Segments.Count; index++)
{
SmoothingFixtureSegment segment = record.Segments[index];
Append(builder, segment.SegmentIndex); Append(builder, (int)segment.Direction); Append(builder, segment.StartIndex);
Append(builder, segment.EndIndex); Append(builder, segment.StartsAtGearSwitch ? 1 : 0); Append(builder, segment.EndsAtGearSwitch ? 1 : 0);
}
return builder.ToString();
}
private static void Append(StringBuilder builder, string value) { builder.Append(value ?? string.Empty).Append('|'); }
private static void Append(StringBuilder builder, int value) { builder.Append(value.ToString(CultureInfo.InvariantCulture)).Append('|'); }
private static void Append(StringBuilder builder, float value) { builder.Append(value.ToString("R", CultureInfo.InvariantCulture)).Append('|'); }
private static void Append(StringBuilder builder, double value) { builder.Append(value.ToString("R", CultureInfo.InvariantCulture)).Append('|'); }
}