Skip to content

Commit 52a11ce

Browse files
committed
Tests
1 parent 55bfc33 commit 52a11ce

11 files changed

+286
-16
lines changed

.github/dependabot.yml

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
version: 2
2+
updates:
3+
# Maintain dependencies for GitHub Actions
4+
- package-ecosystem: "github-actions"
5+
directory: "/"
6+
schedule:
7+
# Check for updates to GitHub Actions every week
8+
interval: "weekly"

.github/workflows/CompatHelper.yml

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: CompatHelper
2+
3+
on:
4+
schedule:
5+
- cron: '00 * * * *'
6+
7+
jobs:
8+
CompatHelper:
9+
runs-on: ${{ matrix.os }}
10+
strategy:
11+
matrix:
12+
julia-version: [1]
13+
julia-arch: [x86]
14+
os: [ubuntu-latest]
15+
steps:
16+
- uses: julia-actions/setup-julia@latest
17+
with:
18+
version: ${{ matrix.julia-version }}
19+
- name: Pkg.add("CompatHelper")
20+
run: julia -e 'using Pkg; Pkg.add("CompatHelper")'
21+
- name: CompatHelper.main()
22+
env:
23+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
24+
run: julia -e 'using CompatHelper; CompatHelper.main()'

.github/workflows/TagBot.yml

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: TagBot
2+
on:
3+
issue_comment:
4+
types:
5+
- created
6+
workflow_dispatch:
7+
inputs:
8+
lookback:
9+
default: 3
10+
permissions:
11+
actions: read
12+
checks: read
13+
contents: write
14+
deployments: read
15+
issues: read
16+
discussions: read
17+
packages: read
18+
pages: read
19+
pull-requests: read
20+
repository-projects: read
21+
security-events: read
22+
statuses: read
23+
jobs:
24+
TagBot:
25+
if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot'
26+
runs-on: ubuntu-latest
27+
steps:
28+
- uses: JuliaRegistries/TagBot@v1
29+
with:
30+
token: ${{ secrets.GITHUB_TOKEN }}
31+
ssh: ${{ secrets.DOCUMENTER_KEY }}

.github/workflows/ci.yml

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
name: CI
2+
on:
3+
pull_request:
4+
branches:
5+
- main
6+
push:
7+
branches:
8+
- main
9+
tags: '*'
10+
jobs:
11+
test:
12+
name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }}
13+
runs-on: ${{ matrix.os }}
14+
strategy:
15+
fail-fast: false
16+
matrix:
17+
version:
18+
- '1.10'
19+
- '1'
20+
- 'nightly'
21+
os:
22+
- ubuntu-latest
23+
arch:
24+
- x64
25+
steps:
26+
- uses: actions/checkout@v4
27+
- uses: julia-actions/setup-julia@v1
28+
with:
29+
version: ${{ matrix.version }}
30+
arch: ${{ matrix.arch }}
31+
- uses: actions/cache@v4
32+
env:
33+
cache-name: cache-artifacts
34+
with:
35+
path: ~/.julia/artifacts
36+
key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }}
37+
restore-keys: |
38+
${{ runner.os }}-test-${{ env.cache-name }}-
39+
${{ runner.os }}-test-
40+
${{ runner.os }}-
41+
- uses: julia-actions/julia-buildpkg@v1
42+
- uses: julia-actions/julia-runtest@v1
43+
- uses: julia-actions/julia-processcoverage@v1
44+
- uses: codecov/codecov-action@v4
45+
with:
46+
file: lcov.info
47+
token: ${{ secrets.CODECOV_TOKEN }}
48+
docs:
49+
name: Documentation
50+
runs-on: ubuntu-latest
51+
steps:
52+
- uses: actions/checkout@v4
53+
- uses: julia-actions/setup-julia@v1
54+
with:
55+
version: '1'
56+
- run: |
57+
julia --project=./docs -e '
58+
using Pkg
59+
Pkg.develop(PackageSpec(path=pwd()))
60+
Pkg.instantiate()'
61+
- run: |
62+
julia --project=./docs -e '
63+
using Documenter: doctest
64+
using DynamicSampling
65+
doctest(DynamicSampling)'
66+
- run: julia -t 2 --project=./docs ./docs/make.jl
67+
env:
68+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
69+
DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }}

.github/workflows/doccleanup.yml

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Doc Preview Cleanup
2+
3+
on:
4+
pull_request:
5+
types: [closed]
6+
7+
jobs:
8+
doc-preview-cleanup:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- name: Checkout gh-pages branch
12+
uses: actions/checkout@v4
13+
with:
14+
ref: gh-pages
15+
16+
- name: Delete preview and history
17+
run: |
18+
git config user.name "Documenter.jl"
19+
git config user.email "[email protected]"
20+
git rm -rf "previews/PR$PRNUM"
21+
git commit -m "delete preview"
22+
git branch gh-pages-new $(echo "delete history" | git commit-tree HEAD^{tree})
23+
env:
24+
PRNUM: ${{ github.event.number }}
25+
26+
- name: Push changes
27+
run: |
28+
git push --force origin gh-pages-new:gh-pages

README.md

+22-7
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,39 @@
11

22
# DynamicSampling.jl
33

4-
This package implements samplers which can be used to sample with replacement from collections
5-
of items while being able to remove and add elements from the sampler in constant time.
6-
4+
This package implements efficient samplers which can be used to sample from
5+
(weighted) indices while being able to remove and add elements from the sampler in
6+
constant time.
77

88
# Example
99

1010
```julia
1111
julia> using DynamicSampling
1212

13-
julia> sampler = DynamicSampler(100);
13+
julia> sampler = DynamicSampler();
1414

15-
julia> for i in 1:100
15+
julia> for i in 1:10
1616
push!(sampler, (i, Float64(i)))
1717
end
1818

1919
julia> rand(sampler)
20-
46
20+
7
2121

22-
julia> deleteat!(sampler, 10);
22+
julia> deleteat!(sampler, 7)
23+
DynamicSampler(indices = [1, 2, 3, 4, 5, 6, 8, 9, 10], weights = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 8.0, 9.0, 10.0])
2324
```
2425

26+
Importantly using `deleteat!` as above will incur a non-constant overhead but
27+
if you happen to require to remove from the sampler already samples elements
28+
you can use instead
29+
30+
```julia
31+
julia> i = rand(sampler; info=true)
32+
IndexInfo(idx = 9, weight = 9.0)
33+
34+
julia> deleteat!(sampler, i)
35+
DynamicSampler(indices = [1, 2, 3, 4, 5, 6, 8, 10], weights = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 8.0, 10.0])
36+
```
37+
38+
which will be instead a `O(1)` operation.
39+

docs/make.jl

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
2+
using Documenter
3+
using DynamicSampling
4+
5+
println("Documentation Build")
6+
makedocs(
7+
modules = [DynamicSampling],
8+
sitename = "DynamicSampling.jl",
9+
pages = [
10+
],
11+
warnonly = [:doctest, :missing_docs, :cross_references],
12+
)
13+
14+
@info "Deploying Documentation"
15+
CI = get(ENV, "CI", nothing) == "true" || get(ENV, "GITHUB_TOKEN", nothing) !== nothing
16+
if CI
17+
deploydocs(
18+
repo = "github.com/JuliaDynamics/DynamicSampling.jl.git",
19+
target = "build",
20+
push_preview = true,
21+
devbranch = "main",
22+
)
23+
end
24+
println("Finished boulding and deploying docs.")

src/DynamicSampling.jl

+17-8
Original file line numberDiff line numberDiff line change
@@ -28,22 +28,22 @@ function DynamicSampler(rng)
2828
level_max = Float64[0.0]
2929
level_inds = Int[]
3030
return DynamicSampler(rng, Ref(0), Ref(totweight), weights, level_weights,
31-
level_buckets, level_max, level_inds)
31+
level_buckets, level_max, level_inds)
3232
end
3333

34-
struct DynamicIndex
34+
struct IndexInfo
3535
idx::Int
3636
weight::Float64
3737
level::Int
3838
idx_in_level::Int
3939
end
40-
DynamicIndex(idx, weight) = DynamicIndex(idx, weight, 0, 0)
40+
IndexInfo(idx, weight) = IndexInfo(idx, weight, 0, 0)
4141

4242
Base.sizehint!(s::DynamicSampler, N) = resize_w!(s, N)
4343

4444
function Base.push!(S::DynamicSampler, e::Tuple)
4545
idx, weight = e
46-
resize_w!(s, idx)
46+
resize_w!(S, idx)
4747
S.weights[idx] != 0.0 && error()
4848
S.totweight[] += weight
4949
S.totvalues[] += 1
@@ -53,7 +53,7 @@ function Base.push!(S::DynamicSampler, e::Tuple)
5353
S.level_max[level] = max(S.level_max[level], weight)
5454
S.level_weights[level] += weight
5555
push!(S.level_buckets[level], idx)
56-
S.weights[idx] = weight
56+
S.weights[idx] = weight
5757
return S
5858
end
5959

@@ -117,7 +117,7 @@ function Base.rand(S::DynamicSampler; info = false)
117117
weight = S.weights[idx]
118118
rand(S.rng) * level_max <= weight && break
119119
end
120-
return info == false ? idx : DynamicIndex(idx, weight, level, idx_in_level)
120+
return info == false ? idx : IndexInfo(idx, weight, level, idx_in_level)
121121
end
122122

123123
function Base.deleteat!(S::DynamicSampler, idx)
@@ -128,7 +128,7 @@ function Base.deleteat!(S::DynamicSampler, idx)
128128
_deleteat!(S, idx, weight, level, idx_in_level)
129129
return S
130130
end
131-
function Base.deleteat!(S::DynamicSampler, e::DynamicIndex)
131+
function Base.deleteat!(S::DynamicSampler, e::IndexInfo)
132132
idx, weight, level, idx_in_level = e.idx, e.weight, e.level, e.idx_in_level
133133
_deleteat!(S, idx, weight, level, idx_in_level)
134134
return S
@@ -148,6 +148,15 @@ Base.isempty(S::DynamicSampler) = S.totvalues[] == 0
148148

149149
allvalues(s::DynamicSampler) = reduce(vcat, s.level_buckets)
150150

151+
function Base.show(io::IO, mime::MIME"text/plain", s::DynamicSampler)
152+
inds = allvalues(s)
153+
print("DynamicSampler(indices = $(inds), weights = $(s.weights[inds]))")
154+
end
155+
156+
function Base.show(io::IO, mime::MIME"text/plain", di::IndexInfo)
157+
print("IndexInfo(idx = $(di.idx), weight = $(di.weight))")
158+
end
159+
151160
function resize_w!(s, N)
152161
N_curr = length(s.weights)
153162
if N > N_curr
@@ -184,4 +193,4 @@ end
184193

185194
getlevel(minlevel, weight) = ceil(Int, log2(weight)) - minlevel + 1
186195

187-
end
196+
end

test/Project.toml

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf"
55
HypothesisTests = "09f84164-cd44-5f33-b23f-e6b0d136a0d5"
66
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
77
StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3"
8+
StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91"
89
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
910

1011
[compat]

test/runtests.jl

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11

22
using DynamicSampling
3-
using Test
3+
4+
using HypothesisTests
45
using Random
6+
using StableRNGs
7+
using StatsBase
8+
using Test
59

610
@testset "DynamicSampling.jl Tests" begin
11+
include("weighted_sampler_tests.jl")
712
include("benchmark_tests.jl")
813
end

test/weighted_sampler_tests.jl

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
2+
@testset "DynamicSampling.jl Tests" begin
3+
b = 100
4+
range = 1:b
5+
weights = [Float64(i) for i in range]
6+
weights2 = (Float64(i) for i in range)
7+
8+
s1 = DynamicSampler()
9+
for i in range
10+
push!(s1, (i, weights[i]))
11+
end
12+
13+
@test sort!(allvalues(s1)) == collect(range)
14+
@test all(x -> 1 <= x <= b, [rand(s1) for _ in 1:10^3])
15+
16+
deleteat!(s1, 1)
17+
deleteat!(s1, 2)
18+
19+
@test all(x -> 3 <= x <= b, [rand(s1) for _ in 1:10^3])
20+
21+
s2 = DynamicSampler()
22+
append!(s2, (range, weights2))
23+
24+
@test sort!(allvalues(s2)) == collect(range)
25+
@test all(x -> 1 <= x <= b, [rand(s2) for _ in 1:10^3])
26+
27+
e1 = rand(s2; info=true)
28+
e2 = rand(s2; info=true)
29+
30+
deleteat!(s2, e1)
31+
deleteat!(s2, e2)
32+
@test all(x -> x != e1.idx && x != e2.idx && 1 <= x <= b, [rand(s2) for _ in 1:10^3])
33+
34+
rng = StableRNG(41)
35+
36+
s3 = DynamicSampler(rng)
37+
append!(s3, (range, weights2))
38+
39+
samples_counts = countmap([rand(s3) for _ in 1:10^5])
40+
counts_est = [samples_counts[i] for i in 1:b]
41+
ps_exact = [i/((b ÷ 2)*(b+1)) for i in 1:b]
42+
43+
chisq_test = ChisqTest(counts_est, ps_exact)
44+
@test pvalue(chisq_test) > 0.05
45+
46+
for i in 1:(b ÷ 2)
47+
deleteat!(s3, i)
48+
end
49+
50+
samples_counts = countmap([rand(s3) for _ in 1:10^5])
51+
counts_est = [samples_counts[i] for i in (b ÷ 2 + 1):b]
52+
ps_exact = [i/((b ÷ 2)*(b+1) - (b ÷ 4)*(b ÷ 2 + 1)) for i in 51:b]
53+
54+
chisq_test = ChisqTest(counts_est, ps_exact)
55+
@test pvalue(chisq_test) > 0.05
56+
end

0 commit comments

Comments
 (0)