Skip to content

Commit

Permalink
s2: Add benchmark scaffolding for EdgeQuery.
Browse files Browse the repository at this point in the history
Comparing the Go version to the C++ version, the performance pattern
matches the level pretty well as the size scales.

Signed-off-by: David Symonds <[email protected]>
  • Loading branch information
rsned authored and dsymonds committed Sep 16, 2019
1 parent 602b39b commit f02de57
Show file tree
Hide file tree
Showing 2 changed files with 303 additions and 2 deletions.
60 changes: 59 additions & 1 deletion s2/edge_query_closest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,64 @@ func TestClosestEdgeQueryTargetPolygonContainingIndexedPoints(t *testing.T) {
}
}

func BenchmarkEdgeQueryFindEdgesClosestFractal(b *testing.B) {
// Test searching within the general vicinity of the indexed shapes.
opts := &edgeQueryBenchmarkOptions{
fact: fractalLoopShapeIndexGenerator,
includeInteriors: false,
targetType: queryTypePoint,
numTargetEdges: 0,
chooseTargetFromIndex: false,
radiusKm: 1000,
maxDistanceFraction: -1,
maxErrorFraction: -1,
targetRadiusFraction: 0.0,
centerSeparationFraction: -2.0,
}

benchmarkEdgeQueryFindClosest(b, opts)
}

func BenchmarkEdgeQueryFindEdgesClosestInterior(b *testing.B) {
// Test searching within the general vicinity of the indexed shapes including interiors.
opts := &edgeQueryBenchmarkOptions{
fact: fractalLoopShapeIndexGenerator,
includeInteriors: true,
targetType: queryTypePoint,
numTargetEdges: 0,
chooseTargetFromIndex: false,
radiusKm: 1000,
maxDistanceFraction: -1,
maxErrorFraction: -1,
targetRadiusFraction: 0.0,
centerSeparationFraction: -2.0,
}

benchmarkEdgeQueryFindClosest(b, opts)
}

func BenchmarkEdgeQueryFindEdgesClosestErrorPercent(b *testing.B) {
// Test searching with an error tolerance. Allowing 1% error makes searches
// 6x faster in the case of regular loops with a large number of vertices.
opts := &edgeQueryBenchmarkOptions{
fact: fractalLoopShapeIndexGenerator,
includeInteriors: false,
targetType: queryTypePoint,
numTargetEdges: 0,
chooseTargetFromIndex: false,
radiusKm: 1000,
maxDistanceFraction: -1,
maxErrorFraction: 0.01,
targetRadiusFraction: 0.0,
centerSeparationFraction: -2.0,
}

benchmarkEdgeQueryFindClosest(b, opts)

opts.maxErrorFraction = 0.1
benchmarkEdgeQueryFindClosest(b, opts)
}

// TODO(roberts): Remaining tests to implement.
//
// TestClosestEdgeQueryTestReuseOfQuery) {
Expand All @@ -194,4 +252,4 @@ func TestClosestEdgeQueryTargetPolygonContainingIndexedPoints(t *testing.T) {
// TestClosestEdgeQueryPointCloudEdges) {
// TestClosestEdgeQueryConservativeCellDistanceIsUsed) {
//
// Benchmarking code.
// More of the Benchmarking code.
245 changes: 244 additions & 1 deletion s2/edge_query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@
package s2

import (
"fmt"
"math/rand"
"reflect"
"testing"

"github.com/golang/geo/s1"
)

// Note that most of the actual testing is done in s2edge_query_{closest|furthest}_test.
Expand Down Expand Up @@ -187,6 +191,15 @@ func pointCloudShapeIndexGenerator(c Cap, numPoints int, index *ShapeIndex) {
index.Add(&points)
}

type queryTargetType int

const (
queryTypePoint queryTargetType = iota
queryTypeEdge
queryTypeCell
queryTypeIndex
)

const edgeQueryTestNumIndexes = 50
const edgeQueryTestNumEdges = 100
const edgeQueryTestNumQueries = 200
Expand Down Expand Up @@ -283,8 +296,238 @@ func testEdgeQueryWithGenerator(t *testing.T,
target := NewMaxDistanceToShapeIndexTarget(indexes[jIndex])
target.setIncludeInteriors(oneIn(2))
testFindEdges(target, query)
}
}
}
*/

// benchmarkEdgeQueryFindClosest calls FindEdges the given number of times on
// a ShapeIndex with approximately numIndexEdges edges generated by the given
// generator. The geometry is generated within a Cap of the radius given.
//
// Each query uses a target of the given targetType.
//
// - If maxDistanceFraction > 0, then DistanceLimit is set to the given
// fraction of the index radius.
//
// - If maxErrorFraction > 0, then MaxError is set to the given
// fraction of the index radius.
//
// TODO(roberts): If there is a need to benchmark Furthest as well, this will need
// some changes to not use just the Closest variants of parts.
// Furthest isn't doing anything different under the covers than Closest, so there
// isn't really a huge need for benchmarking both.
func benchmarkEdgeQueryFindClosest(b *testing.B, bmOpts *edgeQueryBenchmarkOptions) {
const numIndexSamples = 8

b.StopTimer()
index := NewShapeIndex()
opts := NewClosestEdgeQueryOptions().MaxResults(1).IncludeInteriors(bmOpts.includeInteriors)

radius := kmToAngle(bmOpts.radiusKm.Radians())
if bmOpts.maxDistanceFraction > 0 {
opts.DistanceLimit(s1.ChordAngleFromAngle(s1.Angle(bmOpts.maxDistanceFraction) * radius))
}
if bmOpts.maxErrorFraction > 0 {
opts.MaxError(s1.ChordAngleFromAngle(s1.Angle(bmOpts.maxErrorFraction) * radius))
}

// TODO(roberts): Once there is a non-brute-force option, set it here.
// opts.UseBruteForce(opts.bruteForce)

query := NewClosestEdgeQuery(index, opts)

delta := 0 // Bresenham-type algorithm for geometry sampling.
var targets []distanceTarget

// To follow the sizing on the C++ tests to ease comparisons, the number of
// edges in the index range on 3 * 4^n (up to 16384).
bmOpts.numIndexEdges = 3
for n := 1; n <= 7; n++ {
bmOpts.numIndexEdges *= 4
b.Run(fmt.Sprintf("%d", bmOpts.numIndexEdges),
func(b *testing.B) {
iTarget := 0
for i := 0; i < b.N; i++ {
delta -= numIndexSamples
if delta < 0 {
// Generate a new index and a new set of
// targets to go with it. Reset the random
// number seed so that we use the same sequence
// of indexed shapes no matter how many
// iterations are specified.
b.StopTimer()
delta += i
targets, _ = generateEdgeQueryWithTargets(bmOpts, query, index)
b.StartTimer()
}
query.FindEdges(targets[iTarget])
iTarget++
if iTarget == len(targets) {
iTarget = 0
}
}
})
}
}

// edgeQueryBenchmarkOptions holds the various parameters than can be adjusted by the
// benchmarking runners.
type edgeQueryBenchmarkOptions struct {
iters int
fact shapeIndexGeneratorFunc
numIndexEdges int
includeInteriors bool
targetType queryTargetType
numTargetEdges int
chooseTargetFromIndex bool
radiusKm s1.Angle
maxDistanceFraction float64
maxErrorFraction float64
targetRadiusFraction float64
centerSeparationFraction float64
randomSeed int64
}

// generateEdgeQueryWithTargets generates and adds geometry to a ShapeIndex for
// use in an edge query.
//
// Approximately numIndexEdges will be generated by the requested generator and
// inserted. The geometry is generated within a Cap of the radius specified
// by radiusKm (the index radius). Parameters with fraction in their
// names are expressed as a fraction of this radius.
//
// Also generates a set of target geometries for the query, based on the
// targetType and the input parameters. If targetType is INDEX, then:
// (i) the target will have approximately numTargetEdges edges.
// (ii) includeInteriors will be set on the target index.
//
// - If chooseTargetFromIndex is true, then the target will be chosen
// from the geometry in the index itself, otherwise it will be chosen
// randomly according to the parameters below:
//
// - If targetRadiusFraction > 0, the target radius will be approximately
// the given fraction of the index radius; if targetRadiusFraction < 0,
// it will be chosen randomly up to corresponding positive fraction.
//
// - If centerSeparationFraction > 0, then the centers of index and target
// bounding caps will be separated by the given fraction of the index
// radius; if centerSeparationFraction < 0, they will be separated by up
// to the corresponding positive fraction.
//
// - The randomSeed is used to initialize an internal seed, which is
// incremented at the start of each call to generateEdgeQueryWithTargets.
// This is for debugging purposes.
//
func generateEdgeQueryWithTargets(opts *edgeQueryBenchmarkOptions, query *EdgeQuery, queryIndex *ShapeIndex) (targets []distanceTarget, targetIndexes []*ShapeIndex) {

// To save time, we generate at most this many distinct targets per index.
const maxTargetsPerIndex = 100

// Set a specific seed to allow repeatabilty
rand.Seed(opts.randomSeed)
opts.randomSeed++
indexCap := CapFromCenterAngle(randomPoint(), opts.radiusKm)

queryIndex.Reset()
opts.fact(indexCap, opts.numIndexEdges, queryIndex)

targets = make([]distanceTarget, 0)
targetIndexes = make([]*ShapeIndex, 0)

numTargets := maxTargetsPerIndex
if opts.targetType == queryTypeIndex {
// Limit the total number of target edges to reduce the benchmark running times.
numTargets = minInt(numTargets, 500000/opts.numTargetEdges)
}

for i := 0; i < numTargets; i++ {
targetDist := fractionToRadius(opts.centerSeparationFraction, opts.radiusKm.Radians())
targetCap := CapFromCenterAngle(
sampleBoundaryFromCap(CapFromCenterAngle(indexCap.Center(), targetDist)),
fractionToRadius(opts.targetRadiusFraction, opts.radiusKm.Radians()))

switch opts.targetType {
case queryTypePoint:
var pt Point
if opts.chooseTargetFromIndex {
pt = sampleEdgeFromIndex(queryIndex).V0
} else {
pt = targetCap.Center()
}
targets = append(targets, NewMinDistanceToPointTarget(pt))

case queryTypeEdge:
var edge Edge
if opts.chooseTargetFromIndex {
edge = sampleEdgeFromIndex(queryIndex)
} else {
edge.V0 = sampleBoundaryFromCap(targetCap)
edge.V1 = sampleBoundaryFromCap(targetCap)
}
targets = append(targets, NewMinDistanceToEdgeTarget(edge))
case queryTypeCell:
var cellID CellID
if opts.chooseTargetFromIndex {
cellID = sampleCellFromIndex(queryIndex)
} else {
cellID = cellIDFromPoint(targetCap.Center()).Parent(
MaxDiagMetric.ClosestLevel(targetCap.Radius().Radians()))
}
targets = append(targets, NewMinDistanceToCellTarget(CellFromCellID(cellID)))
case queryTypeIndex:
targetIndex := NewShapeIndex()
if opts.chooseTargetFromIndex {
var shape edgeVectorShape
for i := 0; i < opts.numTargetEdges; i++ {
edge := sampleEdgeFromIndex(queryIndex)
shape.Add(edge.V0, edge.V1)
}
targetIndex.Add(&shape)
} else {
opts.fact(targetCap, opts.numTargetEdges, targetIndex)
}
target := NewMinDistanceToShapeIndexTarget(targetIndex)
target.setIncludeInteriors(opts.includeInteriors)
targets = append(targets, target)
default:
panic(fmt.Sprintf("unknown query target type %v", opts.targetType))
}
}

return targets, targetIndexes
}

func sampleBoundaryFromCap(c Cap) Point {
return InterpolateAtDistance(c.Radius(), c.Center(), randomPoint())
}

func sampleEdgeFromIndex(index *ShapeIndex) Edge {
e := randomUniformInt(index.NumEdges())

for _, shape := range index.shapes {
if e < shape.NumEdges() {
return shape.Edge(e)
}
e -= shape.NumEdges()
}
// This should only happen if the index has no edges at all.
panic("index with no edges")
}

func sampleCellFromIndex(index *ShapeIndex) CellID {
iter := index.Iterator()
for i := randomUniformInt(len(index.cells)); i >= 0; i-- {
iter.Next()
continue
}
return iter.CellID()
}

func fractionToRadius(fraction, radiusKm float64) s1.Angle {
if fraction < 0 {
fraction = -randomFloat64() * fraction
}
return s1.Angle(fraction) * kmToAngle(radiusKm)
}

0 comments on commit f02de57

Please sign in to comment.