docs: keep split-scale tests out of production API
This commit is contained in:
@@ -105,6 +105,9 @@ In `verify_path_smoothing_local_g2_candidates.ps1`, replace `$acceptedSoftCandid
|
|||||||
```powershell
|
```powershell
|
||||||
$acceptedSplitCandidates = @()
|
$acceptedSplitCandidates = @()
|
||||||
$maximumAnchorError = 0.0
|
$maximumAnchorError = 0.0
|
||||||
|
$maximumConnectionPositionError = 0.0
|
||||||
|
$maximumTangentDirectionError = 0.0
|
||||||
|
$maximumCurvatureError = 0.0
|
||||||
foreach ($realCandidate in $realCandidates) {
|
foreach ($realCandidate in $realCandidates) {
|
||||||
$evaluation = (Get-InternalMethod $evaluatorType 'Evaluate').Invoke($realEvaluator, @(
|
$evaluation = (Get-InternalMethod $evaluatorType 'Evaluate').Invoke($realEvaluator, @(
|
||||||
$preparedPath, $preparedPath, $region, $realCandidate, $singleTurnRequest, $options,
|
$preparedPath, $preparedPath, $region, $realCandidate, $singleTurnRequest, $options,
|
||||||
@@ -116,6 +119,15 @@ foreach ($realCandidate in $realCandidates) {
|
|||||||
foreach ($diagnostic in (Get-InternalProperty $realCandidate 'InternalScaleDiagnostics')) {
|
foreach ($diagnostic in (Get-InternalProperty $realCandidate 'InternalScaleDiagnostics')) {
|
||||||
$anchorError = [double](Get-InternalProperty $diagnostic 'AnchorPositionErrorMeters')
|
$anchorError = [double](Get-InternalProperty $diagnostic 'AnchorPositionErrorMeters')
|
||||||
$maximumAnchorError = [Math]::Max($maximumAnchorError, $anchorError)
|
$maximumAnchorError = [Math]::Max($maximumAnchorError, $anchorError)
|
||||||
|
$maximumConnectionPositionError = [Math]::Max(
|
||||||
|
$maximumConnectionPositionError,
|
||||||
|
[double](Get-InternalProperty $diagnostic 'ConnectionPositionErrorMeters'))
|
||||||
|
$maximumTangentDirectionError = [Math]::Max(
|
||||||
|
$maximumTangentDirectionError,
|
||||||
|
[double](Get-InternalProperty $diagnostic 'TangentDirectionErrorRadians'))
|
||||||
|
$maximumCurvatureError = [Math]::Max(
|
||||||
|
$maximumCurvatureError,
|
||||||
|
[double](Get-InternalProperty $diagnostic 'CurvatureErrorPerMeter'))
|
||||||
$incoming = [double](Get-InternalProperty $diagnostic 'IncomingDerivativeScale')
|
$incoming = [double](Get-InternalProperty $diagnostic 'IncomingDerivativeScale')
|
||||||
$outgoing = [double](Get-InternalProperty $diagnostic 'OutgoingDerivativeScale')
|
$outgoing = [double](Get-InternalProperty $diagnostic 'OutgoingDerivativeScale')
|
||||||
if ([Math]::Abs($incoming - $outgoing) -gt 1e-10) { $hasDifferentScale = $true }
|
if ([Math]::Abs($incoming - $outgoing) -gt 1e-10) { $hasDifferentScale = $true }
|
||||||
@@ -128,23 +140,16 @@ Assert-Equal 0 $duplicateFailures.Count `
|
|||||||
'SingleTurn candidates must not fail raw-window analysis on coincident boundary points.'
|
'SingleTurn candidates must not fail raw-window analysis on coincident boundary points.'
|
||||||
Assert-True ($maximumAnchorError -le 1e-9) `
|
Assert-True ($maximumAnchorError -le 1e-9) `
|
||||||
'Every internal primitive boundary must remain at its original coordinate.'
|
'Every internal primitive boundary must remain at its original coordinate.'
|
||||||
|
Assert-True ($maximumConnectionPositionError -le 1e-9) `
|
||||||
|
'Every real split-scale connection must be position continuous.'
|
||||||
|
Assert-True ($maximumTangentDirectionError -le 1e-8) `
|
||||||
|
'Every real split-scale connection must preserve unit tangent direction.'
|
||||||
|
Assert-True ($maximumCurvatureError -le 1e-8) `
|
||||||
|
'Every real split-scale connection must preserve geometric curvature.'
|
||||||
Assert-True ($acceptedSplitCandidates.Count -gt 0) `
|
Assert-True ($acceptedSplitCandidates.Count -gt 0) `
|
||||||
'SingleTurn must accept a zero-coordinate-offset candidate with different incoming/outgoing scales.'
|
'SingleTurn must accept a zero-coordinate-offset candidate with different incoming/outgoing scales.'
|
||||||
```
|
```
|
||||||
|
|
||||||
Add the analytical connection assertion:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
$connectionMethod = $hooksType.GetMethod(
|
|
||||||
'ExecuteSplitScaleConnection', [Reflection.BindingFlags]'Public,Static')
|
|
||||||
$connection = $connectionMethod.Invoke($null, @())
|
|
||||||
Assert-Near 0.0 $connection.PositionErrorMeters 1e-9 'Split-scale connection position must be continuous.'
|
|
||||||
Assert-Near 0.0 $connection.TangentDirectionErrorRadians 1e-8 'Split-scale connection unit tangent must be continuous.'
|
|
||||||
Assert-Near 0.0 $connection.CurvatureErrorPerMeter 1e-8 'Split-scale connection geometric curvature must be continuous.'
|
|
||||||
Assert-True ([Math]::Abs($connection.IncomingDerivativeScale - $connection.OutgoingDerivativeScale) -gt 1e-10) `
|
|
||||||
'The connection test must use genuinely different parameter-speed scales.'
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: Run the permanent test against the old shared builder and confirm RED**
|
- [ ] **Step 2: Run the permanent test against the old shared builder and confirm RED**
|
||||||
|
|
||||||
Run:
|
Run:
|
||||||
@@ -153,7 +158,7 @@ Run:
|
|||||||
powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1
|
powershell -ExecutionPolicy Bypass -File ClumsyPilot/tests/verify_path_smoothing_local_g2_candidates.ps1
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected: FAIL because `InternalScaleDiagnostics` and `ExecuteSplitScaleConnection` do not exist, or because no accepted zero-offset split-scale candidate exists. A pass at this point means the test did not exercise the new contract; correct the test before continuing.
|
Expected: FAIL because the real candidate diagnostics do not expose the fixed-anchor/split-scale G2 measurements, or because no accepted zero-offset split-scale candidate exists. A pass at this point means the test did not exercise the new contract; correct the test before continuing.
|
||||||
|
|
||||||
- [ ] **Step 3: Create an isolated feasibility copy and capture the `bd08a9b` baseline**
|
- [ ] **Step 3: Create an isolated feasibility copy and capture the `bd08a9b` baseline**
|
||||||
|
|
||||||
@@ -206,23 +211,35 @@ internal sealed class LocalG2InternalScaleDiagnostic
|
|||||||
double arcLengthMeters,
|
double arcLengthMeters,
|
||||||
double incomingDerivativeScale,
|
double incomingDerivativeScale,
|
||||||
double outgoingDerivativeScale,
|
double outgoingDerivativeScale,
|
||||||
double anchorPositionErrorMeters)
|
double anchorPositionErrorMeters,
|
||||||
|
double connectionPositionErrorMeters,
|
||||||
|
double tangentDirectionErrorRadians,
|
||||||
|
double curvatureErrorPerMeter)
|
||||||
{
|
{
|
||||||
if (!NumericGuard.IsFinite(arcLengthMeters) ||
|
if (!NumericGuard.IsFinite(arcLengthMeters) ||
|
||||||
!NumericGuard.IsPositiveFinite(incomingDerivativeScale) ||
|
!NumericGuard.IsPositiveFinite(incomingDerivativeScale) ||
|
||||||
!NumericGuard.IsPositiveFinite(outgoingDerivativeScale) ||
|
!NumericGuard.IsPositiveFinite(outgoingDerivativeScale) ||
|
||||||
!NumericGuard.IsFinite(anchorPositionErrorMeters) || anchorPositionErrorMeters < 0d)
|
!NumericGuard.IsFinite(anchorPositionErrorMeters) || anchorPositionErrorMeters < 0d ||
|
||||||
|
!NumericGuard.IsFinite(connectionPositionErrorMeters) || connectionPositionErrorMeters < 0d ||
|
||||||
|
!NumericGuard.IsFinite(tangentDirectionErrorRadians) || tangentDirectionErrorRadians < 0d ||
|
||||||
|
!NumericGuard.IsFinite(curvatureErrorPerMeter) || curvatureErrorPerMeter < 0d)
|
||||||
throw new ArgumentOutOfRangeException(nameof(arcLengthMeters));
|
throw new ArgumentOutOfRangeException(nameof(arcLengthMeters));
|
||||||
ArcLengthMeters = arcLengthMeters;
|
ArcLengthMeters = arcLengthMeters;
|
||||||
IncomingDerivativeScale = incomingDerivativeScale;
|
IncomingDerivativeScale = incomingDerivativeScale;
|
||||||
OutgoingDerivativeScale = outgoingDerivativeScale;
|
OutgoingDerivativeScale = outgoingDerivativeScale;
|
||||||
AnchorPositionErrorMeters = anchorPositionErrorMeters;
|
AnchorPositionErrorMeters = anchorPositionErrorMeters;
|
||||||
|
ConnectionPositionErrorMeters = connectionPositionErrorMeters;
|
||||||
|
TangentDirectionErrorRadians = tangentDirectionErrorRadians;
|
||||||
|
CurvatureErrorPerMeter = curvatureErrorPerMeter;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal double ArcLengthMeters { get; }
|
internal double ArcLengthMeters { get; }
|
||||||
internal double IncomingDerivativeScale { get; }
|
internal double IncomingDerivativeScale { get; }
|
||||||
internal double OutgoingDerivativeScale { get; }
|
internal double OutgoingDerivativeScale { get; }
|
||||||
internal double AnchorPositionErrorMeters { get; }
|
internal double AnchorPositionErrorMeters { get; }
|
||||||
|
internal double ConnectionPositionErrorMeters { get; }
|
||||||
|
internal double TangentDirectionErrorRadians { get; }
|
||||||
|
internal double CurvatureErrorPerMeter { get; }
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -374,7 +391,7 @@ private static bool TryGetDerivatives(
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Do not alter transition X/Y construction. Build one `LocalG2InternalScaleDiagnostic` for every internal node with the actual incoming/outgoing scales and `AnchorPositionErrorMeters = 0d`.
|
Do not alter transition X/Y construction. Step 7 builds one `LocalG2InternalScaleDiagnostic` for every real internal connection from the actual incoming/outgoing curve endpoint derivatives.
|
||||||
|
|
||||||
- [ ] **Step 6: Add deterministic representative scheduling and lazy Tier 2 construction in the disposable copy**
|
- [ ] **Step 6: Add deterministic representative scheduling and lazy Tier 2 construction in the disposable copy**
|
||||||
|
|
||||||
@@ -679,49 +696,110 @@ internal sealed class LocalG2CandidateBuildSession
|
|||||||
|
|
||||||
Change `TryBuildCandidate` to receive `LocalG2DerivativeScaleProfile profile` and `int candidateTier`, call `TryAssignDerivativeScales(nodes, profile)`, and pass the profile, tier, and diagnostics into `LocalG2CandidateGeometry`.
|
Change `TryBuildCandidate` to receive `LocalG2DerivativeScaleProfile profile` and `int candidateTier`, call `TryAssignDerivativeScales(nodes, profile)`, and pass the profile, tier, and diagnostics into `LocalG2CandidateGeometry`.
|
||||||
|
|
||||||
- [ ] **Step 7: Add the analytical split-scale TestHook in the disposable copy**
|
- [ ] **Step 7: Measure real split-scale connections without adding a test-only production method**
|
||||||
|
|
||||||
Add this public snapshot next to the existing `CandidateTestSnapshot`:
|
Construct all adjacent curves first, measure each real internal connection, then sample the same curve objects. Add:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
public sealed class SplitScaleConnectionTestSnapshot
|
private static bool TryMeasureConnection(
|
||||||
|
QuinticHermiteCurve2D incomingCurve,
|
||||||
|
QuinticHermiteCurve2D outgoingCurve,
|
||||||
|
BoundaryNode node,
|
||||||
|
out LocalG2InternalScaleDiagnostic diagnostic)
|
||||||
{
|
{
|
||||||
internal SplitScaleConnectionTestSnapshot(
|
diagnostic = null;
|
||||||
double positionErrorMeters,
|
incomingCurve.Evaluate(1d,
|
||||||
double tangentDirectionErrorRadians,
|
out double lx, out double ly,
|
||||||
double curvatureErrorPerMeter,
|
out double ldx, out double ldy,
|
||||||
double incomingDerivativeScale,
|
out double lddx, out double lddy);
|
||||||
double outgoingDerivativeScale)
|
outgoingCurve.Evaluate(0d,
|
||||||
{
|
out double rx, out double ry,
|
||||||
PositionErrorMeters = positionErrorMeters;
|
out double rdx, out double rdy,
|
||||||
TangentDirectionErrorRadians = tangentDirectionErrorRadians;
|
out double rddx, out double rddy);
|
||||||
CurvatureErrorPerMeter = curvatureErrorPerMeter;
|
double leftNorm = Math.Sqrt(ldx * ldx + ldy * ldy);
|
||||||
IncomingDerivativeScale = incomingDerivativeScale;
|
double rightNorm = Math.Sqrt(rdx * rdx + rdy * rdy);
|
||||||
OutgoingDerivativeScale = outgoingDerivativeScale;
|
if (!NumericGuard.IsPositiveFinite(leftNorm) ||
|
||||||
}
|
!NumericGuard.IsPositiveFinite(rightNorm))
|
||||||
public double PositionErrorMeters { get; }
|
return false;
|
||||||
public double TangentDirectionErrorRadians { get; }
|
|
||||||
public double CurvatureErrorPerMeter { get; }
|
double leftAnchorError = Distance(lx, ly, node.X, node.Y);
|
||||||
public double IncomingDerivativeScale { get; }
|
double rightAnchorError = Distance(rx, ry, node.X, node.Y);
|
||||||
public double OutgoingDerivativeScale { get; }
|
double anchorError = Math.Max(leftAnchorError, rightAnchorError);
|
||||||
|
double connectionPositionError = Distance(lx, ly, rx, ry);
|
||||||
|
double cosine = (ldx * rdx + ldy * rdy) / (leftNorm * rightNorm);
|
||||||
|
double tangentError = Math.Acos(Math.Max(-1d, Math.Min(1d, cosine)));
|
||||||
|
double leftCurvature = (ldx * lddy - ldy * lddx) /
|
||||||
|
(leftNorm * leftNorm * leftNorm);
|
||||||
|
double rightCurvature = (rdx * rddy - rdy * rddx) /
|
||||||
|
(rightNorm * rightNorm * rightNorm);
|
||||||
|
double curvatureError = Math.Abs(leftCurvature - rightCurvature);
|
||||||
|
if (!NumericGuard.IsFinite(anchorError) ||
|
||||||
|
!NumericGuard.IsFinite(connectionPositionError) ||
|
||||||
|
!NumericGuard.IsFinite(tangentError) ||
|
||||||
|
!NumericGuard.IsFinite(curvatureError))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
diagnostic = new LocalG2InternalScaleDiagnostic(
|
||||||
|
node.ArcLengthMeters,
|
||||||
|
node.IncomingDerivativeScale,
|
||||||
|
node.OutgoingDerivativeScale,
|
||||||
|
anchorError,
|
||||||
|
connectionPositionError,
|
||||||
|
tangentError,
|
||||||
|
curvatureError);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double Distance(double x0, double y0, double x1, double y1)
|
||||||
|
{
|
||||||
|
double dx = x1 - x0;
|
||||||
|
double dy = y1 - y0;
|
||||||
|
return Math.Sqrt(dx * dx + dy * dy);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`ExecuteSplitScaleConnection` builds straight zero-curvature nodes at `(0,0)`, `(1,0)`, `(2,0)`, with the internal node's incoming scale `0.75` and outgoing scale `1.25`. Construct both curves through `TryCreateCurve`, evaluate the left curve at `u=1` and right curve at `u=0`, then compute:
|
Replace the single create-and-sample loop with this exact ordering:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
positionError = Math.Sqrt((lx - rx) * (lx - rx) + (ly - ry) * (ly - ry));
|
var curves = new List<QuinticHermiteCurve2D>(nodes.Count - 1);
|
||||||
tangentError = Math.Acos(Math.Max(-1d, Math.Min(1d,
|
for (int nodeIndex = 1; nodeIndex < nodes.Count; nodeIndex++)
|
||||||
(ldx * rdx + ldy * rdy) /
|
{
|
||||||
(Math.Sqrt(ldx * ldx + ldy * ldy) * Math.Sqrt(rdx * rdx + rdy * rdy)))));
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
leftCurvature = (ldx * lddy - ldy * lddx) /
|
if (!TryCreateCurve(nodes[nodeIndex - 1], nodes[nodeIndex], directionSign,
|
||||||
Math.Pow(ldx * ldx + ldy * ldy, 1.5d);
|
out QuinticHermiteCurve2D curve))
|
||||||
rightCurvature = (rdx * rddy - rdy * rddx) /
|
return false;
|
||||||
Math.Pow(rdx * rdx + rdy * rdy, 1.5d);
|
curves.Add(curve);
|
||||||
curvatureError = Math.Abs(leftCurvature - rightCurvature);
|
}
|
||||||
|
|
||||||
|
var scaleDiagnostics = new List<LocalG2InternalScaleDiagnostic>();
|
||||||
|
for (int nodeIndex = 1; nodeIndex < nodes.Count - 1; nodeIndex++)
|
||||||
|
{
|
||||||
|
if (!TryMeasureConnection(
|
||||||
|
curves[nodeIndex - 1],
|
||||||
|
curves[nodeIndex],
|
||||||
|
nodes[nodeIndex],
|
||||||
|
out LocalG2InternalScaleDiagnostic diagnostic))
|
||||||
|
return false;
|
||||||
|
scaleDiagnostics.Add(diagnostic);
|
||||||
|
}
|
||||||
|
|
||||||
|
var sampled = new List<SmoothingPoint2D>();
|
||||||
|
for (int curveIndex = 0; curveIndex < curves.Count; curveIndex++)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
if (!TryAppendCurveSamples(
|
||||||
|
curves[curveIndex],
|
||||||
|
nodes[curveIndex],
|
||||||
|
nodes[curveIndex + 1],
|
||||||
|
segment,
|
||||||
|
outputSpacingMeters,
|
||||||
|
sampled,
|
||||||
|
cancellationToken))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Return the snapshot with the actual `0.75` and `1.25` scales.
|
Pass `scaleDiagnostics` into the candidate geometry. These measurements are internal feasibility diagnostics required by the design; they are not exposed as a new callable test API.
|
||||||
|
|
||||||
- [ ] **Step 8: Run the isolated feasibility gate**
|
- [ ] **Step 8: Run the isolated feasibility gate**
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user