docs: keep split-scale tests out of production API

This commit is contained in:
梁薄云
2026-08-01 09:29:34 +08:00
parent 366b20780b
commit c6b69e9f87
@@ -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**