245 lines
14 KiB
C#
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('|'); }
|
|
}
|