diff --git a/Project.toml b/Project.toml index 88fb4faf..ffe5b29e 100644 --- a/Project.toml +++ b/Project.toml @@ -3,7 +3,7 @@ uuid = "80f14c24-f653-4e6a-9b94-39d6b0f70001" keywords = ["markov chain monte carlo", "probabilistic programming"] license = "MIT" desc = "A lightweight interface for common MCMC methods." -version = "5.14.0" +version = "5.15.0" [deps] BangBang = "198e06fe-97b7-11e9-32a5-e1d131e6ad66" @@ -18,7 +18,6 @@ ProgressLogging = "33c8b6b6-d38a-422a-b730-caa89a2f386c" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" TerminalLoggers = "5d786b92-1e48-4d6f-9151-6b4477ca9bed" -Transducers = "28d57a85-8fef-5791-bfe6-a80928e7c999" UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" [weakdeps] @@ -42,7 +41,6 @@ Statistics = "1" StatsBase = "0.32, 0.33, 0.34" TensorBoardLogger = "0.1" TerminalLoggers = "0.1" -Transducers = "0.4.30" UUIDs = "<0.0.1, 1" julia = "1.10" diff --git a/docs/src/api.md b/docs/src/api.md index 5acfc696..48f9fa9a 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -30,13 +30,6 @@ AbstractMCMC.steps(::AbstractRNG, ::AbstractMCMC.AbstractModel, ::AbstractMCMC.A AbstractMCMC.steps(::AbstractRNG, ::Any, ::AbstractMCMC.AbstractSampler) ``` -### Transducer - -```@docs -AbstractMCMC.Sample(::AbstractRNG, ::AbstractMCMC.AbstractModel, ::AbstractMCMC.AbstractSampler) -AbstractMCMC.Sample(::AbstractRNG, ::Any, ::AbstractMCMC.AbstractSampler) -``` - ## Sampling multiple chains in parallel ```@docs @@ -87,7 +80,7 @@ Common keyword arguments for regular and parallel sampling are: is passed `initial_state` as the `state` argument. !!! info - The common keyword arguments `progress`, `chain_type`, and `callback` are not supported by the iterator [`AbstractMCMC.steps`](@ref) and the transducer [`AbstractMCMC.Sample`](@ref). + The common keyword arguments `progress`, `chain_type`, and `callback` are not supported by the iterator [`AbstractMCMC.steps`](@ref). There is no "official" way for providing initial parameter values yet. However, multiple packages such as [EllipticalSliceSampling.jl](https://github.com/TuringLang/EllipticalSliceSampling.jl) and [AdvancedMH.jl](https://github.com/TuringLang/AdvancedMH.jl) support an `initial_params` keyword argument for setting the initial values when sampling a single chain. diff --git a/docs/src/design.md b/docs/src/design.md index 8d7cabb0..ea2632ea 100644 --- a/docs/src/design.md +++ b/docs/src/design.md @@ -1,25 +1,19 @@ # Design This page explains the default implementations and design choices of AbstractMCMC. -It is not intended for users but for developers that want to implement the AbstractMCMC -interface for Markov chain Monte Carlo sampling. The user-facing API is explained in -[API](@ref). +It is not intended for users but for developers that want to implement the AbstractMCMC interface for Markov chain Monte Carlo sampling. +The user-facing API is explained in [API](@ref). ## Overview -AbstractMCMC provides a default implementation of the user-facing interface described -in [API](@ref). You can completely neglect these and define your own implementation of the -interface. However, as described below, in most use cases the default implementation -allows you to obtain support of parallel sampling, progress logging, callbacks, iterators, -and transducers for free by just defining the sampling step of your inference algorithm, -drastically reducing the amount of code you have to write. In general, the docstrings -of the functions described below might be helpful if you intend to make use of the default -implementations. +AbstractMCMC provides a default implementation of the user-facing interface described in [API](@ref). +You can completely neglect these and define your own implementation of the interface. +However, as described below, in most use cases the default implementation allows you to obtain support of parallel sampling, progress logging, callbacks, and iterators for free by just defining the sampling step of your inference algorithm, drastically reducing the amount of code you have to write. +In general, the docstrings of the functions described below might be helpful if you intend to make use of the default implementations. ## Basic structure -The simplified structure for regular sampling (the actual implementation contains -some additional error checks and support for progress logging and callbacks) is +The simplified structure for regular sampling (the actual implementation contains some additional error checks and support for progress logging and callbacks) is ```julia StatsBase.sample( @@ -50,14 +44,12 @@ StatsBase.sample( end ``` -All other default implementations make use of the same structure and in particular -call the same methods. +All other default implementations make use of the same structure and in particular call the same methods. ## Sampling step -The only method for which no default implementation is provided (and hence which -downstream packages *have* to implement) is [`AbstractMCMC.step`](@ref). It defines -the sampling step of the inference method. +The only method for which no default implementation is provided (and hence which downstream packages *have* to implement) is [`AbstractMCMC.step`](@ref). +It defines the sampling step of the inference method. ```@docs AbstractMCMC.step @@ -69,47 +61,40 @@ If one also has some special handling of the warmup-stage of sampling, then this AbstractMCMC.step_warmup ``` -which will be used for the first `num_warmup` iterations, as specified as a keyword argument to [`AbstractMCMC.sample`](@ref). +which will be used for the first `num_warmup` iterations, as specified as a keyword argument to [`AbstractMCMC.sample`](@ref). Note that this is optional; by default it simply calls [`AbstractMCMC.step`](@ref) from above. ## Collecting samples !!! note - This section does not apply to the iterator and transducer interface. + This section does not apply to the iterator interface. -After the initial sample is obtained, the default implementations for regular and parallel sampling -(not for the iterator and the transducer since it is not needed there) create a container for all -samples (the initial one and all subsequent samples) using `AbstractMCMC.samples`. +After the initial sample is obtained, the default implementations for regular and parallel sampling (not for the iterator since it is not needed there) create a container for all samples (the initial one and all subsequent samples) using `AbstractMCMC.samples`. ```@docs AbstractMCMC.samples ``` -In each step, the sample is saved in the container by `AbstractMCMC.save!!`. The notation `!!` -follows the convention of the package [BangBang.jl](https://github.com/JuliaFolds/BangBang.jl) -which is used in the default implementation of `AbstractMCMC.save!!`. It indicates that the -sample is pushed to the container but a "widening" fallback is used if the container type -does not allow saving the sample. Therefore `AbstractMCMC.save!!` *always has* to return the container. +In each step, the sample is saved in the container by `AbstractMCMC.save!!`. +The notation `!!` follows the convention of the package [BangBang.jl](https://github.com/JuliaFolds/BangBang.jl) which is used in the default implementation of `AbstractMCMC.save!!`. +It indicates that the sample is pushed to the container but a "widening" fallback is used if the container type does not allow saving the sample. +Therefore `AbstractMCMC.save!!` *always has* to return the container. ```@docs AbstractMCMC.save!! ``` -For most use cases the default implementation of `AbstractMCMC.samples` and `AbstractMCMC.save!!` -should work out of the box and hence need not be overloaded in downstream code. +For most use cases the default implementation of `AbstractMCMC.samples` and `AbstractMCMC.save!!` should work out of the box and hence need not be overloaded in downstream code. ## Creating chains !!! note - This section does not apply to the iterator and transducer interface. + This section does not apply to the iterator interface. -At the end of the sampling procedure for regular and parallel sampling we transform -the collection of samples to the desired output type by calling `AbstractMCMC.bundle_samples`. +At the end of the sampling procedure for regular and parallel sampling we transform the collection of samples to the desired output type by calling `AbstractMCMC.bundle_samples`. ```@docs AbstractMCMC.bundle_samples ``` -The default implementation should be fine in most use cases, but downstream packages -could, e.g., save the final state of the sampler as well if they overload -`AbstractMCMC.bundle_samples`. +The default implementation should be fine in most use cases, but downstream packages could, e.g., save the final state of the sampler as well if they overload `AbstractMCMC.bundle_samples`. diff --git a/docs/src/index.md b/docs/src/index.md index 2285d162..4e394b79 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -3,9 +3,6 @@ *Abstract types and interfaces for Markov chain Monte Carlo methods.* AbstractMCMC defines an interface for sampling and combining Markov chains. -It comes with a default sampling algorithm that provides support of progress -bars, parallel sampling (multithreaded and multicore), and user-provided callbacks -out of the box. Typically developers only have to define the sampling step -of their inference method in an iterator-like fashion to make use of this -functionality. Additionally, the package defines an iterator and a transducer -for sampling Markov chains based on the interface. +It comes with a default sampling algorithm that provides support of progress bars, parallel sampling (multithreaded and multicore), and user-provided callbacks out of the box. +Typically developers only have to define the sampling step of their inference method in an iterator-like fashion to make use of this functionality. +Additionally, the package defines an iterator interface for obtaining successive samples from Markov chains. diff --git a/src/AbstractMCMC.jl b/src/AbstractMCMC.jl index f3169952..9a87462c 100644 --- a/src/AbstractMCMC.jl +++ b/src/AbstractMCMC.jl @@ -7,7 +7,6 @@ using LoggingExtras: LoggingExtras using ProgressLogging: ProgressLogging using StatsBase: StatsBase using TerminalLoggers: TerminalLoggers -using Transducers: Transducers using FillArrays: FillArrays using Distributed: Distributed @@ -197,7 +196,6 @@ include("logging.jl") include("interface.jl") include("sample.jl") include("stepper.jl") -include("transducer.jl") include("logdensityproblems.jl") include("callbacks.jl") diff --git a/src/transducer.jl b/src/transducer.jl deleted file mode 100644 index 63bff3fd..00000000 --- a/src/transducer.jl +++ /dev/null @@ -1,102 +0,0 @@ -struct Sample{A<:Random.AbstractRNG,M<:AbstractModel,S<:AbstractSampler,K} <: - Transducers.Transducer - rng::A - model::M - sampler::S - kwargs::K -end - -function Sample(model_or_logdensity, sampler::AbstractSampler; kwargs...) - return Sample(Random.default_rng(), model_or_logdensity, sampler; kwargs...) -end - -""" - Sample( - rng::Random.AbstractRNG=Random.default_rng(), - model::AbstractModel, - sampler::AbstractSampler; - kwargs..., - ) - -Create a transducer that returns samples from the `model` with the Markov chain Monte Carlo -`sampler`. - -# Examples - -```jldoctest; setup=:(using AbstractMCMC: Sample) -julia> struct MyModel <: AbstractMCMC.AbstractModel end - -julia> struct MySampler <: AbstractMCMC.AbstractSampler end - -julia> function AbstractMCMC.step(rng, ::MyModel, ::MySampler, state=nothing; kwargs...) - # all samples are zero - return 0.0, state - end - -julia> transducer = Sample(MyModel(), MySampler()); - -julia> collect(transducer(1:10)) == zeros(10) -true -``` -""" -function Sample( - rng::Random.AbstractRNG, model::AbstractModel, sampler::AbstractSampler; kwargs... -) - return Sample(rng, model, sampler, kwargs) -end - -# Initial sample. -function Transducers.start(rf::Transducers.R_{<:Sample}, result) - # Unpack transducer. - td = Transducers.xform(rf) - rng = td.rng - model = td.model - sampler = td.sampler - kwargs = td.kwargs - discard_initial = get(kwargs, :discard_initial, 0)::Int - - # Start sampling algorithm and discard initial samples if desired. - sample, state = step(rng, model, sampler; kwargs...) - for _ in 1:discard_initial - sample, state = step(rng, model, sampler, state; kwargs...) - end - - return Transducers.wrap( - rf, (sample, state), Transducers.start(Transducers.inner(rf), result) - ) -end - -# Subsequent samples. -function Transducers.next(rf::Transducers.R_{<:Sample}, result, input) - # Unpack transducer. - td = Transducers.xform(rf) - rng = td.rng - model = td.model - sampler = td.sampler - kwargs = td.kwargs - thinning = get(kwargs, :thinning, 1)::Int - - let rng = rng, - model = model, - sampler = sampler, - kwargs = kwargs, - thinning = thinning, - inner_rf = Transducers.inner(rf) - - Transducers.wrapping(rf, result) do (sample, state), iresult - iresult2 = Transducers.next(inner_rf, iresult, sample) - - # Perform thinning if desired. - for _ in 1:(thinning - 1) - _, state = step(rng, model, sampler, state; kwargs...) - end - - return step(rng, model, sampler, state; kwargs...), iresult2 - end - end -end - -function Transducers.complete(rf::Transducers.R_{Sample}, result) - _, inner_result = Transducers.unwrap(rf, result) - return Transducers.complete(Transducers.inner(rf), inner_result) -end diff --git a/test/logdensityproblems.jl b/test/logdensityproblems.jl index 9eaa3913..2555f8dd 100644 --- a/test/logdensityproblems.jl +++ b/test/logdensityproblems.jl @@ -65,13 +65,6 @@ @test length(samples4) == N @test all(x.a == y.a && x.b == y.b for (x, y) in zip(samples, samples4)) - # Same chain if sampling is performed with transducer - Random.seed!(1234) - xf = AbstractMCMC.Sample(ℓ, MySampler()) - samples5 = collect(xf(1:N)) - @test length(samples5) == N - @test all(x.a == y.a && x.b == y.b for (x, y) in zip(samples, samples5)) - # Parallel sampling for alg in (MCMCSerial(), MCMCDistributed(), MCMCThreads()) chains = sample(ℓ, MySampler(), alg, N, 2) @@ -91,7 +84,6 @@ mylogdensity, MySampler(), MCMCDistributed(), N, 2 ) @test_throws ArgumentError AbstractMCMC.steps(mylogdensity, MySampler()) - @test_throws ArgumentError AbstractMCMC.Sample(mylogdensity, MySampler()) end # Remove workers diff --git a/test/runtests.jl b/test/runtests.jl index c7c793a8..1c775d15 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -6,7 +6,6 @@ using LogDensityProblems using LoggingExtras: TeeLogger, EarlyFilteredLogger using TerminalLoggers: TerminalLogger using FillArrays: FillArrays -using Transducers using Distributed using Logging: Logging @@ -23,7 +22,6 @@ include("utils.jl") @testset "AbstractMCMC" begin include("sample.jl") include("stepper.jl") - include("transducer.jl") include("logdensityproblems.jl") end diff --git a/test/transducer.jl b/test/transducer.jl deleted file mode 100644 index b161151c..00000000 --- a/test/transducer.jl +++ /dev/null @@ -1,99 +0,0 @@ -@testset "transducer.jl" begin - Random.seed!(1234) - - @testset "Basic sampling" begin - N = 1_000 - local chain - Logging.with_logger(TerminalLogger()) do - xf = AbstractMCMC.Sample(MyModel(), MySampler(); sleepy=true, logger=true) - chain = collect(xf(withprogress(1:N; interval=1e-3))) - end - - # test output type and size - @test chain isa Vector{<:MySample} - @test length(chain) == N - - # test some statistical properties - tail_chain = @view chain[2:end] - @test mean(x.a for x in tail_chain) ≈ 0.5 atol = 6e-2 - @test var(x.a for x in tail_chain) ≈ 1 / 12 atol = 5e-3 - @test mean(x.b for x in tail_chain) ≈ 0.0 atol = 5e-2 - @test var(x.b for x in tail_chain) ≈ 1 atol = 6e-2 - end - - @testset "drop" begin - xf = AbstractMCMC.Sample(MyModel(), MySampler()) - chain = collect(Drop(1)(xf(1:10))) - @test chain isa Vector{MySample{Float64,Float64}} - @test length(chain) == 9 - end - - # Reproduce iterator example - @testset "iterator example" begin - # filter missing values and split transitions - xf = opcompose( - AbstractMCMC.Sample(MyModel(), MySampler()), - OfType(MySample{Float64,Float64}), - Map(x -> (x.a, x.b)), - ) - as, bs = foldl(xf, 1:999; init=(Float64[], Float64[])) do (as, bs), (a, b) - push!(as, a) - push!(bs, b) - as, bs - end - - @test length(as) == length(bs) == 998 - - @test mean(as) ≈ 0.5 atol = 2e-2 - @test var(as) ≈ 1 / 12 atol = 5e-3 - @test mean(bs) ≈ 0.0 atol = 5e-2 - @test var(bs) ≈ 1 atol = 5e-2 - end - - @testset "Discard initial samples" begin - # Create a chain of `N` samples after discarding some initial samples. - Random.seed!(1234) - N = 50 - discard_initial = 10 - xf = opcompose( - AbstractMCMC.Sample(MyModel(), MySampler(); discard_initial=discard_initial), - Map(x -> (x.a, x.b)), - ) - as, bs = foldl(xf, 1:N; init=([], [])) do (as, bs), (a, b) - push!(as, a) - push!(bs, b) - as, bs - end - - # Repeat sampling with `sample`. - Random.seed!(1234) - chain = sample( - MyModel(), MySampler(), N; discard_initial=discard_initial, progress=false - ) - @test all(as[i] == chain[i].a for i in 1:N) - @test all(bs[i] == chain[i].b for i in 1:N) - end - - @testset "Thin chain by a factor of `thinning`" begin - # Create a thinned chain with a thinning factor of `thinning`. - Random.seed!(1234) - N = 50 - thinning = 3 - xf = opcompose( - AbstractMCMC.Sample(MyModel(), MySampler(); thinning=thinning), - Map(x -> (x.a, x.b)), - ) - as, bs = foldl(xf, 1:N; init=([], [])) do (as, bs), (a, b) - push!(as, a) - push!(bs, b) - as, bs - end - - # Repeat sampling with `sample`. - Random.seed!(1234) - chain = sample(MyModel(), MySampler(), N; thinning=thinning, progress=false) - @test as[1] === chain[1].a === missing - @test all(as[i] == chain[i].a for i in 2:N) - @test all(bs[i] == chain[i].b for i in 1:N) - end -end