Add CI workflow and update documentation for KDTree version
This commit is contained in:
parent
43c37f900f
commit
ec61d6afde
7 changed files with 119 additions and 8 deletions
42
.github/workflows/ci.yml
vendored
Normal file
42
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main, master ]
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
go-version: [ '1.22.x', '1.23.x' ]
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go-version }}
|
||||||
|
|
||||||
|
- name: Verify go.mod is tidy
|
||||||
|
run: |
|
||||||
|
go mod tidy
|
||||||
|
git diff --exit-code -- go.mod go.sum
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: go build ./...
|
||||||
|
|
||||||
|
- name: Test (race)
|
||||||
|
run: go test -race ./...
|
||||||
|
|
||||||
|
- name: Vet
|
||||||
|
run: go vet ./...
|
||||||
|
|
||||||
|
- name: Govulncheck
|
||||||
|
uses: golang/govulncheck-action@v1
|
||||||
|
with:
|
||||||
|
go-version-input: ${{ matrix.go-version }}
|
||||||
|
# Run against packages in the module
|
||||||
|
args: ./...
|
||||||
4
doc.go
Normal file
4
doc.go
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
// Package poindexter provides sorting utilities and a KDTree with simple
|
||||||
|
// nearest-neighbour queries. It also includes helper functions to build
|
||||||
|
// normalised, weighted KD points for 2D/3D/4D use-cases.
|
||||||
|
package poindexter
|
||||||
|
|
@ -13,13 +13,13 @@ func Version() string
|
||||||
Returns the current version of the library.
|
Returns the current version of the library.
|
||||||
|
|
||||||
**Returns:**
|
**Returns:**
|
||||||
- `string`: The version string (e.g., "0.1.0")
|
- `string`: The version string (e.g., "0.2.0")
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
|
|
||||||
```go
|
```go
|
||||||
version := poindexter.Version()
|
version := poindexter.Version()
|
||||||
fmt.Println(version) // Output: 0.1.0
|
fmt.Println(version) // Output: 0.2.0
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
36
examples_test.go
Normal file
36
examples_test.go
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
package poindexter_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
poindexter "github.com/Snider/Poindexter"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExampleNewKDTree() {
|
||||||
|
pts := []poindexter.KDPoint[string]{
|
||||||
|
{ID: "A", Coords: []float64{0, 0}, Value: "alpha"},
|
||||||
|
{ID: "B", Coords: []float64{1, 0}, Value: "bravo"},
|
||||||
|
}
|
||||||
|
tr, _ := poindexter.NewKDTree(pts)
|
||||||
|
p, _, _ := tr.Nearest([]float64{0.2, 0})
|
||||||
|
fmt.Println(p.ID)
|
||||||
|
// Output: A
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleBuild2D() {
|
||||||
|
type rec struct{ ping, hops float64 }
|
||||||
|
items := []rec{{ping: 20, hops: 3}, {ping: 30, hops: 2}, {ping: 15, hops: 4}}
|
||||||
|
weights := [2]float64{1.0, 1.0}
|
||||||
|
invert := [2]bool{false, false}
|
||||||
|
pts, _ := poindexter.Build2D(items,
|
||||||
|
func(r rec) string { return "" },
|
||||||
|
func(r rec) float64 { return r.ping },
|
||||||
|
func(r rec) float64 { return r.hops },
|
||||||
|
weights, invert,
|
||||||
|
)
|
||||||
|
tr, _ := poindexter.NewKDTree(pts, poindexter.WithMetric(poindexter.ManhattanDistance{}))
|
||||||
|
_, _, _ = tr.Nearest([]float64{0, 0})
|
||||||
|
// Querying the origin (0,0) in normalized space tends to favor minima on each axis.
|
||||||
|
fmt.Printf("dim=%d len=%d", tr.Dim(), tr.Len())
|
||||||
|
// Output: dim=2 len=3
|
||||||
|
}
|
||||||
2
go.mod
2
go.mod
|
|
@ -1,3 +1,3 @@
|
||||||
module github.com/Snider/Poindexter
|
module github.com/Snider/Poindexter
|
||||||
|
|
||||||
go 1.24.9
|
go 1.23
|
||||||
|
|
|
||||||
37
kdtree.go
37
kdtree.go
|
|
@ -6,6 +6,17 @@ import (
|
||||||
"sort"
|
"sort"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrEmptyPoints indicates that no points were provided to build a KDTree.
|
||||||
|
ErrEmptyPoints = errors.New("kdtree: no points provided")
|
||||||
|
// ErrZeroDim indicates that points or tree dimension must be at least 1.
|
||||||
|
ErrZeroDim = errors.New("kdtree: points must have at least one dimension")
|
||||||
|
// ErrDimMismatch indicates inconsistent dimensionality among points.
|
||||||
|
ErrDimMismatch = errors.New("kdtree: inconsistent dimensionality in points")
|
||||||
|
// ErrDuplicateID indicates a duplicate point ID was encountered.
|
||||||
|
ErrDuplicateID = errors.New("kdtree: duplicate point ID")
|
||||||
|
)
|
||||||
|
|
||||||
// KDPoint represents a point with coordinates and an attached payload/value.
|
// KDPoint represents a point with coordinates and an attached payload/value.
|
||||||
// ID should be unique within a tree to enable O(1) deletes by ID.
|
// ID should be unique within a tree to enable O(1) deletes by ID.
|
||||||
// Coords must all have the same dimensionality within a given KDTree.
|
// Coords must all have the same dimensionality within a given KDTree.
|
||||||
|
|
@ -89,20 +100,20 @@ type KDTree[T any] struct {
|
||||||
// All points must have the same dimensionality (>0).
|
// All points must have the same dimensionality (>0).
|
||||||
func NewKDTree[T any](pts []KDPoint[T], opts ...KDOption) (*KDTree[T], error) {
|
func NewKDTree[T any](pts []KDPoint[T], opts ...KDOption) (*KDTree[T], error) {
|
||||||
if len(pts) == 0 {
|
if len(pts) == 0 {
|
||||||
return nil, errors.New("no points provided")
|
return nil, ErrEmptyPoints
|
||||||
}
|
}
|
||||||
dim := len(pts[0].Coords)
|
dim := len(pts[0].Coords)
|
||||||
if dim == 0 {
|
if dim == 0 {
|
||||||
return nil, errors.New("points must have at least one dimension")
|
return nil, ErrZeroDim
|
||||||
}
|
}
|
||||||
idIndex := make(map[string]int, len(pts))
|
idIndex := make(map[string]int, len(pts))
|
||||||
for i, p := range pts {
|
for i, p := range pts {
|
||||||
if len(p.Coords) != dim {
|
if len(p.Coords) != dim {
|
||||||
return nil, errors.New("inconsistent dimensionality in points")
|
return nil, ErrDimMismatch
|
||||||
}
|
}
|
||||||
if p.ID != "" {
|
if p.ID != "" {
|
||||||
if _, exists := idIndex[p.ID]; exists {
|
if _, exists := idIndex[p.ID]; exists {
|
||||||
return nil, errors.New("duplicate point ID: " + p.ID)
|
return nil, ErrDuplicateID
|
||||||
}
|
}
|
||||||
idIndex[p.ID] = i
|
idIndex[p.ID] = i
|
||||||
}
|
}
|
||||||
|
|
@ -120,6 +131,24 @@ func NewKDTree[T any](pts []KDPoint[T], opts ...KDOption) (*KDTree[T], error) {
|
||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewKDTreeFromDim constructs an empty KDTree with the specified dimension.
|
||||||
|
// Call Insert to add points after construction.
|
||||||
|
func NewKDTreeFromDim[T any](dim int, opts ...KDOption) (*KDTree[T], error) {
|
||||||
|
if dim <= 0 {
|
||||||
|
return nil, ErrZeroDim
|
||||||
|
}
|
||||||
|
cfg := kdOptions{metric: EuclideanDistance{}}
|
||||||
|
for _, o := range opts {
|
||||||
|
o(&cfg)
|
||||||
|
}
|
||||||
|
return &KDTree[T]{
|
||||||
|
points: nil,
|
||||||
|
dim: dim,
|
||||||
|
metric: cfg.metric,
|
||||||
|
idIndex: make(map[string]int),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Dim returns the number of dimensions.
|
// Dim returns the number of dimensions.
|
||||||
func (t *KDTree[T]) Dim() int { return t.dim }
|
func (t *KDTree[T]) Dim() int { return t.dim }
|
||||||
|
|
||||||
|
|
|
||||||
2
sort.go
2
sort.go
|
|
@ -39,7 +39,7 @@ func SortBy[T any](data []T, less func(i, j int) bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// SortByKey sorts a slice by extracting a comparable key from each element.
|
// SortByKey sorts a slice by extracting a comparable key from each element.
|
||||||
// The key function should return a value that implements constraints.Ordered.
|
// K is restricted to int, float64, or string.
|
||||||
func SortByKey[T any, K int | float64 | string](data []T, key func(T) K) {
|
func SortByKey[T any, K int | float64 | string](data []T, key func(T) K) {
|
||||||
sort.Slice(data, func(i, j int) bool {
|
sort.Slice(data, func(i, j int) bool {
|
||||||
return key(data[i]) < key(data[j])
|
return key(data[i]) < key(data[j])
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue