fix: validate coarse path before smoothing
This commit is contained in:
@@ -37,6 +37,21 @@ public sealed class PathSmoothingService
|
|||||||
if (!_preprocessor.TryPrepare(request, out PreparedPath preparedPath, out reason))
|
if (!_preprocessor.TryPrepare(request, out PreparedPath preparedPath, out reason))
|
||||||
return Failure(PathSmoothingStatus.InvalidInput, stopwatch, 0, 0d, reason);
|
return Failure(PathSmoothingStatus.InvalidInput, stopwatch, 0, 0d, reason);
|
||||||
|
|
||||||
|
if (!TryAnalyzeAndValidate(
|
||||||
|
request,
|
||||||
|
preparedPath,
|
||||||
|
preparedPath.Segments,
|
||||||
|
configuration,
|
||||||
|
false,
|
||||||
|
cancellationToken,
|
||||||
|
out _,
|
||||||
|
out _,
|
||||||
|
out _,
|
||||||
|
out reason))
|
||||||
|
{
|
||||||
|
return Failure(PathSmoothingStatus.InvalidInput, stopwatch, 0, 0d, reason);
|
||||||
|
}
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
var input = new SmoothingAlgorithmInput(
|
var input = new SmoothingAlgorithmInput(
|
||||||
preparedPath,
|
preparedPath,
|
||||||
@@ -121,8 +136,37 @@ public sealed class PathSmoothingService
|
|||||||
if (!_preprocessor.TryPrepare(request, out PreparedPath revalidatedPath, out reason)) return false;
|
if (!_preprocessor.TryPrepare(request, out PreparedPath revalidatedPath, out reason)) return false;
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
return TryAnalyzeAndValidate(
|
||||||
|
request,
|
||||||
|
revalidatedPath,
|
||||||
|
ToFallbackSegments(revalidatedPath),
|
||||||
|
configuration,
|
||||||
|
true,
|
||||||
|
cancellationToken,
|
||||||
|
out fallbackPath,
|
||||||
|
out fallbackSegments,
|
||||||
|
out fallbackMetrics,
|
||||||
|
out reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryAnalyzeAndValidate(
|
||||||
|
PathSmoothingRequest request,
|
||||||
|
PreparedPath originalPath,
|
||||||
|
IReadOnlyList<PreparedDirectionSegment> candidateSegments,
|
||||||
|
PathSmoothingConfiguration configuration,
|
||||||
|
bool useFallbackSource,
|
||||||
|
CancellationToken cancellationToken,
|
||||||
|
out IReadOnlyList<SmoothedPathPoint> safePath,
|
||||||
|
out IReadOnlyList<SmoothedPathSegment> safeSegments,
|
||||||
|
out PathQualityMetrics metrics,
|
||||||
|
out string reason)
|
||||||
|
{
|
||||||
|
safePath = null;
|
||||||
|
safeSegments = null;
|
||||||
|
metrics = null;
|
||||||
|
reason = string.Empty;
|
||||||
if (!_analyzer.TryAnalyze(
|
if (!_analyzer.TryAnalyze(
|
||||||
ToFallbackSegments(revalidatedPath),
|
candidateSegments,
|
||||||
configuration.OutputSpacingMeters,
|
configuration.OutputSpacingMeters,
|
||||||
out PathGeometryAnalysis analysis,
|
out PathGeometryAnalysis analysis,
|
||||||
out reason))
|
out reason))
|
||||||
@@ -130,25 +174,26 @@ public sealed class PathSmoothingService
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
IReadOnlyList<SmoothedPathPoint> fallbackSourcePath = ToFallbackPoints(analysis.Path);
|
IReadOnlyList<SmoothedPathPoint> pathForValidation = useFallbackSource
|
||||||
|
? ToFallbackPoints(analysis.Path)
|
||||||
|
: analysis.Path;
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
if (!_validator.TryValidate(
|
if (!_validator.TryValidate(
|
||||||
fallbackSourcePath,
|
pathForValidation,
|
||||||
analysis.Segments,
|
analysis.Segments,
|
||||||
revalidatedPath,
|
originalPath,
|
||||||
request.Map,
|
request.Map,
|
||||||
request.Vehicle,
|
request.Vehicle,
|
||||||
configuration.MaximumCollisionCheckStepMeters,
|
configuration.MaximumCollisionCheckStepMeters,
|
||||||
out IReadOnlyList<SmoothedPathPoint> safePath,
|
out safePath,
|
||||||
out double minimumClearanceMeters,
|
out double minimumClearanceMeters,
|
||||||
out reason))
|
out reason))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
fallbackPath = safePath;
|
safeSegments = analysis.Segments;
|
||||||
fallbackSegments = analysis.Segments;
|
metrics = CreateMetrics(analysis, minimumClearanceMeters);
|
||||||
fallbackMetrics = CreateMetrics(analysis, minimumClearanceMeters);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,16 +21,31 @@ function Get-RequiredType([string]$Name) {
|
|||||||
return $assembly.GetType($Name, $true)
|
return $assembly.GetType($Name, $true)
|
||||||
}
|
}
|
||||||
|
|
||||||
function New-EmptyMap {
|
function New-Map([bool]$WithObstacle) {
|
||||||
$mapRequest = [Activator]::CreateInstance($mapRequestType)
|
$mapRequest = [Activator]::CreateInstance($mapRequestType)
|
||||||
$mapRequest.Bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]5000, [single]0, [single]5000))
|
$mapRequest.Bounds = [Activator]::CreateInstance($boundsType, @([single]0, [single]5000, [single]0, [single]5000))
|
||||||
$mapRequest.ResolutionMm = [single]50
|
$mapRequest.ResolutionMm = [single]50
|
||||||
$mapRequest.AllowExplicitEmptyMap = $true
|
if ($WithObstacle) {
|
||||||
|
$obstacle = [Activator]::CreateInstance($rectangleType, @([single]900, [single]1100, [single]400, [single]600))
|
||||||
|
$obstacles = [Array]::CreateInstance($obstacleType, 1)
|
||||||
|
$obstacles.SetValue($obstacle, 0)
|
||||||
|
$source = [Activator]::CreateInstance($manualSourceType, @('service-safety-obstacle', [long]1, $true, $obstacles))
|
||||||
|
$sources = [Array]::CreateInstance($obstacleSourceType, 1)
|
||||||
|
$sources.SetValue($source, 0)
|
||||||
|
$mapRequest.ObstacleSources = $sources
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$mapRequest.AllowExplicitEmptyMap = $true
|
||||||
|
}
|
||||||
$map = [Activator]::CreateInstance($mapFactoryType).Create($mapRequest).Map
|
$map = [Activator]::CreateInstance($mapFactoryType).Create($mapRequest).Map
|
||||||
Assert-True ($null -ne $map) 'Service test must create an explicit empty planning map.'
|
Assert-True ($null -ne $map) 'Service test must create a planning map.'
|
||||||
return $map
|
return $map
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function New-EmptyMap { return New-Map $false }
|
||||||
|
|
||||||
|
function New-CollidingMap { return New-Map $true }
|
||||||
|
|
||||||
function New-Vehicle {
|
function New-Vehicle {
|
||||||
$vehicle = [Activator]::CreateInstance($vehicleType)
|
$vehicle = [Activator]::CreateInstance($vehicleType)
|
||||||
$vehicle.LengthMeters = [double]0.20
|
$vehicle.LengthMeters = [double]0.20
|
||||||
@@ -58,7 +73,8 @@ function New-Configuration($Method = $cubicBSpline) {
|
|||||||
return $configuration
|
return $configuration
|
||||||
}
|
}
|
||||||
|
|
||||||
function New-Request([object[]]$Points, $Configuration) {
|
function New-Request([object[]]$Points, $Configuration, $Map = $null) {
|
||||||
|
if ($null -eq $Map) { $Map = New-EmptyMap }
|
||||||
$typedPoints = [Array]::CreateInstance($coarsePointType, $Points.Count)
|
$typedPoints = [Array]::CreateInstance($coarsePointType, $Points.Count)
|
||||||
for ($index = 0; $index -lt $Points.Count; $index++) {
|
for ($index = 0; $index -lt $Points.Count; $index++) {
|
||||||
$typedPoints.SetValue($Points[$index], $index)
|
$typedPoints.SetValue($Points[$index], $index)
|
||||||
@@ -67,7 +83,7 @@ function New-Request([object[]]$Points, $Configuration) {
|
|||||||
$segments = [Array]::CreateInstance($coarseSegmentType, 1)
|
$segments = [Array]::CreateInstance($coarseSegmentType, 1)
|
||||||
$segments.SetValue([Activator]::CreateInstance($coarseSegmentType, @(
|
$segments.SetValue([Activator]::CreateInstance($coarseSegmentType, @(
|
||||||
0, $forward, 0, ($Points.Count - 1), $false, $false)), 0)
|
0, $forward, 0, ($Points.Count - 1), $false, $false)), 0)
|
||||||
return [Activator]::CreateInstance($requestType, @($typedPoints, $segments, (New-EmptyMap), (New-Vehicle), $Configuration))
|
return [Activator]::CreateInstance($requestType, @($typedPoints, $segments, $Map, (New-Vehicle), $Configuration))
|
||||||
}
|
}
|
||||||
|
|
||||||
function New-StraightRequest($Configuration) {
|
function New-StraightRequest($Configuration) {
|
||||||
@@ -119,6 +135,10 @@ $directionType = Get-RequiredType ($coarsePath + 'TravelDirection')
|
|||||||
$coarsePointSourceType = Get-RequiredType ($coarsePath + 'CoarsePathPointSource')
|
$coarsePointSourceType = Get-RequiredType ($coarsePath + 'CoarsePathPointSource')
|
||||||
$vehicleType = Get-RequiredType ($coarsePath + 'VehicleParameters')
|
$vehicleType = Get-RequiredType ($coarsePath + 'VehicleParameters')
|
||||||
$boundsType = Get-RequiredType ($mapping + 'MapBoundsMm')
|
$boundsType = Get-RequiredType ($mapping + 'MapBoundsMm')
|
||||||
|
$obstacleType = Get-RequiredType ($mapping + 'IMapObstacle')
|
||||||
|
$rectangleType = Get-RequiredType ($mapping + 'AxisAlignedRectangleObstacle')
|
||||||
|
$obstacleSourceType = Get-RequiredType ($mapping + 'IMapObstacleSource')
|
||||||
|
$manualSourceType = Get-RequiredType ($mapping + 'ManualObstacleSource')
|
||||||
$mapRequestType = Get-RequiredType ($mapping + 'PlanningMapRequest')
|
$mapRequestType = Get-RequiredType ($mapping + 'PlanningMapRequest')
|
||||||
$mapFactoryType = Get-RequiredType ($mapping + 'PlanningMapFactory')
|
$mapFactoryType = Get-RequiredType ($mapping + 'PlanningMapFactory')
|
||||||
|
|
||||||
@@ -185,6 +205,15 @@ Assert-Equal 'InvalidInput' $invalidCoarseResult.Status.ToString() 'A non-finite
|
|||||||
Assert-NoGeometry $invalidCoarseResult 'Invalid coarse path'
|
Assert-NoGeometry $invalidCoarseResult 'Invalid coarse path'
|
||||||
Assert-Equal 0 $invalidCoarseResult.Diagnostics.RetryCount 'Invalid coarse input must be rejected before retries.'
|
Assert-Equal 0 $invalidCoarseResult.Diagnostics.RetryCount 'Invalid coarse input must be rejected before retries.'
|
||||||
|
|
||||||
|
# A finite, structurally valid coarse path may still be unsafe for the requested map and vehicle.
|
||||||
|
# It must be rejected before method selection/retries and may never use fallback to publish the unsafe geometry.
|
||||||
|
$unsafeCoarseResult = Invoke-Smooth (New-Request @(
|
||||||
|
(New-CoarsePoint 0.5 0.5 0.0 $forward),
|
||||||
|
(New-CoarsePoint 1.5 0.5 1.0 $forward)) (New-Configuration) (New-CollidingMap))
|
||||||
|
Assert-Equal 'InvalidInput' $unsafeCoarseResult.Status.ToString() 'A colliding coarse path must be invalid before smoothing starts.'
|
||||||
|
Assert-NoGeometry $unsafeCoarseResult 'Unsafe coarse path'
|
||||||
|
Assert-Equal 0 $unsafeCoarseResult.Diagnostics.RetryCount 'Unsafe coarse geometry must be rejected before retry execution.'
|
||||||
|
|
||||||
$cancelledConfiguration = New-Configuration
|
$cancelledConfiguration = New-Configuration
|
||||||
$cancellationSource = [Threading.CancellationTokenSource]::new()
|
$cancellationSource = [Threading.CancellationTokenSource]::new()
|
||||||
$cancellationSource.Cancel()
|
$cancellationSource.Cancel()
|
||||||
|
|||||||
Reference in New Issue
Block a user