Data update

This commit is contained in:
Ingy döt Net 2025-08-11 18:05:26 -07:00
parent 4d5544505c
commit 4924dd0264
3073 changed files with 55820 additions and 4408 deletions

View file

@ -0,0 +1,262 @@
defmodule KDTree do
@moduledoc """
K-D Tree implementation for nearest neighbor search in k-dimensional space.
"""
# Tree data structure
# {:node, value, left, right} | :empty
# KDTree: %{dims: [function], tree: tree}
defstruct dims: [], tree: :empty
@doc """
Create an empty k-d tree with the given dimensional accessors.
"""
def empty(dims) do
%KDTree{dims: dims, tree: :empty}
end
@doc """
Create a k-d tree with a single value.
"""
def singleton(dims, value) do
empty(dims) |> insert(value)
end
@doc """
Insert a value into a k-d tree.
"""
def insert(%KDTree{dims: dims, tree: tree} = kdtree, value) do
cyclic_dims = Stream.cycle(dims)
new_tree = ins(cyclic_dims, tree, value)
%{kdtree | tree: new_tree}
end
defp ins(_dims, :empty, value) do
{:node, value, :empty, :empty}
end
defp ins(dims, {:node, split, left, right}, value) do
[d] = Enum.take(dims, 1)
remaining_dims = Stream.drop(dims, 1)
if d.(value) < d.(split) do
{:node, split, ins(remaining_dims, left, value), right}
else
{:node, split, left, ins(remaining_dims, right, value)}
end
end
@doc """
Create a k-d tree from a list using the median-finding algorithm.
"""
def from_list(dims, values) do
cyclic_dims = Stream.cycle(dims)
tree = f_list(cyclic_dims, values)
%KDTree{dims: dims, tree: tree}
end
defp f_list(_dims, []) do
:empty
end
defp f_list(dims, values) do
[d] = Enum.take(dims, 1)
remaining_dims = Stream.drop(dims, 1)
sorted = Enum.sort_by(values, d)
length = length(sorted)
mid_index = div(length, 2)
{lower, higher} = Enum.split(sorted, mid_index)
case higher do
[] ->
:empty
[median | rest] ->
{:node, median, f_list(remaining_dims, lower), f_list(remaining_dims, rest)}
end
end
@doc """
Create a k-d tree from a list by repeatedly inserting values.
Faster than median-finding but can create unbalanced trees.
"""
def from_list_linear(dims, values) do
Enum.reduce(values, empty(dims), &insert(&2, &1))
end
@doc """
Find the nearest value to a given value.
Returns {nearest_value_or_nil, nodes_visited}.
"""
def nearest(%KDTree{dims: dims, tree: tree}, value) do
cyclic_dims = Stream.cycle(dims)
near(cyclic_dims, tree, value, dims)
end
defp near(_dims, :empty, _value, _all_dims) do
{nil, 1}
end
defp near(_dims, {:node, split, :empty, :empty}, _value, _all_dims) do
{split, 1}
end
defp near(dims, {:node, split, left, right}, value, all_dims) do
[d] = Enum.take(dims, 1)
remaining_dims = Stream.drop(dims, 1)
split_dist = sqr_dist(all_dims, value, split)
hyperplane_dist = square(d.(value) - d.(split))
{best_left, left_count} = near(remaining_dims, left, value, all_dims)
{best_right, right_count} = near(remaining_dims, right, value, all_dims)
{{maybe_this_best, this_count}, {maybe_other_best, other_count}} =
if d.(value) < d.(split) do
{{best_left, left_count}, {best_right, right_count}}
else
{{best_right, right_count}, {best_left, left_count}}
end
case maybe_this_best do
nil ->
count = 1 + this_count + other_count
case maybe_other_best do
nil ->
{split, count}
other_best ->
if sqr_dist(all_dims, value, other_best) < split_dist do
{other_best, count}
else
{split, count}
end
end
this_best ->
this_best_dist = sqr_dist(all_dims, value, this_best)
{best, best_dist} =
if split_dist < this_best_dist do
{split, split_dist}
else
{this_best, this_best_dist}
end
if best_dist < hyperplane_dist do
{best, 1 + this_count}
else
count = 1 + this_count + other_count
case maybe_other_best do
nil ->
{best, count}
other_best ->
if best_dist < sqr_dist(all_dims, value, other_best) do
{best, count}
else
{other_best, count}
end
end
end
end
end
@doc """
Calculate squared Euclidean distance between two points.
"""
def sqr_dist(dims, a, b) do
a_values = Enum.map(dims, & &1.(a))
b_values = Enum.map(dims, & &1.(b))
Enum.zip(a_values, b_values)
|> Enum.map(fn {x, y} -> square(x - y) end)
|> Enum.sum()
end
defp square(x), do: x * x
@doc """
Dimensional accessors for 2-tuple.
"""
def tuple_2d do
[&elem(&1, 0), &elem(&1, 1)]
end
@doc """
Dimensional accessors for 3-tuple.
"""
def tuple_3d do
[&elem(&1, 0), &elem(&1, 1), &elem(&1, 2)]
end
@doc """
Naive nearest search for verification.
"""
def linear_nearest(_dims, _value, []) do
nil
end
def linear_nearest(dims, value, [head | tail]) do
Enum.min_by([head | tail], &sqr_dist(dims, value, &1))
end
@doc """
Print search results.
"""
def print_results(point, {nearest, visited}, dims) do
case nearest do
nil ->
IO.puts("Could not find nearest.")
value ->
dist = :math.sqrt(sqr_dist(dims, point, value))
IO.puts("Point: #{inspect(point)}")
IO.puts("Nearest: #{inspect(value)}")
IO.puts("Distance: #{dist}")
IO.puts("Visited: #{visited}")
IO.puts("")
end
end
@doc """
Generate random 3D points.
"""
def random_3d_points(n, {min_x, min_y, min_z}, {max_x, max_y, max_z}) do
for _ <- 1..n do
{
min_x + :rand.uniform() * (max_x - min_x),
min_y + :rand.uniform() * (max_y - min_y),
min_z + :rand.uniform() * (max_z - min_z)
}
end
end
@doc """
Main demonstration function.
"""
def main do
# Wikipedia example
wiki_values = [{2, 3}, {5, 4}, {9, 6}, {4, 7}, {8, 1}, {7, 2}]
wiki_tree = from_list(tuple_2d(), wiki_values)
wiki_search = {9, 2}
wiki_nearest = nearest(wiki_tree, wiki_search)
IO.puts("Wikipedia example:")
print_results(wiki_search, wiki_nearest, tuple_2d())
# Random 3D example
:rand.seed(:exsplus, {1, 2, 3}) # Seed for reproducible results
rand_range = {{0, 0, 0}, {1000, 1000, 1000}}
rand_values = random_3d_points(1000, elem(rand_range, 0), elem(rand_range, 1))
rand_search = hd(random_3d_points(1, elem(rand_range, 0), elem(rand_range, 1)))
rand_tree = from_list(tuple_3d(), rand_values)
rand_nearest = nearest(rand_tree, rand_search)
rand_nearest_linear = linear_nearest(tuple_3d(), rand_search, rand_values)
IO.puts("1000 random 3D points on the range of [0, 1000):")
print_results(rand_search, rand_nearest, tuple_3d())
IO.puts("Confirm naive nearest:")
IO.puts("#{inspect(rand_nearest_linear)}")
end
end
KDTree.main()

View file

@ -0,0 +1,203 @@
import Foundation
// point is a k-dimensional point.
typealias Point = [Double]
extension Point {
// sqd returns the square of the euclidean distance.
func sqd(to q: Point) -> Double {
var sum: Double = 0
for dim in 0..<self.count {
let d = self[dim] - q[dim]
sum += d * d
}
return sum
}
}
// kdNode following field names in the paper.
// rangeElt would be whatever data is associated with the point.
class KDNode {
let domElt: Point
let split: Int
let left: KDNode?
let right: KDNode?
init(domElt: Point, split: Int, left: KDNode?, right: KDNode?) {
self.domElt = domElt
self.split = split
self.left = left
self.right = right
}
}
struct HyperRect {
var min: Point
var max: Point
// Create a copy of the hyperrect
func copy() -> HyperRect {
return HyperRect(min: Array(self.min), max: Array(self.max))
}
}
class KDTree {
let root: KDNode?
let bounds: HyperRect
init(root: KDNode?, bounds: HyperRect) {
self.root = root
self.bounds = bounds
}
// nearest. find nearest neighbor. return values are:
// nearest neighbor - the point within the tree that is nearest p.
// square of the distance to that point.
// a count of the nodes visited in the search.
func nearest(to target: Point) -> (nearest: Point?, distSqd: Double, nodesVisited: Int) {
return nn(kd: root, target: target, hr: bounds, maxDistSqd: .infinity)
}
// algorithm is table 6.4 from the paper
private func nn(kd: KDNode?, target: Point, hr: HyperRect,
maxDistSqd: Double) -> (nearest: Point?, distSqd: Double, nodesVisited: Int) {
if kd == nil {
return (nil, .infinity, 0)
}
var nodesVisited = 1
let s = kd!.split
let pivot = kd!.domElt
var leftHr = hr.copy()
var rightHr = hr.copy()
leftHr.max[s] = pivot[s]
rightHr.min[s] = pivot[s]
let targetInLeft = target[s] <= pivot[s]
let (nearerKd, nearerHr, furtherKd, furtherHr): (KDNode?, HyperRect, KDNode?, HyperRect)
if targetInLeft {
nearerKd = kd!.left
nearerHr = leftHr
furtherKd = kd!.right
furtherHr = rightHr
} else {
nearerKd = kd!.right
nearerHr = rightHr
furtherKd = kd!.left
furtherHr = leftHr
}
var (nearest, distSqd, nv) = nn(kd: nearerKd, target: target, hr: nearerHr, maxDistSqd: maxDistSqd)
nodesVisited += nv
var maxDist = maxDistSqd
if distSqd < maxDist {
maxDist = distSqd
}
var d = pivot[s] - target[s]
d *= d
if d > maxDist {
return (nearest, distSqd, nodesVisited)
}
d = pivot.sqd(to: target)
if d < distSqd {
nearest = pivot
distSqd = d
maxDist = distSqd
}
let (tempNearest, tempSqd, nv2) = nn(kd: furtherKd, target: target, hr: furtherHr, maxDistSqd: maxDist)
nodesVisited += nv2
if tempSqd < distSqd {
nearest = tempNearest
distSqd = tempSqd
}
return (nearest, distSqd, nodesVisited)
}
}
// Helper function to create a new KD tree
func newKd(pts: [Point], bounds: HyperRect) -> KDTree {
func nk2(exset: [Point], split: Int) -> KDNode? {
if exset.isEmpty {
return nil
}
// Sort points by the split dimension
let sortedPoints = exset.sorted { $0[split] < $1[split] }
var m = sortedPoints.count / 2
let d = sortedPoints[m]
// Find largest index of points with median value
while m+1 < sortedPoints.count && sortedPoints[m+1][split] == d[split] {
m += 1
}
// Next split
var s2 = split + 1
if s2 == d.count {
s2 = 0
}
return KDNode(
domElt: d,
split: split,
left: nk2(exset: Array(sortedPoints[0..<m]), split: s2),
right: nk2(exset: Array(sortedPoints[(m+1)...]), split: s2)
)
}
return KDTree(root: nk2(exset: pts, split: 0), bounds: bounds)
}
// Helper functions to generate random points
func randomPt(dim: Int) -> Point {
var p = Point(repeating: 0, count: dim)
for d in 0..<dim {
p[d] = Double.random(in: 0...1)
}
return p
}
func randomPts(dim: Int, n: Int) -> [Point] {
var p = [Point](repeating: [], count: n)
for i in 0..<n {
p[i] = randomPt(dim: dim)
}
return p
}
func showNearest(heading: String, kd: KDTree, p: Point) {
print()
print(heading)
print("point: ", p)
let (nn, ssq, nv) = kd.nearest(to: p)
print("nearest neighbor:", nn ?? "nil")
print("distance: ", sqrt(ssq))
print("nodes visited: ", nv)
}
// Main function
func main() {
// Set random seed
srand48(Int(time(nil)))
var kd = newKd(
pts: [[2, 3], [5, 4], [9, 6], [4, 7], [8, 1], [7, 2]],
bounds: HyperRect(min: [0, 0], max: [10, 10])
)
showNearest(heading: "WP example data", kd: kd, p: [9, 2])
kd = newKd(
pts: randomPts(dim: 3, n: 1000),
bounds: HyperRect(min: [0, 0, 0], max: [1, 1, 1])
)
showNearest(heading: "1000 random 3d points", kd: kd, p: randomPt(dim: 3))
}
// Run the main function
main()

View file

@ -0,0 +1,358 @@
const std = @import("std");
const print = std.debug.print;
const ArrayList = std.ArrayList;
const Allocator = std.mem.Allocator;
const Random = std.Random;
const Point = struct {
coords: []f32,
allocator: Allocator,
const Self = @This();
pub fn init(allocator: Allocator, coords: []const f32) !Self {
const owned_coords = try allocator.dupe(f32, coords);
return Self{
.coords = owned_coords,
.allocator = allocator,
};
}
pub fn deinit(self: Self) void {
self.allocator.free(self.coords);
}
pub fn clone(self: Self) !Self {
return Self.init(self.allocator, self.coords);
}
pub fn sub(self: Self, other: Self, allocator: Allocator) !Point {
std.debug.assert(self.coords.len == other.coords.len);
const result_coords = try allocator.alloc(f32, self.coords.len);
for (0..self.coords.len) |i| {
result_coords[i] = self.coords[i] - other.coords[i];
}
return Point{
.coords = result_coords,
.allocator = allocator,
};
}
pub fn normSq(self: Self) f32 {
var sum: f32 = 0.0;
for (self.coords) |coord| {
sum += coord * coord;
}
return sum;
}
pub fn format(self: Self, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void {
_ = fmt;
_ = options;
try writer.writeAll("[");
for (self.coords, 0..) |coord, i| {
if (i > 0) try writer.writeAll(", ");
try writer.print("{d:.1}", .{coord});
}
try writer.writeAll("]");
}
};
const KDTreeNode = struct {
point: Point,
dim: usize,
left: ?*KDTreeNode,
right: ?*KDTreeNode,
allocator: Allocator,
const Self = @This();
pub fn init(allocator: Allocator, points: []Point, dim: usize) !*Self {
const points_len = points.len;
if (points_len == 1) {
const node = try allocator.create(Self);
node.* = Self{
.point = try points[0].clone(),
.dim = dim,
.left = null,
.right = null,
.allocator = allocator,
};
return node;
}
// Split around the median
const pivot = try quickselectBy(points, points_len / 2, dim, allocator);
const left = if (points_len >= 2)
try Self.init(allocator, points[0..points_len / 2], (dim + 1) % pivot.coords.len)
else
null;
const right = if (points_len >= 3)
try Self.init(allocator, points[points_len / 2 + 1..points_len], (dim + 1) % pivot.coords.len)
else
null;
const node = try allocator.create(Self);
node.* = Self{
.point = pivot,
.dim = dim,
.left = left,
.right = right,
.allocator = allocator,
};
return node;
}
pub fn deinit(self: *Self) void {
self.point.deinit();
if (self.left) |left| {
left.deinit();
self.allocator.destroy(left);
}
if (self.right) |right| {
right.deinit();
self.allocator.destroy(right);
}
}
pub fn findNearestNeighbor(self: *const Self, point: Point, allocator: Allocator) !struct { point: Point, n_visited: usize } {
const diff = try point.sub(self.point, allocator);
defer diff.deinit();
const initial_dist_sq = diff.normSq();
const result = try self.findNearestNeighborHelper(point, self.point, initial_dist_sq, 1, allocator);
return .{ .point = try result.point.clone(), .n_visited = result.n_visited };
}
fn findNearestNeighborHelper(
self: *const Self,
point: Point,
best: Point,
best_dist_sq: f32,
n_visited: usize,
allocator: Allocator,
) !struct { point: Point, n_visited: usize } {
var my_best = best;
var my_best_dist_sq = best_dist_sq;
var my_n_visited = n_visited;
// Examine the near side first
if (self.point.coords[self.dim] < point.coords[self.dim] and self.right != null) {
const result = try self.right.?.findNearestNeighborHelper(
point, my_best, my_best_dist_sq, my_n_visited, allocator
);
my_best = result.point;
my_n_visited = result.n_visited;
} else if (self.left != null) {
const result = try self.left.?.findNearestNeighborHelper(
point, my_best, my_best_dist_sq, my_n_visited, allocator
);
my_best = result.point;
my_n_visited = result.n_visited;
}
// Distance along this node's axis
const axis_dist_sq = std.math.pow(f32, self.point.coords[self.dim] - point.coords[self.dim], 2);
if (axis_dist_sq <= my_best_dist_sq) {
// Check if this node is closer than current best
const self_diff = try point.sub(self.point, allocator);
defer self_diff.deinit();
const self_dist_sq = self_diff.normSq();
if (self_dist_sq < my_best_dist_sq) {
my_best = self.point;
my_best_dist_sq = self_dist_sq;
}
my_n_visited += 1;
// Check the far side of the split
if (self.point.coords[self.dim] < point.coords[self.dim] and self.left != null) {
const result = try self.left.?.findNearestNeighborHelper(
point, my_best, my_best_dist_sq, my_n_visited, allocator
);
my_best = result.point;
my_n_visited = result.n_visited;
} else if (self.right != null) {
const result = try self.right.?.findNearestNeighborHelper(
point, my_best, my_best_dist_sq, my_n_visited, allocator
);
my_best = result.point;
my_n_visited = result.n_visited;
}
}
return .{ .point = my_best, .n_visited = my_n_visited };
}
};
fn quickselectBy(arr: []Point, position: usize, dim: usize, allocator: Allocator) !Point {
if (arr.len == 1) return try arr[0].clone();
var rng = std.Random.DefaultPrng.init(@intCast(std.time.timestamp()));
const random = rng.random();
var pivot_index = random.uintLessThan(usize, arr.len);
pivot_index = partitionBy(arr, pivot_index, dim);
const array_len = arr.len;
if (position == pivot_index) {
return try arr[position].clone();
} else if (position < pivot_index) {
return quickselectBy(arr[0..pivot_index], position, dim, allocator);
} else {
return quickselectBy(arr[pivot_index + 1..array_len], position - pivot_index - 1, dim, allocator);
}
}
fn partitionBy(arr: []Point, pivot_index: usize, dim: usize) usize {
const array_len = arr.len;
std.mem.swap(Point, &arr[pivot_index], &arr[array_len - 1]);
var store_index: usize = 0;
for (0..array_len - 1) |i| {
if (arr[i].coords[dim] < arr[array_len - 1].coords[dim]) {
std.mem.swap(Point, &arr[i], &arr[store_index]);
store_index += 1;
}
}
std.mem.swap(Point, &arr[array_len - 1], &arr[store_index]);
return store_index;
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var rng = std.Random.DefaultPrng.init(@intCast(std.time.timestamp()));
const random = rng.random();
// Wikipedia example
const wp_coords = [_][]const f32{
&[_]f32{ 2.0, 3.0 },
&[_]f32{ 5.0, 4.0 },
&[_]f32{ 9.0, 6.0 },
&[_]f32{ 4.0, 7.0 },
&[_]f32{ 8.0, 1.0 },
&[_]f32{ 7.0, 2.0 },
};
var wp_points = ArrayList(Point).init(allocator);
defer {
for (wp_points.items) |point| {
point.deinit();
}
wp_points.deinit();
}
for (wp_coords) |coords| {
try wp_points.append(try Point.init(allocator, coords));
}
const wp_tree = try KDTreeNode.init(allocator, wp_points.items, 0);
defer {
wp_tree.deinit();
allocator.destroy(wp_tree);
}
const wp_target = try Point.init(allocator, &[_]f32{ 9.0, 2.0 });
defer wp_target.deinit();
const wp_result = try wp_tree.findNearestNeighbor(wp_target, allocator);
defer wp_result.point.deinit();
const wp_diff = try wp_result.point.sub(wp_target, allocator);
defer wp_diff.deinit();
print("Wikipedia example data:\n", .{});
print("Point: {}\n", .{wp_target});
print("Nearest neighbor: {}\n", .{wp_result.point});
print("Distance: {d:.6}\n", .{@sqrt(wp_diff.normSq())});
print("Nodes visited: {}\n", .{wp_result.n_visited});
// Randomly generated 3D points
const n_random = 1000;
var random_points = ArrayList(Point).init(allocator);
defer {
for (random_points.items) |point| {
point.deinit();
}
random_points.deinit();
}
for (0..n_random) |_| {
const coords = [_]f32{
(random.float(f32) - 0.5) * 1000.0,
(random.float(f32) - 0.5) * 1000.0,
(random.float(f32) - 0.5) * 1000.0,
};
try random_points.append(try Point.init(allocator, &coords));
}
const start_cons_time = std.time.nanoTimestamp();
const random_tree = try KDTreeNode.init(allocator, random_points.items, 0);
const cons_time = std.time.nanoTimestamp() - start_cons_time;
defer {
random_tree.deinit();
allocator.destroy(random_tree);
}
print("1,000 3d points (Construction time: {}ms)\n", .{@divTrunc(cons_time, 1_000_000)});
const random_target_coords = [_]f32{
(random.float(f32) - 0.5) * 1000.0,
(random.float(f32) - 0.5) * 1000.0,
(random.float(f32) - 0.5) * 1000.0,
};
const random_target = try Point.init(allocator, &random_target_coords);
defer random_target.deinit();
const random_result = try random_tree.findNearestNeighbor(random_target, allocator);
defer random_result.point.deinit();
const random_diff = try random_result.point.sub(random_target, allocator);
defer random_diff.deinit();
print("Point: {}\n", .{random_target});
print("Nearest neighbor: {}\n", .{random_result.point});
print("Distance: {d:.6}\n", .{@sqrt(random_diff.normSq())});
print("Nodes visited: {}\n", .{random_result.n_visited});
// Benchmark search time
const n_searches = 1000;
var random_targets = ArrayList(Point).init(allocator);
defer {
for (random_targets.items) |point| {
point.deinit();
}
random_targets.deinit();
}
for (0..n_searches) |_| {
const coords = [_]f32{
(random.float(f32) - 0.5) * 1000.0,
(random.float(f32) - 0.5) * 1000.0,
(random.float(f32) - 0.5) * 1000.0,
};
try random_targets.append(try Point.init(allocator, &coords));
}
const start_search_time = std.time.nanoTimestamp();
var total_n_visited: usize = 0;
for (random_targets.items) |target| {
const result = try random_tree.findNearestNeighbor(target, allocator);
defer result.point.deinit();
total_n_visited += result.n_visited;
}
const search_time = std.time.nanoTimestamp() - start_search_time;
print("Visited an average of {d:.1} nodes on {} searches in {} ms\n", .{
@as(f32, @floatFromInt(total_n_visited)) / @as(f32, @floatFromInt(n_searches)),
n_searches,
@divTrunc(search_time, 1_000_000),
});
}