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
|
||||
$acceptedSplitCandidates = @()
|
||||
$maximumAnchorError = 0.0
|
||||
$maximumConnectionPositionError = 0.0
|
||||
$maximumTangentDirectionError = 0.0
|
||||
$maximumCurvatureError = 0.0
|
||||
foreach ($realCandidate in $realCandidates) {
|
||||
$evaluation = (Get-InternalMethod $evaluatorType 'Evaluate').Invoke($realEvaluator, @(
|
||||
$preparedPath, $preparedPath, $region, $realCandidate, $singleTurnRequest, $options,
|
||||
@@ -116,6 +119,15 @@ foreach ($realCandidate in $realCandidates) {
|
||||
foreach ($diagnostic in (Get-InternalProperty $realCandidate 'InternalScaleDiagnostics')) {
|
||||
$anchorError = [double](Get-InternalProperty $diagnostic 'AnchorPositionErrorMeters')
|
||||
$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')
|
||||
$outgoing = [double](Get-InternalProperty $diagnostic 'OutgoingDerivativeScale')
|
||||
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.'
|
||||
Assert-True ($maximumAnchorError -le 1e-9) `
|
||||
'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) `
|
||||
'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**
|
||||
|
||||
Run:
|
||||
@@ -153,7 +158,7 @@ Run:
|
||||
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**
|
||||
|
||||
@@ -206,23 +211,35 @@ internal sealed class LocalG2InternalScaleDiagnostic
|
||||
double arcLengthMeters,
|
||||
double incomingDerivativeScale,
|
||||
double outgoingDerivativeScale,
|
||||
double anchorPositionErrorMeters)
|
||||
double anchorPositionErrorMeters,
|
||||
double connectionPositionErrorMeters,
|
||||
double tangentDirectionErrorRadians,
|
||||
double curvatureErrorPerMeter)
|
||||
{
|
||||
if (!NumericGuard.IsFinite(arcLengthMeters) ||
|
||||
!NumericGuard.IsPositiveFinite(incomingDerivativeScale) ||
|
||||
!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));
|
||||
ArcLengthMeters = arcLengthMeters;
|
||||
IncomingDerivativeScale = incomingDerivativeScale;
|
||||
OutgoingDerivativeScale = outgoingDerivativeScale;
|
||||
AnchorPositionErrorMeters = anchorPositionErrorMeters;
|
||||
ConnectionPositionErrorMeters = connectionPositionErrorMeters;
|
||||
TangentDirectionErrorRadians = tangentDirectionErrorRadians;
|
||||
CurvatureErrorPerMeter = curvatureErrorPerMeter;
|
||||
}
|
||||
|
||||
internal double ArcLengthMeters { get; }
|
||||
internal double IncomingDerivativeScale { get; }
|
||||
internal double OutgoingDerivativeScale { 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**
|
||||
|
||||
@@ -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`.
|
||||
|
||||
- [ ] **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
|
||||
public sealed class SplitScaleConnectionTestSnapshot
|
||||
private static bool TryMeasureConnection(
|
||||
QuinticHermiteCurve2D incomingCurve,
|
||||
QuinticHermiteCurve2D outgoingCurve,
|
||||
BoundaryNode node,
|
||||
out LocalG2InternalScaleDiagnostic diagnostic)
|
||||
{
|
||||
internal SplitScaleConnectionTestSnapshot(
|
||||
double positionErrorMeters,
|
||||
double tangentDirectionErrorRadians,
|
||||
double curvatureErrorPerMeter,
|
||||
double incomingDerivativeScale,
|
||||
double outgoingDerivativeScale)
|
||||
{
|
||||
PositionErrorMeters = positionErrorMeters;
|
||||
TangentDirectionErrorRadians = tangentDirectionErrorRadians;
|
||||
CurvatureErrorPerMeter = curvatureErrorPerMeter;
|
||||
IncomingDerivativeScale = incomingDerivativeScale;
|
||||
OutgoingDerivativeScale = outgoingDerivativeScale;
|
||||
diagnostic = null;
|
||||
incomingCurve.Evaluate(1d,
|
||||
out double lx, out double ly,
|
||||
out double ldx, out double ldy,
|
||||
out double lddx, out double lddy);
|
||||
outgoingCurve.Evaluate(0d,
|
||||
out double rx, out double ry,
|
||||
out double rdx, out double rdy,
|
||||
out double rddx, out double rddy);
|
||||
double leftNorm = Math.Sqrt(ldx * ldx + ldy * ldy);
|
||||
double rightNorm = Math.Sqrt(rdx * rdx + rdy * rdy);
|
||||
if (!NumericGuard.IsPositiveFinite(leftNorm) ||
|
||||
!NumericGuard.IsPositiveFinite(rightNorm))
|
||||
return false;
|
||||
|
||||
double leftAnchorError = Distance(lx, ly, node.X, node.Y);
|
||||
double rightAnchorError = Distance(rx, ry, node.X, node.Y);
|
||||
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;
|
||||
}
|
||||
public double PositionErrorMeters { get; }
|
||||
public double TangentDirectionErrorRadians { get; }
|
||||
public double CurvatureErrorPerMeter { get; }
|
||||
public double IncomingDerivativeScale { get; }
|
||||
public double OutgoingDerivativeScale { get; }
|
||||
|
||||
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
|
||||
positionError = Math.Sqrt((lx - rx) * (lx - rx) + (ly - ry) * (ly - ry));
|
||||
tangentError = Math.Acos(Math.Max(-1d, Math.Min(1d,
|
||||
(ldx * rdx + ldy * rdy) /
|
||||
(Math.Sqrt(ldx * ldx + ldy * ldy) * Math.Sqrt(rdx * rdx + rdy * rdy)))));
|
||||
leftCurvature = (ldx * lddy - ldy * lddx) /
|
||||
Math.Pow(ldx * ldx + ldy * ldy, 1.5d);
|
||||
rightCurvature = (rdx * rddy - rdy * rddx) /
|
||||
Math.Pow(rdx * rdx + rdy * rdy, 1.5d);
|
||||
curvatureError = Math.Abs(leftCurvature - rightCurvature);
|
||||
var curves = new List<QuinticHermiteCurve2D>(nodes.Count - 1);
|
||||
for (int nodeIndex = 1; nodeIndex < nodes.Count; nodeIndex++)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (!TryCreateCurve(nodes[nodeIndex - 1], nodes[nodeIndex], directionSign,
|
||||
out QuinticHermiteCurve2D curve))
|
||||
return false;
|
||||
curves.Add(curve);
|
||||
}
|
||||
|
||||
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**
|
||||
|
||||
|
||||
Reference in New Issue
Block a user