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
$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;
}
public double PositionErrorMeters { get; }
public double TangentDirectionErrorRadians { get; }
public double CurvatureErrorPerMeter { get; }
public double IncomingDerivativeScale { get; }
public double OutgoingDerivativeScale { get; }
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;
}
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**