diff --git a/hilbert.go b/hilbert.go new file mode 100644 index 0000000..d49adab --- /dev/null +++ b/hilbert.go @@ -0,0 +1,125 @@ +// https://github.com/google/hilbert/ + +// Copyright 2015 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "errors" + "fmt" +) + +var ( + ErrNotPositive = errors.New("N must be greater than zero") + ErrNotPowerOfTwo = errors.New("N must be a power of two") + ErrOutOfRange = errors.New("value is out of range") +) + +type Hilbert struct { + N int +} + +// NewHilbert returns a Hilbert space which maps integers to and from the curve. +// n must be a power of two. +func NewHilbert(n int) (*Hilbert, error) { + if n <= 0 { + return nil, ErrNotPositive + } + + // Test if power of two + if (n & (n - 1)) != 0 { + return nil, ErrNotPowerOfTwo + } + + return &Hilbert{ + N: n, + }, nil +} + +// GetDimensions returns the width and height of the 2D space. +func (s *Hilbert) GetDimensions() (int, int) { + return s.N, s.N +} + +// Map transforms a one dimension value, t, in the range [0, n^2-1] to coordinates on the Hilbert +// curve in the two-dimension space, where x and y are within [0,n-1]. +func (s *Hilbert) Map(t int) (x, y int, err error) { + if t < 0 || t >= s.N*s.N { + return 0, 0, fmt.Errorf("hilbert Map(t) value is out of range: (0 < t=%d < s.N*s.N=%d)", t, s.N*s.N) + } + + for i := int(1); i < s.N; i = i * 2 { + rx := t&2 == 2 + ry := t&1 == 1 + if rx { + ry = !ry + } + + x, y = s.rotate(i, x, y, rx, ry) + + if rx { + x = x + i + } + if ry { + y = y + i + } + + t /= 4 + } + + return +} + +// MapInverse transform coordinates on Hilbert curve from (x,y) to t. +func (s *Hilbert) MapInverse(x, y int) (t int, err error) { + if x < 0 || x >= s.N || y < 0 || y >= s.N { + return 0, fmt.Errorf("hilbert MapInverse(x,y) value is out of range: x(0 < %d < s.N=%d) || y(0 < %d < s.N=%d)", x, s.N, y, s.N) + } + + for i := s.N / 2; i > 0; i = i / 2 { + rx := (x & i) > 0 + ry := (y & i) > 0 + + var a int = 0 + if rx { + a = 3 + } + t += i * i * (a ^ b2i(ry)) + + x, y = s.rotate(i, x, y, rx, ry) + } + + return +} + +// rotate rotates and flips the quadrant appropriately. +func (s *Hilbert) rotate(n, x, y int, rx, ry bool) (int, int) { + if !ry { + if rx { + x = n - 1 - x + y = n - 1 - y + } + + x, y = y, x + } + return x, y +} + +func b2i(b bool) int { + if b { + return 1 + } + return 0 +} diff --git a/main.go b/main.go index f08d019..84090dd 100644 --- a/main.go +++ b/main.go @@ -7,8 +7,6 @@ import ( "image/color" "math" "time" - - spatialIndex "git.sequentialread.com/forest/modular-spatial-index" // OR: github.com/go-gl/gl/v2.1/gl ) @@ -30,8 +28,8 @@ func main() { rectMaxX := rectX + rectSize rectMaxY := rectY + rectSize - inputMin, inputMax := spatialIndex.GetValidInputRange() - _, outputMaxBytes := spatialIndex.GetOutputRange() + inputMin, inputMax := GetValidInputRange() + _, outputMaxBytes := GetOutputRange() curveLength := int(binary.BigEndian.Uint64(outputMaxBytes)) //log.Printf("inputMin: %d, inputMax: %d, curveLength: %d", inputMin, inputMax, curveLength) @@ -40,7 +38,7 @@ func main() { remappedRectXMax := int(lerp(float64(inputMin), float64(inputMax), float64(rectX+rectSize)/float64(dim))) remappedRectSize := remappedRectXMax - remappedRectXMin - byteRanges, err := spatialIndex.RectangleToIndexedRanges(remappedRectXMin, remappedRectYMin, remappedRectSize, remappedRectSize, 1) + byteRanges, err := RectangleToIndexedRanges(remappedRectXMin, remappedRectYMin, remappedRectSize, remappedRectSize, 1) if err != nil { panic(err) } @@ -92,7 +90,7 @@ func main() { continue } - curvePointBytes, err := spatialIndex.GetIndexedPoint(remappedX, remappedY) + curvePointBytes, err := GetIndexedPoint(remappedX, remappedY) curvePoint := int(binary.BigEndian.Uint64(curvePointBytes)) if err != nil { panic(err) diff --git a/peano.go b/peano.go new file mode 100644 index 0000000..417d45e --- /dev/null +++ b/peano.go @@ -0,0 +1,131 @@ +// Copyright 2015 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +// Peano represents a 2D Peano curve of order N for mapping to and from. +// Implements SpaceFilling interface. +type Peano struct { + N int // Always a power of three, and is the width/height of the space. +} + +// isPow3 returns true if n is a power of 3. +func isPow3(n float64) bool { + // I wanted to do the following, but due to subtle floating point issues it didn't work + // const ln3 = 1.098612288668109691395245236922525704647490557822749451734694333637494 // https://oeis.org/A002391 + //return n == math.Pow(3, math.Trunc(math.Log(n) / ln3)) + for n >= 1 { + if n == 1 { + return true + } + n = n / 3 + } + return false +} + +// NewPeano returns a new Peano space filling curve which maps integers to and from the curve. +// n must be a power of three. +func NewPeano(n int) (*Peano, error) { + if n <= 0 { + return nil, ErrNotPositive + } + + if !isPow3(float64(n)) { + return nil, ErrNotPowerOfTwo + } + + return &Peano{ + N: n, + }, nil +} + +// GetDimensions returns the width and height of the 2D space. +func (p *Peano) GetDimensions() (int, int) { + return p.N, p.N +} + +// Map transforms a one dimension value, t, in the range [0, n^3-1] to coordinates on the Peano +// curve in the two-dimension space, where x and y are within [0,n-1]. +func (p *Peano) Map(t int) (x, y int, err error) { + if t < 0 || t >= p.N*p.N { + return -1, -1, ErrOutOfRange + } + + for i := 1; i < p.N; i = i * 3 { + s := t % 9 + + // rx/ry are the coordinates in the 3x3 grid + rx := int(s / 3) + ry := int(s % 3) + if rx == 1 { + ry = 2 - ry + } + + // now based on depth rotate our points + if i > 1 { + x, y = p.rotate(i, x, y, s) + } + + x += rx * i + y += ry * i + + t /= 9 + } + + return x, y, nil +} + +// rotate rotates the x and y coordinates depending on the current n depth. +func (p *Peano) rotate(n, x, y, s int) (int, int) { + + if n == 1 { + // Special case + return x, y + } + + n = n - 1 + switch s { + case 0: + return x, y // normal + case 1: + return n - x, y // fliph + case 2: + return x, y // normal + case 3: + return x, n - y // flipv + case 4: + return n - x, n - y // flipv and fliph + case 5: + return x, n - y // flipv + case 6: + return x, y // normal + case 7: + return n - x, y // fliph + case 8: + return x, y // normal + } + + panic("assertion failure: this line should never be reached") +} + +// MapInverse transform coordinates on the Peano curve from (x,y) to t. +// NOT IMPLEMENTED YET +func (p *Peano) MapInverse(x, y int) (t int, err error) { + if x < 0 || x >= p.N || y < 0 || y >= p.N { + return -1, ErrOutOfRange + } + + panic("Not finished") + return -1, nil +} diff --git a/spatial_index_2d.go b/spatial_index_2d.go new file mode 100644 index 0000000..67cee7e --- /dev/null +++ b/spatial_index_2d.go @@ -0,0 +1,215 @@ +package main + +import ( + "encoding/binary" + "math/bits" + "sort" +) + +// The edges of the hilbert curve plane must have a power-of-two size, +// and for efficient arithmetic on the CPU, the length of the hilbert curve filling this plane +// has to fit within the CPU architecture's `int` registers (32 bit versus 64 bit). +// +// As it is a space filling curve, length == area. So the length of the curve is equal to width*height. +// therefore, the edge length of the plane should be the largest power of two less than +// - For 32 bit CPUs: sqrt(math.MaxInt32) +// - For 64 bit CPUs: sqrt(math.MaxInt64) + +// (because it's a square root, in general, it will have half as many bits. +// and since its the power of two which is LESS than the square root, it turns out to be half as many bits minus 1) + +func getHilbertPlaneEdgeSizeBitsForCurrentProcessor() int { + return (bits.UintSize / 3) - 2 +} + +// returns the minimum and maximum values for x and y coordinates passed into the index. +func GetValidInputRange() (int, int) { + halfHilbertEdgeLength := 1 << (getHilbertPlaneEdgeSizeBitsForCurrentProcessor() - 1) + return -halfHilbertEdgeLength + 1, halfHilbertEdgeLength - 1 +} + +// returns two byte slices of length 8, one representing the smallest key in the index +// and the other representing the largest possible key in the index +func GetOutputRange() ([]byte, []byte) { + min := make([]byte, 8) + binary.BigEndian.PutUint64(min, uint64(0)) + hilbertPlaneEdgeLength := 1 << getHilbertPlaneEdgeSizeBitsForCurrentProcessor() + max := make([]byte, 8) + binary.BigEndian.PutUint64(max, uint64(hilbertPlaneEdgeLength*hilbertPlaneEdgeLength)) + return min, max +} + +// Returns a slice of 8 bytes which can be used as a key in a database index, +// to be spatial-range-queried by RectangleToIndexedRanges +func GetIndexedPoint(x int, y int) ([]byte, error) { + + curve, err := NewPeano(1 << getHilbertPlaneEdgeSizeBitsForCurrentProcessor()) + if err != nil { + panic(err) + } + // curve := Peano{ + // N: 1 << getHilbertPlaneEdgeSizeBitsForCurrentProcessor(), + // } + + // x and y can be negative, but the hilbert curve implementation is only defined over positive integers. + // so we have to attempt to transform x and y such that they are always positive. + // btw, `(index.N >> 1)` is just half of the edge length of the hilbert plane. + // so by adding (index.N >> 1) we are mapping from a -0.5..0.5 range to a 0..1 range. + // MapInverse will handle any out-of-bounds inputs & return ErrOutOfRange for us. + + mappedPoint, err := curve.MapInverse(x+(curve.N>>1), y+(curve.N>>1)) + if err != nil { + return nil, err + } + + // BigEndian puts the most significant bits first, so when the bits are used + // by the database engine for sorting, they will be sorted properly. + // For example, BigEndian looks like: + // 168a07830e039b46 + // 168a0783b786e61d + // 168a0784033846da + // LittleEndian looks like: + // b93c2fc17c078a16 + // 798575177d078a16 + // 26213f4c7d078a16 + toReturn := make([]byte, 8) + binary.BigEndian.PutUint64(toReturn, uint64(mappedPoint)) + return toReturn, nil +} + +// Use this with a range query on a database index. +type ByteRange struct { + Start []byte + End []byte +} + +// Returns a slice of 1 or more byte ranges (typically 1-4). +// The union of the results of database range queries over these ranges will contain AT LEAST +// all GetIndexedPoint(x,y) keys present within the rectangle defined by [x,y,width,height]. +// +// The results will probably also contain records outside the rectangle, it's up to you to filter them out. +// +// iopsCostParam allows you to adjust a tradeoff between wasted I/O bandwidth and # of individual I/O operations. +// I think 1.0 is actually a very reasonable value to use for SSD & HDD +// (waste ~50% of bandwidth, save a lot of unneccessary I/O operations) +// if you have an extremely fast NVME SSD with a good driver, you might try 0.5 or 0.1, but I doubt it will make it any faster. +// 2 is probably way too much for any modern disk to benefit from, unless your data is VERY sparse +func RectangleToIndexedRanges(x, y, width, height int, iopsCostParam float32) ([]ByteRange, error) { + + // scale the universe down (rounding in such a way that the original rectangle is never cropped) + // until we reach a scale where sampling the hilbert curve over the entire area of the query rectangle + // will be quick and painless for the CPU. + reducedBits := 0 + for width*height > 128 { + halfX := x / 2 + if halfX != 0 { + x = halfX + (x % halfX) + } else { + x = 0 + } + halfY := y / 2 + if halfY != 0 { + y = halfY + (y % halfY) + } else { + y = 0 + } + halfWidth := width / 2 + halfHeight := width / 2 + if halfWidth != 0 { + width = halfWidth + (width % halfWidth) + } else { + width = 1 + } + if halfWidth != 0 { + width = halfWidth + (width % halfWidth) + } else { + width = 1 + } + if halfHeight != 0 { + height = halfHeight + (height % halfHeight) + } else { + height = 1 + } + reducedBits++ + } + + reducedHilbertPlaneEdgeLength := 1 << (getHilbertPlaneEdgeSizeBitsForCurrentProcessor() - reducedBits) + + // I noticed that this method of reducing the detail is not always accurate. + // (small sections along the edge of the rectangle can be missed by rouding errors) + // so I also expanded the rectangle on all sides by 1 "pixel" at the downscaled size, + // which seemed to eliminate about 90% of the errors. + // The remaining errors I noticed were so minor I felt like I could ignore them. + if reducedBits > 0 { + if x > 0 { + x-- + } + if y > 0 { + y-- + } + if x+width < reducedHilbertPlaneEdgeLength { + width++ + } + if x+width < reducedHilbertPlaneEdgeLength { + width++ + } + if y+height < reducedHilbertPlaneEdgeLength { + height++ + } + if y+height < reducedHilbertPlaneEdgeLength { + height++ + } + } + + curve := Hilbert{N: reducedHilbertPlaneEdgeLength} + curvePoints := make([]int, width*height) + + for i := 0; i < width; i++ { + for j := 0; j < height; j++ { + p, err := curve.MapInverse(x+i+(curve.N>>1), y+j+(curve.N>>1)) + if err != nil { + return nil, err + } + curvePoints[j*width+i] = p + } + } + + sort.Ints(curvePoints) + + ranges := [][]int{{curvePoints[0], curvePoints[0]}} + for i := 1; i < len(curvePoints); i++ { + distance := curvePoints[i] - curvePoints[i-1] + if float32(distance) > float32(width*height)*iopsCostParam { + ranges[len(ranges)-1][1] = curvePoints[i-1] + ranges = append(ranges, []int{curvePoints[i], curvePoints[i]}) + } + } + ranges[len(ranges)-1][1] = curvePoints[len(curvePoints)-1] + + byteRanges := make([]ByteRange, len(ranges)) + for i, intRange := range ranges { + + // Here is where we scale the universe back up before returning the result. + // shift the bits of the resulting curve points representing the beginning and ending + // of the segments to be queried. + // + // Note that they are shifted twice as many bits (aka, squared) + // because the units here are curve length / area. + + startCurvePoint := (intRange[0] << (reducedBits * 2)) + endCurvePoint := (intRange[1] << (reducedBits * 2)) + + startBytes := make([]byte, 8) + binary.BigEndian.PutUint64(startBytes, uint64(startCurvePoint)) + + endBytes := make([]byte, 8) + binary.BigEndian.PutUint64(endBytes, uint64(endCurvePoint)) + + byteRanges[i] = ByteRange{ + Start: startBytes, + End: endBytes, + } + } + + return byteRanges, nil +}
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: