├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── diagrams_test.go ├── examples_test.go ├── spherand.go └── spherand_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Diagrams 2 | *.png 3 | 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.dll 7 | *.so 8 | *.dylib 9 | 10 | # Test binary, build with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 17 | .glide/ 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.x 4 | - 1.12.x 5 | - 1.13.x 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2017, Michael McLoughlin 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any purpose 6 | with or without fee is hereby granted, provided that the above copyright notice 7 | and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 11 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 13 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 14 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 15 | THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | go test -v 3 | 4 | diagrams: 5 | go test -tags diagrams -run TestDiagrams 6 | 7 | clean: 8 | $(RM) *.png 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # spherand 2 | 3 | Random points on a sphere in Golang. 4 | 5 | [![go.dev Reference](https://img.shields.io/badge/doc-reference-007d9b?logo=go&style=flat-square)](https://pkg.go.dev/github.com/mmcloughlin/spherand) 6 | [![Build status](https://img.shields.io/travis/mmcloughlin/spherand.svg?style=flat-square)](https://travis-ci.org/mmcloughlin/spherand) 7 | 8 | ## The Problem 9 | 10 | If you generate latitude and longitude uniformly in the ranges `[-90, 90]` and 11 | `[-180, 180]` you will get distributions biased towards the poles. 12 | 13 | ![Distribution of broken generator](https://i.imgur.com/zLOdR0J.png) 14 | 15 | This package [correctly picks points on a 16 | sphere](http://mathworld.wolfram.com/SpherePointPicking.html), providing a 17 | uniform distribution visualized below. 18 | 19 | ![Distribution of correct generator](https://i.imgur.com/2GikHe6.png) 20 | 21 | ## Getting Started 22 | 23 | Install with 24 | 25 | ```sh 26 | $ go get -u github.com/mmcloughlin/spherand 27 | ``` 28 | 29 | Generate a geographical point with 30 | 31 | ```go 32 | lat, lng := spherand.Geographical() 33 | ``` 34 | 35 | You can also generate spherical coordinates with `Spherical()`. 36 | 37 | If you need to control the random source, use a generator 38 | 39 | ```go 40 | g := spherand.NewGenerator(rand.New(rand.NewSource(42))) 41 | lat, lng := g.Geographical() 42 | ``` 43 | 44 | See [go.dev](https://pkg.go.dev/github.com/mmcloughlin/spherand) for reference. 45 | 46 | ## Diagrams 47 | 48 | Generated with [globe](https://github.com/mmcloughlin/globe). 49 | -------------------------------------------------------------------------------- /diagrams_test.go: -------------------------------------------------------------------------------- 1 | // +build diagrams 2 | 3 | package spherand 4 | 5 | import ( 6 | "image/color" 7 | "math/rand" 8 | "testing" 9 | 10 | "github.com/mmcloughlin/globe" 11 | ) 12 | 13 | type GeographicalGenerator func() (lat, lng float64) 14 | 15 | func Wrong() (lat, lng float64) { 16 | lat = rand.Float64()*180 - 90 17 | lng = rand.Float64()*360 - 180 18 | return 19 | } 20 | 21 | func Diagram(gen GeographicalGenerator, n int) *globe.Globe { 22 | radius := 0.04 23 | c := color.NRGBA{0, 0, 0, 48} 24 | 25 | g := globe.New() 26 | g.DrawGraticule(10.0) 27 | for i := 0; i < n; i++ { 28 | lat, lng := gen() 29 | g.DrawDot(lat, lng, radius, globe.Color(c)) 30 | } 31 | g.CenterOn(75, 0) 32 | return g 33 | } 34 | 35 | func TestDiagrams(t *testing.T) { 36 | n := 100000 37 | side := 400 38 | diagrams := []struct { 39 | Generator GeographicalGenerator 40 | Filename string 41 | }{ 42 | {Wrong, "wrong.png"}, 43 | {Geographical, "right.png"}, 44 | } 45 | for _, diagram := range diagrams { 46 | g := Diagram(diagram.Generator, n) 47 | g.SavePNG(diagram.Filename, side) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /examples_test.go: -------------------------------------------------------------------------------- 1 | package spherand_test 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | 7 | "github.com/mmcloughlin/spherand" 8 | ) 9 | 10 | func ExampleGenerator_Geographical() { 11 | g := spherand.NewGenerator(rand.New(rand.NewSource(1))) 12 | fmt.Println(g.Geographical()) 13 | // Output: 61.76543033984557 37.67770367266306 14 | } 15 | -------------------------------------------------------------------------------- /spherand.go: -------------------------------------------------------------------------------- 1 | // Package spherand generates random points uniformly on a sphere. 2 | package spherand 3 | 4 | import ( 5 | "math" 6 | "math/rand" 7 | ) 8 | 9 | // UniformSampler generates samples from the uniform distribution on [0,1). 10 | type UniformSampler interface { 11 | Float64() float64 12 | } 13 | 14 | // UniformSamplerFunc can satisfy the UniformSampler interface with a plain 15 | // function. 16 | type UniformSamplerFunc func() float64 17 | 18 | // Float64 calls f. 19 | func (f UniformSamplerFunc) Float64() float64 { 20 | return f() 21 | } 22 | 23 | // Generator can generate spherical points based on a configured source of 24 | // uniform random values. 25 | type Generator struct { 26 | sampler UniformSampler 27 | } 28 | 29 | // NewGenerator builds a Generator backed by the given uniform sampler. Note 30 | // that the standard library rand.Rand can be used as a UniformSampler. 31 | func NewGenerator(sampler UniformSampler) Generator { 32 | return Generator{ 33 | sampler: sampler, 34 | } 35 | } 36 | 37 | // global is the internal generator used by package-level functions. It is 38 | // backed by the global Float64 generator. 39 | var global = NewGenerator(UniformSamplerFunc(rand.Float64)) 40 | 41 | // Spherical generates a random point on the unit sphere in spherical 42 | // coordinates. This returns an azimuthal angle in radians between 0 and 2pi and 43 | // a polar angle between 0 and pi. 44 | func (g Generator) Spherical() (azimuth, polar float64) { 45 | azimuth = 2 * math.Pi * g.sampler.Float64() 46 | polar = math.Acos(2*g.sampler.Float64() - 1) 47 | return 48 | } 49 | 50 | // Spherical generates a random point on the unit sphere in spherical 51 | // coordinates. This returns an azimuthal angle in radians between 0 and 2pi and 52 | // a polar angle between 0 and pi. 53 | // This package level function uses the default random source. 54 | func Spherical() (azimuth, polar float64) { 55 | return global.Spherical() 56 | } 57 | 58 | // Geographical returns a random geographical point as latitude in degrees 59 | // between -90 and 90, and longitude between -180 and 180. 60 | func (g Generator) Geographical() (lat, lng float64) { 61 | azimuth, polar := g.Spherical() 62 | lat = 90 - 180.0*polar/math.Pi 63 | lng = 180.0*azimuth/math.Pi - 180 64 | return 65 | } 66 | 67 | // Geographical returns a random geographical point as latitude in degrees 68 | // between -90 and 90, and longitude between -180 and 180. 69 | // This package level function uses the default random source. 70 | func Geographical() (lat, lng float64) { 71 | return global.Geographical() 72 | } 73 | -------------------------------------------------------------------------------- /spherand_test.go: -------------------------------------------------------------------------------- 1 | package spherand 2 | 3 | import ( 4 | "math" 5 | "math/rand" 6 | "testing" 7 | ) 8 | 9 | func TestUniformSamplerInterface(t *testing.T) { 10 | var _ UniformSampler = new(rand.Rand) 11 | } 12 | 13 | func AssertRange(t *testing.T, x, min, max float64) { 14 | if x < min || x > max { 15 | t.Errorf("value outside allowed range: got %f expected [%f, %f]", x, min, max) 16 | } 17 | } 18 | 19 | func TestSphericalRange(t *testing.T) { 20 | for i := 0; i < 100; i++ { 21 | azimuth, polar := Spherical() 22 | AssertRange(t, azimuth, 0, 2*math.Pi) 23 | AssertRange(t, polar, 0, math.Pi) 24 | } 25 | } 26 | 27 | func TestGeographicalRange(t *testing.T) { 28 | for i := 0; i < 100; i++ { 29 | lat, lng := Geographical() 30 | AssertRange(t, lat, -90, 90) 31 | AssertRange(t, lng, -180, 180) 32 | } 33 | } 34 | --------------------------------------------------------------------------------