Refactor CI configuration and documentation; improve error handling in KDTree functions
This commit is contained in:
parent
8e62b4e51d
commit
5d1ee3f0ea
17 changed files with 65 additions and 28 deletions
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
|
|
@ -8,6 +8,7 @@ on:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
actions: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-test-wasm:
|
build-test-wasm:
|
||||||
|
|
|
||||||
|
|
@ -17,5 +17,4 @@ issues:
|
||||||
- errcheck
|
- errcheck
|
||||||
linters-settings:
|
linters-settings:
|
||||||
errcheck:
|
errcheck:
|
||||||
# Be pragmatic: don't require checking of Close for defer patterns in tests/examples.
|
# Using default settings; test file exclusions are handled by exclude-rules above.
|
||||||
# (Keep defaults; exclusions above handle test files.)
|
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
|
|
||||||
This project has adopted the Contributor Covenant Code of Conduct.
|
This project has adopted the Contributor Covenant Code of Conduct.
|
||||||
|
|
||||||
- Version: https://www.contributor-covenant.org/version/2/1/code_of_conduct/
|
- Version: [Contributor Covenant v2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/)
|
||||||
- FAQ: https://www.contributor-covenant.org/faq
|
- FAQ: [Contributor Covenant FAQ](https://www.contributor-covenant.org/faq)
|
||||||
- Translations: https://www.contributor-covenant.org/translations
|
- Translations: [Available Translations](https://www.contributor-covenant.org/translations)
|
||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the maintainers via GitHub issues or by email listed on the repository profile. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances.
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the maintainers via GitHub issues or by email listed on the repository profile. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances.
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ Thanks for your interest in contributing! This document describes how to build,
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
- Go 1.22+ (1.23 preferred)
|
- Go 1.23+
|
||||||
- `git clone https://github.com/Snider/Poindexter`
|
- `git clone https://github.com/Snider/Poindexter`
|
||||||
- `cd Poindexter`
|
- `cd Poindexter`
|
||||||
|
|
||||||
|
|
|
||||||
4
Makefile
4
Makefile
|
|
@ -147,3 +147,7 @@ docs-serve: ## Serve MkDocs locally (requires mkdocs-material)
|
||||||
.PHONY: docs-build
|
.PHONY: docs-build
|
||||||
docs-build: ## Build MkDocs site into site/
|
docs-build: ## Build MkDocs site into site/
|
||||||
$(MKDOCS) build
|
$(MKDOCS) build
|
||||||
|
|
||||||
|
.PHONY: clean
|
||||||
|
clean: ## Remove generated files and directories
|
||||||
|
rm -rf $(DIST_DIR) $(COVEROUT) $(COVERHTML) $(BENCHOUT)
|
||||||
|
|
|
||||||
|
|
@ -470,7 +470,9 @@ func Build4DWithStats[T any](
|
||||||
) ([]KDPoint[T], error)
|
) ([]KDPoint[T], error)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
#### Example (2D)
|
#### Example (2D)
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// Compute stats once over your baseline set
|
// Compute stats once over your baseline set
|
||||||
stats := poindexter.ComputeNormStats2D(peers,
|
stats := poindexter.ComputeNormStats2D(peers,
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,7 @@ func main() {
|
||||||
### Why does querying with `[0]` work?
|
### Why does querying with `[0]` work?
|
||||||
We use Euclidean distance in 1D, so `distance = |ping - target|`. With target `0`, minimizing the distance is equivalent to minimizing the ping itself.
|
We use Euclidean distance in 1D, so `distance = |ping - target|`. With target `0`, minimizing the distance is equivalent to minimizing the ping itself.
|
||||||
|
|
||||||
|
|
||||||
### Extending the metric/space
|
### Extending the metric/space
|
||||||
- Multi-objective: encode more routing features (lower is better) as extra dimensions, e.g. `[ping_ms, hops, queue_delay_ms]`.
|
- Multi-objective: encode more routing features (lower is better) as extra dimensions, e.g. `[ping_ms, hops, queue_delay_ms]`.
|
||||||
- Metric choice:
|
- Metric choice:
|
||||||
|
|
@ -109,6 +110,7 @@ We use Euclidean distance in 1D, so `distance = |ping - target|`. With target `0
|
||||||
- `ChebyshevDistance` (L∞): cares about the worst dimension.
|
- `ChebyshevDistance` (L∞): cares about the worst dimension.
|
||||||
- Normalization: when mixing units (ms, hops, km), normalize or weight dimensions so the metric reflects your priority.
|
- Normalization: when mixing units (ms, hops, km), normalize or weight dimensions so the metric reflects your priority.
|
||||||
|
|
||||||
|
|
||||||
### Notes
|
### Notes
|
||||||
- This KDTree currently uses an internal linear scan for queries. The API is stable and designed so it can be swapped to use `gonum.org/v1/gonum/spatial/kdtree` under the hood later for sub-linear queries on large datasets.
|
- This KDTree currently uses an internal linear scan for queries. The API is stable and designed so it can be swapped to use `gonum.org/v1/gonum/spatial/kdtree` under the hood later for sub-linear queries on large datasets.
|
||||||
- IDs are optional but recommended for O(1)-style deletes; keep them unique per tree.
|
- IDs are optional but recommended for O(1)-style deletes; keep them unique per tree.
|
||||||
|
|
|
||||||
|
|
@ -123,5 +123,5 @@ func main() {
|
||||||
|
|
||||||
- Check out the [API Reference](api.md) for detailed documentation
|
- Check out the [API Reference](api.md) for detailed documentation
|
||||||
- Try the example: [Find the best (lowest‑ping) DHT peer](dht-best-ping.md)
|
- Try the example: [Find the best (lowest‑ping) DHT peer](dht-best-ping.md)
|
||||||
- Explore multi-dimensional KDTree over ping/hops/geo/score: [Multi-Dimensional KDTree (DHT)](kdtree-multidimensional.md)
|
- Explore multidimensional KDTree over ping/hops/geo/score: [Multidimensional KDTree (DHT)](kdtree-multidimensional.md)
|
||||||
- Read about the [License](license.md)
|
- Read about the [License](license.md)
|
||||||
|
|
|
||||||
|
|
@ -237,23 +237,32 @@ func main() {
|
||||||
// Initial 2‑D build (ping + hops)
|
// Initial 2‑D build (ping + hops)
|
||||||
weights2 := [2]float64{1.0, 1.0}
|
weights2 := [2]float64{1.0, 1.0}
|
||||||
invert2 := [2]bool{false, false}
|
invert2 := [2]bool{false, false}
|
||||||
pts, _ := poindexter.Build2D(
|
|
||||||
|
// Compute normalization stats once over your baseline set
|
||||||
|
stats := poindexter.ComputeNormStats2D(
|
||||||
|
peers,
|
||||||
|
func(p Peer) float64 { return p.PingMS },
|
||||||
|
func(p Peer) float64 { return p.Hops },
|
||||||
|
)
|
||||||
|
|
||||||
|
// Build using the precomputed stats so future inserts share the same scale
|
||||||
|
pts, _ := poindexter.Build2DWithStats(
|
||||||
peers,
|
peers,
|
||||||
func(p Peer) string { return p.ID },
|
func(p Peer) string { return p.ID },
|
||||||
func(p Peer) float64 { return p.PingMS },
|
func(p Peer) float64 { return p.PingMS },
|
||||||
func(p Peer) float64 { return p.Hops },
|
func(p Peer) float64 { return p.Hops },
|
||||||
weights2, invert2,
|
weights2, invert2, stats,
|
||||||
)
|
)
|
||||||
tree, _ := poindexter.NewKDTree(pts)
|
tree, _ := poindexter.NewKDTree(pts)
|
||||||
|
|
||||||
// Insert a new peer: rebuild its point using the same helper.
|
// Insert a new peer: reuse the same normalization stats to keep scale consistent
|
||||||
newPeer := Peer{ID: "Z", PingMS: 12, Hops: 2}
|
newPeer := Peer{ID: "Z", PingMS: 12, Hops: 2}
|
||||||
addPts, _ := poindexter.Build2D(
|
addPts, _ := poindexter.Build2DWithStats(
|
||||||
[]Peer{newPeer},
|
[]Peer{newPeer},
|
||||||
func(p Peer) string { return p.ID },
|
func(p Peer) string { return p.ID },
|
||||||
func(p Peer) float64 { return p.PingMS },
|
func(p Peer) float64 { return p.PingMS },
|
||||||
func(p Peer) float64 { return p.Hops },
|
func(p Peer) float64 { return p.Hops },
|
||||||
weights2, invert2,
|
weights2, invert2, stats,
|
||||||
)
|
)
|
||||||
_ = tree.Insert(addPts[0])
|
_ = tree.Insert(addPts[0])
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ Run them locally:
|
||||||
go test -bench . -benchmem -run=^$ ./...
|
go test -bench . -benchmem -run=^$ ./...
|
||||||
```
|
```
|
||||||
|
|
||||||
GitHub Actions publishes benchmark artifacts for Go 1.22 and 1.23 on every push/PR. Look for artifacts named `bench-<go-version>.txt` in the CI run.
|
GitHub Actions publishes benchmark artifacts for Go 1.23 on every push/PR. Look for artifacts named `bench-<go-version>.txt` in the CI run.
|
||||||
|
|
||||||
## What to expect (rule of thumb)
|
## What to expect (rule of thumb)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ To assemble the npm package folder with the built artifacts:
|
||||||
make npm-pack
|
make npm-pack
|
||||||
```
|
```
|
||||||
|
|
||||||
This populates `npm/poindexter-wasm/` with `dist/`, license and readme files. You can then create a tarball for local testing:
|
This populates `npm/poindexter-wasm/` with `dist/`, licence and readme files. You can then create a tarball for local testing:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm pack ./npm/poindexter-wasm
|
npm pack ./npm/poindexter-wasm
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,8 @@ func TestExample1D(t *testing.T) {
|
||||||
if best.Value.Ping != 35 {
|
if best.Value.Ping != 35 {
|
||||||
t.Fatalf("expected best ping 35ms, got %d", best.Value.Ping)
|
t.Fatalf("expected best ping 35ms, got %d", best.Value.Ping)
|
||||||
}
|
}
|
||||||
if d <= 0 {
|
// Distance from [0] to [35] should be 35
|
||||||
t.Fatalf("expected positive distance, got %v", d)
|
if d != 35 {
|
||||||
|
t.Fatalf("expected distance 35, got %v", d)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,14 +21,23 @@ func main() {
|
||||||
}
|
}
|
||||||
weights := [2]float64{1.0, 1.0}
|
weights := [2]float64{1.0, 1.0}
|
||||||
invert := [2]bool{false, false}
|
invert := [2]bool{false, false}
|
||||||
pts, _ := poindexter.Build2D(
|
pts, err := poindexter.Build2D(
|
||||||
peers,
|
peers,
|
||||||
func(p Peer2) string { return p.ID },
|
func(p Peer2) string { return p.ID },
|
||||||
func(p Peer2) float64 { return p.PingMS },
|
func(p Peer2) float64 { return p.PingMS },
|
||||||
func(p Peer2) float64 { return p.Hops },
|
func(p Peer2) float64 { return p.Hops },
|
||||||
weights, invert,
|
weights, invert,
|
||||||
)
|
)
|
||||||
tr, _ := poindexter.NewKDTree(pts, poindexter.WithMetric(poindexter.ManhattanDistance{}))
|
if err != nil {
|
||||||
best, _, _ := tr.Nearest([]float64{0, 0.3})
|
panic(fmt.Sprintf("Build2D failed: %v", err))
|
||||||
|
}
|
||||||
|
tr, err := poindexter.NewKDTree(pts, poindexter.WithMetric(poindexter.ManhattanDistance{}))
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("NewKDTree failed: %v", err))
|
||||||
|
}
|
||||||
|
best, _, ok := tr.Nearest([]float64{0, 0.3})
|
||||||
|
if !ok {
|
||||||
|
panic("no nearest neighbour found")
|
||||||
|
}
|
||||||
fmt.Println("2D best:", best.ID)
|
fmt.Println("2D best:", best.ID)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ func main() {
|
||||||
}
|
}
|
||||||
weights := [4]float64{1.0, 0.7, 0.2, 1.2}
|
weights := [4]float64{1.0, 0.7, 0.2, 1.2}
|
||||||
invert := [4]bool{false, false, false, true}
|
invert := [4]bool{false, false, false, true}
|
||||||
pts, _ := poindexter.Build4D(
|
pts, err := poindexter.Build4D(
|
||||||
peers,
|
peers,
|
||||||
func(p Peer4) string { return p.ID },
|
func(p Peer4) string { return p.ID },
|
||||||
func(p Peer4) float64 { return p.PingMS },
|
func(p Peer4) float64 { return p.PingMS },
|
||||||
|
|
@ -32,7 +32,16 @@ func main() {
|
||||||
func(p Peer4) float64 { return p.Score },
|
func(p Peer4) float64 { return p.Score },
|
||||||
weights, invert,
|
weights, invert,
|
||||||
)
|
)
|
||||||
tr, _ := poindexter.NewKDTree(pts, poindexter.WithMetric(poindexter.EuclideanDistance{}))
|
if err != nil {
|
||||||
best, _, _ := tr.Nearest([]float64{0, weights[1] * 0.2, weights[2] * 0.3, 0})
|
panic(err)
|
||||||
|
}
|
||||||
|
tr, err := poindexter.NewKDTree(pts, poindexter.WithMetric(poindexter.EuclideanDistance{}))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
best, _, ok := tr.Nearest([]float64{0, weights[1] * 0.2, weights[2] * 0.3, 0})
|
||||||
|
if !ok {
|
||||||
|
panic("no nearest neighbour found")
|
||||||
|
}
|
||||||
fmt.Println("4D best:", best.ID)
|
fmt.Println("4D best:", best.ID)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -246,7 +246,7 @@ func Build2DWithStats[T any](items []T, id func(T) string, f1, f2 func(T) float6
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
if len(stats.Stats) != 2 {
|
if len(stats.Stats) != 2 {
|
||||||
return nil, nil
|
return nil, ErrStatsDimMismatch
|
||||||
}
|
}
|
||||||
pts := make([]KDPoint[T], len(items))
|
pts := make([]KDPoint[T], len(items))
|
||||||
for i, it := range items {
|
for i, it := range items {
|
||||||
|
|
@ -317,7 +317,7 @@ func Build3DWithStats[T any](items []T, id func(T) string, f1, f2, f3 func(T) fl
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
if len(stats.Stats) != 3 {
|
if len(stats.Stats) != 3 {
|
||||||
return nil, nil
|
return nil, ErrStatsDimMismatch
|
||||||
}
|
}
|
||||||
pts := make([]KDPoint[T], len(items))
|
pts := make([]KDPoint[T], len(items))
|
||||||
for i, it := range items {
|
for i, it := range items {
|
||||||
|
|
@ -400,7 +400,7 @@ func Build4DWithStats[T any](items []T, id func(T) string, f1, f2, f3, f4 func(T
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
if len(stats.Stats) != 4 {
|
if len(stats.Stats) != 4 {
|
||||||
return nil, nil
|
return nil, ErrStatsDimMismatch
|
||||||
}
|
}
|
||||||
pts := make([]KDPoint[T], len(items))
|
pts := make([]KDPoint[T], len(items))
|
||||||
for i, it := range items {
|
for i, it := range items {
|
||||||
|
|
|
||||||
|
|
@ -48,8 +48,8 @@ func TestDeleteByID_SwapDelete(t *testing.T) {
|
||||||
if ids["B"] {
|
if ids["B"] {
|
||||||
t.Fatalf("B should not be present after delete")
|
t.Fatalf("B should not be present after delete")
|
||||||
}
|
}
|
||||||
if !ids["A"] && !ids["C"] {
|
if !ids["A"] || !ids["C"] {
|
||||||
t.Fatalf("expected either A or C to be nearest for respective queries: %v", ids)
|
t.Fatalf("expected both A and C to be found after deleting B, got: %v", ids)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,8 @@ export async function init(options = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run the Go program (it registers globals like pxNewTree, etc.)
|
// Run the Go program (it registers globals like pxNewTree, etc.)
|
||||||
await go.run(result.instance);
|
// Do not await: the Go WASM main may block (e.g., via select{}), so awaiting never resolves.
|
||||||
|
go.run(result.instance);
|
||||||
|
|
||||||
const api = {
|
const api = {
|
||||||
version: async () => call('pxVersion'),
|
version: async () => call('pxVersion'),
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue