diff --git a/.github/workflows/experiments.yml b/.github/workflows/experiments.yml deleted file mode 100644 index e6cc2b002..000000000 --- a/.github/workflows/experiments.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Experiments -on: - push: - branches: ["main"] - pull_request: -jobs: - test: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - experiment: [Markov, TensorNetworks] - - steps: - - uses: actions/checkout@v3 - - name: "Set up Julia" - uses: julia-actions/setup-julia@latest - with: - version: "1" - # XXX: Cannot change working directory for "uses" steps. - #- name: "Run tests" - # uses: julia-actions/julia-runtest@main - - name: "Run tests" - run: | - cd experiments/${{ matrix.experiment }} - julia --color=yes --project -e \ - 'using Pkg; Pkg.develop(PackageSpec(path="../..")); Pkg.test(coverage=true)' diff --git a/experiments/Markov/Project.toml b/experiments/Markov/Project.toml deleted file mode 100644 index 29be0ba11..000000000 --- a/experiments/Markov/Project.toml +++ /dev/null @@ -1,10 +0,0 @@ -name = "Markov" -uuid = "dac345a6-b16d-456a-a2c5-d9fc9368bae6" -authors = ["Evan Patterson "] -version = "0.1.0" - -[deps] -Catlab = "134e5e36-593f-5add-ad60-77f754baafbe" -Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" -GATlab = "f0ffcf3b-d13a-433e-917c-cc44ccf5ead2" -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/experiments/Markov/src/Markov.jl b/experiments/Markov/src/Markov.jl deleted file mode 100644 index bc6e8064e..000000000 --- a/experiments/Markov/src/Markov.jl +++ /dev/null @@ -1,6 +0,0 @@ -module Markov - -include("MarkovCategories.jl") -include("StochExpr.jl") - -end diff --git a/experiments/Markov/src/MarkovCategories.jl b/experiments/Markov/src/MarkovCategories.jl deleted file mode 100644 index bb047fa3f..000000000 --- a/experiments/Markov/src/MarkovCategories.jl +++ /dev/null @@ -1,50 +0,0 @@ -module MarkovCategories -export ThMarkovCategory, FreeMarkovCategory, - Ob, Hom, dom, codom, compose, ⋅, ∘, otimes, ⊗, braid, mcopy, Δ, delete, ◊, - expectation, 𝔼 - -using Catlab.Theories, Catlab.WiringDiagrams -import GATlab: show_latex -import Catlab.Theories: Ob, Hom, dom, codom, compose, ⋅, ∘, otimes, ⊗, braid, - mcopy, Δ, delete, ◊ - -# Theories -########### - -""" Theory of *Markov categories with expectation* -""" -@signature ThMarkovCategory <: ThMonoidalCategoryWithDiagonals begin - expectation(M::(A → B))::(A → B) ⊣ [A::Ob, B::Ob] - @op (𝔼) := expectation -end - -@symbolic_model FreeMarkovCategory{ObExpr,HomExpr} ThMarkovCategory begin - otimes(A::Ob, B::Ob) = associate_unit(new(A,B), munit) - otimes(f::Hom, g::Hom) = associate(new(f,g)) - compose(f::Hom, g::Hom) = associate_unit(new(f,g; strict=true), id) -end - -function show_latex(io::IO, expr::HomExpr{:expectation}; kw...) - print(io, "\\mathbb{E}\\left(") - show_latex(io, first(expr)) - print(io, "\\right)") -end - -# Wiring diagrams -################# - -mcopy(A::Ports{ThMarkovCategory.Meta.T}, n::Int) = implicit_mcopy(A, n) - -function expectation(M::WiringDiagram{ThMarkovCategory.Meta.T}) - if nboxes(M) <= 1 - functor(M, identity, expectation_box) - else - singleton_diagram(ThMarkovCategory, expectation_box(M)) - end -end - -expectation_box(box::AbstractBox) = BoxOp{:expectation}(box) -expectation_box(exp::BoxOp{:expectation}) = exp -expectation_box(junction::Junction) = junction - -end diff --git a/experiments/Markov/src/StochExpr.jl b/experiments/Markov/src/StochExpr.jl deleted file mode 100644 index b24f158c2..000000000 --- a/experiments/Markov/src/StochExpr.jl +++ /dev/null @@ -1,41 +0,0 @@ -export crand - -using Catlab.Theories - - -# How do you give semantics to a stochastic map? You call it. -function crand(f::FreeCartesianCategory.Hom{:generator}, args...) - f.args[1](args...) -end - -# Compositional structure -crand(f::FreeCartesianCategory.Hom{:id}, args...) = (args) -function crand(f::FreeCartesianCategory.Hom{:compose}, args...) - if length(f.args) > 2 - return crand(f.args[end], crand(compose(f.args[1:end-1]...), args...)...) - end - return crand(f.args[end], crand(f.args[1], args...)...) -end - -# Monoidal Structure -function crand(f::FreeCartesianCategory.Hom{:otimes}, args...) - dims = cumsum(map(ndims∘dom, f.args)) - map(1:length(f.args)) do i - if i == 1 - crand(f.args[i], args[1:dims[1]]...) - else - crand(f.args[i], args[dims[i-1]+1:dims[i]]...) - end - end |> xs->filter(xs) do x # handle the () you get from deletes - x != () - end -end -function crand(f::FreeCartesianCategory.Hom{:braid}, args...) - y = args[1:ndims(f.args[1])] - x = args[(ndims(f.args[1])+1):end] - return (x...,y...) -end - -# Diagonal Comonoid -crand(f::FreeCartesianCategory.Hom{:mcopy}, args...) = (args..., args...) -crand(f::FreeCartesianCategory.Hom{:delete}, args...) = () diff --git a/experiments/Markov/test/MarkovCategories.jl b/experiments/Markov/test/MarkovCategories.jl deleted file mode 100644 index ea44c9c37..000000000 --- a/experiments/Markov/test/MarkovCategories.jl +++ /dev/null @@ -1,33 +0,0 @@ -module TestMarkovCategories -using Test - -using GATlab, Catlab.WiringDiagrams -using Markov.MarkovCategories - -# Theories -########### - -A, B = Ob(FreeMarkovCategory, :A, :B) -M = Hom(:M, A, B) - -# Domains and codomains -@test dom(expectation(M)) == A -@test codom(expectation(M)) == B - -# Extra syntax -@test 𝔼(M) == expectation(M) - -# LaTeX notation -@test sprint(show_latex, expectation(M)) == "\\mathbb{E}\\left(M\\right)" - -# Wiring diagrams -################# - -A, B, C = [ Ports{ThMarkovCategory.Meta.T}([sym]) for sym in [:A, :B, :C] ] -M = singleton_diagram(ThMarkovCategory, Box(:M,[:A],[:B])) -N = singleton_diagram(ThMarkovCategory, Box(:N,[:B],[:C])) - -# Non-functoriality of expectation. -@test expectation(compose(M,N)) != compose(expectation(M),expectation(N)) - -end diff --git a/experiments/Markov/test/StochMapsExpr.jl b/experiments/Markov/test/StochMapsExpr.jl deleted file mode 100644 index 72c5ace89..000000000 --- a/experiments/Markov/test/StochMapsExpr.jl +++ /dev/null @@ -1,56 +0,0 @@ -using Test - -using Catlab.Theories -using Markov - -X = Ob(FreeCartesianCategory, :Float64) -u = Hom(x->x*rand(), X, X) - -u₀ = Hom(()->rand(), munit(FreeCartesianCategory.Ob), X) -crand(u₀⊗u₀) - -meantest(f::Function, μ::Real, n::Int, ϵ::Real) = begin - μ̂ = sum(map(f, 1:n))/n - @test μ - ϵ < μ̂ - @test μ̂ < μ + ϵ -end - -meantest(1/1, 10000, 1e-1) do i - crand(((u⊗u)⋅braid(X,X)), 1,2)[1] -end -meantest(1/2, 10000, 1e-1) do i - crand(((u⊗u)⋅braid(X,X)), 1,2)[2] -end -meantest(1/1, 10000, 1e1) do i - crand(mcopy(X),2)[1] -end -meantest(1/1, 10000, 1e1) do i - crand(mcopy(X),2)[2] -end - -@test crand(delete(X), 1) == () -@test crand(u⋅delete(X), 1) == () -meantest(1/2, 1000, 1e-1) do i - crand(u⋅mcopy(X)⋅(id(X)⊗delete(X)), 1)[1][1] -end - -@testset "Uniforms" begin - u₀ = Hom(()->rand(), munit(FreeCartesianCategory.Ob), X) - @test 0 <= crand(u, 1.0) <= 1.0 - @test 0 <= crand(u, 2) <= 2 - @test compose(u,u).args |> length == 2 - @test dom(compose(u,u)) == X - @test codom(compose(u,u)) == X - @test 0 <= crand(compose(u,u), 0.5) <= 1/2 - @test 0 <= crand(u₀) <= 1 - @test crand(u₀⊗u₀) |> length == 2 - @test crand(u⊗u₀, 1.0) |> length == 2 - @test crand(u₀⊗u, 1.0) |> length == 2 - @test crand(otimes(u₀,u₀,u), 2.0) |> length == 3 - @test crand(otimes(u₀,u,u₀), 2.0) |> length == 3 - @test crand(otimes(u₀,u,u), 1.0, 2.0) |> length == 3 - @test crand(otimes(u₀,u,u)⋅otimes(u,u,u), 1.0, 2.0) |> length == 3 - @test crand(otimes(u,u,u), 1.0, 2.0, 3.0) |> length == 3 -end - -@test crand(compose(id(X), id(X), id(X)), 1)[1] == 1 diff --git a/experiments/Markov/test/runtests.jl b/experiments/Markov/test/runtests.jl deleted file mode 100644 index 35c0df1fe..000000000 --- a/experiments/Markov/test/runtests.jl +++ /dev/null @@ -1,6 +0,0 @@ -using Test - -@testset "Core" begin - include("MarkovCategories.jl") - include("StochMapsExpr.jl") -end diff --git a/experiments/README.md b/experiments/README.md deleted file mode 100644 index ff8f72a78..000000000 --- a/experiments/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# Experiments in Catlab - -This folder contains experimental projects applying Catlab to various domains. -These projects will most likely not be contained in the Catlab main library -because they are too specialized or require too many external dependancies. The -larger and more successful projects will eventually be spun out into their own -repositories. - -You are welcome to try out these projects and report issues but no guarantees -are made about their future. diff --git a/experiments/TensorNetworks/Project.toml b/experiments/TensorNetworks/Project.toml deleted file mode 100644 index 53cbc8b23..000000000 --- a/experiments/TensorNetworks/Project.toml +++ /dev/null @@ -1,10 +0,0 @@ -name = "TensorNetworks" -uuid = "4f0dc37e-1665-447c-84ab-50c7685a7b95" -authors = ["Evan Patterson "] -version = "0.1.0" - -[deps] -Catlab = "134e5e36-593f-5add-ad60-77f754baafbe" -LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" -MLStyle = "d8e11817-5142-5d16-987a-aa16d5891078" -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/experiments/TensorNetworks/src/TensorNetworks.jl b/experiments/TensorNetworks/src/TensorNetworks.jl deleted file mode 100644 index 82e3c7ac4..000000000 --- a/experiments/TensorNetworks/src/TensorNetworks.jl +++ /dev/null @@ -1,257 +0,0 @@ -""" Tensor networks via the operad of undirected wiring diagrams. -""" -module TensorNetworks -export RelationDiagram, @tensor_network, parse_tensor_network, - contract_tensor_network, @contract_tensors_with, gen_tensor_notation - -using MLStyle: @match - -using Catlab.CategoricalAlgebra.CSets -using Catlab.WiringDiagrams.UndirectedWiringDiagrams -import Catlab.WiringDiagrams: oapply -using Catlab.Programs.RelationalPrograms: RelationDiagram, UntypedRelationDiagram - -# Evaluation -############ - -""" Contract tensors according to UWD. - -This method simply wraps [`contract_tensor_network`](@ref), which also presents -an interface closer to the standard tensor algebra packages. -""" -oapply(d::UndirectedWiringDiagram, tensors::AbstractVector{<:AbstractArray}) = - contract_tensor_network(d, tensors) - -""" Contract a tensor network all at once. - -Performs a generalized contraction of a list of tensors of arbitrary rank. In -the binary case, this function includes matrix multiplication, tensor product, -and trace as special cases. In general, it implements the "generalized matrix -multiplication" described in [Spivak et al's "Pixel -Arrays"](https://arxiv.org/abs/1609.00061). - -Be warned that this method is inefficient for contracting networks with more -than two junctions. Specifically, if the network has ``n`` junctions with -dimensions ``k₁, …, kₙ``, then this function performs exactly ``k₁ ⋯ kₙ`` -multiplications in the base ring (or rig). For example, a binary matrix multiply -of square matrices performs ``k³`` multiplications. A ternary matrix multiply -performs ``k⁴`` multiplications, whereas a sequence of two binary multiplies -performs ``2k³``. Thus, when there are more than two junctions, it is usually -better to schedule a computation using [`schedule`](@ref) or another package, -possibly via [`@contract_tensors_with`](@ref). - -In addition to the method taking an undirected wiring diagram, this function has -methods similar to the `ncon` ("Network CONtractor") function in: - -- the [NCON package](https://arxiv.org/abs/1402.0939) for MATLAB -- the Julia package [TensorOperations.jl](https://github.com/Jutho/TensorOperations.jl) - -except that here the outer junctions are represented explictly by a third -argument rather than implicitly by using negative numbers in the second -argument. -""" -function contract_tensor_network(d::UndirectedWiringDiagram, - tensors::AbstractVector{<:AbstractArray}) - @assert nboxes(d) == length(tensors) - juncs = [junction(d, ports(d, b)) for b in boxes(d)] - j_out = junction(d, ports(d, outer=true), outer=true) - contract_tensor_network(tensors, juncs, j_out) -end - -function contract_tensor_network(tensors::AbstractVector{<:AbstractArray{T}}, - juncs::AbstractVector, j_out) where T - # Handle important binary case with specialized code. - if length(tensors) == 2 && length(juncs) == 2 - return contract_tensor_network(Tuple(tensors), Tuple(juncs), j_out) - end - - jsizes = Tuple(infer_junction_sizes(tensors, juncs, j_out)) - juncs, j_out = map(Tuple, juncs), Tuple(j_out) - C = zeros(T, Tuple(jsizes[j] for j in j_out)) - for index in CartesianIndices(jsizes) - x = one(T) - for (A, junc) in zip(tensors, juncs) - x *= A[(index[j] for j in junc)...] - end - C[(index[j] for j in j_out)...] += x - end - return C -end - -function contract_tensor_network( # Binary case. - (A, B)::Tuple{<:AbstractArray{T},<:AbstractArray{T}}, - (jA, jB), j_out) where T - jsizes = Tuple(infer_junction_sizes((A, B), (jA, jB), j_out)) - jA, jB, j_out = Tuple(jA), Tuple(jB), Tuple(j_out) - C = zeros(T, Tuple(jsizes[j] for j in j_out)) - for index in CartesianIndices(jsizes) - C[(index[j] for j in j_out)...] += - A[(index[j] for j in jA)...] * B[(index[j] for j in jB)...] - end - return C -end - -function infer_junction_sizes(tensors, juncs, j_out) - @assert length(tensors) == length(juncs) - njunc = maximum(Iterators.flatten((Iterators.flatten(juncs), j_out))) - jsizes = fill(-1, njunc) - for (A, junc) in zip(tensors, juncs) - for (i, j) in enumerate(junc) - if jsizes[j] == -1 - jsizes[j] = size(A, i) - else - @assert jsizes[j] == size(A, i) - end - end - end - @assert all(s >= 0 for s in jsizes) - jsizes -end - -# Parsing and code generation -############################# - -""" Construct an undirected wiring diagram using tensor notation. - -The syntax is compatible with the Einstein-style tensor notation used by tensor -algebra packages like -[TensorOperations.jl](https://github.com/Jutho/TensorOperations.jl) and -[TensorCast.jl](https://github.com/mcabbott/TensorCast.jl). For example, the -wiring diagram for the composite of two matrices (or two binary relations) is -constructed by - -```julia -@tensor_network (i,j,k) C[i,k] := A[i,j] * B[j,k] -``` - -The leading context, or list of variables, may be omitted, in which case it is -inferred from the variables used in the tensor expression. So, in this example, -an equivalent macro call is - -```julia -@tensor_network C[i,k] := A[i,j] * B[j,k] -``` - -See also: [`gen_tensor_notation`](@ref), the "inverse" to this macro. -""" -macro tensor_network(exprs...) - :(parse_tensor_network($((QuoteNode(expr) for expr in exprs)...))) -end - -""" Parse an undirected wiring diagram from a tensor expression. - -For more information, see the corresponding macro [`@tensor_network`](@ref). -""" -function parse_tensor_network(context::Expr, expr::Expr) - all_vars = @match context begin - Expr(:tuple, args...) => collect(Symbol, args) - Expr(:vect, args...) => collect(Symbol, args) - end - parse_tensor_network(expr, all_vars=all_vars) -end - -function parse_tensor_network(expr::Expr; all_vars=nothing) - # Parse tensor expression. - (outer_name, outer_vars), body = @match expr begin - Expr(:(=), outer, body) => (parse_tensor_term(outer), body) - Expr(:(:=), outer, body) => (parse_tensor_term(outer), body) - _ => error("Tensor expression $expr must be an assignment, either = or :=") - end - names_and_vars = map(parse_tensor_term, @match body begin - Expr(:call, :(*), args...) => args - 1 => [] # No terms - arg => [arg] # One term - end) - - # Check compatibility of used variables with declared variables. - used_vars = unique!(reduce(vcat, ([[outer_vars]; last.(names_and_vars)]))) - if isnothing(all_vars) - all_vars = sort!(used_vars) - else - used_vars ⊆ all_vars || error("One of variables $used_vars is not declared") - end - - # Construct the undirected wiring diagram. - d = UntypedRelationDiagram{Symbol,Symbol}(length(outer_vars)) - add_junctions!(d, length(all_vars), variable=all_vars) - set_junction!(d, ports(d, outer=true), - only.(incident(d, outer_vars, :variable)), outer=true) - for (name, vars) in names_and_vars - box = add_box!(d, length(vars), name=name) - set_junction!(d, ports(d, box), only.(incident(d, vars, :variable))) - end - return d -end - -function parse_tensor_term(expr) - @match expr begin - Expr(:ref, name::Symbol, args...) => (name, collect(Symbol, args)) - name::Symbol => (name, Symbol[]) # Scalar - _ => error("Invalid syntax in term $expr in tensor expression") - end -end - -""" Contract a tensor network using a macro from another package. - -This macro takes two arguments: an undirected wiring diagram and a macro call -supporting the tensor contraction notation that is de facto standard among Julia -tensor packages. For example, to evaluate a tensor network using the `@tullio` -macro, use: - -```julia -A = @contract_tensors_with @tullio diagram -``` - -The following macros should work: - -- `@tensor` from - [TensorOperations.jl](https://github.com/Jutho/TensorOperations.jl). -- `@tullio` from [Tullio.jl](https://github.com/mcabbott/Tullio.jl) -- `@einsum` from [Einsum.jl](https://github.com/ahwillia/Einsum.jl) -- `@ein` from [OMEinsum.jl](https://github.com/under-Peter/OMEinsum.jl) - -However, the macros `@cast` and `@reduce` from -[TensorCast.jl](https://github.com/mcabbott/TensorCast.jl) will *not* work -because they do not support implicit summation. -""" -macro contract_tensors_with(diagram, macro_expr) - # XXX: We cannot use `GeneralizedGenerated.mk_function` here because packages - # like Tullio generate code with type parameters, which GG does not allow. - compile_expr = :(gen_tensor_notation($(esc(diagram)), - assign_op=:(:=), assign_name=gensym("out"))) - Expr(:call, esc(:eval), - :(_contract_tensors_with($compile_expr, $(QuoteNode(macro_expr))))) -end -function _contract_tensors_with(tensor_expr, macro_expr) - @match macro_expr begin - Expr(:macrocall, args...) => Expr(:macrocall, args..., tensor_expr) - _ => error("Expression $macro_expr is not a macro call") - end -end - -""" Generate Julia expression in tensor notation from undirected wiring diagram. - -This function is used to implement [`@contract_tensors_with`](@ref) but may be -useful in its own right. - -See also [`@tensor_network`](@ref), the "inverse" to this function. -""" -function gen_tensor_notation(d::UndirectedWiringDiagram; - assign_op::Symbol=:(=), assign_name::Symbol=:out) - vars = j -> subpart(d, j, :variable) - outer_vars = vars(junction(d, ports(d, outer=true), outer=true)) - terms = map(boxes(d)) do box - ref_expr(subpart(d, box, :name), vars(junction(d, ports(d, box)))) - end - lhs = ref_expr(assign_name, outer_vars) - rhs = if isempty(terms); 1 - elseif length(terms) == 1; first(terms) - else Expr(:call, :(*), terms...) end - Expr(assign_op, lhs, rhs) -end - -function ref_expr(name::Symbol, vars) - isempty(vars) ? name : Expr(:ref, name, vars...) -end - -end diff --git a/experiments/TensorNetworks/test/ScheduleUndirected.jl b/experiments/TensorNetworks/test/ScheduleUndirected.jl deleted file mode 100644 index 43de76c9b..000000000 --- a/experiments/TensorNetworks/test/ScheduleUndirected.jl +++ /dev/null @@ -1,73 +0,0 @@ -module TestScheduleUndirectedWiringDiagrams -using Test - -using LinearAlgebra: dot, tr - -using Catlab.Theories: codom -using Catlab.CategoricalAlgebra.CSets -using Catlab.WiringDiagrams -using Catlab.WiringDiagrams.ScheduleUndirectedWiringDiagrams: - composites, composite_junction, composite_ports, parent, box_parent -using TensorNetworks.TensorNetworks: @tensor_network - -composite_junctions(x::ACSet, c) = - composite_junction.(Ref(x), composite_ports(x, c)) - -# Open linear path -################## - -d = @tensor_network out[v,z] = A[v,w] * B[w,x] * C[x,y] * D[y,z] -s = schedule(d, order=reverse(boxes(d))) -@test length(composites(s)) == 3 -@test parent(s) == [2,3,3] -@test box_parent(s) == [3,2,1,1] - -nd = to_nested_diagram(s) -@test parent(nd) == [2,3,3] -@test box_parent(nd) == [3,2,1,1] -@test composite_junctions(nd, 1:3) == [[3,5], [2,5], [1,5]] - -matrices = map(randn, [(3,4), (4,5), (5,6), (6,7)]) -out = eval_schedule(nd, matrices) -@test out ≈ foldr(*, matrices) - -# Closed cycle -############## - -d = @tensor_network out[] = A[w,x] * B[x,y] * C[y,z] * D[z,w] -s = schedule(d) -nd = to_nested_diagram(s) -@test parent(nd) == [2,3,3] -@test box_parent(nd) == [1,1,2,3] -@test map(length, composite_ports(nd, 1:3)) == [2,2,0] -@test composite_junctions(nd, 1:2) == [[1,3], [1,4]] - -matrices = map(randn, [(10,5), (5,5), (5,5), (5,10)]) -out = eval_schedule(nd, matrices) -@test out[] ≈ tr(foldl(*, matrices)) - -# Tensor product -################ - -d = @tensor_network out[w,x,y,z] = A[w,x] * B[y,z] -s = schedule(d) -@test parent(s) == [1] -@test box_parent(s) == [1,1] - -A, B = randn((3,4)), randn((5,6)) -out = eval_schedule(s, [A, B]) -@test out ≈ (reshape(A, (3,4,1,1)) .* reshape(B, (1,1,5,6))) - -# Frobenius inner product -######################### - -d = @tensor_network out[] = A[x,y] * B[x,y] -s = schedule(d) -@test parent(s) == [1] -@test box_parent(s) == [1,1] - -A, B = randn((5,5)), randn((5,5)) -out = eval_schedule(s, [A, B]) -@test out[] ≈ dot(vec(A), vec(B)) - -end diff --git a/experiments/TensorNetworks/test/TensorNetworks.jl b/experiments/TensorNetworks/test/TensorNetworks.jl deleted file mode 100644 index 7afc3d787..000000000 --- a/experiments/TensorNetworks/test/TensorNetworks.jl +++ /dev/null @@ -1,80 +0,0 @@ -module TestTensorNetworks -using Test - -using LinearAlgebra: tr - -using Catlab.CategoricalAlgebra.CSets, Catlab.WiringDiagrams -using TensorNetworks.TensorNetworks - -# Parsing and code generation -############################# - -# Parsing -#-------- - -parsed = @tensor_network (i,j,k,ℓ) D[i,j,k] = A[i,ℓ] * B[j,ℓ] * C[k,ℓ] -d = RelationDiagram(3) -add_box!(d, 2, name=:A); add_box!(d, 2, name=:B); add_box!(d, 2, name=:C) -add_junctions!(d, 4, variable=[:i,:j,:k,:ℓ]) -set_junction!(d, [1,4,2,4,3,4]) -set_junction!(d, 1:3, outer=true) -@test parsed == d - -parsed = @tensor_network D[i,j,k] = A[i,ℓ] * B[j,ℓ] * C[k,ℓ] -@test parsed == d -parsed = @tensor_network D[i,j,k] := A[i,ℓ] * B[j,ℓ] * C[k,ℓ] -@test parsed == d - -# Degenerate case: single term. -parsed = @tensor_network B[i,j,k] = A[i,j,k] -d = singleton_diagram(RelationDiagram, 3, name=:A) -set_subpart!(d, :variable, [:i,:j,:k]) -@test parsed == d - -# Degenerate case: no terms. -parsed = @tensor_network A[i,j] = 1 -d = RelationDiagram(2) -add_junctions!(d, 2, variable=[:i,:j]) -set_junction!(d, 1:2, outer=true) -@test parsed == d - -# Round trip: parse then compile -#------------------------------- - -macro roundtrip_tensor(tensor) - quote - compiled = gen_tensor_notation(@tensor_network($tensor), - assign_op=:(=), assign_name=:out) - @test compiled == $(QuoteNode(tensor)) - end -end - -@roundtrip_tensor out[i,k] = A[i,j] * B[j,k] -@roundtrip_tensor out[i,j,k] = A[i,ℓ] * B[j,ℓ] * C[k,ℓ] -@roundtrip_tensor out = u[i] * A[i,j] * v[j] -@roundtrip_tensor out[j] = α * a[i] * B[i,j] - -# Evaluation -############ - -# Binary matrix multiply. -A, B = randn((3,4)), randn((4,5)) -network = @tensor_network C[i,k] = A[i,j] * B[j,k] -@test oapply(network, [A,B]) ≈ A*B - -# Ternary matrix multiply. -C = randn((5,6)) -network = @tensor_network D[i,ℓ] = A[i,j] * B[j,k] * C[k,ℓ] -@test oapply(network, [A,B,C]) ≈ A*B*C - -# Trace. -A = randn((5,5)) -network = @tensor_network out[] = A[i,i] -@test oapply(network, [A])[] ≈ tr(A) - -# Tensor product. -A, B = randn((3,4)), randn((5,6)) -network = @tensor_network C[i,j,k,ℓ] = A[i,j] * B[k,ℓ] -@test oapply(network, [A,B]) ≈ (reshape(A, (3,4,1,1)) .* reshape(B, (1,1,5,6))) - -end diff --git a/experiments/TensorNetworks/test/runtests.jl b/experiments/TensorNetworks/test/runtests.jl deleted file mode 100644 index c9119cb63..000000000 --- a/experiments/TensorNetworks/test/runtests.jl +++ /dev/null @@ -1,9 +0,0 @@ -using Test - -@testset "Core" begin - include("TensorNetworks.jl") -end - -@testset "Scheduling" begin - include("ScheduleUndirected.jl") -end diff --git a/src/wiring_diagrams/ScheduleUndirected.jl b/src/wiring_diagrams/ScheduleUndirected.jl deleted file mode 100644 index 4c7c27e0d..000000000 --- a/src/wiring_diagrams/ScheduleUndirected.jl +++ /dev/null @@ -1,239 +0,0 @@ -""" Scheduling and evaluation of undirected wiring diagrams. - -In category-theoretic terms, this module is about evaluating arbitrary -composites of morphisms in hypergraph categories. -""" -module ScheduleUndirectedWiringDiagrams -export AbstractNestedUWD, AbstractScheduledUWD, NestedUWD, ScheduledUWD, - SchedulingAlgorithm, SequentialSchedule, - eval_schedule, to_nested_diagram, schedule - -import Base: schedule -using DataStructures: IntDisjointSets, union!, in_same_set - -using GATlab, ...CategoricalAlgebra.CSets, ...CategoricalAlgebra.FinSets -using ..UndirectedWiringDiagrams, ..WiringDiagramAlgebras -using ..UndirectedWiringDiagrams: flat - -# Data types -############ - -""" Abstract type for C-sets that contain a scheduled UWDS. - -This type includes [`ScheduledUWD`](@ref) and [`NestedUWD`](@ref). -""" -@abstract_acset_type HasScheduledUWD <: HasUWD - -@present SchScheduledUWD <: SchUWD begin - Composite::Ob - - parent::Hom(Composite, Composite) - box_parent::Hom(Box, Composite) - - # Po-category axiom enforcing that composites form a rooted forest. - # parent <= id(Composite) -end - -@abstract_acset_type AbstractScheduledUWD <: HasScheduledUWD - -""" Scheduled undirected wiring diagram. - -A scheduled UWD consists of a UWD together with a set of *composite* nodes -forming a rooted tree, or in general a rooted forest, whose leaves are the -diagram's boxes. - -See also: [`NestedUWD`](@ref). -""" -@acset_type ScheduledUWD(SchScheduledUWD, - index=[:box, :junction, :outer_junction, :parent, :box_parent]) <: AbstractScheduledUWD - -ncomposites(x::HasScheduledUWD) = nparts(x, :Composite) -composites(x::HasScheduledUWD) = parts(x, :Composite) -parent(x::HasScheduledUWD, args...) = subpart(x, args..., :parent) -children(x::HasScheduledUWD, c::Int) = - filter(c′ -> c′ != c, incident(x, c, :parent)) -box_parent(x::HasScheduledUWD, args...) = subpart(x, args..., :box_parent) -box_children(x::HasScheduledUWD, args...) = incident(x, args..., :box_parent) - -""" Abstract type for C-sets that contained a nested UWD. -""" -@abstract_acset_type HasNestedUWD <: HasScheduledUWD - -@present SchNestedUWD <: SchScheduledUWD begin - CompositePort::Ob - - composite::Hom(CompositePort, Composite) - composite_junction::Hom(CompositePort, Junction) -end - -@abstract_acset_type AbstractNestedUWD <: HasNestedUWD - -""" Nested undirected wiring diagram. - -A nested UWD is a scheduled UWD whose composite nodes have been given ports, -making explicit the intermediate boxes in the composition. - -Nested UWDs are very similar but not quite identical to Robin Milner's -[bigraphs](https://doi.org/10.1017/CBO9780511626661). - -See also: [`ScheduledUWD`](@ref). -""" -@acset_type NestedUWD(SchNestedUWD, - index=[:box, :junction, :outer_junction, - :composite, :composite_junction, :parent, :box_parent]) <: AbstractNestedUWD - -composite_ports(x::HasNestedUWD, args...) = incident(x, args..., :composite) -composite_junction(x::HasNestedUWD, args...) = - subpart(x, args..., :composite_junction) -composite_ports_with_junction(x::HasNestedUWD, args...) = - incident(x, args..., :composite_junction) - -# Evaluation -############ - -""" Evaluate a scheduled UWD given a set of generators for the boxes. - -The optional first argument `f` should be a callable with the same signature as -[`oapply`](@ref) for UWD algebras, which by default is `oapply` itself. - -```julia -f(composite::UndirectedWiringDiagram, values::AbstractVector) -``` - -Nested UWDs are used as an auxiliary data structure during the evaluation. If -the schedule will be evaluated multiple times, you should explicitly convert it -to a nested UWD using [`to_nested_diagram`](@ref) and then call `eval_schedule` -on the resulting object instead. -""" -eval_schedule(schedule, generators::AbstractVector) = - eval_schedule(oapply, schedule, generators) - -eval_schedule(f, schedule::AbstractScheduledUWD, generators::AbstractVector) = - eval_schedule(f, to_nested_diagram(schedule), generators) - -function eval_schedule(f, d::AbstractNestedUWD, generators::AbstractVector) - # Evaluate `f` after constructing UWD for the composite. - function do_eval(values, juncs, outer_junc) - # Create diagram, adding boxes and ports. - composite = UndirectedWiringDiagram(length(outer_junc)) - for junc in juncs - add_box!(composite, length(junc)) - end - - # Set junctions, normalizing junction IDs to consecutive numbers `1:n`. - jmap = Dict{Int,Int}() - mapj!(j) = get!(jmap, j) do; add_junction!(composite) end - for (port, j) in enumerate(Iterators.flatten(juncs)) - set_subpart!(composite, port, :junction, mapj!(j)) - end - for (port, j) in enumerate(outer_junc) - set_subpart!(composite, port, :outer_junction, mapj!(j)) - end - - f(composite, values) - end - - # Mutually recursively evaluate children of composite `c`. - function eval_children(c::Int) - bs, cs = box_children(d, c), children(d, c) - values = isempty(cs) ? generators[bs] : # XXX: Avoid Any[]. - [ generators[bs]; map(eval_composite, cs) ] - juncs = [ [junction(d, ports(d, b)) for b in bs]; - [composite_junction(d, composite_ports(d, c′)) for c′ in cs] ] - (values, juncs) - end - - # Mutually recursively evaluate composite `c`. - function eval_composite(c::Int) - values, juncs = eval_children(c) - outer_junc = composite_junction(d, composite_ports(d, c)) - do_eval(values, juncs, outer_junc) - end - - # Evaluate diagram starting at the root, assumed to be unique. The root - # composite must be handled specially due to outer ports. - root = only(filter(c -> parent(d, c) == c, composites(d))) - values, juncs = eval_children(root) - do_eval(values, juncs, junction(d, outer=true)) -end - -""" Convert a scheduled UWD to a nested UWD. -""" -function to_nested_diagram(s::AbstractScheduledUWD) - d = NestedUWD() - copy_parts!(d, s) - - n = nboxes(s) - sets = IntDisjointSets(n + ncomposites(s)) - - function add_composite_ports!(c::Int) - # Recursively add ports to all child composites. - foreach(add_composite_ports!, children(d, c)) - - # Get all junctions incident to any child box or child composite. - js = unique!(sort!(flat([ - [ junction(d, ports(d, b)) for b in box_children(d, c) ]; - [ composite_junction(d, composite_ports(d, c′)) for c′ in children(d, c) ] - ]))) - - # Filter for "outgoing" junctions, namely those incident to any outer port - # or to any port of a box that is not a descendant of this composite. - c_rep = n+c - for b in box_children(d, c); union!(sets, c_rep, b) end - for c′ in children(d, c); union!(sets, c_rep, n+c′) end - js = filter!(js) do j - !(all(in_same_set(sets, c_rep, box(d, port)) - for port in ports_with_junction(d, j)) && - isempty(ports_with_junction(d, j, outer=true))) - end - - # Add port for each outgoing junction. - add_parts!(d, :CompositePort, length(js), composite=c, composite_junction=js) - end - - # Add ports to each composite, starting at roots. - roots = filter(c -> parent(d, c) == c, composites(d)) - foreach(add_composite_ports!, roots) - return d -end - -# Scheduling -############ - -abstract type SchedulingAlgorithm end - -""" Schedule diagram as a sequential chain of binary composites. - -This is the simplest possible scheduling algorithm, the equivalent of `foldl` -for undirected wiring diagrams. Unless otherwise specified, the boxes are folded -according to the order of their IDs, which is arbitrary. -""" -struct SequentialSchedule <: SchedulingAlgorithm end - -""" Schedule an undirected wiring diagram. - -By default, a simple sequential schedule is used. -""" -function schedule(d::AbstractUWD; - alg::SchedulingAlgorithm=SequentialSchedule(), kw...) - schedule(d, alg; kw...) -end - -function schedule(d::AbstractUWD, ::SequentialSchedule; - order::Union{AbstractVector{Int},Nothing}=nothing) - if isnothing(order) - order = boxes(d) - end - nb = nboxes(d) - @assert length(order) == nb - nc = max(1, nb-1) - - schedule = ScheduledUWD() - copy_parts!(schedule, d) - add_parts!(schedule, :Composite, nc, parent=[2:nc; nc]) - set_subpart!(schedule, order[1:min(2,nb)], :box_parent, 1) - set_subpart!(schedule, order[3:nb], :box_parent, 2:nc) - schedule -end - -end diff --git a/src/wiring_diagrams/WiringDiagrams.jl b/src/wiring_diagrams/WiringDiagrams.jl index ad6aa5798..97f4a291b 100644 --- a/src/wiring_diagrams/WiringDiagrams.jl +++ b/src/wiring_diagrams/WiringDiagrams.jl @@ -10,7 +10,6 @@ include("MonoidalUndirected.jl") include("Algebras.jl") include("Algorithms.jl") include("Expressions.jl") -include("ScheduleUndirected.jl") @reexport using .DirectedWiringDiagrams @reexport using .CPortGraphs @@ -20,7 +19,6 @@ include("ScheduleUndirected.jl") @reexport using .WiringDiagramAlgebras @reexport using .WiringDiagramAlgorithms @reexport using .WiringDiagramExpressions -@reexport using .ScheduleUndirectedWiringDiagrams include("Serialization.jl") include("GraphML.jl")