diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2cbdb956..184384bd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,9 +19,9 @@ jobs: strategy: matrix: version: - - '1.6' - - '1.7' - '1.8' + - '1.9' + - '1.10' - 'nightly' os: - ubuntu-latest diff --git a/Project.toml b/Project.toml index fdeac771..12d66c59 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "Scruff" uuid = "6c98c146-7a27-4df0-99b1-e6e7c9a81876" -version = "0.8.2" +version = "0.8.3" [deps] AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" @@ -17,8 +17,6 @@ Parameters = "d96e819e-fc66-5662-9728-84c9c7592b0a" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" PrettyPrint = "8162dcfd-2161-5ef2-ae6c-7681170c5f98" -PyCall = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0" -PyPlot = "d330b81b-6aea-500a-939a-2ce795aea3ee" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" SimpleTraits = "699a6c99-e7fa-54fc-8d76-47d257e15c1d" @@ -38,8 +36,6 @@ MacroTools = "0.5.9" Parameters = "0.12.3" Plots = "1.27.2" PrettyPrint = "0.2" -PyCall = "1.95.1" -PyPlot = "2.10" SimpleTraits = "0.9.4" StatsBase = "0.33.16" StatsFuns = "1" diff --git a/README.md b/README.md index 7253fc65..a771c48d 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Scruff is an AI framework to build agents that sense, reason, and learn in the world using a variety of models. It aims to integrate many different kinds of models in a coherent framework, provide flexibility in spatiotemporal modeling, and provide tools to compose, share, and reuse models and model components. -Scruff is provided as a [Julia](https://julialang.org/) package and is licensed under the BSD-3-Clause License. It should be run using Julia v1.6 or v1.7. +Scruff is provided as a [Julia](https://julialang.org/) package and is licensed under the BSD-3-Clause License. > *Warning*: Scruff is rapidly evolving beta research software. Although the software already has a lot of functionality, we intend to expand on this in the future and cannot promise stability of the code or the APIs at the moment. diff --git a/docs/src/tutorial/examples.md b/docs/src/tutorial/examples.md index 8b43393e..48b6b216 100644 --- a/docs/src/tutorial/examples.md +++ b/docs/src/tutorial/examples.md @@ -4,3 +4,4 @@ * [novelty_example.jl](https://github.com/p2t2/Scruff.jl/tree/main/docs/examples/novelty_example.jl) * [novelty_lazy.jl](https://github.com/p2t2/Scruff.jl/tree/main/docs/examples/novelty_lazy.jl) * [novelty_filtering.jl](https://github.com/p2t2/Scruff.jl/tree/main/docs/examples/novelty_filtering.jl) +* [soccer_example.jl](https://github.com/charles-river-analytics/Scruff.jl/tree/main/docs/examples/soccer_example.jl) diff --git a/src/operators/op_defs.jl b/src/operators/op_defs.jl index 858c6e16..e2e95f1e 100644 --- a/src/operators/op_defs.jl +++ b/src/operators/op_defs.jl @@ -18,10 +18,12 @@ __Opt{T} = Union{Nothing, T} # to support MultiInterface.get_imp(::Nothing, args...) = nothing +@interface forward(sf::SFunc{I,O}, i::I)::Dist{O} where {I,O} +@interface inverse(sf::SFunc{I,O}, o::O)::Score{I} where {I,O} @interface is_deterministic(sf::SFunc)::Bool @interface sample(sf::SFunc{I,O}, i::I)::O where {I,O} @interface sample_logcpdf(sf::SFunc{I,O}, i::I)::Tuple{O, AbstractFloat} where {I,O} -@interface invert(sf::SFunc{I,O}, o::O)::I where {I,O} +# @interface invert(sf::SFunc{I,O}, o::O)::I where {I,O} @interface lambda_msg(sf::SFunc{I,O}, i::SFunc{<:__Opt{Tuple{}}, O})::SFunc{<:__Opt{Tuple{}}, I} where {I,O} @interface marginalize(sf::SFunc{I,O}, i::SFunc{<:__Opt{Tuple{}}, I})::SFunc{<:__Opt{Tuple{}}, O} where {I,O} @interface logcpdf(sf::SFunc{I,O}, i::I, o::O)::AbstractFloat where {I,O} diff --git a/src/sfuncs.jl b/src/sfuncs.jl index ea46d34b..ab47241e 100644 --- a/src/sfuncs.jl +++ b/src/sfuncs.jl @@ -14,10 +14,12 @@ include("sfuncs/dist/cat.jl") include("sfuncs/dist/constant.jl") include("sfuncs/dist/flip.jl") include("sfuncs/dist/normal.jl") +include("sfuncs/dist/uniform.jl") include("sfuncs/score/score.jl") include("sfuncs/score/hardscore.jl") include("sfuncs/score/softscore.jl") +include("sfuncs/score/multiplescore.jl") include("sfuncs/score/logscore.jl") include("sfuncs/score/functionalscore.jl") include("sfuncs/score/normalscore.jl") diff --git a/src/sfuncs/conddist/invertible.jl b/src/sfuncs/conddist/invertible.jl index fddb8139..f10b17ef 100644 --- a/src/sfuncs/conddist/invertible.jl +++ b/src/sfuncs/conddist/invertible.jl @@ -2,7 +2,7 @@ export Invertible """ - struct Invertible{I,O} <: SFunc{Tuple{I},O,Nothing} + struct Invertible{I,O} <: SFunc{Tuple{I},O} An invertible sfunc, with both a `forward` and a `inverse` function provided. diff --git a/src/sfuncs/dist/uniform.jl b/src/sfuncs/dist/uniform.jl new file mode 100644 index 00000000..a3cc3f73 --- /dev/null +++ b/src/sfuncs/dist/uniform.jl @@ -0,0 +1,123 @@ +# Continuous uniform sfunc + +export Uniform + +import Distributions + +mutable struct Uniform <: Dist{Float64} + params :: Tuple{Float64, Float64} + Uniform(l, u) = new((l,u)) +end + +lb(u::Uniform) = u.params[1] +ub(u::Uniform) = u.params[2] + +mean(u::Uniform) = (lb(u) + ub(u)) / 2.0 + +sd(u::Uniform) = (ub(u) - lb(u)) / sqrt(12.0) + +dist(u::Uniform) = Distributions.Uniform(lb(u), ub(u)) + +@impl begin + struct UniformSupport end + function support( + sf::Uniform, + ::NTuple, + size::Integer, + curr::Vector{Float64} + ) + newsize = size - length(curr) + result = curr + if newsize > 0 + x = lb(sf) + push!(result, x) + numsteps = newsize - 1 + step = (ub(sf) - lb(sf)) / numsteps + for i in 1:numsteps + x += step + push!(result, x) + end + end + unique(result) + end +end + + +@impl begin + struct UniformSupportQuality end + function support_quality(::Uniform, parranges) + :IncrementalSupport + end +end + + +@impl begin + struct UniformSample end + function sample(sf::Uniform, x::Tuple{})::Float64 + rand(dist(sf)) + end +end + +@impl begin + struct UniformLogcpdf end + function logcpdf(sf::Uniform, i::Tuple{}, o::Float64)::AbstractFloat + Distributions.logpdf(dist(sf), o) + end +end + +@impl begin + struct UniformBoundedProbs end + + # assumes range is sorted + function bounded_probs( + sf::Uniform, + range::Vector{Float64}, + ::NTuple + ) + l = lb(sf) + u = ub(sf) + d = u - l + n = length(range) + + # Each element in the range is associated with the interval between the midpoint + # of it and the point below and the midpoint between it and the point above, + # except for the end intervals which go to negative or positive infinity. + points = [-Inf64] + for i in 2:n + push!(points, (range[i-1] + range[i]) / 2) + end + push!(points, Inf64) + + # Each interval might be completely, partially, or not contained in the bounds + # of the uniform distribution. The following code determines the length of each + # interval that is in the bounds. + lengthsinbound = Float64[] + for i in 1:n + a = max(points[i], l) + b = min(points[i+1], u) + push!(lengthsinbound, max(b-a, 0.0)) + end + + ps = [lengthsinbound[i] / d for i in 1:n] + return (ps, ps) + end + +end + +@impl begin + struct UniformComputePi end + + function compute_pi(sf::Uniform, + range::Vector{Float64}, + ::NTuple, + ::Tuple)::Cat{Float64} + + ps = bounded_probs(sf, range, ())[1] + Cat(range, ps) + end +end + + + + + diff --git a/src/sfuncs/op_impls/basic_ops.jl b/src/sfuncs/op_impls/basic_ops.jl index 3a3ac64d..ada1c2fa 100644 --- a/src/sfuncs/op_impls/basic_ops.jl +++ b/src/sfuncs/op_impls/basic_ops.jl @@ -1,17 +1,32 @@ -# Default implementations of basic operators +#= +# Default implementations of operators. These are defined using other operators. +# They will always be called if a more specific implementation is not provided. +# If the operators they rely on are not implemented, they will produce a runtime error. +# Writers of default implementations should avoid infinite loops. +=# -# cpdf and logcpdf have default operators in terms of the other. Sfuncs should implement one or the other. +# if forward is defined, we get a default implementation of sample +@impl begin + struct DefaultSample end + # This should not produce an infinite loop, because a dist should not implement forward, + # since forwards maps a parent to a dist, but here the parent is empty. + function sample(sf::SFunc{I,O}, i::I)::O where {I,O} + d = forward(sf, i) + return sample(d, tuple()) + end +end @impl begin - struct SFuncCpdf end function cpdf(sf::SFunc{I,O}, i::I, o::O)::AbstractFloat where {I,O} exp(logcpdf(sf, i, o)) end end @impl begin - struct SFuncLogcpdf end function logcpdf(sf::SFunc{I,O}, i::I, o::O)::AbstractFloat where {I,O} log(cpdf(sf, i, o)) end end + +# TODO: Create default implementations of compute_pi and send_lambda +# TODO: Create weighted_sample operator with default implementation using inverse diff --git a/src/sfuncs/score/multiplescore.jl b/src/sfuncs/score/multiplescore.jl new file mode 100644 index 00000000..06c3a6ef --- /dev/null +++ b/src/sfuncs/score/multiplescore.jl @@ -0,0 +1,15 @@ +# MultipleScore lets you assert multiple evidence on the same variable +export MultipleScore + +struct MultipleScore{I} <: Score{I} + components :: Vector{<:Score{I}} +end + +function get_log_score(ms::MultipleScore{I}, i::I) where I + tot = 0.0 + for m in ms.components + tot += get_log_score(m, i) + end + tot +end + diff --git a/test/runtests.jl b/test/runtests.jl index 44895a4d..7b2865ed 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -8,6 +8,7 @@ using Test include("test_sfuncs.jl") include("test_score.jl") include("test_utils.jl") + include("test_ops.jl") include("test_ve.jl") include("test_lsfi.jl") include("test_bp.jl") diff --git a/test/test_core.jl b/test/test_core.jl index 9ad7d0c0..b1819d7d 100644 --- a/test/test_core.jl +++ b/test/test_core.jl @@ -215,4 +215,4 @@ wienerprocess = WienerProcess(0.1) end end -end \ No newline at end of file +end diff --git a/test/test_ops.jl b/test/test_ops.jl new file mode 100644 index 00000000..381a567c --- /dev/null +++ b/test/test_ops.jl @@ -0,0 +1,122 @@ +using Test + +import Logging + +using Scruff +using Scruff.Operators +using Scruff.MultiInterface: @impl + +import Scruff.Operators: sample, forward, cpdf, logcpdf +import Scruff.SFuncs: Constant + +# Test default implementation of operators + +logger = Logging.SimpleLogger(stderr, Logging.Error+1) + +struct SF1 <: SFunc{Tuple{Int},Int} end + +@impl begin + struct SF1Forward end + function forward(sf::SF1, i::Tuple{Int})::Dist{Int} + Constant(1) + end +end + +@impl begin + struct SF1Sample end + function sample(sf::SF1, i::Tuple{Int})::Int + 0 + end +end + +struct SF2 <: SFunc{Tuple{Int},Int} end + +@impl begin + struct SF2Forward end + function forward(sf::SF2, i::Tuple{Int})::Dist{Int} + Constant(1) + end +end + +struct SF3 <: SFunc{Tuple{Int}, Int} end + +struct SF4 <: SFunc{Tuple{Int}, Int} end + +@impl begin + struct SF4Cpdf end + function cpdf(sf::SF4, i::Tuple{Int}, o::Int) + 0.0 + end +end + +@impl begin + struct SF4Logcpdf end + function logcpdf(sf::SF4, i::Tuple{Int}, o::Int) + 0.0 # so cpdf is 1.0 + end +end + +struct SF5 <: SFunc{Tuple{Int}, Int} end + +@impl begin + struct SF5Cpdf end + function cpdf(sf::SF5, i::Tuple{Int}, o::Int) + 0.0 + end +end + +struct SF6 <: SFunc{Tuple{Int}, Int} end + +@impl begin + struct SF6Logcpdf end + function logcpdf(sf::SF6, i::Tuple{Int}, o::Int) + 0.0 + end +end + +struct SF7 <: SFunc{Tuple{Int}, Int} end + +Logging.with_logger(logger) do + +@testset "Implementation of sample using forward" begin + @testset "When explicit sample defined that is different" begin + # To test the calls, we implement forward and sample in contradictory ways + # The explicit sample should be used + @test sample(SF1(), (2,)) == 0 + end + + @testset "When forward is implemented explicitly but not sample" begin + # sample should use forward + @test sample(SF2(), (2,)) == 1 + end + + @testset "When neither sample nor forward are implemented explicitly" begin + # Should throw a MethodError because forward is not found when using the default implemenation of sample`` + @test_throws MethodError sample(SF3(), (2,)) + end +end + +@testset "Default implementations of cpdf and logcpdf in terms of each other" begin + @testset "When both cpdf and logcpdf are implemented explicitly" begin + # To test the calls, we implement cpdf and logcpdf in contradictory ways + @test cpdf(SF4(), (2,), 1) == 0.0 + @test logcpdf(SF4(), (2,), 1) == 0.0 + end + + @testset "When only cpdf is implemented explicitly" begin + @test cpdf(SF5(), (2,), 1) == 0.0 + @test logcpdf(SF5(), (2,), 1) == -Inf64 + end + + @testset "When only cpdf is implemented explicitly" begin + @test cpdf(SF6(), (2,), 1) == 1.0 + @test logcpdf(SF6(), (2,), 1) == 0.0 + end + + # @testset "When neither cpdf nor logcpdf are implemented explicitly" begin + # # should detect infinite loop and throw error + # @test_throws ErrorException cpdf(SF7(), (2,), 1) + # @test_throws ErrorException logcpdf(SF7(), (2,), 1) + # end +end +end \ No newline at end of file diff --git a/test/test_score.jl b/test/test_score.jl index b1c1c059..53a91a54 100644 --- a/test/test_score.jl +++ b/test/test_score.jl @@ -34,6 +34,15 @@ using Scruff.Utils @test get_log_score(s, :b) == -2.0 end + @testset "Multiple score" begin + s1 = SoftScore([:a, :b], [0.1, 0.2]) + s2 = LogScore([:b, :c], [-1.0, -2.0]) + s = MultipleScore([s1, s2]) + @test isapprox(get_log_score(s, :a), -Inf64) + @test isapprox(get_log_score(s, :b), log(0.2) - 1.0) + @test isapprox(get_log_score(s, :c), -Inf64) + end + @testset "Functional score" begin f(x) = 1.0 / x s = FunctionalScore{Float64}(f) diff --git a/test/test_sfuncs.jl b/test/test_sfuncs.jl old mode 100644 new mode 100755 index 5ce563c4..18be3e82 --- a/test/test_sfuncs.jl +++ b/test/test_sfuncs.jl @@ -145,6 +145,34 @@ end @test isapprox(compute_pi(f, [false, true], (), ()).params, [0.3, 0.7]) end + @testset "Uniform" begin + u = SFuncs.Uniform(-1.0, 3.0) + + test_support(u, (), [-1.0, 0.0, 1.0, 2.0, 3.0], :IncrementalSupport; size = 5) + test_support(u, (), [-1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0], :IncrementalSupport; + size = 9, curr = [-0.5, 0.5, 1.5, 2.5]) + test_support(u, (), [-1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0], :IncrementalSupport; + size = 10, curr = [-0.5, 0.5, 1.0, 1.5, 2.5]) + + cs = [0.0, 0.0, 0.0, 0.0] + tot = 1000 + for i in 1:tot + x = sample(u, ()) + cs[Int(floor(x)) + 2] += 1 + end + for j in 1:4 + @test isapprox(cs[j] / tot, 0.25; atol = 0.05) + end + @test isapprox(logcpdf(u, (), 0.0), log(0.25)) + @test isapprox(logcpdf(u, (), 5.0), -Inf64) + + @test isapprox(bounded_probs(u, [-1.0, 0.0, 1.0, 2.0, 3.0], ())[1], + [0.125, 0.25, 0.25, 0.25, 0.125]) + @test isapprox(bounded_probs(u, [-1.0, 0.0, 1.0, 2.0, 3.0], ())[2], + [0.125, 0.25, 0.25, 0.25, 0.125]) + @test isapprox(bounded_probs(u, [-1.0, -9.0, -8.0], ())[1], [0.0, 0.0, 1.0]) + end + @testset "Normal" begin n = SFuncs.Normal(-1.0,1.0) dist = Distributions.Normal(-1.0, 1.0)