Poindexter/kdtree_helpers_test.go

225 lines
6.5 KiB
Go

package poindexter
import (
"fmt"
"testing"
)
func TestBuild2D_NormalizationAndInversion(t *testing.T) {
type rec struct{ a, b float64 }
items := []rec{{a: 0, b: 100}, {a: 10, b: 300}}
// f1 over [0,10], f2 over [100,300]
pts, err := Build2D(items,
func(r rec) string { return "" },
func(r rec) float64 { return r.a },
func(r rec) float64 { return r.b },
[2]float64{2.0, 0.5},
[2]bool{true, false}, // invert first axis, not second
)
if err != nil {
t.Fatalf("Build2D err: %v", err)
}
if len(pts) != 2 {
t.Fatalf("expected 2 points, got %d", len(pts))
}
// item0: a=0 -> n1=0 -> invert -> 1 -> *2 = 2; b=100 -> n2=0 -> *0.5 = 0
if got := fmt.Sprintf("%.1f,%.1f", pts[0].Coords[0], pts[0].Coords[1]); got != "2.0,0.0" {
t.Fatalf("coords[0] = %s, want 2.0,0.0", got)
}
// item1: a=10 -> n1=1 -> invert -> 0 -> *2 = 0; b=300 -> n2=1 -> *0.5=0.5
if got := fmt.Sprintf("%.1f,%.1f", pts[1].Coords[0], pts[1].Coords[1]); got != "0.0,0.5" {
t.Fatalf("coords[1] = %s, want 0.0,0.5", got)
}
}
func TestBuild3D_AllEqualSafe(t *testing.T) {
type rec struct{ x, y, z float64 }
items := []rec{{1, 1, 1}, {1, 1, 1}}
pts, err := Build3D(items,
func(r rec) string { return "id" },
func(r rec) float64 { return r.x },
func(r rec) float64 { return r.y },
func(r rec) float64 { return r.z },
[3]float64{1, 1, 1},
[3]bool{false, false, false},
)
if err != nil {
t.Fatalf("Build3D err: %v", err)
}
if len(pts) != 2 {
t.Fatalf("len = %d", len(pts))
}
for i := range pts {
if len(pts[i].Coords) != 3 {
t.Fatalf("dim = %d", len(pts[i].Coords))
}
for _, c := range pts[i].Coords {
if c != 0 {
t.Fatalf("expected 0 when min==max, got %v", c)
}
}
}
}
// Example-style end-to-end sanity on 4D using the documented Peer data
func TestBuild4D_EndToEnd_Example(t *testing.T) {
type Peer struct {
ID string
PingMS float64
Hops float64
GeoKM float64
Score float64
}
peers := []Peer{
{ID: "A", PingMS: 22, Hops: 3, GeoKM: 1200, Score: 0.86},
{ID: "B", PingMS: 34, Hops: 2, GeoKM: 800, Score: 0.91},
{ID: "C", PingMS: 15, Hops: 4, GeoKM: 4500, Score: 0.70},
{ID: "D", PingMS: 55, Hops: 1, GeoKM: 300, Score: 0.95},
{ID: "E", PingMS: 18, Hops: 2, GeoKM: 2200, Score: 0.80},
}
weights := [4]float64{1.0, 0.7, 0.2, 1.2}
invert := [4]bool{false, false, false, true} // flip score so higher score -> lower cost
pts, err := Build4D(peers,
func(p Peer) string { return p.ID },
func(p Peer) float64 { return p.PingMS },
func(p Peer) float64 { return p.Hops },
func(p Peer) float64 { return p.GeoKM },
func(p Peer) float64 { return p.Score },
weights, invert,
)
if err != nil {
t.Fatalf("Build4D err: %v", err)
}
if len(pts) != len(peers) {
t.Fatalf("len pts=%d", len(pts))
}
// Build KDTree and query near origin in normalized/weighted space (prefer minima on all axes)
tree, err := NewKDTree(pts, WithMetric(EuclideanDistance{}))
if err != nil {
t.Fatalf("NewKDTree err: %v", err)
}
if tree.Dim() != 4 {
t.Fatalf("dim=%d", tree.Dim())
}
best, _, ok := tree.Nearest([]float64{0, 0, 0, 0})
if !ok {
t.Fatalf("no nearest")
}
// With these weights and inversions, peer B emerges as closest in this setup.
if best.ID != "B" {
t.Fatalf("expected best B, got %s", best.ID)
}
}
func TestComputeNormStatsAndWithStats_Parity2D(t *testing.T) {
type rec struct{ a, b float64 }
items := []rec{{0, 10}, {5, 20}, {10, 30}}
weights := [2]float64{1, 2}
invert := [2]bool{false, true}
// Build using automatic stats
autoPts, err := Build2D(items,
func(r rec) string { return "" },
func(r rec) float64 { return r.a },
func(r rec) float64 { return r.b },
weights, invert,
)
if err != nil {
t.Fatalf("auto build err: %v", err)
}
// Compute stats and build with stats
stats := ComputeNormStats2D(items,
func(r rec) float64 { return r.a },
func(r rec) float64 { return r.b },
)
withPts, err := Build2DWithStats(items,
func(r rec) string { return "" },
func(r rec) float64 { return r.a },
func(r rec) float64 { return r.b },
weights, invert, stats,
)
if err != nil {
t.Fatalf("with-stats build err: %v", err)
}
if len(withPts) != len(autoPts) {
t.Fatalf("len mismatch")
}
for i := range withPts {
if len(withPts[i].Coords) != 2 {
t.Fatalf("dim mismatch")
}
if withPts[i].Coords[0] != autoPts[i].Coords[0] || withPts[i].Coords[1] != autoPts[i].Coords[1] {
t.Fatalf("coords mismatch at %d: %v vs %v", i, withPts[i].Coords, autoPts[i].Coords)
}
}
}
func TestBuild3DWithStats_MinEqualsMax_Safe(t *testing.T) {
type rec struct{ x, y, z float64 }
items := []rec{{1, 2, 3}, {1, 5, 3}, {1, 9, 3}}
weights := [3]float64{1, 1, 1}
invert := [3]bool{false, false, false}
// x and z min==max across items for x=1, z=3
stats := NormStats{Stats: []AxisStats{{Min: 1, Max: 1}, {Min: 2, Max: 9}, {Min: 3, Max: 3}}}
pts, err := Build3DWithStats(items,
func(r rec) string { return "" },
func(r rec) float64 { return r.x },
func(r rec) float64 { return r.y },
func(r rec) float64 { return r.z },
weights, invert, stats,
)
if err != nil {
t.Fatalf("err: %v", err)
}
for _, p := range pts {
if p.Coords[0] != 0 || p.Coords[2] != 0 {
t.Fatalf("expected zero for min==max axes, got %v", p.Coords)
}
}
}
func TestBuild4DWithStats_DynamicUpdateExample(t *testing.T) {
type Peer struct {
ID string
Ping, Hops, Geo, Score float64
}
base := []Peer{{"A", 20, 3, 1000, 0.8}, {"B", 30, 2, 800, 0.9}}
weights := [4]float64{1, 1, 0.2, 1.2}
invert := [4]bool{false, false, false, true}
stats := ComputeNormStats4D(base,
func(p Peer) float64 { return p.Ping },
func(p Peer) float64 { return p.Hops },
func(p Peer) float64 { return p.Geo },
func(p Peer) float64 { return p.Score },
)
pts, err := Build4DWithStats(base,
func(p Peer) string { return p.ID },
func(p Peer) float64 { return p.Ping },
func(p Peer) float64 { return p.Hops },
func(p Peer) float64 { return p.Geo },
func(p Peer) float64 { return p.Score },
weights, invert, stats,
)
if err != nil {
t.Fatalf("err: %v", err)
}
tr, err := NewKDTree(pts)
if err != nil {
t.Fatalf("kdt err: %v", err)
}
// add a new peer using same stats
newPeer := Peer{"Z", 15, 2, 1200, 0.85}
newPts, _ := Build4DWithStats([]Peer{newPeer},
func(p Peer) string { return p.ID },
func(p Peer) float64 { return p.Ping },
func(p Peer) float64 { return p.Hops },
func(p Peer) float64 { return p.Geo },
func(p Peer) float64 { return p.Score },
weights, invert, stats,
)
if !tr.Insert(newPts[0]) {
t.Fatalf("insert failed")
}
if tr.Dim() != 4 {
t.Fatalf("dim != 4")
}
}