Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 189 additions & 0 deletions src/UnitTests/Evaluation/BasisEvaluationTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,197 @@ public void BasisFunctionTest()
degree = 3;
span = evaluator.ExposeFindSpan(degree, knots, 0.1);
Assert.That(span, Is.EqualTo(3));
}

/// <summary>
/// B-spline basis functions sum to 1 (partition of unity) at any parameter.
/// </summary>
[Test]
public void BasisFunction_PartitionOfUnity()
{
DummyEvaluator evaluator = new DummyEvaluator();
double[] knots = new double[] { 0, 0, 0, 0, 1, 2, 3, 3, 3, 3 };
int degree = 3;
int n = knots.Length - degree - 2; // last control point index

double[] sampleParams = { 0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0 };

foreach (double u in sampleParams)
{
double sum = 0.0;
for (int i = 0; i <= n; i++)
sum += evaluator.ExposeBasisFunction(i, degree, knots, u);
Assert.That(sum, Is.EqualTo(1.0).Within(1e-12),
$"Partition of unity violated at u={u}: sum={sum}");
}
}

/// <summary>
/// B-spline basis functions are non-negative everywhere.
/// </summary>
[Test]
public void BasisFunction_NonNegativity()
{
DummyEvaluator evaluator = new DummyEvaluator();
double[] knots = new double[] { 0, 0, 0, 0, 1, 2, 3, 3, 3, 3 };
int degree = 3;
int n = knots.Length - degree - 2;

for (int sample = 0; sample <= 30; sample++)
{
double u = 3.0 * sample / 30.0;
for (int i = 0; i <= n; i++)
{
double val = evaluator.ExposeBasisFunction(i, degree, knots, u);
Assert.That(val, Is.GreaterThanOrEqualTo(-1e-14),
$"Negative basis function N_{i},{degree} at u={u}: {val}");
}
}
}

/// <summary>
/// Linear basis functions (degree 1) on uniform knots produce hat functions.
/// </summary>
[Test]
public void BasisFunction_Degree1_HatFunction()
{
DummyEvaluator evaluator = new DummyEvaluator();
// Degree 1, 3 control points -> knots [0,0,0.5,1,1]
double[] knots = new double[] { 0, 0, 0.5, 1, 1 };
int degree = 1;

// At u=0: N_0=1, N_1=0, N_2=0
Assert.That(evaluator.ExposeBasisFunction(0, degree, knots, 0.0), Is.EqualTo(1.0).Within(1e-12));
Assert.That(evaluator.ExposeBasisFunction(1, degree, knots, 0.0), Is.EqualTo(0.0).Within(1e-12));

// At u=0.5: N_0=0, N_1=1, N_2=0 (N_1 peaks at the interior knot)
double n0 = evaluator.ExposeBasisFunction(0, degree, knots, 0.5);
double n1 = evaluator.ExposeBasisFunction(1, degree, knots, 0.5);
Assert.That(n0, Is.EqualTo(0.0).Within(1e-12));
Assert.That(n1, Is.EqualTo(1.0).Within(1e-12)); // at knot, N_1=1

// At u=1: N_last=1
Assert.That(evaluator.ExposeBasisFunction(2, degree, knots, 1.0), Is.EqualTo(1.0).Within(1e-12));

// Intermediate values verify piecewise-linear (hat) shape:
// N_0 linearly ramps from 1 at u=0 to 0 at u=0.5 -> at u=0.25, N_0=0.5
Assert.That(evaluator.ExposeBasisFunction(0, degree, knots, 0.25), Is.EqualTo(0.5).Within(1e-12));
// N_1 ramps up from 0 at u=0 to 1 at u=0.5, then back to 0 at u=1.0
// at u=0.25 (half way on [0,0.5]), N_1=0.5
Assert.That(evaluator.ExposeBasisFunction(1, degree, knots, 0.25), Is.EqualTo(0.5).Within(1e-12));
// at u=0.75 (half way on [0.5,1.0]), N_1=0.5
Assert.That(evaluator.ExposeBasisFunction(1, degree, knots, 0.75), Is.EqualTo(0.5).Within(1e-12));
// N_2 ramps from 0 at u=0.5 to 1 at u=1.0 -> at u=0.75, N_2=0.5
Assert.That(evaluator.ExposeBasisFunction(2, degree, knots, 0.75), Is.EqualTo(0.5).Within(1e-12));
}

/// <summary>
/// First derivative of B-spline basis sums to zero (derivative of partition of unity = 0).
/// </summary>
[Test]
public void BasisFunctionDerivative_SumsToZero()
{
DummyEvaluator evaluator = new DummyEvaluator();
double[] knots = new double[] { 0, 0, 0, 0, 1, 2, 3, 3, 3, 3 };
int degree = 3;
int n = knots.Length - degree - 2;

// Sample inside the domain, avoiding knot values for derivative continuity
double[] sampleParams = { 0.5, 1.5, 2.5 };

foreach (double u in sampleParams)
{
double sum = 0.0;
for (int i = 0; i <= n; i++)
sum += evaluator.ExposeBasisFunctionDerivative(i, degree, knots, u, 1);
Assert.That(sum, Is.EqualTo(0.0).Within(1e-10),
$"Sum of first derivatives should be 0 at u={u}: got {sum}");
}
}

/// <summary>
/// DeBoor algorithm on a degree-1 curve with two control points reproduces linear interpolation.
/// </summary>
[Test]
public void DeBoor_Degree1_LinearInterpolation()
{
DummyEvaluator evaluator = new DummyEvaluator();
double[] knots = new double[] { 0, 0, 1, 1 };
int degree = 1;

Vector4Double[] cps = new Vector4Double[]
{
new Vector4Double(0, 0, 0, 1),
new Vector4Double(10, 5, 0, 1)
};

int span0 = evaluator.ExposeFindSpan(degree, knots, 0.0);
int span1 = evaluator.ExposeFindSpan(degree, knots, 1.0);
int spanMid = evaluator.ExposeFindSpan(degree, knots, 0.5);

var p0 = evaluator.ExposeDeBoor(degree, knots, span0, cps, 0.0);
var p1 = evaluator.ExposeDeBoor(degree, knots, span1, cps, 1.0);
var pm = evaluator.ExposeDeBoor(degree, knots, spanMid, cps, 0.5);

using (Assert.EnterMultipleScope())
{
Assert.That(p0.X / p0.W, Is.EqualTo(0.0).Within(1e-12));
Assert.That(p0.Y / p0.W, Is.EqualTo(0.0).Within(1e-12));
Assert.That(p1.X / p1.W, Is.EqualTo(10.0).Within(1e-12));
Assert.That(p1.Y / p1.W, Is.EqualTo(5.0).Within(1e-12));
Assert.That(pm.X / pm.W, Is.EqualTo(5.0).Within(1e-12));
Assert.That(pm.Y / pm.W, Is.EqualTo(2.5).Within(1e-12));
}
}

/// <summary>
/// DeBoor on a degree-2 (quadratic) curve passes through end control points.
/// </summary>
[Test]
public void DeBoor_Degree2_Quadratic_EndPointInterpolation()
{
DummyEvaluator evaluator = new DummyEvaluator();
double[] knots = new double[] { 0, 0, 0, 1, 1, 1 };
int degree = 2;

Vector4Double[] cps = new Vector4Double[]
{
new Vector4Double(0, 0, 0, 1),
new Vector4Double(1, 2, 0, 1),
new Vector4Double(2, 0, 0, 1)
};

int span0 = evaluator.ExposeFindSpan(degree, knots, 0.0);
int span1 = evaluator.ExposeFindSpan(degree, knots, 1.0);

var p0 = evaluator.ExposeDeBoor(degree, knots, span0, cps, 0.0);
var p1 = evaluator.ExposeDeBoor(degree, knots, span1, cps, 1.0);

using (Assert.EnterMultipleScope())
{
// Clamped B-spline interpolates first and last control points
Assert.That(p0.X / p0.W, Is.EqualTo(0.0).Within(1e-12));
Assert.That(p0.Y / p0.W, Is.EqualTo(0.0).Within(1e-12));
Assert.That(p1.X / p1.W, Is.EqualTo(2.0).Within(1e-12));
Assert.That(p1.Y / p1.W, Is.EqualTo(0.0).Within(1e-12));
}
}

/// <summary>
/// FindSpan returns consistent results for out-of-range parameters (clamping behavior).
/// </summary>
[Test]
public void FindSpan_ClampingBehavior()
{
DummyEvaluator evaluator = new DummyEvaluator();
double[] knots = new double[] { 0, 0, 0, 1, 1, 1 };
int degree = 2;

// Below minimum: should return degree
Assert.That(evaluator.ExposeFindSpan(degree, knots, -5.0), Is.EqualTo(degree));
// Above maximum: should return n = knots.Length - degree - 2
int expected = knots.Length - degree - 2;
Assert.That(evaluator.ExposeFindSpan(degree, knots, 5.0), Is.EqualTo(expected));
}
}

Expand Down
148 changes: 134 additions & 14 deletions src/UnitTests/Evaluation/VolumeEvaluateTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,10 @@ namespace UnitTests.Evaluation
[TestFixture]
public class VolumeEvaluateTest
{


[Test]
public void NurbsVolumeTestA()
private static NurbsVolume BuildLinearUnitCube()
{
// Bilinear NURBS volue (degree 1 in both directions)
int degreeU = 1;
int degreeV = 1;
int degreeW = 1;

double[] knotsU = [0, 0, 1, 1];
double[] knotsV = [0, 0, 1, 1];
double[] knotsW = [0, 0, 1, 1];
int degree = 1;
double[] knots = [0, 0, 1, 1];
ControlPoint[][][] controlPoints = new ControlPoint[2][][];
controlPoints[0] = new ControlPoint[2][];
controlPoints[0][0] = [
Expand All @@ -46,7 +37,19 @@ public void NurbsVolumeTestA()
new ControlPoint(0.0, 1.0, 1.5, 1),
new ControlPoint(1.0, 1.0, 2.5, 1)
];
var volume = new NurbsVolume(degreeU, degreeV, degreeW, new KnotVector(knotsU,degreeU), new KnotVector(knotsV,degreeV), new KnotVector(knotsW,degreeW), controlPoints);
return new NurbsVolume(
degree, degree, degree,
new KnotVector(knots, degree),
new KnotVector(knots, degree),
new KnotVector(knots, degree),
controlPoints);
}

[Test]
public void NurbsVolumeTestA()
{
// Bilinear NURBS volume (degree 1 in all directions)
var volume = BuildLinearUnitCube();

var samples = new (double u, double v, double w, Vector3Double expected)[] {
(0.000, 0.000, 0.000, new Vector3Double(0.000000, 0.000000, 0.000000)),
Expand All @@ -72,7 +75,124 @@ public void NurbsVolumeTestA()
}
}


/// <summary>
/// A uniform grid axis-aligned NURBS volume (degree 1) maps parameters linearly to positions.
/// </summary>
[Test]
public void NurbsVolume_LinearAxisAligned_MapsParametersToPositions()
{
// Simple axis-aligned unit cube: P(u,v,w) = (w, v, u) for u,v,w in [0,1]
int degree = 1;
double[] knots = [0, 0, 1, 1];

ControlPoint[][][] cps = new ControlPoint[2][][];
for (int i = 0; i < 2; i++)
{
cps[i] = new ControlPoint[2][];
for (int j = 0; j < 2; j++)
{
cps[i][j] = new ControlPoint[2];
for (int k = 0; k < 2; k++)
{
cps[i][j][k] = new ControlPoint(k, j, i, 1.0);
}
}
}

var volume = new NurbsVolume(
degree, degree, degree,
new KnotVector(knots, degree),
new KnotVector(knots, degree),
new KnotVector(knots, degree),
cps);

// Evaluate at corners and center
var corners = new (double u, double v, double w)[]
{
(0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1),
(1, 1, 1), (0.5, 0.5, 0.5)
};

foreach (var (u, v, w) in corners)
{
var pt = VolumeEvaluator.Evaluate(volume, u, v, w);
using (Assert.EnterMultipleScope())
{
Assert.That(pt.x, Is.EqualTo(w).Within(1e-10));
Assert.That(pt.y, Is.EqualTo(v).Within(1e-10));
Assert.That(pt.z, Is.EqualTo(u).Within(1e-10));
}
}
}

/// <summary>
/// NURBS volume bounding box encompasses all control points.
/// </summary>
[Test]
public void NurbsVolume_BoundingBox_ContainsAllControlPoints()
{
var volume = BuildLinearUnitCube();
var bbox = volume.BoundingBox;

// All evaluated points must lie within the bounding box (with tolerance)
for (int si = 0; si <= 4; si++)
for (int sj = 0; sj <= 4; sj++)
for (int sk = 0; sk <= 4; sk++)
{
double u = si / 4.0;
double v = sj / 4.0;
double w = sk / 4.0;
var pt = VolumeEvaluator.Evaluate(volume, u, v, w);

Assert.That(pt.x, Is.GreaterThanOrEqualTo(bbox.Min.X - 1e-9));
Assert.That(pt.x, Is.LessThanOrEqualTo(bbox.Max.X + 1e-9));
Assert.That(pt.y, Is.GreaterThanOrEqualTo(bbox.Min.Y - 1e-9));
Assert.That(pt.y, Is.LessThanOrEqualTo(bbox.Max.Y + 1e-9));
Assert.That(pt.z, Is.GreaterThanOrEqualTo(bbox.Min.Z - 1e-9));
Assert.That(pt.z, Is.LessThanOrEqualTo(bbox.Max.Z + 1e-9));
}
}

/// <summary>
/// NurbsVolume throws on null argument to Evaluate.
/// </summary>
[Test]
public void NurbsVolume_Evaluate_NullThrows()
{
Assert.Throws<ArgumentNullException>(() =>
{
VolumeEvaluator.Evaluate(null!, 0.5, 0.5, 0.5);
});
}

/// <summary>
/// NurbsVolume constructor validates knot-vector vs. control-point dimensions.
/// </summary>
[Test]
public void NurbsVolume_Constructor_InvalidKnotLengthThrows()
{
int degree = 1;
double[] validKnots = [0, 0, 1, 1];
double[] badKnots = [0, 0, 0.5, 1, 1]; // length 5 instead of 4

ControlPoint[][][] cps = new ControlPoint[2][][];
for (int i = 0; i < 2; i++)
{
cps[i] = new ControlPoint[2][];
for (int j = 0; j < 2; j++)
cps[i][j] = [new ControlPoint(0, 0, 0, 1), new ControlPoint(1, 0, 0, 1)];
}

Assert.Throws<InvalidOperationException>(() =>
{
_ = new NurbsVolume(
degree, degree, degree,
new KnotVector(badKnots, degree),
new KnotVector(validKnots, degree),
new KnotVector(validKnots, degree),
cps);
});
}
}


Expand Down
Loading