diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 60609e16..6a4375b1 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -33,7 +33,7 @@ jobs: steps: - uses: actions/checkout@v6 - - uses: julia-actions/setup-julia@v2 + - uses: julia-actions/setup-julia@v3 with: version: ${{ matrix.runner.version }} @@ -47,28 +47,36 @@ jobs: - uses: julia-actions/julia-processcoverage@v1 - - uses: codecov/codecov-action@v5 + - uses: codecov/codecov-action@v6 with: files: lcov.info token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true ext: - name: Ext (logdensityproblems, ${{ matrix.version }}) + name: Ext (${{ matrix.label }}, ${{ matrix.version }}) runs-on: ubuntu-latest strategy: fail-fast: false matrix: + label: + - ext/differentiationinterface version: - '1' - 'min' steps: - uses: actions/checkout@v6 - - uses: julia-actions/setup-julia@v2 + - uses: julia-actions/setup-julia@v3 with: version: ${{ matrix.version }} - uses: julia-actions/cache@v3 - uses: julia-actions/julia-buildpkg@v1 - - run: julia --project=. test/run_extras.jl + - run: julia --code-coverage=user test/run_extras.jl env: - LABEL: ext/logdensityproblems + LABEL: ${{ matrix.label }} + - uses: julia-actions/julia-processcoverage@v1 + - uses: codecov/codecov-action@v6 + with: + files: lcov.info + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: true diff --git a/AGENTS.md b/AGENTS.md index 3f69c1c2..139496fe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,14 +24,12 @@ AbstractPPL.jl provides shared interfaces and utilities for probabilistic progra - When touching `VarName` or optics code, test symbolic, indexed, nested, and serialization round-trip cases. - Respect evaluator contracts: `VectorEvaluator` is for flat vectors; `NamedTupleEvaluator` is for stable named structures; `!!` derivative APIs may return cache-aliased arrays. - Use `check_dims=false` only for trusted AD hot paths. Public evaluator calls should validate user input. - - Keep `LogDensityProblems` integration vector-only unless its contract changes. ## Tests - Core tests: `GROUP=Tests julia --project=test test/runtests.jl` - Doctests: `GROUP=Doctests julia --project=test test/runtests.jl` - Full package tests: `julia --project=. -e 'using Pkg; Pkg.test()'` - - LogDensityProblems extension: `LABEL=ext/logdensityproblems julia --project=. test/run_extras.jl` - Docs: `julia --project=docs docs/make.jl` Run the smallest relevant test first, then broaden when changing public interfaces, extensions, or downstream-facing behaviour. Do not weaken tests just to make CI pass. diff --git a/JULIA.md b/JULIA.md index 4067ee32..15e8b55d 100644 --- a/JULIA.md +++ b/JULIA.md @@ -1,6 +1,6 @@ # JULIA.md -Shared day-to-day Julia practices. DynamicPPL-specific review notes live in `AGENTS.md`; newcomer context lives in `docs/src/onboarding.md`. +Shared day-to-day Julia practices. AbstractPPL-specific review notes live in `AGENTS.md`. ## Engineering diff --git a/Project.toml b/Project.toml index 3678edc6..e767cf77 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "AbstractPPL" uuid = "7a57a42e-76ec-4ea3-a279-07e840d6d9cf" -keywords = ["probablistic programming"] +keywords = ["probabilistic programming"] license = "MIT" desc = "Common interfaces for probabilistic programming" version = "0.14.3" @@ -19,20 +19,22 @@ Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" [weakdeps] +DifferentiationInterface = "a0c0ee7d-e4b9-4e03-894e-1c5f64a51d63" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" -LogDensityProblems = "6fdf6af0-433a-55f7-b3ed-c6c6e0b8df7c" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [extensions] +AbstractPPLDifferentiationInterfaceExt = ["DifferentiationInterface"] AbstractPPLDistributionsExt = ["Distributions", "LinearAlgebra"] -AbstractPPLLogDensityProblemsExt = ["LogDensityProblems"] +AbstractPPLTestExt = ["Test"] [compat] ADTypes = "1" -LogDensityProblems = "2" AbstractMCMC = "2, 3, 4, 5" Accessors = "0.1" BangBang = "0.4" DensityInterface = "0.4" +DifferentiationInterface = "0.6, 0.7" Distributions = "0.25" JSON = "0.19 - 0.21, 1" LinearAlgebra = "<0.0.1, 1" @@ -40,4 +42,5 @@ MacroTools = "0.5" OrderedCollections = "1.8.1" Random = "1.6" StatsBase = "0.32, 0.33, 0.34" +Test = "1" julia = "1.10.8" diff --git a/ext/AbstractPPLDifferentiationInterfaceExt.jl b/ext/AbstractPPLDifferentiationInterfaceExt.jl new file mode 100644 index 00000000..80f95ee1 --- /dev/null +++ b/ext/AbstractPPLDifferentiationInterfaceExt.jl @@ -0,0 +1,100 @@ +module AbstractPPLDifferentiationInterfaceExt + +using AbstractPPL: AbstractPPL +using AbstractPPL.Evaluators: Evaluators, Prepared, VectorEvaluator +using ADTypes: AbstractADType, AutoReverseDiff +using DifferentiationInterface: DifferentiationInterface as DI + +# Differentiate only `x`; the evaluator is passed as a `DI.Constant` context so +# that in DynamicPPL the model and other evaluator state stay constant. +@inline _call_evaluator(x, evaluator) = evaluator(x) + +struct DICache{F,GP,JP} + target::F + gradient_prep::GP + jacobian_prep::JP + use_context::Bool +end + +# Compiled ReverseDiff only reuses a compiled tape on the one-argument path; +# `DI.Constant` deactivates tape recording, so close the evaluator into the +# target and call DI without contexts. +function _prepare_di(prep::F, adtype::AutoReverseDiff{true}, x, evaluator) where {F} + target = Base.Fix2(_call_evaluator, evaluator) + return target, prep(target, adtype, x), false +end + +function _prepare_di(prep::F, adtype::AbstractADType, x, evaluator) where {F} + return _call_evaluator, prep(_call_evaluator, adtype, x, DI.Constant(evaluator)), true +end + +function AbstractPPL.prepare( + adtype::AbstractADType, problem, x::AbstractVector{<:Real}; check_dims::Bool=true +) + evaluator = AbstractPPL.prepare(problem, x; check_dims)::VectorEvaluator + y = evaluator(x) + y isa Union{Number,AbstractVector} || throw( + ArgumentError( + "A prepared AD evaluator must return a scalar or AbstractVector; got $(typeof(y)).", + ), + ) + if length(x) == 0 + # DI prep crashes on length-0 input (e.g. ForwardDiff `BoundsError`); the + # `Val(0)` sentinel keeps the `gradient_prep === nothing` arity check meaningful. + gp, jp = y isa Number ? (Val(0), nothing) : (nothing, Val(0)) + return Prepared(adtype, evaluator, DICache(_call_evaluator, gp, jp, true)) + end + if y isa Number + target, gradient_prep, use_context = _prepare_di( + DI.prepare_gradient, adtype, x, evaluator + ) + return Prepared( + adtype, evaluator, DICache(target, gradient_prep, nothing, use_context) + ) + end + target, jacobian_prep, use_context = _prepare_di( + DI.prepare_jacobian, adtype, x, evaluator + ) + return Prepared(adtype, evaluator, DICache(target, nothing, jacobian_prep, use_context)) +end + +@inline function AbstractPPL.value_and_gradient!!( + p::Prepared{<:AbstractADType,<:VectorEvaluator,<:DICache}, x::AbstractVector{T} +) where {T<:Real} + p.cache.gradient_prep === nothing && + throw(ArgumentError("`value_and_gradient!!` requires a scalar-valued function.")) + T <: Integer && Evaluators._reject_integer_input(x) + Evaluators._check_vector_length(p.evaluator.dim, x) + # Bypass DI on length-0 input — DI prep paths fail (e.g. ForwardDiff + # `BoundsError`); typed `T[]` matches the caller's element type. + length(x) == 0 && return (p.evaluator(x), T[]) + return if p.cache.use_context + DI.value_and_gradient( + p.cache.target, p.cache.gradient_prep, p.adtype, x, DI.Constant(p.evaluator) + ) + else + DI.value_and_gradient(p.cache.target, p.cache.gradient_prep, p.adtype, x) + end +end + +@inline function AbstractPPL.value_and_jacobian!!( + p::Prepared{<:AbstractADType,<:VectorEvaluator,<:DICache}, x::AbstractVector{T} +) where {T<:Real} + p.cache.jacobian_prep === nothing && + throw(ArgumentError("`value_and_jacobian!!` requires a vector-valued function.")) + T <: Integer && Evaluators._reject_integer_input(x) + Evaluators._check_vector_length(p.evaluator.dim, x) + if length(x) == 0 + val = p.evaluator(x) + return (val, similar(x, length(val), 0)) + end + return if p.cache.use_context + DI.value_and_jacobian( + p.cache.target, p.cache.jacobian_prep, p.adtype, x, DI.Constant(p.evaluator) + ) + else + DI.value_and_jacobian(p.cache.target, p.cache.jacobian_prep, p.adtype, x) + end +end + +end # module diff --git a/ext/AbstractPPLLogDensityProblemsExt.jl b/ext/AbstractPPLLogDensityProblemsExt.jl deleted file mode 100644 index 8f00ebb7..00000000 --- a/ext/AbstractPPLLogDensityProblemsExt.jl +++ /dev/null @@ -1,38 +0,0 @@ -module AbstractPPLLogDensityProblemsExt - -using AbstractPPL: AbstractPPL -using AbstractPPL.Evaluators: Prepared, VectorEvaluator -using LogDensityProblems: LogDensityProblems - -# LDP integration is restricted to vector-input evaluators; `NamedTupleEvaluator` -# does not satisfy LDP's vector-input contract. Scalar output is a runtime -# contract the user must satisfy. - -LogDensityProblems.logdensity(p::Prepared{<:Any,<:VectorEvaluator}, x) = p(x) -LogDensityProblems.logdensity(e::VectorEvaluator, x) = e(x) - -function LogDensityProblems.dimension(p::Prepared{<:Any,<:VectorEvaluator}) - return LogDensityProblems.dimension(p.evaluator) -end -LogDensityProblems.dimension(e::VectorEvaluator) = e.dim - -# Order 0 by default. AD-backend extensions (DifferentiationInterface, -# ForwardDiff, Mooncake, etc.) must overload this for their cache type to -# advertise `LogDensityOrder{1}` — without that overload, -# `logdensity_and_gradient` will hit the `value_and_gradient!!` stub and fail. -# LDP defines `capabilities(x) = capabilities(typeof(x))`, so the type method -# alone covers both call shapes. -function LogDensityProblems.capabilities(::Type{<:Prepared{<:Any,<:VectorEvaluator}}) - return LogDensityProblems.LogDensityOrder{0}() -end -function LogDensityProblems.capabilities(::Type{<:VectorEvaluator}) - return LogDensityProblems.LogDensityOrder{0}() -end - -function LogDensityProblems.logdensity_and_gradient(p::Prepared{<:Any,<:VectorEvaluator}, x) - val, grad = AbstractPPL.value_and_gradient!!(p, x) - # `value_and_gradient!!` may alias internal storage; LDP requires a stable result. - return (val, copy(grad)) -end - -end # module diff --git a/ext/AbstractPPLTestExt.jl b/ext/AbstractPPLTestExt.jl new file mode 100644 index 00000000..6f7463da --- /dev/null +++ b/ext/AbstractPPLTestExt.jl @@ -0,0 +1,181 @@ +module AbstractPPLTestExt + +using AbstractPPL: AbstractPPL, generate_testcases, run_testcases +using Test: @test, @test_throws, @testset + +struct QuadraticProblem end +(::QuadraticProblem)(x::AbstractVector{<:Real}) = sum(xi -> xi^2, x) + +struct VectorValuedProblem end +(::VectorValuedProblem)(x::AbstractVector{<:Real}) = [x[1] * x[2], x[2] + x[3]] + +struct ValueCase + name::String + f::Any + x_proto::Any + x::Any + value::Any + gradient::Any + jacobian::Any +end + +struct ErrorCase + name::String + f::Any + x_proto::Any + x::Any + op::Any + exception::Any +end + +function AbstractPPL.generate_testcases(::Val{:vector}) + return ( + ValueCase( + "quadratic (scalar output)", + QuadraticProblem(), + zeros(3), + [3.0, 1.0, 2.0], + 14.0, + [6.0, 2.0, 4.0], + nothing, + ), + ValueCase( + "vector-valued (vector output)", + VectorValuedProblem(), + zeros(3), + [2.0, 3.0, 4.0], + [6.0, 7.0], + nothing, + [3.0 2.0 0.0; 0.0 1.0 1.0], + ), + ValueCase( + "empty input, scalar output", + x -> 7.5, + Float64[], + Float64[], + 7.5, + Float64[], + nothing, + ), + ValueCase( + "empty input, vector output", + x -> [2.0, 3.0], + Float64[], + Float64[], + [2.0, 3.0], + nothing, + zeros(2, 0), + ), + ) +end + +function AbstractPPL.generate_testcases(::Val{:edge}) + return ( + ErrorCase( + "wrong vector length", + QuadraticProblem(), + zeros(3), + [3.0, 1.0, 2.0, 99.0], + (prepared, x) -> prepared(x), + DimensionMismatch, + ), + ErrorCase( + "non-floating-point vector", + QuadraticProblem(), + zeros(3), + [3, 1, 2], + (prepared, x) -> prepared(x), + r"floating-point", + ), + ErrorCase( + "gradient of vector-valued output", + VectorValuedProblem(), + zeros(3), + [2.0, 3.0, 4.0], + (prepared, x) -> AbstractPPL.value_and_gradient!!(prepared, x), + ArgumentError, + ), + ErrorCase( + "gradient of vector-valued output, empty input", + x -> [2.0, 3.0], + Float64[], + Float64[], + (prepared, x) -> AbstractPPL.value_and_gradient!!(prepared, x), + r"scalar-valued", + ), + ErrorCase( + "jacobian of scalar output, empty input", + x -> 7.5, + Float64[], + Float64[], + (prepared, x) -> AbstractPPL.value_and_jacobian!!(prepared, x), + r"vector-valued", + ), + ErrorCase( + "value_and_gradient!! wrong vector length", + QuadraticProblem(), + zeros(3), + [3.0, 1.0, 2.0, 99.0], + (prepared, x) -> AbstractPPL.value_and_gradient!!(prepared, x), + DimensionMismatch, + ), + ErrorCase( + "value_and_jacobian!! wrong vector length", + VectorValuedProblem(), + zeros(3), + [2.0, 3.0, 4.0, 5.0], + (prepared, x) -> AbstractPPL.value_and_jacobian!!(prepared, x), + DimensionMismatch, + ), + ErrorCase( + "value_and_gradient!! non-floating-point vector", + QuadraticProblem(), + zeros(3), + [3, 1, 2], + (prepared, x) -> AbstractPPL.value_and_gradient!!(prepared, x), + r"floating-point", + ), + ErrorCase( + "value_and_jacobian!! non-floating-point vector", + VectorValuedProblem(), + zeros(3), + [2, 3, 4], + (prepared, x) -> AbstractPPL.value_and_jacobian!!(prepared, x), + r"floating-point", + ), + ) +end + +function AbstractPPL.run_testcases( + ::Val{:vector}, prepare_fn=AbstractPPL.prepare; adtype, atol=0, rtol=1e-10 +) + for case in generate_testcases(Val(:vector)) + @testset "$(case.name)" begin + prepared = prepare_fn(adtype, case.f, case.x_proto) + @test prepared(case.x) ≈ case.value atol = atol rtol = rtol + if case.gradient !== nothing + val, grad = AbstractPPL.value_and_gradient!!(prepared, case.x) + @test val ≈ case.value atol = atol rtol = rtol + @test grad ≈ case.gradient atol = atol rtol = rtol + end + if case.jacobian !== nothing + val, jac = AbstractPPL.value_and_jacobian!!(prepared, case.x) + @test val ≈ case.value atol = atol rtol = rtol + @test jac ≈ case.jacobian atol = atol rtol = rtol + end + end + end + return nothing +end + +function AbstractPPL.run_testcases(::Val{:edge}, prepare_fn=AbstractPPL.prepare; adtype) + for case in generate_testcases(Val(:edge)) + @testset "$(case.name)" begin + prepared = prepare_fn(adtype, case.f, case.x_proto) + @test_throws case.exception case.op(prepared, case.x) + end + end + return nothing +end + +end # module diff --git a/src/AbstractPPL.jl b/src/AbstractPPL.jl index 48a2cb44..0b50ca9a 100644 --- a/src/AbstractPPL.jl +++ b/src/AbstractPPL.jl @@ -12,8 +12,34 @@ include("abstractprobprog.jl") include("evaluate.jl") include("evaluators/Evaluators.jl") using .Evaluators: prepare, value_and_gradient!!, value_and_jacobian!! + +""" + generate_testcases(::Val{group}) + +Return a tuple of test cases for the conformance `group`. Implemented by the +`Test` extension (`AbstractPPLTestExt`). Reserved group keys (extensions must +not redefine these): `:vector` for value/gradient/jacobian round-trips on +vector-input evaluators; `:edge` for error-path cases. Downstream packages may +add other keys. +""" +function generate_testcases end + +""" + run_testcases(::Val{group}, prepare_fn=AbstractPPL.prepare; adtype, kwargs...) + +Run the test cases produced by [`generate_testcases`](@ref) against an AD +backend, using `prepare_fn` (default `AbstractPPL.prepare`) to construct each +prepared evaluator. Implemented by the `Test` extension. See +[`generate_testcases`](@ref) for reserved group keys. +""" +function run_testcases end + @static if VERSION >= v"1.11.0" - eval(Meta.parse("public prepare, value_and_gradient!!, value_and_jacobian!!")) + eval( + Meta.parse( + "public prepare, value_and_gradient!!, value_and_jacobian!!, generate_testcases, run_testcases", + ), + ) end include("varname/optic.jl") diff --git a/src/evaluators/Evaluators.jl b/src/evaluators/Evaluators.jl index ff2d5407..a88f2910 100644 --- a/src/evaluators/Evaluators.jl +++ b/src/evaluators/Evaluators.jl @@ -55,6 +55,11 @@ prepares gradient or jacobian machinery for vector inputs. the input shape on each call. Pass `check_dims=false` to skip the per-call check, e.g. inside an AD backend's hot path where the input shape is already guaranteed. + +The three-argument AD-aware form may invoke `problem` once during preparation +to detect output arity (scalar vs vector) and select gradient or jacobian +machinery accordingly. Avoid `prepare` calls when `problem` has side effects +that should fire only on user-driven evaluations. """ function prepare end @@ -165,13 +170,16 @@ function _reject_integer_input(x) ) end +function _check_vector_length(dim::Int, x) + length(x) == dim || throw( + DimensionMismatch("Expected a vector of length $dim, but got length $(length(x))."), + ) + return nothing +end + function (e::VectorEvaluator{true})(x::AbstractVector{T}) where {T} T <: Integer && _reject_integer_input(x) - length(x) == e.dim || throw( - DimensionMismatch( - "Expected a vector of length $(e.dim), but got length $(length(x))." - ), - ) + _check_vector_length(e.dim, x) return e.f(x) end @@ -258,6 +266,15 @@ function __init__() "\nCalling `prepare` with an AD backend requires loading the corresponding extension (e.g., `using DifferentiationInterface`).", ) end + # Same fire-only-when-no-backend-loaded logic as the `prepare` hint above. + Base.Experimental.register_error_hint(MethodError) do io, exc, args, kwargs + exc.f === value_and_gradient!! || exc.f === value_and_jacobian!! || return nothing + isempty(methods(exc.f)) || return nothing + print( + io, + "\nNo AD backend extension is loaded. Load `DifferentiationInterface` (with a backend like `ForwardDiff`) or `Mooncake` to enable gradient/jacobian computation.", + ) + end end end # module diff --git a/test/Aqua.jl b/test/Aqua.jl index 27d4103f..9fd508a4 100644 --- a/test/Aqua.jl +++ b/test/Aqua.jl @@ -3,6 +3,13 @@ module AquaTests using Aqua: Aqua using AbstractPPL -Aqua.test_all(AbstractPPL) +# `persistent_tasks` spawns a subprocess that runs `Pkg.precompile()` on a +# wrapper package depending on AbstractPPL. On Julia 1.10, this hits +# "Declaring __precompile__(false) is not allowed in files that are being +# precompiled" inside the wrapper's extension precompile path — a known +# brittleness in the Aqua/Julia 1.10 interaction, not in our extensions +# (the dedicated `Ext` CI jobs load and exercise every extension on `min` +# Julia and pass). Re-enable once `min` is bumped past 1.10. +Aqua.test_all(AbstractPPL; persistent_tasks=VERSION >= v"1.11") end diff --git a/test/ext/differentiationinterface/Project.toml b/test/ext/differentiationinterface/Project.toml new file mode 100644 index 00000000..af28cf08 --- /dev/null +++ b/test/ext/differentiationinterface/Project.toml @@ -0,0 +1,15 @@ +[deps] +ADTypes = "47edcb42-4c32-4615-8424-f2b9edc5f35b" +AbstractPPL = "7a57a42e-76ec-4ea3-a279-07e840d6d9cf" +DifferentiationInterface = "a0c0ee7d-e4b9-4e03-894e-1c5f64a51d63" +ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" +Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +ReverseDiff = "37e2e3b7-166d-5795-8a7a-e32c996b4267" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[compat] +ADTypes = "1" +DifferentiationInterface = "0.6, 0.7" +ForwardDiff = "1" +ReverseDiff = "1" +julia = "1.10" diff --git a/test/ext/differentiationinterface/main.jl b/test/ext/differentiationinterface/main.jl new file mode 100644 index 00000000..4e19a8ce --- /dev/null +++ b/test/ext/differentiationinterface/main.jl @@ -0,0 +1,29 @@ +using Pkg +Pkg.activate(@__DIR__) +Pkg.develop(; path=joinpath(@__DIR__, "..", "..", "..")) +Pkg.instantiate() + +using AbstractPPL: AbstractPPL, run_testcases +using ADTypes: AutoForwardDiff, AutoReverseDiff +using DifferentiationInterface +using ForwardDiff +using ReverseDiff +using Test + +@testset "AbstractPPLDifferentiationInterfaceExt" begin + run_testcases(Val(:vector); adtype=AutoForwardDiff(), atol=1e-6, rtol=1e-6) + run_testcases(Val(:edge); adtype=AutoForwardDiff()) + + @testset "AutoReverseDiff compiled tape (no-context path)" begin + ad = AutoReverseDiff(; compile=true) + p_scalar = AbstractPPL.prepare(ad, x -> sum(abs2, x), zeros(3)) + p_vector = AbstractPPL.prepare(ad, x -> [x[1] * x[2], x[2] + x[3]], zeros(3)) + + @test !p_scalar.cache.use_context + @test !isnothing(p_scalar.cache.gradient_prep.tape) + @test !p_vector.cache.use_context + @test !isnothing(p_vector.cache.jacobian_prep.tape) + + run_testcases(Val(:vector); adtype=ad, atol=1e-6, rtol=1e-6) + end +end diff --git a/test/ext/logdensityproblems/Project.toml b/test/ext/logdensityproblems/Project.toml deleted file mode 100644 index a6d089ab..00000000 --- a/test/ext/logdensityproblems/Project.toml +++ /dev/null @@ -1,11 +0,0 @@ -[deps] -AbstractPPL = "7a57a42e-76ec-4ea3-a279-07e840d6d9cf" -ADTypes = "47edcb42-4c32-4615-8424-f2b9edc5f35b" -LogDensityProblems = "6fdf6af0-433a-55f7-b3ed-c6c6e0b8df7c" -Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" - -[compat] -ADTypes = "1" -LogDensityProblems = "2" -julia = "1.10" diff --git a/test/ext/logdensityproblems/main.jl b/test/ext/logdensityproblems/main.jl deleted file mode 100644 index c0176035..00000000 --- a/test/ext/logdensityproblems/main.jl +++ /dev/null @@ -1,70 +0,0 @@ -using Pkg -Pkg.activate(@__DIR__) -Pkg.develop(; path=joinpath(@__DIR__, "..", "..", "..")) -Pkg.instantiate() - -using AbstractPPL -using AbstractPPL.Evaluators: Prepared, VectorEvaluator, NamedTupleEvaluator -using ADTypes: AbstractADType, AutoForwardDiff -using LogDensityProblems: LogDensityProblems -using Test - -# A NamedTupleEvaluator does not satisfy LDP's vector-input contract, so the -# extension does not define LDP methods for it. - -struct TestADType <: AbstractADType end - -function AbstractPPL.value_and_gradient!!( - p::Prepared{TestADType}, x::AbstractVector{<:Real} -) - return (p(x), ones(length(x))) -end - -# Backend extensions opt into gradient capability by overloading `capabilities` -# (typically on their cache type, e.g. `<:Prepared{<:Any,<:VectorEvaluator,<:MyCache}`). -# Here we dispatch on the AD type for simplicity. -function LogDensityProblems.capabilities(::Type{<:Prepared{TestADType,<:VectorEvaluator}}) - return LogDensityProblems.LogDensityOrder{1}() -end - -@testset "AbstractPPLLogDensityProblemsExt" begin - @testset "VectorEvaluator" begin - ve = VectorEvaluator(sum, 3) - @test LogDensityProblems.dimension(ve) == 3 - @test LogDensityProblems.logdensity(ve, [1.0, 2.0, 3.0]) == 6.0 - # A bare VectorEvaluator never advertises gradient capability; - # only the wrapping `Prepared` does. - @test LogDensityProblems.capabilities(ve) == LogDensityProblems.LogDensityOrder{0}() - end - - @testset "Prepared capabilities" begin - # Without a backend overload the fallback advertises order 0 only. - p_no_overload = Prepared(AutoForwardDiff(), VectorEvaluator(sum, 3)) - @test LogDensityProblems.capabilities(p_no_overload) == - LogDensityProblems.LogDensityOrder{0}() - - # A backend that overloads capabilities advertises order 1. - p_overloaded = Prepared(TestADType(), VectorEvaluator(sum, 3)) - @test LogDensityProblems.capabilities(p_overloaded) == - LogDensityProblems.LogDensityOrder{1}() - @test LogDensityProblems.capabilities(typeof(p_overloaded)) == - LogDensityProblems.LogDensityOrder{1}() - - # NamedTupleEvaluator-backed Prepared has no LDP methods defined; the - # extension only integrates vector-input evaluators. - p_nt = Prepared( - AutoForwardDiff(), NamedTupleEvaluator(x -> x.a + sum(x.b), (a=0.0, b=zeros(2))) - ) - @test_throws MethodError LogDensityProblems.dimension(p_nt) - @test LogDensityProblems.capabilities(p_nt) === nothing - end - - @testset "logdensity_and_gradient" begin - f = x -> -0.5 * sum(abs2, x) - p = Prepared(TestADType(), VectorEvaluator(f, 3)) - x = [1.0, 2.0, 3.0] - val, grad = LogDensityProblems.logdensity_and_gradient(p, x) - @test val ≈ f(x) - @test grad ≈ ones(3) - end -end diff --git a/test/run_extras.jl b/test/run_extras.jl index 0e71bbcd..e557f363 100644 --- a/test/run_extras.jl +++ b/test/run_extras.jl @@ -1,9 +1,9 @@ # Run a named extension test in its own isolated Julia environment. # # Usage (from the repo root): -# LABEL=ext/logdensityproblems julia test/run_extras.jl +# LABEL=ext/differentiationinterface julia test/run_extras.jl -const VALID_LABELS = ("ext/logdensityproblems",) +const VALID_LABELS = ("ext/differentiationinterface",) label = get(ENV, "LABEL", nothing) label in VALID_LABELS ||