Initial commit: Lux math library

This commit is contained in:
2025-08-18 13:18:19 -05:00
commit a60905ab45
27 changed files with 2660 additions and 0 deletions
+28
View File
@@ -0,0 +1,28 @@
# Lux Math Library
A comprehensive mathematical utilities library for the Lux ecosystem.
## Features
- **Math utilities**: Safe math operations, averagers, and meters
- **Set operations**: Efficient set implementations including bit sets
- **Numerical computing**: Integration with gonum for advanced mathematical operations
## Installation
```bash
go get github.com/luxfi/math
```
## Usage
```go
import (
"github.com/luxfi/math/math"
"github.com/luxfi/math/set"
)
```
## License
See the LICENSE file for licensing terms.
+23
View File
@@ -0,0 +1,23 @@
module github.com/luxfi/math
go 1.24.6
require (
github.com/luxfi/geth v1.16.34
github.com/luxfi/ids v1.0.2
github.com/luxfi/node v1.16.15
github.com/stretchr/testify v1.10.0
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6
)
require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/gorilla/rpc v1.2.1 // indirect
github.com/luxfi/crypto v1.2.9 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
go.uber.org/mock v0.5.2 // indirect
golang.org/x/crypto v0.40.0 // indirect
gonum.org/v1/gonum v0.16.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+41
View File
@@ -0,0 +1,41 @@
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gorilla/rpc v1.2.1 h1:yC+LMV5esttgpVvNORL/xX4jvTTEUE30UZhZ5JF7K9k=
github.com/gorilla/rpc v1.2.1/go.mod h1:uNpOihAlF5xRFLuTYhfR0yfCTm0WTQSQttkMSptRfGk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/luxfi/crypto v1.2.9 h1:Ecqznogm85AK9IniUL5VRHXehGnlMLO6wE9Tsqhudfo=
github.com/luxfi/crypto v1.2.9/go.mod h1:XuwNS3m4bJEgxt4myw0woWszz3/aFy5EgZInR7JLu9Q=
github.com/luxfi/geth v1.16.34 h1:61IMMZMWfi4bM4SASVBvp3x+wYmpcTpIIHDiKVVPDiQ=
github.com/luxfi/geth v1.16.34/go.mod h1:kjF/Vwzd9Iz6+9PpHyF7ZtfVJYsJdBYGhnoXLDxx7b8=
github.com/luxfi/ids v1.0.2 h1:OnbCWBgdmuBPZat1r1vRikk1FFrdHMfSknpfKmX0KBg=
github.com/luxfi/ids v1.0.2/go.mod h1:cMbto3Q8N3IaBxdz4Lzaq9wYl33coGXUdlr2+YwXeUw=
github.com/luxfi/node v1.16.15 h1:ZRsteUu6ObB7Or3E6ZliNY2w/cL4tByGaSm7J37sVU8=
github.com/luxfi/node v1.16.15/go.mod h1:05FhncqqiDSp5XfSPjoPvTBBUVokCdKpHL72aRCxs4Y=
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo=
github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/thepudds/fzgen v0.4.3 h1:srUP/34BulQaEwPP/uHZkdjUcUjIzL7Jkf4CBVryiP8=
github.com/thepudds/fzgen v0.4.3/go.mod h1:BhhwtRhzgvLWAjjcHDJ9pEiLD2Z9hrVIFjBCHJ//zJ4=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE=
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+16
View File
@@ -0,0 +1,16 @@
// Copyright (C) 2019-2024, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package math
import "time"
// Averager tracks a continuous time exponential moving average of the provided
// values.
type Averager interface {
// Observe the value at the given time
Observe(value float64, currentTime time.Time)
// Read returns the average of the provided values.
Read() float64
}
+65
View File
@@ -0,0 +1,65 @@
// Copyright (C) 2019-2024, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package math
import (
"github.com/luxfi/ids"
"github.com/luxfi/node/utils/heap"
)
var _ AveragerHeap = (*averagerHeap)(nil)
// TODO replace this interface with utils/heap
// AveragerHeap maintains a heap of the averagers.
type AveragerHeap interface {
// Add the average to the heap. If [nodeID] is already in the heap, the
// average will be replaced and the old average will be returned. If there
// was not an old average, false will be returned.
Add(nodeID ids.NodeID, averager Averager) (Averager, bool)
// Remove attempts to remove the average that was added with the provided
// [nodeID], if none is contained in the heap, [false] will be returned.
Remove(nodeID ids.NodeID) (Averager, bool)
// Pop attempts to remove the node with either the largest or smallest
// average, depending on if this is a max heap or a min heap, respectively.
Pop() (ids.NodeID, Averager, bool)
// Peek attempts to return the node with either the largest or smallest
// average, depending on if this is a max heap or a min heap, respectively.
Peek() (ids.NodeID, Averager, bool)
// Len returns the number of nodes that are currently in the heap.
Len() int
}
type averagerHeap struct {
heap heap.Map[ids.NodeID, Averager]
}
// NewMaxAveragerHeap returns a new empty max heap. The returned heap is not
// thread safe.
func NewMaxAveragerHeap() AveragerHeap {
return averagerHeap{
heap: heap.NewMap[ids.NodeID, Averager](func(a, b Averager) bool {
return a.Read() > b.Read()
}),
}
}
func (h averagerHeap) Add(nodeID ids.NodeID, averager Averager) (Averager, bool) {
return h.heap.Push(nodeID, averager)
}
func (h averagerHeap) Remove(nodeID ids.NodeID) (Averager, bool) {
return h.heap.Remove(nodeID)
}
func (h averagerHeap) Pop() (ids.NodeID, Averager, bool) {
return h.heap.Pop()
}
func (h averagerHeap) Peek() (ids.NodeID, Averager, bool) {
return h.heap.Peek()
}
func (h averagerHeap) Len() int {
return h.heap.Len()
}
+104
View File
@@ -0,0 +1,104 @@
// Copyright (C) 2019-2024, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package math
import (
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/luxfi/ids"
)
func TestAveragerHeap(t *testing.T) {
n0 := ids.GenerateTestNodeID()
n1 := ids.GenerateTestNodeID()
n2 := ids.GenerateTestNodeID()
tests := []struct {
name string
h AveragerHeap
a []Averager
}{
{
name: "max heap",
h: NewMaxAveragerHeap(),
a: []Averager{
NewAverager(0, time.Second, time.Now()),
NewAverager(-1, time.Second, time.Now()),
NewAverager(-2, time.Second, time.Now()),
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require := require.New(t)
_, _, ok := test.h.Pop()
require.False(ok)
_, _, ok = test.h.Peek()
require.False(ok)
l := test.h.Len()
require.Zero(l)
_, ok = test.h.Add(n1, test.a[1])
require.False(ok)
n, a, ok := test.h.Peek()
require.True(ok)
require.Equal(n1, n)
require.Equal(test.a[1], a)
l = test.h.Len()
require.Equal(1, l)
a, ok = test.h.Add(n1, test.a[1])
require.True(ok)
require.Equal(test.a[1], a)
l = test.h.Len()
require.Equal(1, l)
_, ok = test.h.Add(n0, test.a[0])
require.False(ok)
_, ok = test.h.Add(n2, test.a[2])
require.False(ok)
n, a, ok = test.h.Pop()
require.True(ok)
require.Equal(n0, n)
require.Equal(test.a[0], a)
l = test.h.Len()
require.Equal(2, l)
a, ok = test.h.Remove(n1)
require.True(ok)
require.Equal(test.a[1], a)
l = test.h.Len()
require.Equal(1, l)
_, ok = test.h.Remove(n1)
require.False(ok)
l = test.h.Len()
require.Equal(1, l)
a, ok = test.h.Add(n2, test.a[0])
require.True(ok)
require.Equal(test.a[2], a)
n, a, ok = test.h.Pop()
require.True(ok)
require.Equal(n2, n)
require.Equal(test.a[0], a)
})
}
}
+72
View File
@@ -0,0 +1,72 @@
// Copyright (C) 2019-2024, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package math
import (
"math"
"time"
)
var convertEToBase2 = math.Log(2)
type continuousAverager struct {
halflife float64
weightedSum float64
normalizer float64
lastUpdated time.Time
}
// NewUninitializedAverager creates a new averager with the given halflife. If
// [Read] is called before [Observe], the zero value will be returned. When
// [Observe] is called the first time, the averager will be initialized with
// [value] at that time.
func NewUninitializedAverager(halfLife time.Duration) Averager {
// Use 0 as the initialPrediction and 0 as the currentTime, so that when the
// first observation occurs (at a non-zero time) the initial prediction's
// weight will become negligible.
return NewAverager(0, halfLife, time.Time{})
}
func NewAverager(
initialPrediction float64,
halflife time.Duration,
currentTime time.Time,
) Averager {
return &continuousAverager{
halflife: float64(halflife) / convertEToBase2,
weightedSum: initialPrediction,
normalizer: 1,
lastUpdated: currentTime,
}
}
func (a *continuousAverager) Observe(value float64, currentTime time.Time) {
delta := a.lastUpdated.Sub(currentTime)
switch {
case delta < 0:
// If the times are called in order, scale the previous values to keep the
// sizes manageable
newWeight := math.Exp(float64(delta) / a.halflife)
a.weightedSum = value + newWeight*a.weightedSum
a.normalizer = 1 + newWeight*a.normalizer
a.lastUpdated = currentTime
case delta == 0:
// If this is called multiple times at the same wall clock time, no
// scaling needs to occur
a.weightedSum += value
a.normalizer++
default:
// If the times are called out of order, don't scale the previous values
newWeight := math.Exp(float64(-delta) / a.halflife)
a.weightedSum += newWeight * value
a.normalizer += newWeight
}
}
func (a *continuousAverager) Read() float64 {
return a.weightedSum / a.normalizer
}
@@ -0,0 +1,35 @@
// Copyright (C) 2019-2024, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package math
import (
"fmt"
"testing"
"time"
)
func BenchmarkAveragers(b *testing.B) {
periods := []time.Duration{
time.Millisecond,
time.Duration(0),
-time.Millisecond,
}
for _, period := range periods {
name := fmt.Sprintf("period=%s", period)
b.Run(name, func(b *testing.B) {
a := NewAverager(0, time.Second, time.Now())
AveragerBenchmark(b, a, period)
})
}
}
func AveragerBenchmark(b *testing.B, a Averager, period time.Duration) {
currentTime := time.Now()
b.ResetTimer()
for i := 0; i < b.N; i++ {
currentTime = currentTime.Add(period)
a.Observe(float64(i), currentTime)
}
}
+54
View File
@@ -0,0 +1,54 @@
// Copyright (C) 2019-2024, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package math
import (
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestAverager(t *testing.T) {
require := require.New(t)
halflife := time.Second
currentTime := time.Now()
a := NewSyncAverager(NewAverager(0, halflife, currentTime))
require.Zero(a.Read())
currentTime = currentTime.Add(halflife)
a.Observe(1, currentTime)
require.Equal(1.0/1.5, a.Read())
}
func TestAveragerTimeTravel(t *testing.T) {
require := require.New(t)
halflife := time.Second
currentTime := time.Now()
a := NewSyncAverager(NewAverager(1, halflife, currentTime))
require.Equal(float64(1), a.Read())
currentTime = currentTime.Add(-halflife)
a.Observe(0, currentTime)
require.Equal(1.0/1.5, a.Read())
}
func TestUninitializedAverager(t *testing.T) {
require := require.New(t)
halfLife := time.Second
currentTime := time.Now()
firstObservation := float64(10)
a := NewUninitializedAverager(halfLife)
require.Zero(a.Read())
a.Observe(firstObservation, currentTime)
require.Equal(firstObservation, a.Read())
}
+79
View File
@@ -0,0 +1,79 @@
// Copyright (C) 2019-2024, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package meter
import (
"math"
"time"
)
var (
convertEToBase2 = math.Log(2)
_ Factory = (*ContinuousFactory)(nil)
_ Meter = (*continuousMeter)(nil)
)
// ContinuousFactory implements the Factory interface by returning a continuous
// time meter.
type ContinuousFactory struct{}
func (ContinuousFactory) New(halflife time.Duration) Meter {
return NewMeter(halflife)
}
type continuousMeter struct {
halflife float64
value float64
numCoresRunning float64
lastUpdated time.Time
}
// NewMeter returns a new Meter with the provided halflife
func NewMeter(halflife time.Duration) Meter {
return &continuousMeter{
halflife: float64(halflife) / convertEToBase2,
}
}
func (a *continuousMeter) Inc(now time.Time, numCores float64) {
a.Read(now)
a.numCoresRunning += numCores
}
func (a *continuousMeter) Dec(now time.Time, numCores float64) {
a.Read(now)
a.numCoresRunning -= numCores
}
func (a *continuousMeter) Read(now time.Time) float64 {
timeSincePreviousUpdate := a.lastUpdated.Sub(now)
if timeSincePreviousUpdate >= 0 {
return a.value
}
a.lastUpdated = now
factor := math.Exp(float64(timeSincePreviousUpdate) / a.halflife)
a.value *= factor
a.value += a.numCoresRunning * (1 - factor)
return a.value
}
func (a *continuousMeter) TimeUntil(now time.Time, value float64) time.Duration {
currentValue := a.Read(now)
if currentValue <= value {
return time.Duration(0)
}
// Note that [factor] >= 1
factor := currentValue / value
// Note that [numHalfLives] >= 0
numHalflives := math.Log(factor)
duration := numHalflives * a.halflife
// Overflow protection
if duration > math.MaxInt64 {
return time.Duration(math.MaxInt64)
}
return time.Duration(duration)
}
+12
View File
@@ -0,0 +1,12 @@
// Copyright (C) 2019-2024, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package meter
import "time"
// Factory returns new meters.
type Factory interface {
// New returns a new meter with the provided halflife.
New(halflife time.Duration) Meter
}
+28
View File
@@ -0,0 +1,28 @@
// Copyright (C) 2019-2024, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package meter
import "time"
// Meter tracks a continuous exponential moving average of the % of time this
// meter has been running.
type Meter interface {
// Inc the meter, the read value will be monotonically increasing while
// the meter is running.
Inc(time.Time, float64)
// Dec the meter, the read value will be exponentially decreasing while the
// meter is off.
Dec(time.Time, float64)
// Read the current value of the meter, this can be used to approximate the
// percent of time the meter has been running recently. The definition of
// recently depends on the halflife of the decay function.
Read(time.Time) float64
// Returns the duration between [now] and when the value of this meter
// reaches [value], assuming that the number of cores running is always 0.
// If the value of this meter is already <= [value], returns the zero duration.
TimeUntil(now time.Time, value float64) time.Duration
}
+39
View File
@@ -0,0 +1,39 @@
// Copyright (C) 2019-2024, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package meter
import (
"fmt"
"testing"
"time"
)
func BenchmarkMeters(b *testing.B) {
for _, meterDef := range meters {
period := time.Second + 500*time.Millisecond
name := fmt.Sprintf("%s-%s", meterDef.name, period)
b.Run(name, func(b *testing.B) {
m := meterDef.factory.New(halflife)
MeterBenchmark(b, m, period)
})
period = time.Millisecond
name = fmt.Sprintf("%s-%s", meterDef.name, period)
b.Run(name, func(b *testing.B) {
m := meterDef.factory.New(halflife)
MeterBenchmark(b, m, period)
})
}
}
func MeterBenchmark(b *testing.B, m Meter, period time.Duration) {
currentTime := time.Now()
m.Inc(currentTime, 1)
b.ResetTimer()
for i := 0; i < b.N; i++ {
currentTime = currentTime.Add(period)
m.Read(currentTime)
}
}
+166
View File
@@ -0,0 +1,166 @@
// Copyright (C) 2019-2024, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package meter
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/require"
)
var (
halflife = time.Second
meters = []struct {
name string
factory Factory
}{
{
name: "continuous",
factory: ContinuousFactory{},
},
}
meterTests = []struct {
name string
test func(*testing.T, Factory)
}{
{
name: "new",
test: NewTest,
},
{
name: "standard usage",
test: StandardUsageTest,
},
{
name: "time travel",
test: TimeTravelTest,
},
}
)
func TestMeters(t *testing.T) {
for _, s := range meters {
for _, test := range meterTests {
t.Run(fmt.Sprintf("meter %s test %s", s.name, test.name), func(t *testing.T) {
test.test(t, s.factory)
})
}
}
}
func NewTest(t *testing.T, factory Factory) {
require.NotNil(t, factory.New(halflife))
}
func TimeTravelTest(t *testing.T, factory Factory) {
require := require.New(t)
m := factory.New(halflife)
now := time.Date(1, 2, 3, 4, 5, 6, 7, time.UTC)
m.Inc(now, 1)
now = now.Add(halflife - 1)
delta := 0.0001
require.InDelta(.5, m.Read(now), delta)
m.Dec(now, 1)
now = now.Add(-halflife)
require.InDelta(.5, m.Read(now), delta)
m.Inc(now, 1)
now = now.Add(halflife / 2)
require.InDelta(.5, m.Read(now), delta)
}
func StandardUsageTest(t *testing.T, factory Factory) {
require := require.New(t)
m := factory.New(halflife)
now := time.Date(1, 2, 3, 4, 5, 6, 7, time.UTC)
m.Inc(now, 1)
now = now.Add(halflife - 1)
delta := 0.0001
require.InDelta(.5, m.Read(now), delta)
m.Inc(now, 1)
require.InDelta(.5, m.Read(now), delta)
m.Dec(now, 1)
require.InDelta(.5, m.Read(now), delta)
m.Dec(now, 1)
require.InDelta(.5, m.Read(now), delta)
now = now.Add(halflife)
require.InDelta(.25, m.Read(now), delta)
m.Inc(now, 1)
now = now.Add(halflife)
require.InDelta(.625, m.Read(now), delta)
now = now.Add(34 * halflife)
require.InDelta(1, m.Read(now), delta)
m.Dec(now, 1)
now = now.Add(34 * halflife)
require.InDelta(0, m.Read(now), delta)
m.Inc(now, 1)
now = now.Add(2 * halflife)
require.InDelta(.75, m.Read(now), delta)
// Second start
m.Inc(now, 1)
now = now.Add(34 * halflife)
require.InDelta(2, m.Read(now), delta)
// Stop the second CPU
m.Dec(now, 1)
now = now.Add(34 * halflife)
require.InDelta(1, m.Read(now), delta)
}
func TestTimeUntil(t *testing.T) {
require := require.New(t)
halflife := 5 * time.Second
f := ContinuousFactory{}
m := f.New(halflife)
now := time.Now()
// Start the meter
m.Inc(now, 1)
// One halflife passes; stop the meter
now = now.Add(halflife)
m.Dec(now, 1)
// Read the current value
currentVal := m.Read(now)
// Suppose we want to wait for the value to be
// a third of its current value
desiredVal := currentVal / 3
// See when that should happen
timeUntilDesiredVal := m.TimeUntil(now, desiredVal)
// Get the actual value at that time
now = now.Add(timeUntilDesiredVal)
actualVal := m.Read(now)
// Make sure the actual/expected are close
require.InDelta(desiredVal, actualVal, .00001)
// Make sure TimeUntil returns the zero duration if
// the value provided >= the current value
require.Zero(m.TimeUntil(now, actualVal))
require.Zero(m.TimeUntil(now, actualVal+.1))
}
+76
View File
@@ -0,0 +1,76 @@
// Copyright (C) 2019-2024, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package math
import (
"errors"
"math"
"golang.org/x/exp/constraints"
"github.com/luxfi/node/utils"
)
var (
ErrOverflow = errors.New("overflow")
ErrUnderflow = errors.New("underflow")
)
// Add64 returns:
// 1) a + b
// 2) If there is overflow, an error
//
// Note that we don't have a generic Add function because checking for
// an overflow requires knowing the max size of a given type, which we
// don't know if we're adding generic types.
func Add64(a, b uint64) (uint64, error) {
if a > math.MaxUint64-b {
return 0, ErrOverflow
}
return a + b, nil
}
// Sub returns:
// 1) a - b
// 2) If there is underflow, an error
func Sub[T constraints.Unsigned](a, b T) (T, error) {
if a < b {
return utils.Zero[T](), ErrUnderflow
}
return a - b, nil
}
// Mul64 returns:
// 1) a * b
// 2) If there is overflow, an error
//
// Note that we don't have a generic Mul function because checking for
// an overflow requires knowing the max size of a given type, which we
// don't know if we're adding generic types.
func Mul64(a, b uint64) (uint64, error) {
if b != 0 && a > math.MaxUint64/b {
return 0, ErrOverflow
}
return a * b, nil
}
func AbsDiff[T constraints.Unsigned](a, b T) T {
return max(a, b) - min(a, b)
}
// Min returns the minimum of two uint64 values
func Min(a, b uint64) uint64 {
if a < b {
return a
}
return b
}
// Max returns the maximum of two uint64 values
func Max(a, b uint64) uint64 {
if a > b {
return a
}
return b
}
+106
View File
@@ -0,0 +1,106 @@
// Copyright (C) 2019-2024, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package math
import (
"math"
"testing"
"github.com/stretchr/testify/require"
)
const maxUint64 uint64 = math.MaxUint64
func TestAdd64(t *testing.T) {
require := require.New(t)
sum, err := Add64(0, maxUint64)
require.NoError(err)
require.Equal(maxUint64, sum)
sum, err = Add64(maxUint64, 0)
require.NoError(err)
require.Equal(maxUint64, sum)
sum, err = Add64(uint64(1<<62), uint64(1<<62))
require.NoError(err)
require.Equal(uint64(1<<63), sum)
_, err = Add64(1, maxUint64)
require.ErrorIs(err, ErrOverflow)
_, err = Add64(maxUint64, 1)
require.ErrorIs(err, ErrOverflow)
_, err = Add64(maxUint64, maxUint64)
require.ErrorIs(err, ErrOverflow)
}
func TestSub(t *testing.T) {
require := require.New(t)
got, err := Sub(uint64(2), uint64(1))
require.NoError(err)
require.Equal(uint64(1), got)
got, err = Sub(uint64(2), uint64(2))
require.NoError(err)
require.Zero(got)
got, err = Sub(maxUint64, maxUint64)
require.NoError(err)
require.Zero(got)
got, err = Sub(uint64(3), uint64(2))
require.NoError(err)
require.Equal(uint64(1), got)
_, err = Sub(uint64(1), uint64(2))
require.ErrorIs(err, ErrUnderflow)
_, err = Sub(maxUint64-1, maxUint64)
require.ErrorIs(err, ErrUnderflow)
}
func TestMul64(t *testing.T) {
require := require.New(t)
got, err := Mul64(0, maxUint64)
require.NoError(err)
require.Zero(got)
got, err = Mul64(maxUint64, 0)
require.NoError(err)
require.Zero(got)
got, err = Mul64(uint64(1), uint64(3))
require.NoError(err)
require.Equal(uint64(3), got)
got, err = Mul64(uint64(3), uint64(1))
require.NoError(err)
require.Equal(uint64(3), got)
got, err = Mul64(uint64(2), uint64(3))
require.NoError(err)
require.Equal(uint64(6), got)
got, err = Mul64(maxUint64, 0)
require.NoError(err)
require.Zero(got)
_, err = Mul64(maxUint64-1, 2)
require.ErrorIs(err, ErrOverflow)
}
func TestAbsDiff(t *testing.T) {
require := require.New(t)
require.Equal(maxUint64, AbsDiff(0, maxUint64))
require.Equal(maxUint64, AbsDiff(maxUint64, 0))
require.Equal(uint64(2), AbsDiff(uint64(3), uint64(1)))
require.Equal(uint64(2), AbsDiff(uint64(1), uint64(3)))
require.Zero(AbsDiff(uint64(1), uint64(1)))
require.Zero(AbsDiff(uint64(0), uint64(0)))
}
+34
View File
@@ -0,0 +1,34 @@
// Copyright (C) 2019-2024, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package math
import (
"sync"
"time"
)
type syncAverager struct {
lock sync.RWMutex
averager Averager
}
func NewSyncAverager(averager Averager) Averager {
return &syncAverager{
averager: averager,
}
}
func (a *syncAverager) Observe(value float64, currentTime time.Time) {
a.lock.Lock()
defer a.lock.Unlock()
a.averager.Observe(value, currentTime)
}
func (a *syncAverager) Read() float64 {
a.lock.RLock()
defer a.lock.RUnlock()
return a.averager.Read()
}
+28
View File
@@ -0,0 +1,28 @@
package math
// Re-export commonly used math functions from github.com/luxfi/geth/common/math
import (
"math/big"
"github.com/luxfi/geth/common/math"
)
var (
BigPow = math.BigPow
)
// BigMax returns the larger of x or y.
func BigMax(x, y *big.Int) *big.Int {
if x.Cmp(y) < 0 {
return y
}
return x
}
// BigMin returns the smaller of x or y.
func BigMin(x, y *big.Int) *big.Int {
if x.Cmp(y) > 0 {
return y
}
return x
}
+102
View File
@@ -0,0 +1,102 @@
// Copyright (C) 2019-2024, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package set
import (
"encoding/hex"
"math/big"
"math/bits"
)
// Bits is a bit-set backed by a big.Int
// Holds values ranging from [0, INT_MAX] (arch-dependent)
// Trying to use negative values will result in a panic.
// This implementation is NOT thread-safe.
type Bits struct {
bits *big.Int
}
// NewBits returns a new instance of Bits with [bits] set to 1.
//
// Invariants:
// 1. Negative bits will cause a panic.
// 2. Duplicate bits are allowed but will cause a no-op.
func NewBits(bits ...int) Bits {
b := Bits{new(big.Int)}
for _, bit := range bits {
b.Add(bit)
}
return b
}
// Add sets the [i]'th bit to 1
func (b Bits) Add(i int) {
b.bits.SetBit(b.bits, i, 1)
}
// Union performs the set union with another set.
// This adds all elements in [other] to [b]
func (b Bits) Union(other Bits) {
b.bits.Or(b.bits, other.bits)
}
// Intersection performs the set intersection with another set
// This sets [b] to include only elements in both [b] and [other]
func (b Bits) Intersection(other Bits) {
b.bits.And(b.bits, other.bits)
}
// Difference removes all the elements in [other] from this set
func (b Bits) Difference(other Bits) {
b.bits.AndNot(b.bits, other.bits)
}
// Remove sets the [i]'th bit to 0
func (b Bits) Remove(i int) {
b.bits.SetBit(b.bits, i, 0)
}
// Clear empties out the bitset
func (b Bits) Clear() {
b.bits.SetUint64(0)
}
// Contains returns true if the [i]'th bit is 1, and false otherwise
func (b Bits) Contains(i int) bool {
return b.bits.Bit(i) == 1
}
// BitLen returns the bit length of this bitset
func (b Bits) BitLen() int {
return b.bits.BitLen()
}
// Len returns the amount of 1's in the bitset
//
// This is typically referred to as the "Hamming Weight"
// of a set of bits.
func (b Bits) Len() int {
result := 0
for _, word := range b.bits.Bits() {
result += bits.OnesCount(uint(word))
}
return result
}
// Returns the byte representation of this bitset
func (b Bits) Bytes() []byte {
return b.bits.Bytes()
}
// Inverse of Bits.Bytes()
func BitsFromBytes(bytes []byte) Bits {
return Bits{
bits: new(big.Int).SetBytes(bytes),
}
}
// String returns the hex representation of this bitset
func (b Bits) String() string {
return hex.EncodeToString(b.bits.Bytes())
}
+57
View File
@@ -0,0 +1,57 @@
// Copyright (C) 2019-2024, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package set
import (
"fmt"
"math/bits"
)
// Bits64 is a set that can contain uints in the range [0, 64). All functions
// are O(1). The zero value is the empty set.
type Bits64 uint64
// Add [i] to the set of ints
func (b *Bits64) Add(i uint) {
*b |= 1 << i
}
// Union adds all the elements in [s] to this set
func (b *Bits64) Union(s Bits64) {
*b |= s
}
// Intersection takes the intersection of [s] with this set
func (b *Bits64) Intersection(s Bits64) {
*b &= s
}
// Difference removes all the elements in [s] from this set
func (b *Bits64) Difference(s Bits64) {
*b &^= s
}
// Remove [i] from the set of ints
func (b *Bits64) Remove(i uint) {
*b &^= 1 << i
}
// Clear removes all elements from this set
func (b *Bits64) Clear() {
*b = 0
}
// Contains returns true if [i] was previously added to this set
func (b Bits64) Contains(i uint) bool {
return b&(1<<i) != 0
}
// Len returns the number of elements in this set
func (b Bits64) Len() int {
return bits.OnesCount64(uint64(b))
}
func (b Bits64) String() string {
return fmt.Sprintf("%016x", uint64(b))
}
+117
View File
@@ -0,0 +1,117 @@
// Copyright (C) 2019-2024, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package set
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestBits64(t *testing.T) {
require := require.New(t)
var bs1 Bits64
require.Empty(bs1)
bs1.Add(5)
require.Equal(1, bs1.Len())
require.True(bs1.Contains(5))
bs1.Add(10)
require.Equal(2, bs1.Len())
require.True(bs1.Contains(5))
require.True(bs1.Contains(10))
bs1.Add(10)
require.Equal(2, bs1.Len())
require.True(bs1.Contains(5))
require.True(bs1.Contains(10))
var bs2 Bits64
require.Empty(bs2)
bs2.Add(0)
require.Equal(1, bs2.Len())
require.True(bs2.Contains(0))
bs2.Union(bs1)
require.Equal(2, bs1.Len())
require.True(bs1.Contains(5))
require.True(bs1.Contains(10))
require.Equal(3, bs2.Len())
require.True(bs2.Contains(0))
require.True(bs2.Contains(5))
require.True(bs2.Contains(10))
bs1.Clear()
require.Empty(bs1)
require.Equal(3, bs2.Len())
require.True(bs2.Contains(0))
require.True(bs2.Contains(5))
require.True(bs2.Contains(10))
bs1.Add(63)
require.Equal(1, bs1.Len())
require.True(bs1.Contains(63))
bs1.Add(1)
require.Equal(2, bs1.Len())
require.True(bs1.Contains(1))
require.True(bs1.Contains(63))
bs1.Remove(63)
require.Equal(1, bs1.Len())
require.True(bs1.Contains(1))
var bs3 Bits64
require.Empty(bs3)
bs3.Add(0)
bs3.Add(2)
bs3.Add(5)
var bs4 Bits64
require.Empty(bs4)
bs4.Add(2)
bs4.Add(5)
bs3.Intersection(bs4)
require.Equal(2, bs3.Len())
require.True(bs3.Contains(2))
require.True(bs3.Contains(5))
require.Equal(2, bs4.Len())
require.True(bs4.Contains(2))
require.True(bs4.Contains(5))
var bs5 Bits64
require.Empty(bs5)
bs5.Add(7)
bs5.Add(11)
bs5.Add(9)
var bs6 Bits64
require.Empty(bs6)
bs6.Add(9)
bs6.Add(11)
bs5.Difference(bs6)
require.Equal(1, bs5.Len())
require.True(bs5.Contains(7))
require.Equal(2, bs6.Len())
require.True(bs6.Contains(9))
require.True(bs6.Contains(11))
}
func TestBits64String(t *testing.T) {
var bs Bits64
bs.Add(17)
require.Equal(t, "0000000000020000", bs.String())
}
+508
View File
@@ -0,0 +1,508 @@
// Copyright (C) 2019-2024, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package set
import (
"testing"
"github.com/stretchr/testify/require"
)
func Test_Bits_New(t *testing.T) {
tests := []struct {
name string
bits []int
length int
}{
{
name: "empty",
bits: []int{},
length: 0,
},
{
name: "populated",
bits: []int{0, 9, 99, 999, 9999},
length: 10_000,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require := require.New(t)
b := NewBits(test.bits...)
for _, bit := range test.bits {
require.True(b.Contains(bit))
}
require.Equal(test.length, b.BitLen())
})
}
}
func Test_Bits_AddRemove(t *testing.T) {
tests := []struct {
name string
toAdd []int
toRemove []int
expectedElements []int
expectedLen int
}{
{
name: "empty sets",
toAdd: []int{},
toRemove: []int{},
expectedElements: []int{}, // []
expectedLen: 0,
},
{
name: "add only",
toAdd: []int{0, 1, 2},
toRemove: []int{},
expectedElements: []int{0, 1, 2}, // [1, 1, 1]
expectedLen: 3,
},
{
name: "remove left-most",
toAdd: []int{0, 1, 2},
toRemove: []int{0},
expectedElements: []int{1, 2}, // [1, 1, 0]
expectedLen: 3,
},
{
name: "remove middle",
toAdd: []int{0, 1, 2},
toRemove: []int{1},
expectedElements: []int{2, 0}, // [1, 0, 1]
expectedLen: 3,
},
{
name: "remove right-most",
toAdd: []int{0, 1, 2},
toRemove: []int{2},
expectedElements: []int{0, 1}, // [1, 1]
expectedLen: 2,
},
{
name: "remove all",
toAdd: []int{0, 1, 2},
toRemove: []int{0, 1, 2},
expectedElements: []int{}, // [1, 1, 1]
expectedLen: 0,
},
{
name: "remove reverse-order",
toAdd: []int{0, 1, 2},
toRemove: []int{2, 1, 0},
expectedElements: []int{}, // []
expectedLen: 0,
},
{
name: "remove non-existent elements",
toAdd: []int{0, 1, 2},
toRemove: []int{3, 4, 5},
expectedElements: []int{0, 1, 2}, // [1, 1, 1]
expectedLen: 3,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require := require.New(t)
b := NewBits()
for _, add := range test.toAdd {
b.Add(add)
}
for _, remove := range test.toRemove {
b.Remove(remove)
}
for _, element := range test.expectedElements {
require.True(b.Contains(element))
}
require.Equal(test.expectedLen, b.BitLen())
})
}
}
func Test_Bits_Union(t *testing.T) {
tests := []struct {
name string
left []int
right []int
expected []int
expectedLen int
}{
{
name: "empty sets",
left: []int{},
right: []int{},
expected: []int{}, // []
expectedLen: 0,
},
{
name: "left and right are same",
left: []int{2, 1, 0},
right: []int{2, 1, 0},
expected: []int{2, 1, 0}, // [1, 1, 1]
expectedLen: 3,
},
{
name: "left and no right",
left: []int{2, 1, 0},
right: []int{},
expected: []int{2, 1, 0}, // [1, 1, 1]
expectedLen: 3,
},
{
name: "right and no left",
left: []int{},
right: []int{2, 1, 0},
expected: []int{2, 1, 0}, // [1, 1, 1]
expectedLen: 3,
},
{
name: "left and right overlap",
left: []int{2, 1},
right: []int{1, 0},
expected: []int{2, 1, 0}, // [1, 1, 1]
expectedLen: 3,
},
{
name: "left and right overlap different sizes",
left: []int{5, 3, 1},
right: []int{8, 6, 4, 2, 0},
expected: []int{8, 6, 5, 4, 3, 2, 1, 0}, // [1, 0, 1, 1, 1, 1, 1, 1, 1]
expectedLen: 9,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require := require.New(t)
b := NewBits()
for _, add := range test.left {
b.Add(add)
}
for _, add := range test.right {
b.Add(add)
}
for _, element := range test.expected {
require.True(b.Contains(element))
}
require.Equal(test.expectedLen, b.BitLen())
})
}
}
func Test_Bits_Intersection(t *testing.T) {
tests := []struct {
name string
left []int
right []int
expected []int
expectedLen int
}{
{
name: "empty sets",
left: []int{},
right: []int{},
expected: []int{}, // []
expectedLen: 0,
},
{
name: "left and right are same",
left: []int{2, 1, 0},
right: []int{2, 1, 0},
expected: []int{2, 1, 0}, // [1, 1, 1]
expectedLen: 3,
},
{
name: "left and no right",
left: []int{2, 1, 0},
right: []int{},
expected: []int{}, // []
expectedLen: 0,
},
{
name: "right and no left",
left: []int{},
right: []int{2, 1, 0},
expected: []int{}, // []
expectedLen: 0,
},
{
name: "left and right overlap",
left: []int{2, 1},
right: []int{1, 0},
expected: []int{1}, // [1, 0]
expectedLen: 2,
},
{
name: "left and right overlap different sizes",
left: []int{5, 3, 1},
right: []int{8, 6, 4, 2, 0},
expected: []int{}, // []
expectedLen: 0,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require := require.New(t)
left := NewBits()
right := NewBits()
for _, add := range test.left {
left.Add(add)
}
for _, add := range test.right {
right.Add(add)
}
left.Intersection(right)
expected := NewBits()
for _, element := range test.expected {
expected.Add(element)
}
require.ElementsMatch(left.bits.Bits(), expected.bits.Bits())
})
}
}
func Test_Bits_Difference(t *testing.T) {
tests := []struct {
name string
left []int
right []int
expected []int
expectedLen int
}{
{
name: "empty sets",
left: []int{},
right: []int{},
expected: []int{}, // []
expectedLen: 0,
},
{
name: "left and right are same",
left: []int{2, 1, 0},
right: []int{2, 1, 0},
expected: []int{}, // []
expectedLen: 0,
},
{
name: "left and no right",
left: []int{2, 1, 0},
right: []int{},
expected: []int{2, 1, 0}, // [1, 1, 1]
expectedLen: 3,
},
{
name: "right and no left",
left: []int{},
right: []int{2, 1, 0},
expected: []int{}, // []
expectedLen: 3,
},
{
name: "left and right overlap",
left: []int{2, 1},
right: []int{1, 0},
expected: []int{2}, // [1, 0, 0]
expectedLen: 3,
},
{
name: "left and right overlap different sizes",
left: []int{5, 3, 1},
right: []int{8, 6, 4, 2, 0},
expected: []int{5, 3, 1}, // [1, 0, 1, 0, 1, 0]
expectedLen: 6,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require := require.New(t)
left := NewBits()
right := NewBits()
for _, add := range test.left {
left.Add(add)
}
for _, add := range test.right {
right.Add(add)
}
left.Difference(right)
expected := NewBits()
for _, element := range test.expected {
expected.Add(element)
}
require.ElementsMatch(left.bits.Bits(), expected.bits.Bits())
})
}
}
func Test_Bits_Clear(t *testing.T) {
tests := []struct {
name string
bitset []int
}{
{
name: "empty",
bitset: []int{}, // []
},
{
name: "populated",
bitset: []int{5, 4, 3, 2, 1}, // [1, 1, 1, 1, 1]
},
{
name: "populated - big",
bitset: []int{255}, // [1, 0...]
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require := require.New(t)
b := NewBits()
for bit := range test.bitset {
b.Add(bit)
}
b.Clear()
require.Zero(b.BitLen())
})
}
}
func Test_Bits_String(t *testing.T) {
tests := []struct {
name string
bitset []int
expected string
}{
{
name: "empty",
bitset: []int{},
expected: "", // []
},
{
name: "populated",
bitset: []int{7, 6, 5, 4, 3, 2, 1, 0}, // [1, 1, 1, 1, 1, 1, 1, 1]
expected: "ff",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require := require.New(t)
b := NewBits()
for _, bit := range test.bitset {
b.Add(bit)
}
require.Equal(test.expected, b.String())
})
}
}
func Test_Bits_Len(t *testing.T) {
tests := []struct {
name string
bitset []int
expected int
}{
{
name: "empty",
bitset: []int{}, // []
expected: 0,
},
{
name: "populated - more than one word",
bitset: []int{255}, // [1, 0...]
expected: 1,
},
{
name: "populated - all ones",
bitset: []int{5, 4, 3, 2, 1, 0}, // [1, 1, 1, 1, 1, 1]
expected: 6,
},
{
name: "populated - trailing zeroes",
bitset: []int{5, 4, 3}, // [1, 1, 1, 0, 0, 0]
expected: 3,
},
{
name: "populated - interwoven 1",
bitset: []int{4, 2, 0}, // [1, 0, 1, 0, 1]
expected: 3,
},
{
name: "populated - interwoven 2",
bitset: []int{3, 1}, // [1, 0, 1, 0]
expected: 2,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require := require.New(t)
b := NewBits()
for _, bit := range test.bitset {
b.Add(bit)
}
require.Equal(test.expected, b.Len())
})
}
}
func Test_Bits_Bytes(t *testing.T) {
type test struct {
name string
elts []int
}
tests := []test{
{
name: "empty",
elts: []int{},
},
{
name: "single; element > 63",
elts: []int{1337},
},
{
name: "multiple",
elts: []int{1, 2, 3},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require := require.New(t)
b := NewBits(tt.elts...)
bytes := b.Bytes()
fromBytes := BitsFromBytes(bytes)
require.Equal(len(tt.elts), fromBytes.Len())
for _, elt := range tt.elts {
require.True(fromBytes.Contains(elt))
}
})
}
}
+235
View File
@@ -0,0 +1,235 @@
// Copyright (C) 2019-2024, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package set
import (
"bytes"
"encoding/json"
"slices"
"github.com/luxfi/node/utils"
"github.com/luxfi/node/utils/sampler"
"github.com/luxfi/node/utils/wrappers"
avajson "github.com/luxfi/node/utils/json"
)
var _ json.Marshaler = (*Set[int])(nil)
// SampleableSet is a set of elements that supports sampling.
type SampleableSet[T comparable] struct {
// indices maps the element in the set to the index that it appears in
// elements.
indices map[T]int
elements []T
}
// OfSampleable returns a Set initialized with [elts]
func OfSampleable[T comparable](elts ...T) SampleableSet[T] {
s := NewSampleableSet[T](len(elts))
s.Add(elts...)
return s
}
// Return a new sampleable set with initial capacity [size].
// More or less than [size] elements can be added to this set.
// Using NewSampleableSet() rather than SampleableSet[T]{} is just an
// optimization that can be used if you know how many elements will be put in
// this set.
func NewSampleableSet[T comparable](size int) SampleableSet[T] {
if size < 0 {
return SampleableSet[T]{}
}
return SampleableSet[T]{
indices: make(map[T]int, size),
elements: make([]T, 0, size),
}
}
// Add all the elements to this set.
// If the element is already in the set, nothing happens.
func (s *SampleableSet[T]) Add(elements ...T) {
s.resize(2 * len(elements))
for _, e := range elements {
s.add(e)
}
}
// Union adds all the elements from the provided set to this set.
func (s *SampleableSet[T]) Union(set SampleableSet[T]) {
s.resize(2 * set.Len())
for _, e := range set.elements {
s.add(e)
}
}
// Difference removes all the elements in [set] from [s].
func (s *SampleableSet[T]) Difference(set SampleableSet[T]) {
for _, e := range set.elements {
s.remove(e)
}
}
// Contains returns true iff the set contains this element.
func (s SampleableSet[T]) Contains(e T) bool {
_, contains := s.indices[e]
return contains
}
// Overlaps returns true if the intersection of the set is non-empty
func (s SampleableSet[T]) Overlaps(big SampleableSet[T]) bool {
small := s
if small.Len() > big.Len() {
small, big = big, small
}
for _, e := range small.elements {
if _, ok := big.indices[e]; ok {
return true
}
}
return false
}
// Len returns the number of elements in this set.
func (s SampleableSet[_]) Len() int {
return len(s.elements)
}
// Remove all the given elements from this set.
// If an element isn't in the set, it's ignored.
func (s *SampleableSet[T]) Remove(elements ...T) {
for _, e := range elements {
s.remove(e)
}
}
// Clear empties this set
func (s *SampleableSet[T]) Clear() {
clear(s.indices)
for i := range s.elements {
s.elements[i] = utils.Zero[T]()
}
s.elements = s.elements[:0]
}
// List converts this set into a list
func (s SampleableSet[T]) List() []T {
return slices.Clone(s.elements)
}
// Equals returns true if the sets contain the same elements
func (s SampleableSet[T]) Equals(other SampleableSet[T]) bool {
if len(s.indices) != len(other.indices) {
return false
}
for k := range s.indices {
if _, ok := other.indices[k]; !ok {
return false
}
}
return true
}
func (s SampleableSet[T]) Sample(numToSample int) []T {
if numToSample <= 0 {
return nil
}
uniform := sampler.NewUniform()
uniform.Initialize(uint64(len(s.elements)))
indices, _ := uniform.Sample(min(len(s.elements), numToSample))
elements := make([]T, len(indices))
for i, index := range indices {
elements[i] = s.elements[index]
}
return elements
}
func (s *SampleableSet[T]) UnmarshalJSON(b []byte) error {
str := string(b)
if str == avajson.Null {
return nil
}
var elements []T
if err := json.Unmarshal(b, &elements); err != nil {
return err
}
s.Clear()
s.Add(elements...)
return nil
}
func (s *SampleableSet[_]) MarshalJSON() ([]byte, error) {
var (
elementBytes = make([][]byte, len(s.elements))
err error
)
for i, e := range s.elements {
elementBytes[i], err = json.Marshal(e)
if err != nil {
return nil, err
}
}
// Sort for determinism
slices.SortFunc(elementBytes, bytes.Compare)
// Build the JSON
var (
jsonBuf = bytes.Buffer{}
errs = wrappers.Errs{}
)
_, err = jsonBuf.WriteString("[")
errs.Add(err)
for i, elt := range elementBytes {
_, err := jsonBuf.Write(elt)
errs.Add(err)
if i != len(elementBytes)-1 {
_, err := jsonBuf.WriteString(",")
errs.Add(err)
}
}
_, err = jsonBuf.WriteString("]")
errs.Add(err)
return jsonBuf.Bytes(), errs.Err
}
func (s *SampleableSet[T]) resize(size int) {
if s.elements == nil {
if minSetSize > size {
size = minSetSize
}
s.indices = make(map[T]int, size)
}
}
func (s *SampleableSet[T]) add(e T) {
_, ok := s.indices[e]
if ok {
return
}
s.indices[e] = len(s.elements)
s.elements = append(s.elements, e)
}
func (s *SampleableSet[T]) remove(e T) {
indexToRemove, ok := s.indices[e]
if !ok {
return
}
lastIndex := len(s.elements) - 1
if indexToRemove != lastIndex {
lastElement := s.elements[lastIndex]
s.indices[lastElement] = indexToRemove
s.elements[indexToRemove] = lastElement
}
delete(s.indices, e)
s.elements[lastIndex] = utils.Zero[T]()
s.elements = s.elements[:lastIndex]
}
+174
View File
@@ -0,0 +1,174 @@
// Copyright (C) 2019-2024, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package set
import (
"encoding/json"
"fmt"
"testing"
"github.com/stretchr/testify/require"
)
func TestSampleableSet(t *testing.T) {
require := require.New(t)
id1 := 1
s := SampleableSet[int]{}
s.Add(id1)
require.True(s.Contains(id1))
s.Remove(id1)
require.False(s.Contains(id1))
s.Add(id1)
require.True(s.Contains(id1))
require.Len(s.List(), 1)
require.Equal(id1, s.List()[0])
s.Clear()
require.False(s.Contains(id1))
s.Add(id1)
s2 := SampleableSet[int]{}
require.False(s.Overlaps(s2))
s2.Union(s)
require.True(s2.Contains(id1))
require.True(s.Overlaps(s2))
s2.Difference(s)
require.False(s2.Contains(id1))
require.False(s.Overlaps(s2))
}
func TestSampleableSetClear(t *testing.T) {
require := require.New(t)
set := SampleableSet[int]{}
for i := 0; i < 25; i++ {
set.Add(i)
}
set.Clear()
require.Zero(set.Len())
set.Add(1337)
require.Equal(1, set.Len())
}
func TestSampleableSetMarshalJSON(t *testing.T) {
require := require.New(t)
set := SampleableSet[int]{}
{
asJSON, err := set.MarshalJSON()
require.NoError(err)
require.Equal("[]", string(asJSON))
}
id1, id2 := 1, 2
id1JSON, err := json.Marshal(id1)
require.NoError(err)
id2JSON, err := json.Marshal(id2)
require.NoError(err)
set.Add(id1)
{
asJSON, err := set.MarshalJSON()
require.NoError(err)
require.Equal(fmt.Sprintf("[%s]", string(id1JSON)), string(asJSON))
}
set.Add(id2)
{
asJSON, err := set.MarshalJSON()
require.NoError(err)
require.Equal(fmt.Sprintf("[%s,%s]", string(id1JSON), string(id2JSON)), string(asJSON))
}
}
func TestSampleableSetUnmarshalJSON(t *testing.T) {
require := require.New(t)
set := SampleableSet[int]{}
{
require.NoError(set.UnmarshalJSON([]byte("[]")))
require.Zero(set.Len())
}
id1, id2 := 1, 2
id1JSON, err := json.Marshal(id1)
require.NoError(err)
id2JSON, err := json.Marshal(id2)
require.NoError(err)
{
require.NoError(set.UnmarshalJSON([]byte(fmt.Sprintf("[%s]", string(id1JSON)))))
require.Equal(1, set.Len())
require.True(set.Contains(id1))
}
{
require.NoError(set.UnmarshalJSON([]byte(fmt.Sprintf("[%s,%s]", string(id1JSON), string(id2JSON)))))
require.Equal(2, set.Len())
require.True(set.Contains(id1))
require.True(set.Contains(id2))
}
{
require.NoError(set.UnmarshalJSON([]byte(fmt.Sprintf("[%d,%d,%d]", 3, 4, 5))))
require.Equal(3, set.Len())
require.True(set.Contains(3))
require.True(set.Contains(4))
require.True(set.Contains(5))
}
{
require.NoError(set.UnmarshalJSON([]byte(fmt.Sprintf("[%d,%d,%d, %d]", 3, 4, 5, 3))))
require.Equal(3, set.Len())
require.True(set.Contains(3))
require.True(set.Contains(4))
require.True(set.Contains(5))
}
{
set1 := SampleableSet[int]{}
set2 := SampleableSet[int]{}
require.NoError(set1.UnmarshalJSON([]byte(fmt.Sprintf("[%s,%s]", string(id1JSON), string(id2JSON)))))
require.NoError(set2.UnmarshalJSON([]byte(fmt.Sprintf("[%s,%s]", string(id2JSON), string(id1JSON)))))
require.True(set1.Equals(set2))
}
}
func TestOfSampleable(t *testing.T) {
tests := []struct {
name string
elements []int
expected []int
}{
{
name: "nil",
elements: nil,
expected: []int{},
},
{
name: "empty",
elements: []int{},
expected: []int{},
},
{
name: "unique elements",
elements: []int{1, 2, 3},
expected: []int{1, 2, 3},
},
{
name: "duplicate elements",
elements: []int{1, 2, 3, 1, 2, 3},
expected: []int{1, 2, 3},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require := require.New(t)
s := OfSampleable(tt.elements...)
require.Equal(len(tt.expected), s.Len())
for _, expected := range tt.expected {
require.True(s.Contains(expected))
}
})
}
}
+194
View File
@@ -0,0 +1,194 @@
// Copyright (C) 2019-2024, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package set
import (
"bytes"
"encoding/json"
"slices"
"golang.org/x/exp/maps"
"github.com/luxfi/node/utils"
"github.com/luxfi/node/utils/wrappers"
avajson "github.com/luxfi/node/utils/json"
)
// The minimum capacity of a set
const minSetSize = 16
var _ json.Marshaler = (*Set[int])(nil)
// Set is a set of elements.
type Set[T comparable] map[T]struct{}
// Of returns a Set initialized with [elts]
func Of[T comparable](elts ...T) Set[T] {
s := NewSet[T](len(elts))
s.Add(elts...)
return s
}
// Return a new set with initial capacity [size].
// More or less than [size] elements can be added to this set.
// Using NewSet() rather than Set[T]{} is just an optimization that can
// be used if you know how many elements will be put in this set.
func NewSet[T comparable](size int) Set[T] {
if size < 0 {
return Set[T]{}
}
return make(map[T]struct{}, size)
}
func (s *Set[T]) resize(size int) {
if *s == nil {
if minSetSize > size {
size = minSetSize
}
*s = make(map[T]struct{}, size)
}
}
// Add all the elements to this set.
// If the element is already in the set, nothing happens.
func (s *Set[T]) Add(elts ...T) {
s.resize(2 * len(elts))
for _, elt := range elts {
(*s)[elt] = struct{}{}
}
}
// Union adds all the elements from the provided set to this set.
func (s *Set[T]) Union(set Set[T]) {
s.resize(2 * set.Len())
for elt := range set {
(*s)[elt] = struct{}{}
}
}
// Difference removes all the elements in [set] from [s].
func (s *Set[T]) Difference(set Set[T]) {
for elt := range set {
delete(*s, elt)
}
}
// Contains returns true iff the set contains this element.
func (s *Set[T]) Contains(elt T) bool {
_, contains := (*s)[elt]
return contains
}
// Overlaps returns true if the intersection of the set is non-empty
func (s *Set[T]) Overlaps(big Set[T]) bool {
small := *s
if small.Len() > big.Len() {
small, big = big, small
}
for elt := range small {
if _, ok := big[elt]; ok {
return true
}
}
return false
}
// Len returns the number of elements in this set.
func (s Set[_]) Len() int {
return len(s)
}
// Remove all the given elements from this set.
// If an element isn't in the set, it's ignored.
func (s *Set[T]) Remove(elts ...T) {
for _, elt := range elts {
delete(*s, elt)
}
}
// Clear empties this set
func (s *Set[_]) Clear() {
clear(*s)
}
// List converts this set into a list
func (s Set[T]) List() []T {
return maps.Keys(s)
}
// Equals returns true if the sets contain the same elements
func (s Set[T]) Equals(other Set[T]) bool {
return maps.Equal(s, other)
}
// Removes and returns an element.
// If the set is empty, does nothing and returns false.
func (s *Set[T]) Pop() (T, bool) {
for elt := range *s {
delete(*s, elt)
return elt, true
}
return utils.Zero[T](), false
}
func (s *Set[T]) UnmarshalJSON(b []byte) error {
str := string(b)
if str == avajson.Null {
return nil
}
var elts []T
if err := json.Unmarshal(b, &elts); err != nil {
return err
}
s.Clear()
s.Add(elts...)
return nil
}
func (s Set[_]) MarshalJSON() ([]byte, error) {
var (
eltBytes = make([][]byte, len(s))
i int
err error
)
for elt := range s {
eltBytes[i], err = json.Marshal(elt)
if err != nil {
return nil, err
}
i++
}
// Sort for determinism
slices.SortFunc(eltBytes, bytes.Compare)
// Build the JSON
var (
jsonBuf = bytes.Buffer{}
errs = wrappers.Errs{}
)
_, err = jsonBuf.WriteString("[")
errs.Add(err)
for i, elt := range eltBytes {
_, err := jsonBuf.Write(elt)
errs.Add(err)
if i != len(eltBytes)-1 {
_, err := jsonBuf.WriteString(",")
errs.Add(err)
}
}
_, err = jsonBuf.WriteString("]")
errs.Add(err)
return jsonBuf.Bytes(), errs.Err
}
// Returns a random element. If the set is empty, returns false
func (s *Set[T]) Peek() (T, bool) {
for elt := range *s {
return elt, true
}
return utils.Zero[T](), false
}
+39
View File
@@ -0,0 +1,39 @@
// Copyright (C) 2019-2024, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package set
import (
"strconv"
"testing"
)
func BenchmarkSetList(b *testing.B) {
sizes := []int{5, 25, 100, 100_000} // Test with various sizes
for size := range sizes {
b.Run(strconv.Itoa(size), func(b *testing.B) {
set := Set[int]{}
for i := 0; i < size; i++ {
set.Add(i)
}
b.ResetTimer()
for n := 0; n < b.N; n++ {
set.List()
}
})
}
}
func BenchmarkSetClear(b *testing.B) {
for _, numElts := range []int{10, 25, 50, 100, 250, 500, 1000} {
b.Run(strconv.Itoa(numElts), func(b *testing.B) {
set := NewSet[int](numElts)
for n := 0; n < b.N; n++ {
for i := 0; i < numElts; i++ {
set.Add(i)
}
set.Clear()
}
})
}
}
+228
View File
@@ -0,0 +1,228 @@
// Copyright (C) 2019-2024, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package set
import (
"encoding/json"
"fmt"
"testing"
"github.com/stretchr/testify/require"
)
func TestSet(t *testing.T) {
require := require.New(t)
id1 := 1
s := Set[int]{id1: struct{}{}}
s.Add(id1)
require.True(s.Contains(id1))
s.Remove(id1)
require.False(s.Contains(id1))
s.Add(id1)
require.True(s.Contains(id1))
require.Len(s.List(), 1)
require.Equal(id1, s.List()[0])
s.Clear()
require.False(s.Contains(id1))
s.Add(id1)
s2 := Set[int]{}
require.False(s.Overlaps(s2))
s2.Union(s)
require.True(s2.Contains(id1))
require.True(s.Overlaps(s2))
s2.Difference(s)
require.False(s2.Contains(id1))
require.False(s.Overlaps(s2))
}
func TestOf(t *testing.T) {
tests := []struct {
name string
elements []int
expected []int
}{
{
name: "nil",
elements: nil,
expected: []int{},
},
{
name: "empty",
elements: []int{},
expected: []int{},
},
{
name: "unique elements",
elements: []int{1, 2, 3},
expected: []int{1, 2, 3},
},
{
name: "duplicate elements",
elements: []int{1, 2, 3, 1, 2, 3},
expected: []int{1, 2, 3},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require := require.New(t)
s := Of(tt.elements...)
require.Len(s, len(tt.expected))
for _, expected := range tt.expected {
require.True(s.Contains(expected))
}
})
}
}
func TestSetClear(t *testing.T) {
require := require.New(t)
set := Set[int]{}
for i := 0; i < 25; i++ {
set.Add(i)
}
set.Clear()
require.Empty(set)
set.Add(1337)
require.Len(set, 1)
}
func TestSetPop(t *testing.T) {
require := require.New(t)
var s Set[int]
_, ok := s.Pop()
require.False(ok)
s = make(Set[int])
_, ok = s.Pop()
require.False(ok)
id1, id2 := 0, 1
s.Add(id1, id2)
got, ok := s.Pop()
require.True(ok)
require.True(got == id1 || got == id2)
require.Equal(1, s.Len())
got, ok = s.Pop()
require.True(ok)
require.True(got == id1 || got == id2)
require.Zero(s.Len())
_, ok = s.Pop()
require.False(ok)
}
func TestSetMarshalJSON(t *testing.T) {
require := require.New(t)
set := Set[int]{}
{
asJSON, err := set.MarshalJSON()
require.NoError(err)
require.Equal("[]", string(asJSON))
}
id1, id2 := 1, 2
id1JSON, err := json.Marshal(id1)
require.NoError(err)
id2JSON, err := json.Marshal(id2)
require.NoError(err)
set.Add(id1)
{
asJSON, err := set.MarshalJSON()
require.NoError(err)
require.Equal(fmt.Sprintf("[%s]", string(id1JSON)), string(asJSON))
}
set.Add(id2)
{
asJSON, err := set.MarshalJSON()
require.NoError(err)
require.Equal(fmt.Sprintf("[%s,%s]", string(id1JSON), string(id2JSON)), string(asJSON))
}
}
func TestSetUnmarshalJSON(t *testing.T) {
require := require.New(t)
set := Set[int]{}
{
require.NoError(set.UnmarshalJSON([]byte("[]")))
require.Empty(set)
}
id1, id2 := 1, 2
id1JSON, err := json.Marshal(id1)
require.NoError(err)
id2JSON, err := json.Marshal(id2)
require.NoError(err)
{
require.NoError(set.UnmarshalJSON([]byte(fmt.Sprintf("[%s]", string(id1JSON)))))
require.Len(set, 1)
require.Contains(set, id1)
}
{
require.NoError(set.UnmarshalJSON([]byte(fmt.Sprintf("[%s,%s]", string(id1JSON), string(id2JSON)))))
require.Len(set, 2)
require.Contains(set, id1)
require.Contains(set, id2)
}
{
require.NoError(set.UnmarshalJSON([]byte(fmt.Sprintf("[%d,%d,%d]", 3, 4, 5))))
require.Len(set, 3)
require.Contains(set, 3)
require.Contains(set, 4)
require.Contains(set, 5)
}
{
require.NoError(set.UnmarshalJSON([]byte(fmt.Sprintf("[%d,%d,%d, %d]", 3, 4, 5, 3))))
require.Len(set, 3)
require.Contains(set, 3)
require.Contains(set, 4)
require.Contains(set, 5)
}
{
set1 := Set[int]{}
set2 := Set[int]{}
require.NoError(set1.UnmarshalJSON([]byte(fmt.Sprintf("[%s,%s]", string(id1JSON), string(id2JSON)))))
require.NoError(set2.UnmarshalJSON([]byte(fmt.Sprintf("[%s,%s]", string(id2JSON), string(id1JSON)))))
require.Equal(set1, set2)
}
}
func TestSetReflectJSONMarshal(t *testing.T) {
require := require.New(t)
set := Set[int]{}
{
asJSON, err := json.Marshal(set)
require.NoError(err)
require.Equal("[]", string(asJSON))
}
id1JSON, err := json.Marshal(1)
require.NoError(err)
id2JSON, err := json.Marshal(2)
require.NoError(err)
set.Add(1)
{
asJSON, err := json.Marshal(set)
require.NoError(err)
require.Equal(fmt.Sprintf("[%s]", string(id1JSON)), string(asJSON))
}
set.Add(2)
{
asJSON, err := json.Marshal(set)
require.NoError(err)
require.Equal(fmt.Sprintf("[%s,%s]", string(id1JSON), string(id2JSON)), string(asJSON))
}
}