Skip to content

Commit

Permalink
Merge pull request #16 from nextmv-io/merschformann/remove-alns-sdk-dep
Browse files Browse the repository at this point in the history
Moves nextroute-only sdk packages to the repo
  • Loading branch information
merschformann authored Mar 14, 2024
2 parents 6a56686 + c0e0928 commit 9ed7c12
Show file tree
Hide file tree
Showing 86 changed files with 2,049 additions and 111 deletions.
27 changes: 27 additions & 0 deletions .nextmv/add_header.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Description: This script adds a header to all go files that are missing it.
import glob

HEADER = "// © 2019-present nextmv.io inc"

# List all go files in all subdirectories
go_files = glob.glob("**/*.go", recursive=True)

# Check if the header is the first line of each file
missing = []
checked = 0
for file in go_files:
with open(file, "r") as f:
first_line = f.readline().strip()
if first_line != HEADER:
missing.append(file)
checked += 1

# Add the header to all missing files
for file in missing:
print(f"Adding header to {file}")
with open(file) as f:
content = f.read()
with open(file, "w") as f:
f.write(HEADER + "\n\n" + content)

print(f"Checked {checked} files, added header to {len(missing)} files")
2 changes: 1 addition & 1 deletion check/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (

"github.com/nextmv-io/nextroute"
"github.com/nextmv-io/nextroute/check/schema"
"github.com/nextmv-io/sdk/common"
"github.com/nextmv-io/nextroute/common"
)

// ModelCheck is the check of a model returning a [Output].
Expand Down
127 changes: 127 additions & 0 deletions common/alias.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// © 2019-present nextmv.io inc

package common

import (
"fmt"
"math/rand"
)

// Alias is an interface that allows for sampling from a discrete
// distribution in O(1) time.
type Alias interface {
Sample(rng *rand.Rand) int
}

// NewAlias creates a new Alias from the given weights.
// The weights must be positive and at least one weight must be given.
// The weights are normalized to sum to 1.
// NewAlias([]float64{1, 2, 3}) will return an Alias that will
// return 0 with probability 1/6, 1 with probability 1/3 and 2 with
// probability 1/2.
func NewAlias(weights []float64) (Alias, error) {
n := len(weights)

if n < 1 {
return nil, fmt.Errorf(
"at least one weight is required",
)
}

if int(uint32(n)) != n {
return nil, fmt.Errorf(
"too many weights, max is %d",
1<<32-1,
)
}

sum := 0.0

for idx, weight := range weights {
if weight <= 0 {
return nil,
fmt.Errorf("a weight at index %v is non-positive %v",
idx,
weight,
)
}
sum += weight
}

alias := aliasImpl{
table: make([]int32PieceImpl, n),
}

twins := make([]float64PieceImpl, n)

smallTop := -1
largeBottom := n

multiplier := float64(n) / sum
for i, weight := range weights {
weight *= multiplier
if weight >= 1 {
largeBottom--
twins[largeBottom] = float64PieceImpl{
weight,
uint32(i),
}
} else {
smallTop++
twins[smallTop] = float64PieceImpl{
weight,
uint32(i),
}
}
}
for smallTop >= 0 && largeBottom < n {
l := twins[smallTop]
smallTop--

t := twins[largeBottom]
largeBottom++

alias.table[l.alias].probability = uint32(l.probability * (1<<31 - 1))
alias.table[l.alias].alias = t.alias

t.probability = (t.probability + l.probability) - 1

if t.probability < 1 {
smallTop++
twins[smallTop] = t
} else {
largeBottom--
twins[largeBottom] = t
}
}
for i := n - 1; i >= largeBottom; i-- {
alias.table[twins[i].alias].probability = 1<<31 - 1
}
for i := 0; i <= smallTop; i++ {
alias.table[twins[i].alias].probability = 1<<31 - 1
}
return &alias, nil
}

func (a *aliasImpl) Sample(random *rand.Rand) int {
ri := uint32(random.Int31())
w := ri % uint32(len(a.table))
if ri > a.table[w].probability {
return int(a.table[w].alias)
}
return int(w)
}

type aliasImpl struct {
table []int32PieceImpl
}

type float64PieceImpl struct {
probability float64
alias uint32
}

type int32PieceImpl struct {
probability uint32
alias uint32
}
53 changes: 53 additions & 0 deletions common/alias_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// © 2019-present nextmv.io inc

package common_test

import (
"math"
"math/rand"
"testing"

"github.com/nextmv-io/sdk/common"
)

const (
samples = 1_000_000
errorEpsilon = 0.001
)

func TestAlias(t *testing.T) {
testAlias(t, []float64{2, 2}, 22)
testAlias(t, []float64{1, 2, 3}, 123)
testAlias(t, []float64{6, 2, 1, 4, 2}, 62142)
testAlias(t, []float64{1000, 1, 3, 10}, 10001310)
}

func testAlias(t *testing.T, weights []float64, seed int64) {
sum := 0.0
for i := 0; i < len(weights); i++ {
sum += weights[i]
}
alias, err := common.NewAlias(weights)
if err != nil {
t.Fatal(err)
}

random := rand.New(rand.NewSource(seed))
counts := make([]int64, len(weights))

for i := 0; i < samples; i++ {
counts[alias.Sample(random)]++
}

for i := 0; i < len(weights); i++ {
count := float64(counts[i]) / samples
if math.Abs(count-weights[i]/sum) > errorEpsilon {
t.Errorf(
"Counts did not match, got %v, expected %v, seed %v",
count,
weights[i]/sum,
seed,
)
}
}
}
106 changes: 106 additions & 0 deletions common/boundingbox.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// © 2019-present nextmv.io inc

package common

// BoundingBox contains information about a box.
type BoundingBox interface {
// Maximum returns the maximum location of the bounding box. The right
// lower corner of the bounding box.
Maximum() Location
// Minimum returns the minimum location of the bounding box. The left
// upper corner of the bounding box.
Minimum() Location
// IsValid returns true if the bounding box is valid. A bounding box is
// valid if the maximum location is greater than the minimum location.
IsValid() bool
// Width returns the width of the box.
Width() Distance
// Height returns the height of the box.
Height() Distance
}

// NewInvalidBoundingBox returns an invalid bounding box.
func NewInvalidBoundingBox() BoundingBox {
return boundingBox{
maximum: NewInvalidLocation(),
minimum: NewInvalidLocation(),
}
}

// NewBoundingBox returns a bounding box for the given locations.
func NewBoundingBox(locations Locations) BoundingBox {
if len(locations) == 0 || !locations[0].IsValid() {
return NewInvalidBoundingBox()
}

minLatitude := locations[0].Latitude()
maxLatitude := locations[0].Latitude()
minLongitude := locations[0].Longitude()
maxLongitude := locations[0].Longitude()

for idx := 1; idx < len(locations); idx++ {
if !locations[idx].IsValid() {
return NewInvalidBoundingBox()
}
latitude := locations[idx].Latitude()
longitude := locations[idx].Longitude()
if minLatitude > latitude {
minLatitude = latitude
}
if maxLatitude < latitude {
maxLatitude = latitude
}
if minLongitude > longitude {
minLongitude = longitude
}
if maxLongitude < longitude {
maxLongitude = longitude
}
}
maxLocation, _ := NewLocation(maxLongitude, maxLatitude)
minLocation, _ := NewLocation(minLongitude, minLatitude)
return boundingBox{
maximum: maxLocation,
minimum: minLocation,
}
}

type boundingBox struct {
maximum Location
minimum Location
}

func (b boundingBox) Width() Distance {
if !b.IsValid() {
return NewDistance(0.0, Meters)
}
leftUpper, _ := NewLocation(b.minimum.Longitude(), b.minimum.Latitude())
rightUpper, _ := NewLocation(b.maximum.Longitude(), b.minimum.Latitude())
width, _ := Haversine(leftUpper, rightUpper)
return width
}

func (b boundingBox) Height() Distance {
if !b.IsValid() {
return NewDistance(0.0, Meters)
}
leftUpper, _ := NewLocation(b.minimum.Longitude(), b.minimum.Latitude())
leftLower, _ := NewLocation(b.minimum.Longitude(), b.maximum.Latitude())
height, _ := Haversine(leftUpper, leftLower)
return height
}

func (b boundingBox) IsValid() bool {
return b.maximum.IsValid() &&
b.minimum.IsValid() &&
b.maximum.Longitude() >= b.minimum.Longitude() &&
b.maximum.Latitude() >= b.minimum.Latitude()
}

func (b boundingBox) Maximum() Location {
return b.maximum
}

func (b boundingBox) Minimum() Location {
return b.minimum
}
Loading

0 comments on commit 9ed7c12

Please sign in to comment.