diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml new file mode 100644 index 0000000..a9f918a --- /dev/null +++ b/.buildkite/pipeline.yml @@ -0,0 +1,22 @@ +env: + JULIA_VERSION: "1.9.3" + +steps: + + - label: ":hammer: Build Project" + command: + - "module load julia" + - "julia --project --color=yes -e 'using Pkg; Pkg.instantiate()'" + - "julia --project=docs/ --color=yes -e 'using Pkg; Pkg.instantiate()'" + - "julia --project=docs/ --color=yes -e 'using Pkg; Pkg.precompile()'" + + - wait + + - label: ":scroll: Build docs" + command: + - "srun --mem=8G --time=1:00:00 --output=log_%j.log jobscript.sh" + env: + JULIA_PROJECT: "docs/" + + - wait + diff --git a/Project.toml b/Project.toml index 0195e2c..0f71721 100644 --- a/Project.toml +++ b/Project.toml @@ -1,18 +1,19 @@ -name = "DiagrammaticEquations.jl" -uuid = "b66562e1-fa90-4e8b-9505-c909188fab76" +name = "DiagrammaticEquations" +uuid = "6f00c28b-6bed-4403-80fa-30e0dc12f317" license = "MIT" -authors = authors = [ - "James Fairbanks", - "Andrew Baas", - "Evan Patterson", - "Luke Morris", - "George Rauta", -] +authors = ["James Fairbanks", "Andrew Baas", "Evan Patterson", "Luke Morris", "George Rauta"] version = "0.0.1" [deps] -Catlab = "d3509f1a-f4fe-4f63-b9c2-a8cefdb22603" +AlgebraicRewriting = "725a01d3-f174-5bbd-84e1-b9417bad95d9" +Catlab = "134e5e36-593f-5add-ad60-77f754baafbe" +DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" +MLStyle = "d8e11817-5142-5d16-987a-aa16d5891078" +Unicode = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" [compat] -Catlab = "^0.14" +AlgebraicRewriting = "0.2" +Catlab = "0.15" +DataStructures = "0.18.13" +MLStyle = "0.4.17" julia = "1.6" diff --git a/README.md b/README.md index e8cbab3..2661e32 100644 --- a/README.md +++ b/README.md @@ -1,83 +1,13 @@ -# AlgebraicX.jl +# DiagrammaticEquations.jl -[![Stable Documentation](https://img.shields.io/badge/docs-stable-blue.svg)](https://AlgebraicJulia.github.io/AlgebraicX.jl/stable) -[![Development Documentation](https://img.shields.io/badge/docs-dev-blue.svg)](https://AlgebraicJulia.github.io/AlgebraicX.jl/dev) -[![Code Coverage](https://codecov.io/gh/AlgebraicJulia/AlgebraicX.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/AlgebraicJulia/AlgebraicXe.jl) -[![CI/CD](https://github.com/AlgebraicJulia/AlgebraicX.jl/actions/workflows/julia_ci.yml/badge.svg)](https://github.com/AlgebraicJulia/AlgebraicX.jl/actions/workflows/julia_ci.yml) +[![Stable Documentation](https://img.shields.io/badge/docs-stable-blue.svg)](https://AlgebraicJulia.github.io/DiagrammaticEquations.jl/stable) +[![Development Documentation](https://img.shields.io/badge/docs-dev-blue.svg)](https://AlgebraicJulia.github.io/DiagrammaticEquations.jl/dev) +[![Code Coverage](https://codecov.io/gh/AlgebraicJulia/DiagrammaticEquations.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/AlgebraicJulia/DiagrammaticEquations.jl) +[![CI/CD](https://github.com/AlgebraicJulia/DiagrammaticEquations.jl/actions/workflows/julia_ci.yml/badge.svg)](https://github.com/AlgebraicJulia/DiagrammaticEquations.jl/actions/workflows/julia_ci.yml) -A template repository for making a new AlgebraicJulia package. +`DiagrammaticEquations.jl` is a Julia library implementing category theory to reason formally about diagrammatic representations of systems of algebraic and differential equations. -## πŸ› οΈ Usage +Currently DiagrammaticEquations.jl contains two submodules, `Deca` and `Learn` for supporting `Decapodes.jl` and `AlgebraicLearning.jl` functionality. -1. Use the "Use this template" dropdown to select "Create a new repository" -2. In the new page select "AlgebraicJulia" as the owner, give the repository a name such as "AlgebraicX.jl", and create a new repository from the template -3. Set up Codecov credentials for code coverage (If you have trouble, reach out to an AlgebraicJulia organization owner to help with this) - - 1. Log into [Codecov](https://codecov.io) with your GitHub account (this requires that you are a member of the AlgebraicJulia organization) - 2. Navigate to the [AlgebraicJulia organization](https://app.codecov.io/gh/AlgebraicJulia) - 3. Select your new repository from the list (e.x. "AlgebraicX") - 4. Note down the `CODECOV_TOKEN` value (It may be in the "Settings" tab if it doesn't show up immediately) - 5. Navigate back to your new GitHub repository and go to the Settings tab - 6. Go to "Security", "Secrets and variables", and "Actions" and click the "New repository secret" button - 7. Give the secret name `CODECOV_TOKEN` and the Secret value is the value you noted from the Codecov settings - 8. Click "Add secret" - -4. Clone the new repository, for example in the terminal: - ```sh - git clone https://github.com/AlgebraicJulia/AlgebraicX.jl.git - cd AlgebraicX.jl - ``` -5. Rename the file `src/AlgebraicX.jl` to match the name of your new package (e.x. "AlgebraicX") - ```sh - mv src/AlgebraicX.jl src/AlgebraicX.jl - ``` -6. Replace all instances of the word "AlgebraicX" with your new package name (e.x. "AlgebraicX") - ```sh - # On linux - git grep -l 'AlgebraicX' | xargs sed -i 's/AlgebraicX/AlgebraicX/g' - # On Mac OS X - git grep -l 'AlgebraicX' | xargs sed -i '' -e 's/AlgebraicX/AlgebraicX/g' - ``` -7. Generate a new random version 4 UUID (you can get one here: https://www.uuidgenerator.net/version4) - - We will assume for this example that your new UUID is `` -8. Replace all instances of the template's UUID, "b66562e1-fa90-4e8b-9505-c909188fab76", with your new UUID (e.x. "") - ```sh - # On linux - git grep -l 'b66562e1-fa90-4e8b-9505-c909188fab76' | xargs sed -i 's/b66562e1-fa90-4e8b-9505-c909188fab76//g' - # On Mac OS X - git grep -l 'b66562e1-fa90-4e8b-9505-c909188fab76' | xargs sed -i '' -e 's/b66562e1-fa90-4e8b-9505-c909188fab76//g' - ``` -9. Commit these new changes to your repository - ```sh - git commit -am "Set up skeleton for AlgebraicX.jl" - git push - ``` -10. Go back to your repository and wait until the tests have passed, you can check the status by going to the "Actions" tab in the repository - -### πŸ“” Set Up GitHub Pages (Public Repos Only) - -1. Follow the Usage steps above to set up a new template, make sure all initial GitHub Actions have passed -2. Navigate to the repository settings and go to "Code and automation", "Pages" -3. Make sure the "Source" dropdown is set to "Deploy from a branch" -4. Set the "Branch" dropdown to "gh-pages", make sure the folder is set to "/ (root)", and click "Save" -5. Go back to the main page of your repository and click the gear to the right of the "About" section in the right side column -6. Under "Website" check the checkbox that says "Use your GitHub Pages website" and click "Save changes" -7. You will now see a URL in the "About" section that will link to your package's documentation - -### πŸ›‘οΈ Set Up Branch Protection (Public Repos Only) - -1. Follow the Usage steps above to set up a new template, make sure all initial GitHub Actions have passed -2. Navigate to the repository settings and go to "Code and automation", "Branches" -3. Click "Add branch protection rule" to start adding branch protection -4. Under "Branch name pattern" put `main`, this will add protection to the main branch -5. Make sure to set the following options: - - Check the "Require a pull request before merging" - - Check the "Request status checks to pass before merging" and make sure the following status checks are added to the required list: - - CI / Documentation - - CI / Julia 1 - ubuntu-latest - x64 - push - - CI / Julia 1 - ubuntu-latest - x86 - push - - CI / Julia 1 - windows-latest - x64 - push - - CI / Julia 1 - windows-latest - x86 - push - - CI / Julia 1 - macOS-latest - x64 - push - - Check the "Restrict who can push to matching branches" and add `algebraicjuliabot` to the list of people with push access -6. Click "Save changes" to enable the branch protection +## Relevant Work +* Patterson, E., Baas, A., Hosgood, T., & Fairbanks, J. (2022). A diagrammatic view of differential equations in physics. arXiv preprint arXiv:2204.01843. diff --git a/docs/literate/literate_example.jl b/docs/literate/literate_example.jl index 079c756..98e8e98 100644 --- a/docs/literate/literate_example.jl +++ b/docs/literate/literate_example.jl @@ -1,13 +1,6 @@ # # Code Example # -# This is an example of adding a code example compiled with Literate.jl in the docs. -# -# First we want to load our package with `using` using DiagrammaticEquations -# ## Using `hello()` -# -# We provide the `hello(string)` method which prints "Hello, `string`!" -hello("World") diff --git a/docs/make.jl b/docs/make.jl index 473fa66..36b55d4 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -14,8 +14,8 @@ if !no_literate # Set Literate.jl config if not being compiled on recognized service. config = Dict{String,String}() if !(haskey(ENV, "GITHUB_ACTIONS") || haskey(ENV, "GITLAB_CI")) - config["nbviewer_root_url"] = "https://nbviewer.jupyter.org/github/AlgebraicJulia/AlgebraicX.jl/blob/gh-pages/dev" - config["repo_root_url"] = "https://github.com/AlgebraicJulia/AlgebraicX.jl/blob/main/docs" + config["nbviewer_root_url"] = "https://nbviewer.jupyter.org/github/AlgebraicJulia/DiagrammaticEquations.jl/blob/gh-pages/dev" + config["repo_root_url"] = "https://github.com/AlgebraicJulia/DiagrammaticEquations.jl/blob/main/docs" end for (root, dirs, files) in walkdir(literate_dir) @@ -36,11 +36,11 @@ end makedocs( modules=[DiagrammaticEquations], format=Documenter.HTML(), - sitename="AlgebraicX.jl", + sitename="DE.jl", doctest=false, checkdocs=:none, pages=Any[ - "AlgebraicX.jl"=>"index.md", + "DiagrammaticEquations.jl"=>"index.md", "Examples"=>Any[ "generated/literate_example.md", ], diff --git a/docs/src/api.md b/docs/src/api.md index 87b9db1..3b820c9 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -1,5 +1,5 @@ # Library Reference ```@autodocs -Modules = [DiagrammaticEquations] +Modules = [DiagrammaticEquations, DiagrammaticEquations.Deca, DiagrammaticEquations.Learn] ``` diff --git a/docs/src/index.md b/docs/src/index.md index 29bb77c..5852bec 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -4,4 +4,5 @@ CurrentModule = DiagrammaticEquations ``` -`DiagrammaticEquations.jl` is a Julia library for... +`DiagrammaticEquations.jl` is a Julia library implementing category-theoretic formally reasoning about systems of algebraic and differential equations with diagrams. + diff --git a/jobscript.sh b/jobscript.sh new file mode 100755 index 0000000..d467406 --- /dev/null +++ b/jobscript.sh @@ -0,0 +1,24 @@ +#!/bin/bash +#SBATCH --job-name=daemon_job_test # Job name +#SBATCH --mail-type=END,FAIL # Mail events (NONE, BEGIN, END, FAIL, ALL) +#SBATCH --mail-user=cuffaro.m@ufl.edu # Where to send mail +#SBATCH --ntasks=1 # Run on a single CPU +#SBATCH --mem=8gb # Job memory request +#SBATCH --time=00:05:00 # Time limit hrs:min:sec +#SBATCH --output=daemon_test_%j.log # Standard output and error log +pwd; hostname; date + +module load gcc/12.2.0 openmpi/4.1.5 julia + +echo "Running docs build..." + +julia --project=docs/ -e 'using Pkg; Pkg.status()' +julia --project=docs/ -e 'using Pkg; Pkg.instantiate(); include("docs/make.jl")' +# julia --project -e 'using Pkg; Pkg.status; Pkg.test()' > log_test.md +# ./job.sh + +# mpiexec -np 1 julia --project -e test/runtests.jl + +date + + diff --git a/src/DiagrammaticEquations.jl b/src/DiagrammaticEquations.jl new file mode 100644 index 0000000..f9a2812 --- /dev/null +++ b/src/DiagrammaticEquations.jl @@ -0,0 +1,67 @@ +""" Some description of ths package +""" +module DiagrammaticEquations + +export +DerivOp, append_dot, normalize_unicode, infer_states, +# Deca +recursive_delete_parents, findname, spacename, +op1_inf_rules_1D, op2_inf_rules_1D, op1_inf_rules_2D, op2_inf_rules_2D, op1_res_rules_1D, op2_res_rules_1D, op1_res_rules_2D, op2_res_rules_2D, varname, +## collages +Collage, collate, +## composition +oapply, unique_by, unique_by!, OpenSummationDecapodeOb, OpenSummationDecapode, Open, +## acset +unicode!, +SchDecapode, SchNamedDecapode, AbstractDecapode, AbstractNamedDecapode, NamedDecapode, SummationDecapode, +contract_operators, add_constant!, add_parameter, vec_to_dec!, infer_types!, +fill_names!, dot_rename!, expand_operators, infer_state_names, recognize_types, +resolve_overloads!, +## language +@decapode, Term, parse_decapode, term, Eq, DecaExpr, +# ~~~~~ +Plus, AppCirc1, AppCirc2, Var, Tan, App1, App2, Judgment, +## visualization +to_graphviz_property_graph, typename, +## rewrite +average_rewrite + + +using Catlab +using Catlab.Theories +import Catlab.Theories: otimes, oplus, compose, βŠ—, βŠ•, β‹…, associate, associate_unit, Ob, Hom, dom, codom +using Catlab.Programs +using Catlab.CategoricalAlgebra +using Catlab.WiringDiagrams +using Catlab.WiringDiagrams.DirectedWiringDiagrams +using Catlab.ACSetInterface +using MLStyle +import Unicode + +# using .Deca +# import .Deca: to_graphviz_property_graph + + +## TODO: +## generate schema from a _theory_ +## from presentation in the theory determine what to slice over +## Pavel S., Hypergraph theory string diagram + +normalize_unicode(s::String) = Unicode.normalize(s, compose=true, stable=true, chartransform=Unicode.julia_chartransform) +normalize_unicode(s::Symbol) = Symbol(normalize_unicode(String(s))) + +DerivOp = Symbol("βˆ‚β‚œ") +append_dot(s::Symbol) = Symbol(string(s)*'\U0307') + + +include("acset.jl") +include("language.jl") +include("composition.jl") +include("collages.jl") +include("visualization.jl") +include("rewrite.jl") + +include("deca/Deca.jl") +include("learn/Learn.jl") + +end diff --git a/src/acset.jl b/src/acset.jl new file mode 100644 index 0000000..7e4926c --- /dev/null +++ b/src/acset.jl @@ -0,0 +1,531 @@ +using Catlab +using Catlab.Theories: FreeSchema +using Catlab.DenseACSets +using DataStructures + +## TODO: +## generate schema from a _theory_ +## from presentation in the theory determine what to slice over +## Pavel S., Hypergraph theory string diagram +@present SchDecapode(FreeSchema) begin + (Var, TVar, Op1, Op2)::Ob + (Type, Operator)::AttrType + src::Hom(Op1, Var) + tgt::Hom(Op1, Var) + proj1::Hom(Op2, Var) + proj2::Hom(Op2, Var) # no product + res::Hom(Op2, Var) + incl::Hom(TVar, Var) # calculus? + + op1::Attr(Op1, Operator) + op2::Attr(Op2, Operator) + type::Attr(Var, Type) +end + +@present SchNamedDecapode <: SchDecapode begin + Name::AttrType + name::Attr(Var, Name) +end + +@abstract_acset_type AbstractDecapode +@abstract_acset_type AbstractNamedDecapode <: AbstractDecapode + +@acset_type Decapode(SchDecapode, + index=[:src, :tgt, :res, :incl, :op1, :op2, :type]) <: AbstractDecapode + +@acset_type NamedDecapode(SchNamedDecapode, + index=[:src, :tgt, :res, :incl, :op1, :op2, :type, :name]) <: AbstractNamedDecapode + + + +""" fill_names!(d::AbstractNamedDecapode) + +Provide a variable name to all the variables that don't have names. +""" +function fill_names!(d::AbstractNamedDecapode) + bulletcount = 1 + for i in parts(d, :Var) + if !isassigned(d[:,:name],i) || isnothing(d[i, :name]) + d[i,:name] = Symbol("β€’$bulletcount") + bulletcount += 1 + end + end + for e in incident(d, :βˆ‚β‚œ, :op1) + s = d[e,:src] + t = d[e, :tgt] + String(d[t,:name])[1] != 'β€’' && continue + d[t, :name] = append_dot(d[s,:name]) + end + d +end + +""" find_dep_and_order(d::AbstractNamedDecapode) + +Find the order of each tangent variable in the Decapode, and the index of the variable that it is dependent on. Returns a tuple of (dep, order), both of which respecting the order in which incident(d, :βˆ‚β‚œ, :op1) returns Vars. +""" +function find_dep_and_order(d::AbstractNamedDecapode) + dep = d[incident(d, :βˆ‚β‚œ, :op1), :src] + order = ones(Int, nparts(d, :TVar)) + found = true + while found + found = false + for i in parts(d, :TVar) + deps = incident(d, :βˆ‚β‚œ, :op1) ∩ incident(d, dep[i], :tgt) + if !isempty(deps) + dep[i] = d[first(deps), :src] + order[i] += 1 + found = true + end + end + end + (dep, order) +end + +""" dot_rename!(d::AbstractNamedDecapode) + +Rename tangent variables by their depending variable appended with a dot. +e.g. If D == βˆ‚β‚œ(C), then rename D to CΜ‡. + +If a tangent variable updates multiple vars, choose one arbitrarily. +e.g. If D == βˆ‚β‚œ(C) and D == βˆ‚β‚œ(B), then rename D to either CΜ‡ or B Μ‡. +""" +function dot_rename!(d::AbstractNamedDecapode) + dep, order = find_dep_and_order(d) + for (i,e) in enumerate(incident(d, :βˆ‚β‚œ, :op1)) + t = d[e, :tgt] + name = d[dep[i],:name] + for _ in 1:order[i] + name = append_dot(name) + end + d[t, :name] = name + end + d +end + +function make_sum_mult_unique!(d::AbstractNamedDecapode) + snum = 1 + mnum = 1 + for (i, name) in enumerate(d[:name]) + if(name == :sum) + d[i, :name] = Symbol("sum_$(snum)") + snum += 1 + elseif(name == :mult) + d[i, :name] = Symbol("mult_$(mnum)") + mnum += 1 + end + end +end + +# Note: This hard-bakes in Form0 through Form2, and higher Forms are not +# allowed. +function recognize_types(d::AbstractNamedDecapode) + unrecognized_types = setdiff(d[:type], [:Form0, :Form1, :Form2, :DualForm0, + :DualForm1, :DualForm2, :Literal, :Parameter, + :Constant, :infer]) + isempty(unrecognized_types) || + error("Types $unrecognized_types are not recognized.") +end + +function expand_operators(d::AbstractNamedDecapode) + #e = SummationDecapode{Symbol, Symbol, Symbol}() + e = SummationDecapode{Any, Any, Symbol}() + copy_parts!(e, d, (:Var, :TVar, :Op2)) + expand_operators!(e, d) + return e +end + +function expand_operators!(e::AbstractNamedDecapode, d::AbstractNamedDecapode) + newvar = 0 + for op in parts(d, :Op1) + if !isa(d[op,:op1], AbstractArray) + add_part!(e, :Op1, op1=d[op,:op1], src=d[op, :src], tgt=d[op,:tgt]) + elseif length(d[op, :op1]) == 1 + add_part!(e, :Op1, op1=only(d[op,:op1]), src=d[op, :src], tgt=d[op,:tgt]) + else + for (i, step) in enumerate(d[op, :op1]) + if i == 1 + newvar = add_part!(e, :Var, type=:infer, name=Symbol("β€’_$(op)_$(i)")) + add_part!(e, :Op1, op1=step, src=d[op, :src], tgt=newvar) + elseif i == length(d[op, :op1]) + add_part!(e, :Op1, op1=step, src=newvar, tgt=d[op,:tgt]) + else + newvarβ€² = add_part!(e, :Var, type=:infer, name=Symbol("β€’_$(op)_$(i)")) + add_part!(e, :Op1, op1=step, src=newvar, tgt=newvarβ€²) + newvar = newvarβ€² + end + end + end + end + return newvar +end + +@present SchSummationDecapode <: SchNamedDecapode begin + # Ξ£ are the white nodes in the Decapode drawing + # Summands are the edges that connect white nodes to variables (the projection maps) + # because addition is commutative, we don't need to distinguish the order + (Ξ£, Summand)::Ob + summand::Hom(Summand, Var) + summation::Hom(Summand, Ξ£) + sum::Hom(Ξ£, Var) +end + +@acset_type SummationDecapode(SchSummationDecapode, + index=[:src, :tgt, :res, :incl, :op1, :op2, :type]) <: AbstractNamedDecapode + +## TODO NEW +function infer_states(d::SummationDecapode) + filter(parts(d, :Var)) do v + length(incident(d, v, :tgt)) == 0 && + length(incident(d, v, :res)) == 0 && + length(incident(d, v, :sum)) == 0 && + d[v, :type] != :Literal + end +end + +infer_state_names(d) = d[infer_states(d), :name] + + +""" function expand_operators(d::SummationDecapode) + +Find operations that are compositions, and expand them with intermediate variables. +""" +function expand_operators(d::SummationDecapode) + #e = SummationDecapode{Symbol, Symbol, Symbol}() + e = SummationDecapode{Any, Any, Symbol}() + copy_parts!(e, d, (:Var, :TVar, :Op2, :Ξ£, :Summand)) + expand_operators!(e, d) + return e +end + +""" function contract_operators(d::SummationDecapode) + +Find chains of Op1s in the given Decapode, and replace them with +a single Op1 with a vector of function names. After this process, +all Vars that are not a part of any computation are removed. +""" +function contract_operators(d::SummationDecapode) + e = expand_operators(d) + contract_operators!(e) + #return e +end + +function contract_operators!(d::SummationDecapode) + chains = find_chains(d) + filter!(x -> length(x) != 1, chains) + for chain in chains + add_part!(d, :Op1, src=d[:src][first(chain)], tgt=d[:tgt][last(chain)], op1=Vector{Symbol}(d[:op1][chain])) + end + rem_parts!(d, :Op1, sort!(vcat(chains...))) + remove_neighborless_vars!(d) +end + +""" function remove_neighborless_vars!(d::SummationDecapode) + +Remove all Vars from the given Decapode that are not part of any computation. +""" +function remove_neighborless_vars!(d::SummationDecapode) + neighborless_vars = setdiff(parts(d,:Var), + union(d[:src], + d[:tgt], + d[:proj1], + d[:proj2], + d[:res], + d[:sum], + d[:summand], + d[:incl])) + #union(map(x -> t5_orig[x], [:src, :tgt])...) alternate syntax + #rem_parts!(d, :Var, neighborless_vars) + rem_parts!(d, :Var, sort!(neighborless_vars)) + d +end + +#""" +# function find_chains(d::SummationDecapode) +# +#Find chains of Op1s in the given Decapode. A chain ends when the +#target of the last Op1 is part of an Op2 or sum, or is a target +#of multiple Op1s. +#""" +function find_chains(d::SummationDecapode) + chains = [] + visited = falses(nparts(d, :Op1)) + # TODO: Re-write this without two reduce-vcats. + chain_starts = unique(reduce(vcat, reduce(vcat, + #[incident(d, Decapodes.infer_states(d), :src), + [incident(d, Vector{Int64}(filter(i -> !isnothing(i), DiagrammaticEquations.infer_states(d))), :src), + incident(d, d[:res], :src), + incident(d, d[:sum], :src)]))) + + s = Stack{Int64}() + foreach(x -> push!(s, x), chain_starts) + while !isempty(s) + # Start a new chain. + op_to_visit = pop!(s) + curr_chain = [] + while true + visited[op_to_visit] = true + append!(curr_chain, op_to_visit) + + tgt = d[op_to_visit, :tgt] + next_op1s = incident(d, tgt, :src) + next_op2s = vcat(incident(d, tgt, :proj1), incident(d, tgt, :proj2)) + if (length(next_op1s) != 1 || + length(next_op2s) != 0 || + is_tgt_of_many_ops(d, tgt) || + !isempty(incident(d, tgt, :sum)) || + !isempty(incident(d, tgt, :summand))) + # Terminate chain. + append!(chains, [curr_chain]) + for next_op1 in next_op1s + visited[next_op1] || push!(s, next_op1) + end + break + end + # Try to continue chain. + op_to_visit = only(next_op1s) + end + end + return chains +end + +function add_constant!(d::AbstractNamedDecapode, k::Symbol) + return add_part!(d, :Var, type=:Constant, name=k) +end + +function add_parameter(d::AbstractNamedDecapode, k::Symbol) + return add_part!(d, :Var, type=:Parameter, name=k) +end + + +function infer_summands_and_summations!(d::SummationDecapode) + # Note that we are not doing any type checking here! + # i.e. We are not checking for this: [Form0, Form1, Form0]. + applied = false + for Ξ£_idx in parts(d, :Ξ£) + summands = d[:summand][incident(d, Ξ£_idx, :summation)] + sum = d[:sum][Ξ£_idx] + idxs = [summands; sum] + types = d[:type][idxs] + all(t != :infer for t in types) && continue # We need not infer + all(t == :infer for t in types) && continue # We can not infer + + known_types = types[findall(!=(:infer), types)] + if :Literal ∈ known_types + # If anything is a Literal, then anything not inferred is a Constant. + inferred_type = :Constant + elseif !isnothing(findfirst(!=(:Constant), known_types)) + # If anything is a Form, then any term in this sum is the same kind of Form. + # Note that we are not explicitly changing Constants to Forms here, + # although we should consider doing so. + inferred_type = known_types[findfirst(!=(:Constant), known_types)] + else + # All terms are now a mix of Constant or infer. Set them all to Constant. + inferred_type = :Constant + end + to_infer_idxs = filter(i -> d[:type][i] == :infer, idxs) + d[to_infer_idxs, :type] = inferred_type + applied = true + end + return applied +end + +# TODO: Although the big-O complexity is the same, it might be more efficent on +# average to iterate over edges then rules, instead of rules then edges. This +# might result in more un-maintainable code. If you implement this, you might +# also want to make the rules keys in a Dict. +# It also might be more efficient on average to instead iterate over variables. +""" function infer_types!(d::SummationDecapode, op1_rules::Vector{NamedTuple{(:src_type, :tgt_type, :replacement_type, :op), NTuple{4, Symbol}}}) + +Infer types of Vars given rules wherein one type is known and the other not. +""" +function infer_types!(d::SummationDecapode, op1_rules::Vector{NamedTuple{(:src_type, :tgt_type, :op_names), Tuple{Symbol, Symbol, Vector{Symbol}}}}, op2_rules::Vector{NamedTuple{(:proj1_type, :proj2_type, :res_type, :op_names), Tuple{Symbol, Symbol, Symbol, Vector{Symbol}}}}) + + # This is an optimization so we do not "visit" a row which has no infer types. + # It could be deleted if found to be not worth maintainability tradeoff. + types_known_op1 = ones(Bool, nparts(d, :Op1)) + types_known_op1[incident(d, :infer, [:src, :type])] .= false + types_known_op1[incident(d, :infer, [:tgt, :type])] .= false + + types_known_op2 = zeros(Bool, nparts(d, :Op2)) + types_known_op2[incident(d, :infer, [:proj1, :type])] .= false + types_known_op2[incident(d, :infer, [:proj2, :type])] .= false + types_known_op2[incident(d, :infer, [:res, :type])] .= false + + while true + applied = false + + for rule in op1_rules + for op1_idx in parts(d, :Op1) + types_known_op1[op1_idx] && continue + + this_applied = apply_inference_rule_op1!(d, op1_idx, rule) + + types_known_op1[op1_idx] = this_applied + applied = applied || this_applied + end + end + + for rule in op2_rules + for op2_idx in parts(d, :Op2) + types_known_op2[op2_idx] && continue + + this_applied = apply_inference_rule_op2!(d, op2_idx, rule) + + types_known_op2[op2_idx] = this_applied + applied = applied || this_applied + end + end + + applied = applied || infer_summands_and_summations!(d) + applied || break # Break if no rules were applied. + end + + d +end + +# TODO: When SummationDecapodes are annotated with the degree of their space, +# use dispatch to choose the correct set of rules. +infer_types!(d::SummationDecapode) = + infer_types!(d, op1_inf_rules_2D, op2_inf_rules_2D) + +# TODO: You could write a method which auto-generates these rules given degree N. + +""" +These are the default rules used to do function resolution in the 1D exterior calculus. +""" +op1_res_rules_1D = [ + # Rules for d. + (src_type = :Form0, tgt_type = :Form1, resolved_name = :dβ‚€, op = :d), + (src_type = :DualForm0, tgt_type = :DualForm1, resolved_name = :dual_dβ‚€, op = :d), + # Rules for ⋆. + (src_type = :Form0, tgt_type = :DualForm1, resolved_name = :⋆₀, op = :⋆), + (src_type = :Form1, tgt_type = :DualForm0, resolved_name = :⋆₁, op = :⋆), + (src_type = :DualForm1, tgt_type = :Form0, resolved_name = :⋆₀⁻¹, op = :⋆), + (src_type = :DualForm0, tgt_type = :Form1, resolved_name = :⋆₁⁻¹, op = :⋆), + (src_type = :Form0, tgt_type = :DualForm1, resolved_name = :⋆₀, op = :star), + (src_type = :Form1, tgt_type = :DualForm0, resolved_name = :⋆₁, op = :star), + (src_type = :DualForm1, tgt_type = :Form0, resolved_name = :⋆₀⁻¹, op = :star), + (src_type = :DualForm0, tgt_type = :Form1, resolved_name = :⋆₁⁻¹, op = :star), + # Rules for Ξ΄. + (src_type = :Form1, tgt_type = :Form0, resolved_name = :δ₁, op = :Ξ΄), + (src_type = :Form1, tgt_type = :Form0, resolved_name = :δ₁, op = :codif)] + +# We merge 1D and 2D rules since it seems op2 rules are metric-free. If +# this assumption is false, this needs to change. +op2_res_rules_1D = [ + # Rules for ∧. + (proj1_type = :Form0, proj2_type = :Form0, res_type = :Form0, resolved_name = :βˆ§β‚€β‚€, op = :∧), + (proj1_type = :Form1, proj2_type = :Form0, res_type = :Form1, resolved_name = :βˆ§β‚β‚€, op = :∧), + (proj1_type = :Form0, proj2_type = :Form1, res_type = :Form1, resolved_name = :βˆ§β‚€β‚, op = :∧), + (proj1_type = :Form0, proj2_type = :Form0, res_type = :Form0, resolved_name = :βˆ§β‚€β‚€, op = :wedge), + (proj1_type = :Form1, proj2_type = :Form0, res_type = :Form1, resolved_name = :βˆ§β‚β‚€, op = :wedge), + (proj1_type = :Form0, proj2_type = :Form1, res_type = :Form1, resolved_name = :βˆ§β‚€β‚, op = :wedge), + # Rules for L. + (proj1_type = :Form1, proj2_type = :Form0, res_type = :Form0, resolved_name = :Lβ‚€, op = :L), + (proj1_type = :Form1, proj2_type = :Form1, res_type = :Form1, resolved_name = :L₁, op = :L), + # Rules for i. + (proj1_type = :Form1, proj2_type = :Form1, res_type = :Form0, resolved_name = :i₁, op = :i)] + + +""" +These are the default rules used to do function resolution in the 2D exterior calculus. +""" +op1_res_rules_2D = [ + # Rules for d. + (src_type = :Form0, tgt_type = :Form1, resolved_name = :dβ‚€, op = :d), + (src_type = :Form1, tgt_type = :Form2, resolved_name = :d₁, op = :d), + (src_type = :DualForm0, tgt_type = :DualForm1, resolved_name = :dual_dβ‚€, op = :d), + (src_type = :DualForm1, tgt_type = :DualForm2, resolved_name = :dual_d₁, op = :d), + # Rules for ⋆. + (src_type = :Form0, tgt_type = :DualForm2, resolved_name = :⋆₀, op = :⋆), + (src_type = :Form1, tgt_type = :DualForm1, resolved_name = :⋆₁, op = :⋆), + (src_type = :Form2, tgt_type = :DualForm0, resolved_name = :⋆₂, op = :⋆), + (src_type = :DualForm2, tgt_type = :Form0, resolved_name = :⋆₀⁻¹, op = :⋆), + (src_type = :DualForm1, tgt_type = :Form1, resolved_name = :⋆₁⁻¹, op = :⋆), + (src_type = :DualForm0, tgt_type = :Form2, resolved_name = :⋆₂⁻¹, op = :⋆), + (src_type = :Form0, tgt_type = :DualForm2, resolved_name = :⋆₀, op = :star), + (src_type = :Form1, tgt_type = :DualForm1, resolved_name = :⋆₁, op = :star), + (src_type = :Form2, tgt_type = :DualForm0, resolved_name = :⋆₂, op = :star), + (src_type = :DualForm2, tgt_type = :Form0, resolved_name = :⋆₀⁻¹, op = :star), + (src_type = :DualForm1, tgt_type = :Form1, resolved_name = :⋆₁⁻¹, op = :star), + (src_type = :DualForm0, tgt_type = :Form2, resolved_name = :⋆₂⁻¹, op = :star), + # Rules for Ξ΄. + (src_type = :Form2, tgt_type = :Form1, resolved_name = :Ξ΄β‚‚, op = :Ξ΄), + (src_type = :Form1, tgt_type = :Form0, resolved_name = :δ₁, op = :Ξ΄), + (src_type = :Form2, tgt_type = :Form1, resolved_name = :Ξ΄β‚‚, op = :codif), + (src_type = :Form1, tgt_type = :Form0, resolved_name = :δ₁, op = :codif), + # Rules for βˆ‡Β². + # TODO: Call this :nabla2 in ASCII? + (src_type = :Form0, tgt_type = :Form0, resolved_name = :βˆ‡Β²β‚€, op = :βˆ‡Β²), + (src_type = :Form1, tgt_type = :Form1, resolved_name = :βˆ‡Β²β‚, op = :βˆ‡Β²), + (src_type = :Form2, tgt_type = :Form2, resolved_name = :βˆ‡Β²β‚‚, op = :βˆ‡Β²), + # Rules for Ξ”. + (src_type = :Form0, tgt_type = :Form0, resolved_name = :Ξ”β‚€, op = :Ξ”), + (src_type = :Form1, tgt_type = :Form1, resolved_name = :Δ₁, op = :Ξ”), + (src_type = :Form1, tgt_type = :Form1, resolved_name = :Ξ”β‚‚, op = :Ξ”), + (src_type = :Form0, tgt_type = :Form0, resolved_name = :Ξ”β‚€, op = :lapl), + (src_type = :Form1, tgt_type = :Form1, resolved_name = :Δ₁, op = :lapl), + (src_type = :Form1, tgt_type = :Form1, resolved_name = :Ξ”β‚‚, op = :lapl)] + +# We merge 1D and 2D rules directly here since it seems op2 rules +# are metric-free. If this assumption is false, this needs to change. +op2_res_rules_2D = vcat(op2_res_rules_1D, [ + # Rules for ∧. + (proj1_type = :Form1, proj2_type = :Form1, res_type = :Form2, resolved_name = :βˆ§β‚β‚, op = :∧), + (proj1_type = :Form2, proj2_type = :Form0, res_type = :Form2, resolved_name = :βˆ§β‚‚β‚€, op = :∧), + (proj1_type = :Form0, proj2_type = :Form2, res_type = :Form2, resolved_name = :βˆ§β‚€β‚‚, op = :∧), + (proj1_type = :Form1, proj2_type = :Form1, res_type = :Form2, resolved_name = :βˆ§β‚β‚, op = :wedge), + (proj1_type = :Form2, proj2_type = :Form0, res_type = :Form2, resolved_name = :βˆ§β‚‚β‚€, op = :wedge), + (proj1_type = :Form0, proj2_type = :Form2, res_type = :Form2, resolved_name = :βˆ§β‚€β‚‚, op = :wedge), + # Rules for L. + (proj1_type = :Form1, proj2_type = :Form2, res_type = :Form2, resolved_name = :Lβ‚‚, op = :L), + (proj1_type = :Form1, proj2_type = :DualForm2, res_type = :DualForm2, resolved_name = :Lβ‚‚α΅ˆ, op = :L), + # Rules for i. + (proj1_type = :Form1, proj2_type = :Form2, res_type = :Form1, resolved_name = :iβ‚‚, op = :i)]) + +""" function resolve_overloads!(d::SummationDecapode, op1_rules::Vector{NamedTuple{(:src_type, :tgt_type, :resolved_name, :op), NTuple{4, Symbol}}}) + +Resolve function overloads based on types of src and tgt. +""" +function resolve_overloads!(d::SummationDecapode, op1_rules::Vector{NamedTuple{(:src_type, :tgt_type, :resolved_name, :op), NTuple{4, Symbol}}}, op2_rules::Vector{NamedTuple{(:proj1_type, :proj2_type, :res_type, :resolved_name, :op), NTuple{5, Symbol}}}) + for op1_idx in parts(d, :Op1) + src = d[:src][op1_idx]; tgt = d[:tgt][op1_idx]; op1 = d[:op1][op1_idx] + src_type = d[:type][src]; tgt_type = d[:type][tgt] + for rule in op1_rules + if op1 == rule[:op] && src_type == rule[:src_type] && tgt_type == rule[:tgt_type] + d[op1_idx, :op1] = rule[:resolved_name] + break + end + end + end + + for op2_idx in parts(d, :Op2) + proj1 = d[:proj1][op2_idx]; proj2 = d[:proj2][op2_idx]; res = d[:res][op2_idx]; op2 = d[:op2][op2_idx] + proj1_type = d[:type][proj1]; proj2_type = d[:type][proj2]; res_type = d[:type][res] + for rule in op2_rules + if op2 == rule[:op] && proj1_type == rule[:proj1_type] && proj2_type == rule[:proj2_type] && res_type == rule[:res_type] + d[op2_idx, :op2] = rule[:resolved_name] + break + end + end + end + + d +end + + +function replace_names!(d::SummationDecapode, op1_repls::Vector{Pair{Symbol, Any}}, op2_repls::Vector{Pair{Symbol, Symbol}}) + for (orig,repl) in op1_repls + for i in collect(incident(d, orig, :op1)) + d[i, :op1] = repl + end + end + for (orig,repl) in op2_repls + for i in collect(incident(d, orig, :op2)) + d[i, :op2] = repl + end + end + d +end + + diff --git a/src/collages.jl b/src/collages.jl new file mode 100644 index 0000000..07b528d --- /dev/null +++ b/src/collages.jl @@ -0,0 +1,48 @@ +struct Collage + src::SummationDecapode{Any,Any,Symbol} + tgt::SummationDecapode{Any,Any,Symbol} + uwd::Catlab.Programs.RelationalPrograms.UntypedUnnamedRelationDiagram{Symbol, Symbol} + symbols::Dict{Symbol, Symbol} +end + +collate(c::Collage) = collate(c.src, c.tgt, c.uwd, c.symbols) + +""" function collate(equations, boundaries, uwd, symbols) + +Create a collage of two Decapodes that simulates with boundary conditions. +``` +""" +function collate(equations, boundaries, uwd, symbols) + # TODO: This is assuming only "restriction"-type morphisms. + + f = SummationDecapode{Any, Any, Symbol}() + # TODO: Double-check + copy_parts!(f, equations, (:Var, :TVar, :Op1, :Op2, :Ξ£, :Summand)) + + # TODO: Throw an error if the user tries to use a boundary value differential + # form that is of a different type of the thing that we are applying the bound + # to. i.e. Form1 but target is a Form0. + + # TODO: This sets restrictions as Op1s. They are actually Op2s. i.e. Use `bv`. + for b in boxes(uwd) + ps = incident(uwd, b, :box) + ev = first(ps) + bv = last(ps) + en_key = uwd[junction(uwd, ev), :variable] + bn_key = uwd[junction(uwd, bv), :variable] + en = symbols[en_key] + bn = symbols[bn_key] + var = only(incident(f, en, :name)) + b_var = add_part!(f, :Var, type=f[var, :type], name=f[var, :name]) + f[var, :name] = Symbol("r$(b)_" * string(f[var, :name])) + s_var = add_part!(f, :Var, type=boundaries[only(incident(boundaries, bn, :name)), :type], name=bn) + add_part!(f, :Op2, proj1=b_var, proj2=s_var, res=var, op2=uwd[b, :name]) + + # Update tangent variable pointers, if any. + tangent_op1s = filter(x -> f[x, :op1]==:βˆ‚β‚œ, incident(f, var, :src)) + isempty(tangent_op1s) && continue + f[only(tangent_op1s), :src] = b_var + end + + f +end diff --git a/src/composition.jl b/src/composition.jl new file mode 100644 index 0000000..011b8d9 --- /dev/null +++ b/src/composition.jl @@ -0,0 +1,214 @@ +## TODO: Decapodey + +import Catlab.CategoricalAlgebra: apex, feet, legs +import Catlab.WiringDiagrams: oapply + +OpenSummationDecapodeOb, OpenSummationDecapode = OpenACSetTypes(SummationDecapode, :Var) + +#FIXME: why can't we just add a constructor for OpenSummationDecapode +""" Open(d::SummationDecapode{T,U,V}, names::AbstractVector{Symbol}) where {T,U,V} + +creates an OpenSummationDecapode based on named variables rather than variable indices. +See AlgebraicPetri.jl's Open for the analogous verion for LabelledReactionNetworks. +""" +function Open(d::SummationDecapode{T,U,V}, names::AbstractVector{Symbol}) where {T,U,V} + legs = map(names) do name + FinFunction(incident(d, name, :name), nparts(d, :Var)) + end + OpenSummationDecapode{T,U,V}(d, legs...) +end + +apex(Decapode::OpenSummationDecapode) = apex(Decapode.cospan) +legs(Decapode::OpenSummationDecapode) = legs(Decapode.cospan) +feet(Decapode::OpenSummationDecapode) = Decapode.feet + +""" function unique_by!(acset, column_names::Vector{Symbol}) + +Given column names from the same table, remove duplicate rows. + +WARNING: This function does not check if other tables index into the one given. +Removal of rows is performed with prejudice. + +See also: [`unique_by`](@ref). + +# Examples +```julia-repl +julia> unique_by!(parallel_arrows(Graph, 123), :E, [:src,:tgt]) == parallel_arrows(Graph, 1) +true +``` +""" +function unique_by!(acset, table::Symbol, columns::Vector{Symbol}) + # Note: Declarative CT methods are prefered to imperative index arithmetic. + rows = mapreduce(x -> acset[x], hcat, columns) + rem_parts!(acset, table, + setdiff(parts(acset, table), + unique(i -> rows[i,:], eachindex(eachrow(rows))))) + return acset +end + +""" function unique_by(acset, column_names::Vector{Symbol}) + +Given column names from the same table, return a copy of the acset with +duplicate rows removed. Removal of rows is performed with prejudice. + +WARNING: This function does not check if other tables index into the one given. +Removal of rows is performed with prejudice. + +See also: [`unique_by!`](@ref). + +# Examples +```julia-repl +julia> unique_by(parallel_arrows(Graph, 123), :E, [:src,:tgt]) == parallel_arrows(Graph, 1) +true +``` +""" +function unique_by(acset, table::Symbol, columns::Vector{Symbol}) + acset_copy = copy(acset) + unique_by!(acset_copy, table, columns) +end + +""" function type_check_Decapodes_composition(relation::RelationDiagram, decs::Vector{OpenSummationDecapode}) + +Check that the types of all Vars connected by the same junction match. + +This function only throws an error on the first type mismatch found. +""" +function type_check_Decapodes_composition(relation::RelationDiagram, decs::Vector{D}) where {D<:OpenSummationDecapode} + r = relation + types = [flatten([f[:type] for f in feet(d)]) for d in decs] + return all(map(junctions(r)) do j + ports = incident(r, j, :junction) + ts = types[ports] + all(ts[1] .== ts) + end) +end + + +#function oapply_rename(relation::RelationDiagram, Decapodes::Vector{OpenSummationDecapode}) +function oapply_rename(relation::RelationDiagram, Decapodes::Vector{D}) where D<:OpenSummationDecapode + r = relation + # The deepcopy. is necessary because if multiple Decapodes in the vector are + # OpenPodes of the same SummationDecapode, their apex will point to the same + # spot in memory. This interferes with renaming. + Decapodes_vars = deepcopy.(collect(map(apex, Decapodes))) + # FIXME: in this line, you should cast the SummationDecapode{S,T, Symbol} to SummationDecapode{S,T,Vector{Symbol}} + # This will allow you to return namespace scoped variables. + # Check that the number of Decapodes given matches the number of boxes in the + # relation. + num_boxes = nboxes(r) + num_Decapodes = length(Decapodes_vars) + # TODO: Should this be an ArgumentError? + num_boxes == num_Decapodes || error( + "$(num_boxes) Decapodes were specified in the relation but only "* + "$(num_Decapodes) were given.") + + # Check that the number of variables given in the relation is the same as the + # number of symbols in the corresponding vector of Vars. + # TODO: Should this be an ArgumentError? + for b ∈ boxes(r) + # Note: This only returns the first length mismatch found. + num_junctions = length(incident(r, b, :box)) + num_symbols = length(feet(Decapodes[b])) + num_junctions == num_symbols || let Decapode_name = r[b, :name] + error("Component $(Decapode_name) expects $(num_junctions) interface variables, but number of feet is $(num_symbols).") + end + # FIXME: this should also check the types of the feet. + end + + # Determine the mapping of global ports to local ports. In a RelationDiagram, + # this is baked into the order of rows in the Port table. + # This is a column that one could hcat to the Port table. + # local_ports = [lp for b=boxes(r) for lp=eachindex(ports(r, b))] + + # Check that types of variables connected by the same junction match. + # type_check_Decapodes_composition(relation, Decapodes_vars) || error("Composition Doesn't Typecheck") + + # Do namespacing. + # Append each Var name with the name @relation gave the Decapode. + # FIXME: return a list of symbols here instead of underscore separated list. + for b ∈ boxes(r) + box_name = r[b, :name] + for v ∈ parts(Decapodes_vars[b], :Var) + if Decapodes_vars[b][v, :type] != :Literal + var_name = Decapodes_vars[b][v, :name] + Decapodes_vars[b][v, :name] = Symbol(box_name, '_', var_name) + end + end + end + + # Write over the name fields to be what was specified by @relation. (oapply + # cannot combine objects whose attributes are not equal.) + + newnames = map(boxes(r)) do b + pode = Decapodes[b] + ports = incident(r, b, :box) + localnames = map(enumerate(ports)) do (i,p) + localnamevec = feet(pode)[i][:name] + length(localnamevec) == 1 || error("Decapode arguments to oapply do not support bundling. Each foot should have 1 vertex") + localnamevec[1] + end + + pode_vars = Decapodes_vars[b] + map(zip(ports, localnames)) do (p, lname) + # FIXME: this will break when we add proper namespacing + # Note: only is not necessary but is a useful check the Decapode is + # well-formed. If we ever want e.g. X:Form0 and X:Form1 in a single + # Decapode, this will need refactoring. + name = Symbol(r[b, :name], '_', lname) + var = only(incident(pode_vars, name, :name)) + j = r[p, :junction] + globalname = r[j, :variable] + pode_vars[var, :name] = globalname + return globalname + end + end + + newpodes = map(boxes(r)) do b + Open(Decapodes_vars[b], newnames[b]) + end + + uwd = UndirectedWiringDiagram(0) + copy_parts!(uwd, r) + + #return oapply(r, newpodes) + return oapply(uwd, newpodes) +end + +# Infinite loop: +""" function oapply(relation::RelationDiagram, podes::Vector{D}) where {D<:OpenSummationDecapode} + +Compose a list of Decapodes as specified by the given relation diagram. + +The Decapodes must be given in the same order as they were specified in the +relation. + +State variables (such as the (C,V) given in the head of the following +@relation) do not affect the result of a composition. + +# Examples +```julia-repl +julia> compose_diff_adv = @relation (C,V) begin + diffusion(C, ϕ₁) + advection(C, Ο•β‚‚, V) + superposition(ϕ₁, Ο•β‚‚, Ο•, C) +end; + +julia> oapply(compose_diff_adv, [(Diffusion, [:C, :Ο•]), + (Advection, [:C, :Ο•, :V]), (Superposition, [:ϕ₁, :Ο•β‚‚, :Ο•, :C])]); +``` +""" +oapply(r::RelationDiagram, podes::Vector{D}) where {D<:OpenSummationDecapode} = + oapply_rename(r, podes) +#oapply(r::RelationDiagram, podes::Vector{D}) where {D<:OpenSummationDecapode} = +# invoke(oapply, +# Tuple{UndirectedWiringDiagram, Vector{<:StructuredMulticospan{L}} where L}, +# r, oapply_rename(r, podes)) + +#oapply(r::RelationDiagram, pode::OpenSummationDecapode) = oapply(r, [pode]) +# Luke changed the above line to the below line, for e.g. the case: (Note H should be renamed to N.) +# r = @relation () begin +# heat(N) +# end +# oapply(r, OpenPode(Heat, [:H])) + +oapply(r::RelationDiagram, pode::OpenSummationDecapode) = oapply(r, [pode]) diff --git a/src/deca/Deca.jl b/src/deca/Deca.jl new file mode 100644 index 0000000..b596567 --- /dev/null +++ b/src/deca/Deca.jl @@ -0,0 +1,80 @@ +module Deca +using ..DiagrammaticEquations +using Catlab + +include("deca_acset.jl") +include("deca_visualization.jl") + +export normalize_unicode, varname, infer_states, typename, spacename + + + +## TODO: where? +function infer_states(d::SummationDecapode) + filter(parts(d, :Var)) do v + length(incident(d, v, :tgt)) == 0 && + length(incident(d, v, :res)) == 0 && + length(incident(d, v, :sum)) == 0 && + d[v, :type] != :Literal + end +end + +infer_state_names(d) = d[infer_states(d), :name] + + +""" + function recursive_delete_parents!(d::SummationDecapode, to_delete::Vector{Int64}) + +Delete the given nodes and their parents in the Decapode, recursively. +""" +function recursive_delete_parents(d::SummationDecapode, to_delete::Vector{Int64}) + e = SummationDecapode{Any, Any, Symbol}() + copy_parts!(e, d, (:Var, :TVar, :Op1, :Op2, :Ξ£, :Summand)) + isempty(to_delete) || recursive_delete_parents!(e, to_delete) + return e +end + +function recursive_delete_parents!(d::SummationDecapode, to_delete::Vector{Int64}) + # TODO: We assume to_delete vars have no children. Is that okay? + # TODO: Explicitly check that a Var in to_delete is not a summand. + vars_to_remove = Vector{Int64}() + s = Stack{Int64}() + foreach(v -> push!(s, v), to_delete) + while true + curr = pop!(s) + parents = reduce(vcat, + [d[incident(d, curr, :tgt), :src], + d[incident(d, curr, :res), :proj1], + d[incident(d, curr, :res), :proj2], + d[incident(d, curr, [:summation, :sum]), :summand]]) + + # Remove the operations which have curr as the result. + rem_parts!(d, :TVar, incident(d, curr, :incl)) + rem_parts!(d, :Op1, incident(d, curr, :tgt)) + rem_parts!(d, :Op2, incident(d, curr, :proj1)) + rem_parts!(d, :Op2, incident(d, curr, :proj2)) + rem_parts!(d, :Op2, incident(d, curr, :res)) + # Note: Delete Summands before Ξ£s. + rem_parts!(d, :Summand, incident(d, curr, [:summation, :sum])) + rem_parts!(d, :Ξ£, incident(d, curr, :sum)) + + # Do not remove parents which are used in some other computation. We rely + # on the fact that a parent is guaranteed to point to curr. + filter!(parents) do p + # p must not be the src of any Op1. + isempty(incident(d, p, :src)) && + # p must not be a proj of any Op2. + isempty(incident(d, p, :proj1)) && + isempty(incident(d, p, :proj2)) && + # p must not be a summand of any summation. + isempty(incident(d, p, :summand)) + end + foreach(p -> push!(s, p), parents) + + push!(vars_to_remove, curr) + + isempty(s) && break + end + rem_parts!(d, :Var, sort!(unique!(vars_to_remove))) +end +end diff --git a/src/deca/deca_acset.jl b/src/deca/deca_acset.jl new file mode 100644 index 0000000..38500c3 --- /dev/null +++ b/src/deca/deca_acset.jl @@ -0,0 +1,243 @@ +# TODO: You could write a method which auto-generates these rules given degree N. +""" +These are the default rules used to do type inference in the 1D exterior calculus. +""" +op1_inf_rules_1D = [ + # Rules for βˆ‚β‚œ + (src_type = :Form0, tgt_type = :Form0, op_names = [:βˆ‚β‚œ,:dt]), + (src_type = :Form1, tgt_type = :Form1, op_names = [:βˆ‚β‚œ,:dt]), + + # Rules for d + (src_type = :Form0, tgt_type = :Form1, op_names = [:d, :dβ‚€]), + (src_type = :DualForm0, tgt_type = :DualForm1, op_names = [:d, :dual_dβ‚€, :dΜƒβ‚€]), + + # Rules for ⋆ + (src_type = :Form0, tgt_type = :DualForm1, op_names = [:⋆, :⋆₀, :star]), + (src_type = :Form1, tgt_type = :DualForm0, op_names = [:⋆, :⋆₁, :star]), + (src_type = :DualForm1, tgt_type = :Form0, op_names = [:⋆, :⋆₀⁻¹, :star_inv]), + (src_type = :DualForm0, tgt_type = :Form1, op_names = [:⋆, :⋆₁⁻¹, :star_inv]), + + # Rules for Ξ” + (src_type = :Form0, tgt_type = :Form0, op_names = [:Ξ”, :Ξ”β‚€, :lapl]), + (src_type = :Form1, tgt_type = :Form1, op_names = [:Ξ”, :Δ₁, :lapl]), + + # Rules for Ξ΄ + (src_type = :Form1, tgt_type = :Form0, op_names = [:Ξ΄, :δ₁, :codif]), + + # Rules for negation + (src_type = :Form0, tgt_type = :Form0, op_names = [:neg, :(-)]), + (src_type = :Form1, tgt_type = :Form1, op_names = [:neg, :(-)])] + +op2_inf_rules_1D = [ + # Rules for βˆ§β‚€β‚€, βˆ§β‚β‚€, βˆ§β‚€β‚ + (proj1_type = :Form0, proj2_type = :Form0, res_type = :Form0, op_names = [:∧, :βˆ§β‚€β‚€, :wedge]), + (proj1_type = :Form1, proj2_type = :Form0, res_type = :Form1, op_names = [:∧, :βˆ§β‚β‚€, :wedge]), + (proj1_type = :Form0, proj2_type = :Form1, res_type = :Form1, op_names = [:∧, :βˆ§β‚€β‚, :wedge]), + + # Rules for Lβ‚€, L₁ + (proj1_type = :Form1, proj2_type = :Form0, res_type = :Form0, op_names = [:L, :Lβ‚€]), + (proj1_type = :Form1, proj2_type = :Form1, res_type = :Form1, op_names = [:L, :L₁]), + + # Rules for i₁ + (proj1_type = :Form1, proj2_type = :Form1, res_type = :Form0, op_names = [:i, :i₁]), + + # Rules for divison and multiplication + (proj1_type = :Form0, proj2_type = :Form0, res_type = :Form0, op_names = [:./, :.*]), + (proj1_type = :Form1, proj2_type = :Form1, res_type = :Form1, op_names = [:./, :.*]), + + # WARNING: This parameter type inference might be wrong, depending on what the user gives as a parameter + #= (proj1_type = :Parameter, proj2_type = :Form0, res_type = :Form0, op_names = [:/, :./, :*, :.*]), + (proj1_type = :Parameter, proj2_type = :Form1, res_type = :Form1, op_names = [:/, :./, :*, :.*]), + (proj1_type = :Parameter, proj2_type = :Form2, res_type = :Form2, op_names = [:/, :./, :*, :.*]), + + (proj1_type = :Form0, proj2_type = :Parameter, res_type = :Form0, op_names = [:/, :./, :*, :.*]), + (proj1_type = :Form1, proj2_type = :Parameter, res_type = :Form1, op_names = [:/, :./, :*, :.*]), + (proj1_type = :Form2, proj2_type = :Parameter, res_type = :Form2, op_names = [:/, :./, :*, :.*]),=# + + (proj1_type = :Literal, proj2_type = :Form0, res_type = :Form0, op_names = [:/, :./, :*, :.*]), + (proj1_type = :Literal, proj2_type = :Form1, res_type = :Form1, op_names = [:/, :./, :*, :.*]), + + (proj1_type = :Form0, proj2_type = :Literal, res_type = :Form0, op_names = [:/, :./, :*, :.*]), + (proj1_type = :Form1, proj2_type = :Literal, res_type = :Form1, op_names = [:/, :./, :*, :.*]), + + (proj1_type = :Constant, proj2_type = :Form0, res_type = :Form0, op_names = [:/, :./, :*, :.*]), + (proj1_type = :Constant, proj2_type = :Form1, res_type = :Form1, op_names = [:/, :./, :*, :.*]), + (proj1_type = :Form0, proj2_type = :Constant, res_type = :Form0, op_names = [:/, :./, :*, :.*]), + (proj1_type = :Form1, proj2_type = :Constant, res_type = :Form1, op_names = [:/, :./, :*, :.*]), + + (proj1_type = :Constant, proj2_type = :DualForm0, res_type = :DualForm0, op_names = [:/, :./, :*, :.*]), + (proj1_type = :Constant, proj2_type = :DualForm1, res_type = :DualForm1, op_names = [:/, :./, :*, :.*]), + (proj1_type = :DualForm0, proj2_type = :Constant, res_type = :DualForm0, op_names = [:/, :./, :*, :.*]), + (proj1_type = :DualForm1, proj2_type = :Constant, res_type = :DualForm1, op_names = [:/, :./, :*, :.*])] + +""" +These are the default rules used to do type inference in the 2D exterior calculus. +""" +op1_inf_rules_2D = [ + # Rules for βˆ‚β‚œ + (src_type = :Form0, tgt_type = :Form0, op_names = [:βˆ‚β‚œ, :dt]), + (src_type = :Form1, tgt_type = :Form1, op_names = [:βˆ‚β‚œ, :dt]), + (src_type = :Form2, tgt_type = :Form2, op_names = [:βˆ‚β‚œ, :dt]), + + # Rules for d + (src_type = :Form0, tgt_type = :Form1, op_names = [:d, :dβ‚€]), + (src_type = :Form1, tgt_type = :Form2, op_names = [:d, :d₁]), + (src_type = :DualForm0, tgt_type = :DualForm1, op_names = [:d, :dual_dβ‚€, :dΜƒβ‚€]), + (src_type = :DualForm1, tgt_type = :DualForm2, op_names = [:d, :dual_d₁, :d̃₁]), + + # Rules for ⋆ + (src_type = :Form0, tgt_type = :DualForm2, op_names = [:⋆, :⋆₀, :star]), + (src_type = :Form1, tgt_type = :DualForm1, op_names = [:⋆, :⋆₁, :star]), + (src_type = :Form2, tgt_type = :DualForm0, op_names = [:⋆, :⋆₂, :star]), + + (src_type = :DualForm2, tgt_type = :Form0, op_names = [:⋆, :⋆₀⁻¹, :star_inv]), + (src_type = :DualForm1, tgt_type = :Form1, op_names = [:⋆, :⋆₁⁻¹, :star_inv]), + (src_type = :DualForm0, tgt_type = :Form2, op_names = [:⋆, :⋆₂⁻¹, :star_inv]), + + # Rules for Ξ” + (src_type = :Form0, tgt_type = :Form0, op_names = [:Ξ”, :Ξ”β‚€, :lapl]), + (src_type = :Form1, tgt_type = :Form1, op_names = [:Ξ”, :Δ₁, :lapl]), + (src_type = :Form2, tgt_type = :Form2, op_names = [:Ξ”, :Ξ”β‚‚, :lapl]), + + # Rules for Ξ΄ + (src_type = :Form1, tgt_type = :Form0, op_names = [:Ξ΄, :δ₁, :codif]), + (src_type = :Form2, tgt_type = :Form1, op_names = [:Ξ΄, :Ξ΄β‚‚, :codif]), + + # Rules for negation + (src_type = :Form0, tgt_type = :Form0, op_names = [:neg, :(-)]), + (src_type = :Form1, tgt_type = :Form1, op_names = [:neg, :(-)]), + (src_type = :Form2, tgt_type = :Form2, op_names = [:neg, :(-)])] + +op2_inf_rules_2D = vcat(op2_inf_rules_1D, [ + # Rules for βˆ§β‚β‚, βˆ§β‚‚β‚€, βˆ§β‚€β‚‚ + (proj1_type = :Form1, proj2_type = :Form1, res_type = :Form2, op_names = [:∧, :βˆ§β‚β‚, :wedge]), + (proj1_type = :Form2, proj2_type = :Form0, res_type = :Form2, op_names = [:∧, :βˆ§β‚‚β‚€, :wedge]), + (proj1_type = :Form0, proj2_type = :Form2, res_type = :Form2, op_names = [:∧, :βˆ§β‚€β‚‚, :wedge]), + + (proj1_type = :Form1, proj2_type = :DualForm2, res_type = :DualForm2, op_names = [:L]), + + # Rules for Lβ‚‚ + (proj1_type = :Form1, proj2_type = :Form2, res_type = :Form2, op_names = [:L, :Lβ‚‚]), + + # Rules for i₁ + (proj1_type = :Form1, proj2_type = :Form2, res_type = :Form1, op_names = [:i, :i₁]), + + # Rules for subtraction + (proj1_type = :Form0, proj2_type = :Form0, res_type = :Form0, op_names = [:-, :.-]), + (proj1_type = :Form1, proj2_type = :Form1, res_type = :Form1, op_names = [:-, :.-]), + (proj1_type = :Form2, proj2_type = :Form2, res_type = :Form2, op_names = [:-, :.-]), + (proj1_type = :DualForm0, proj2_type = :DualForm0, res_type = :DualForm0, op_names = [:-, :.-]), + (proj1_type = :DualForm1, proj2_type = :DualForm1, res_type = :DualForm1, op_names = [:-, :.-]), + (proj1_type = :DualForm2, proj2_type = :DualForm2, res_type = :DualForm2, op_names = [:-, :.-]), + + # Rules for divison and multiplication + (proj1_type = :Form2, proj2_type = :Form2, res_type = :Form2, op_names = [:./, :.*]), + (proj1_type = :Literal, proj2_type = :Form2, res_type = :Form2, op_names = [:/, :./, :*, :.*]), + (proj1_type = :Form2, proj2_type = :Literal, res_type = :Form2, op_names = [:/, :./, :*, :.*])]) + +function apply_inference_rule_op1!(d::SummationDecapode, op1_id, rule) + type_src = d[d[op1_id, :src], :type] + type_tgt = d[d[op1_id, :tgt], :type] + + if(type_src != :infer && type_tgt != :infer) + return false + end + + score_src = (rule.src_type == type_src) + score_tgt = (rule.tgt_type == type_tgt) + check_op = (d[op1_id, :op1] in rule.op_names) + + if(check_op && (score_src + score_tgt == 1)) + d[d[op1_id, :src], :type] = rule.src_type + d[d[op1_id, :tgt], :type] = rule.tgt_type + return true + end + + return false +end + +function apply_inference_rule_op2!(d::SummationDecapode, op2_id, rule) + type_proj1 = d[d[op2_id, :proj1], :type] + type_proj2 = d[d[op2_id, :proj2], :type] + type_res = d[d[op2_id, :res], :type] + + if(type_proj1 != :infer && type_proj2 != :infer && type_res != :infer) + return false + end + + score_proj1 = (rule.proj1_type == type_proj1) + score_proj2 = (rule.proj2_type == type_proj2) + score_res = (rule.res_type == type_res) + check_op = (d[op2_id, :op2] in rule.op_names) + + if(check_op && (score_proj1 + score_proj2 + score_res == 2)) + d[d[op2_id, :proj1], :type] = rule.proj1_type + d[d[op2_id, :proj2], :type] = rule.proj2_type + d[d[op2_id, :res], :type] = rule.res_type + return true + end + + return false +end + +ascii_to_unicode_op1 = Pair{Symbol, Any}[ + (:dt => :βˆ‚β‚œ), + (:star => :⋆), + (:lapl => :Ξ”), + (:codif => :Ξ΄), + (:star_inv => :⋆⁻¹)] + +ascii_to_unicode_op2 = [ + (:wedge => :∧)] + +""" function unicode!(d::SummationDecapode) + +Replace ASCII operators with their Unicode equivalents. +""" +unicode!(d::SummationDecapode) = replace_names!(d, ascii_to_unicode_op1, ascii_to_unicode_op2) + +vec_to_dec_op1 = [ + (:grad => :d), + (:div => [:⋆,:d,:⋆]), + (:curl => [:d,:⋆]), + (:βˆ‡ => :d), + (Symbol("βˆ‡α΅ˆ") => [:⋆,:d,:⋆]), + # Note: This is x, not \times. + (Symbol("βˆ‡x") => [:d,:⋆])] + +vec_to_dec_op2 = Pair{Symbol, Symbol}[] + +""" function vec_to_dec!(d::SummationDecapode) + +Replace Vector Calculus operators with Discrete Exterior Calculus equivalents. +""" +function vec_to_dec!(d::SummationDecapode) + # Perform simple substitutions. + replace_names!(d, vec_to_dec_op1, vec_to_dec_op2) + + # Replace `adv` with divergence of ∧. + advs = incident(d, :adv, :op2) + adv_tgts = d[advs, :res] + + # Intermediate wedges. + wedge_tgts = add_parts!(d, :Var, length(adv_tgts), name=map(i -> Symbol("β€’_adv_$i"), eachindex(advs)), type=:infer) + # Divergences. + add_parts!(d, :Op1, length(adv_tgts), src=wedge_tgts, tgt=adv_tgts, op1=fill([:⋆,:d,:⋆],length(advs))) + # Point advs to the intermediates. + d[collect(advs), :res] = wedge_tgts + + # Replace adv with ∧. + d[collect(advs), :op2] = fill(:∧,length(advs)) + + d +end + +# TODO: When SummationDecapodes are annotated with the degree of their space, +# use dispatch to choose the correct set of rules. +""" function resolve_overloads!(d::SummationDecapode) + +Resolve function overloads based on types of src and tgt. +""" +resolve_overloads!(d::SummationDecapode) = + resolve_overloads!(d, op1_res_rules_2D, op2_res_rules_2D) + diff --git a/src/deca/deca_visualization.jl b/src/deca/deca_visualization.jl new file mode 100644 index 0000000..93a54f0 --- /dev/null +++ b/src/deca/deca_visualization.jl @@ -0,0 +1,119 @@ +### Drawing of Decapodes +import Catlab.Graphics.Graphviz +using Catlab.Graphics +using Catlab.Graphics.Graphviz +using Catlab.Graphs.PropertyGraphs +using Catlab.Graphs +using Catlab.Graphs.BasicGraphs + + +# TODO: Change orientation to print +# TODO: generalize ok?? +""" Graphics.to_graphviz(F::AbstractDecapode; directed = true, kw...) + +Visualize the given Decapode through Graphviz. Ensure that you have called `using Catlab.Graphics` before-hand, and have a way of visualizing SVG files in your current environment. +""" +Graphics.to_graphviz(F::AbstractDecapode; directed = true, kw...) = +to_graphviz(GraphvizGraphs.to_graphviz_property_graph(F; typename, directed, kw...)) + +Decapode_edge_label(s::Symbol) = String(s) +Decapode_edge_label(s::Vector{Symbol}) = join(String.(s), "β‹…") +Decapode_edge_label(s::String) = s +Decapode_edge_label(s::Vector{String}) = join(s, "β‹…") + +reg_to_sub = Dict("Form0"=>'β‚€', "Form1"=>"₁", "Form2"=>'β‚‚', "Form3"=>'₃', + "DualForm0"=>'β‚€', "DualForm1"=>"₁", "DualForm2"=>'β‚‚', "DualForm3"=>'₃', + "infer"=>'β€’', "Literal"=>'L', "Constant"=>'C', "Parameter"=>'P', ) + +toSub(digit::String) = get(reg_to_sub, digit, digit) + +## TODO refactor +spacename(d, v) = begin + t = d[v, :type] + subscript = toSub(String(t)) + dom = startswith(String(t), "Dual") ? "Ξ©Μƒ" : "Ξ©" + # TODO: To get rid of omega for non-form types + # dom = endswith(String(t), r"[0-9]") ? dom : "" + return "$dom$subscript" + # return t +end +varname(d, v) = "$(d[v, :name]):$(spacename(d, v))" + + +# TODO: generalize ok?? +function Catlab.Graphics.to_graphviz_property_graph(d::SummationDecapode; typename=spacename, directed = true, prog = "dot", node_attrs=Dict(), edge_attrs=Dict(), graph_attrs=Dict(), node_labels = true, kw...) + + default_graph_attrs = Dict(:rankdir => "TB") + default_edge_attrs = Dict() + default_node_attrs = Dict(:shape => "oval") + + G = to_graphviz_property_graph(Catlab.Graphs.Graph(0); prog, node_labels, + node_attrs = merge!(default_node_attrs, node_attrs), + edge_attrs = merge!(default_edge_attrs, edge_attrs), + graph_attrs = merge!(default_graph_attrs, graph_attrs)) + + vids = map(parts(d, :Var)) do v + add_vertex!(G, label=varname(d,v)) + end + + # Add entry and exit vertices and wires + if(directed) + tempin = add_vertex!(G, shape = "none", label = "") + for state in infer_states(d) + add_edge!(G, tempin, vids[state]) + end + + tempout = add_vertex!(G, shape = "none", label = "") + for tvar in d[:incl] + add_edge!(G, vids[tvar], tempout) + end + end + + map(parts(d, :Op1)) do op + s, t = d[op, :src], d[op, :tgt] + add_edge!(G, vids[s],vids[t], label=Decapode_edge_label(d[op,:op1])) + end + + ## TODO: passing in typename, despite it being a variable, + map(parts(d, :Op2)) do op + s, t = d[op, :proj1], d[op, :proj2] + r = d[op, :res] + # S, T, = typename(d, s), typename(d, t) + # v = add_vertex!(G, label="$(S)Γ—$(T)", shape="rectangle") + v = add_vertex!(G, label="$(spacename(d, s))Γ—$(spacename(d,t))", shape="rectangle") + + # If in directed mode, sources point into the projection field + # Else, everything points out + if(directed) + add_edge!(G, vids[s], v, label="π₁", style="dashed") + add_edge!(G, vids[t], v, label="Ο€β‚‚", style="dashed") + else + add_edge!(G, v, vids[s], label="π₁", style="dashed") + add_edge!(G, v, vids[t], label="Ο€β‚‚", style="dashed") + end + add_edge!(G, v, vids[r], label=Decapode_edge_label(d[op, :op2])) + end + + findvid(G, d, v) = incident(G.graph, [Dict{Symbol, Any}(:label=>varname(d, v))], :vprops) + white_nodes = map(parts(d, :Ξ£)) do s + v = add_vertex!(G, label="Ξ£$s", shape="circle") + u = d[s, :sum] + matches = first(findvid(G, d, u)) + length(matches) == 1 || error("did not find a unique vertex match for Ξ£$s") + uG = first(matches) + add_edge!(G, v, uG, label="+") + return v + end + for e in parts(d, :Summand) + # If directed, point summands into the sum + # Else, everything points outward + if(directed) + e = add_edge!(G, d[e, :summand], white_nodes[d[e, :summation]], style="dashed") + else + e = add_edge!(G, white_nodes[d[e, :summation]], d[e, :summand], style="dashed") + end + end + return G +end + + diff --git a/src/language.jl b/src/language.jl new file mode 100644 index 0000000..2e4750e --- /dev/null +++ b/src/language.jl @@ -0,0 +1,249 @@ +# Definition of the Decapodes DSL AST +# TODO: do functions and tans need to be parameterized by a space? +# TODO: Add support for higher order functions. +# - This is straightforward from a language perspective but unclear the best +# - way to represent this in a Decapode ACSet. +@data Term begin + Var(name::Symbol) + Lit(name::Symbol) + Judgment(var::Var, dim::Symbol, space::Symbol) # Symbol 1: Form0 Symbol 2: X + AppCirc1(fs::Vector{Symbol}, arg::Term) + App1(f::Symbol, arg::Term) + App2(f::Symbol, arg1::Term, arg2::Term) + Plus(args::Vector{Term}) + Mult(args::Vector{Term}) + Tan(var::Term) +end + +@data Equation begin + Eq(lhs::Term, rhs::Term) +end + +# A struct to store a complete Decapode +@as_record struct DecaExpr + context::Vector{Judgment} + equations::Vector{Equation} +end + +term(s::Symbol) = Var(normalize_unicode(s)) +term(s::Number) = Lit(Symbol(s)) + +term(expr::Expr) = begin + @match expr begin + #TODO: Would we want βˆ‚β‚œ to be used with general expressions or just Vars? + Expr(:call, :βˆ‚β‚œ, b) => Tan(Var(b)) + Expr(:call, :dt, b) => Tan(Var(b)) + + Expr(:call, Expr(:call, :∘, a...), b) => AppCirc1(a, term(b)) + Expr(:call, a, b) => App1(a, term(b)) + + Expr(:call, :+, xs...) => Plus(term.(xs)) + Expr(:call, f, x, y) => App2(f, term(x), term(y)) + + # TODO: Will later be converted to Op2's or schema has to be changed to include multiplication + Expr(:call, :*, xs...) => Mult(term.(xs)) + + x => error("Cannot construct term from $x") + end +end + +function parse_decapode(expr::Expr) + stmts = map(expr.args) do line + @match line begin + ::LineNumberNode => missing + # TODO: If user doesn't provide space, this gives a temp space so we can continue to construction + # For now spaces don't matter so this is fine but if they do, this will need to change + Expr(:(::), a::Symbol, b::Symbol) => Judgment(Var(a), b, :I) + Expr(:(::), a::Expr, b::Symbol) => map(sym -> Judgment(Var(sym), b, :I), a.args) + + Expr(:(::), a::Symbol, b) => Judgment(Var(a), b.args[1], b.args[2]) + Expr(:(::), a::Expr, b) => map(sym -> Judgment(Var(sym), b.args[1], b.args[2]), a.args) + + Expr(:call, :(==), lhs, rhs) => Eq(term(lhs), term(rhs)) + _ => error("The line $line is malformed") + end + end |> skipmissing |> collect + judges = [] + eqns = [] + foreach(stmts) do s + @match s begin + ::Judgment => push!(judges, s) + ::Vector{Judgment} => append!(judges, s) + ::Eq => push!(eqns, s) + _ => error("Statement containing $s of type $(typeof(s)) was not added.") + end + end + DecaExpr(judges, eqns) +end +# to_Decapode helper functions +### TODO - Matt: we need to generalize this +reduce_term!(t::Term, d::AbstractDecapode, syms::Dict{Symbol, Int}) = + let ! = reduce_term! + @match t begin + Var(x) => begin + if haskey(syms, x) + syms[x] + else + res_var = add_part!(d, :Var, name = x, type=:infer) + syms[x] = res_var + end + end + Lit(x) => begin + if haskey(syms, x) + syms[x] + else + res_var = add_part!(d, :Var, name = x, type=:Literal) + syms[x] = res_var + end + end + App1(f, t) || AppCirc1(f, t) => begin + res_var = add_part!(d, :Var, type=:infer) + add_part!(d, :Op1, src=!(t,d,syms), tgt=res_var, op1=f) + return res_var + end + App2(f, t1, t2) => begin + res_var = add_part!(d, :Var, type=:infer) + add_part!(d, :Op2, proj1=!(t1,d,syms), proj2=!(t2,d,syms), res=res_var, op2=f) + return res_var + end + Plus(ts) => begin + summands = [!(t,d,syms) for t in ts] + res_var = add_part!(d, :Var, type=:infer, name=:sum) + n = add_part!(d, :Ξ£, sum=res_var) + map(summands) do s + add_part!(d, :Summand, summand=s, summation=n) + end + return res_var + end + # TODO: Just for now assuming we have 2 or more terms + Mult(ts) => begin + multiplicands = [!(t,d,syms) for t in ts] + res_var = add_part!(d, :Var, type=:infer, name=:mult) + m1,m2 = multiplicands[1:2] + add_part!(d, :Op2, proj1=m1, proj2=m2, res=res_var, op2=Symbol("*")) + for m in multiplicands[3:end] + m1 = res_var + m2 = m + res_var = add_part!(d, :Var, type=:infer, name=:mult) + add_part!(d, :Op2, proj1=m1, proj2=m2, res=res_var, op2=Symbol("*")) + end + return res_var + end + Tan(t) => begin + # TODO: this is creating a spurious variable with the same name + txv = add_part!(d, :Var, type=:infer) + tx = add_part!(d, :TVar, incl=txv) + # TODO - Matt: DerivOp being used here + tanop = add_part!(d, :Op1, src=!(t,d,syms), tgt=txv, op1=DerivOp) + return txv #syms[x[1]] + end + _ => throw("Inline type Judgments not yet supported!") + end + end + +function eval_eq!(eq::Equation, d::AbstractDecapode, syms::Dict{Symbol, Int}, deletions::Vector{Int}) + @match eq begin + Eq(t1, t2) => begin + lhs_ref = reduce_term!(t1,d,syms) + rhs_ref = reduce_term!(t2,d,syms) + + # Always let the a named variable take precedence + # TODO: If we have variable to variable equality, we want + # some kind of way to check track of this equality + ref_pair = (t1, t2) + @match ref_pair begin + (Var(a), Var(b)) => return d + (t1, Var(b)) => begin + lhs_ref, rhs_ref = rhs_ref, lhs_ref + end + _ => nothing + end + + # Make rhs_ref equal to lhs_ref and adjust all its incidents + + # Case rhs_ref is a Tan + # WARNING: Don't push to deletion here because all TanVars should have a + # corresponding Op1. Pushing here would create a duplicate which breaks rem_parts! + for rhs in incident(d, rhs_ref, :incl) + d[rhs, :incl] = lhs_ref + end + # Case rhs_ref is a Op1 + for rhs in incident(d, rhs_ref, :tgt) + d[rhs, :tgt] = lhs_ref + push!(deletions, rhs_ref) + end + # Case rhs_ref is a Op2 + for rhs in incident(d, rhs_ref, :res) + d[rhs, :res] = lhs_ref + push!(deletions, rhs_ref) + end + # Case rhs_ref is a Plus + # FIXME: this typeguard is a subsitute for refactoring into multiple dispatch + if isa(d, SummationDecapode) + for rhs in incident(d, rhs_ref, :sum) + d[rhs, :sum] = lhs_ref + push!(deletions, rhs_ref) + end + end + # TODO: delete unused vars. The only thing stopping me from doing + # this is I don't know if CSet deletion preserves incident relations + #rem_parts!(d, :Var, sort(deletions)) + end + end + return d +end + +#""" Takes a DecaExpr (i.e. what should be constructed using the @Decapode macro) +#and gives a Decapode ACSet which represents equalities as two operations with the +#same tgt or res map. +#""" +# Just to get up and running, I tagged untyped variables with :infer +# TODO: write a type checking/inference step for this function to +# overwrite the :infer tags +function Decapode(e::DecaExpr) + d = Decapode{Any, Any}() + symbol_table = Dict{Symbol, Int}() + for Judgment in e.context + var_id = add_part!(d, :Var, type=(Judgment.dim, Judgment.space)) + symbol_table[Judgment.var.name] = var_id + end + deletions = Vector{Int}() + for eq in e.equations + eval_eq!(eq, d, symbol_table, deletions) + end + rem_parts!(d, :Var, sort(deletions)) + recognize_types(d) + return d +end + +function SummationDecapode(e::DecaExpr) + d = SummationDecapode{Any, Any, Symbol}() + symbol_table = Dict{Symbol, Int}() + + for Judgment in e.context + var_id = add_part!(d, :Var, name=Judgment.var.name, type=Judgment.dim) + symbol_table[Judgment.var.name] = var_id + end + + deletions = Vector{Int}() + for eq in e.equations + eval_eq!(eq, d, symbol_table, deletions) + end + rem_parts!(d, :Var, sort(deletions)) + + recognize_types(d) + + fill_names!(d) + d[:name] = normalize_unicode.(d[:name]) + make_sum_mult_unique!(d) + return d +end + + +""" macro Decapode(e) + +Construct a Decapode. +""" +macro decapode(e) + :(SummationDecapode(parse_decapode($(Meta.quot(e))))) +end diff --git a/src/learn/Learn.jl b/src/learn/Learn.jl new file mode 100644 index 0000000..70102ed --- /dev/null +++ b/src/learn/Learn.jl @@ -0,0 +1,3 @@ +module Learn + +end diff --git a/src/rewrite.jl b/src/rewrite.jl new file mode 100644 index 0000000..fbb3920 --- /dev/null +++ b/src/rewrite.jl @@ -0,0 +1,336 @@ +using Catlab +using AlgebraicRewriting +using AlgebraicRewriting: rewrite + +#""" function get_valid_op1s(deca_source::SummationDecapode, varID) +#Searches SummationDecapode, deca_source, at the request varID +#and returns all op1s which are allowed to be averaged. Returns +#an array of indices of valid op1 sources. +# +#Namely this is meant to exclude βˆ‚β‚œ from being included in an average. +#""" +function get_valid_op1s(deca_source::SummationDecapode, varID) + # skip_ops = Set([:βˆ‚β‚œ]) + indices = incident(deca_source, varID, :tgt) + return filter!(x -> deca_source[x, :op1] != :βˆ‚β‚œ, indices) +end + +#""" function is_tgt_of_many_ops(d::SummationDecapode, var) +#Return true if there are two or more distinct operations leading +#into Var var (not counting βˆ‚β‚œ). +#""" +function is_tgt_of_many_ops(d::SummationDecapode, var) + op1Count = length(get_valid_op1s(d, var)) + op2Count = length(incident(d, var, :res)) + sumCount = length(incident(d, var, :sum)) + + op1Count + op2Count + sumCount >= 2 +end + +#""" function find_tgts_of_many_ops(d::SummationDecapode) +#Searches SummationDecapode, d, for all Vars which have two or +#more distinct operations leading into the same variable. +#""" +function find_tgts_of_many_ops(d::SummationDecapode) + filter(var -> is_tgt_of_many_ops(d, var), parts(d, :Var)) +end + +#""" function get_preprocess_indices(deca_source::SummationDecapode) +#Searches SummationDecapode, deca_source, for all Vars which are +#valid for average rewriting preprocessing. Namely this just includes +#all op2 and summation operations. Returns two arrays, first is +#array of valid Op2 ids, second is array of valid Ξ£ ids. +#""" +function get_preprocess_indices(deca_source::SummationDecapode) + targetOp2 = [] + targetSum = [] + + targetVars = find_tgts_of_many_ops(deca_source) + + for var in targetVars + append!(targetOp2, incident(deca_source, var, :res)) + append!(targetSum, incident(deca_source, var, :sum)) + end + + return targetOp2, targetSum +end + +#""" function preprocess_average_rewrite(deca_source::SummationDecapode) +#Preprocesses SummationDecapode, deca_source, for easier average +#rewriting later on. Specifically, all op2 and summation results are +#stored in variables called "Temp" and results are then passed off to +#their original result along an op1 called "temp". This "temp" operation +#is equivalent to an identity function. +#""" +function preprocess_average_rewrite(deca_source::SummationDecapode) + targetOp2, targetSum = get_preprocess_indices(deca_source) + + # If we don't need to preprocess then don't + if(length(targetOp2) == 0 && length(targetSum) == 0) + return deca_source + end + + LHS = [] + RHS = [] + + SuperMatch = [] + SuperVarMap = Vector{Int}() + SuperOp2Map = Vector{Int}() + SuperSigmaMap = Vector{Int}() + SuperSummandMap = Vector{Int}() + + serial = 0 + # Process all of the target rewrites for op2 + for opID in targetOp2 + + vars = [deca_source[opID, :proj1], deca_source[opID, :proj2], deca_source[opID, :res]] + types = deca_source[vars, :type] + names = deca_source[vars, :name] + op2name = deca_source[opID, :op2] + + Match = @acset SummationDecapode{Any, Any, Symbol} begin + Var = 3 + type = types + name = names + + Op2 = 1 + proj1 = [1] + proj2 = [2] + res = [3] + op2 = op2name + end + + I = @acset SummationDecapode{Any, Any, Symbol} begin + Var = 3 + type = types + name = names + end + + Sub = @acset SummationDecapode{Any, Any, Symbol} begin + Var = 4 + type = vcat(types, types[end]) + name = vcat(names, Symbol("Temp_", serial)) + + Op1 = 1 + src = [4] + tgt = [3] + op1 = [:temp] + + Op2 = 1 + proj1 = [1] + proj2 = [2] + res = [4] + op2 = op2name + end + + serial += 1 + + L = ACSetTransformation(I, Match, Var = 1:3) + R = ACSetTransformation(I, Sub, Var = 1:3) + + push!(LHS, L) + push!(RHS, R) + push!(SuperMatch, Match) + + append!(SuperVarMap, vars) + push!(SuperOp2Map, opID) + end + + # Process all of the target rewrites for sums + for sumID in targetSum + summandIDs = incident(deca_source, sumID, :summation) + vars = vcat(deca_source[summandIDs, :summand], deca_source[sumID, :sum]) + types = deca_source[vars, :type] + names = deca_source[vars, :name] + + rewrite_size = length(vars) + nary = rewrite_size - 1 + + Match = @acset SummationDecapode{Any, Any, Symbol} begin + Var = rewrite_size + type = types + name = names + + Ξ£ = 1 + sum = [rewrite_size] + + Summand = nary + summand = 1:nary + summation = fill(1, nary) + end + + I = @acset SummationDecapode{Any, Any, Symbol} begin + Var = rewrite_size + type = types + name = names + end + + Sub = @acset SummationDecapode{Any, Any, Symbol} begin + Var = rewrite_size + 1 + type = vcat(types, types[end]) + name = vcat(names, Symbol("Temp_", serial)) + + Op1 = 1 + src = [rewrite_size + 1] + tgt = [rewrite_size] + op1 = [:temp] + + Ξ£ = 1 + sum = [rewrite_size + 1] + + Summand = nary + summand = 1:nary + summation = fill(1, nary) + end + + serial += 1 + + L = ACSetTransformation(I, Match, Var = 1:rewrite_size) + R = ACSetTransformation(I, Sub, Var = 1:rewrite_size) + + push!(LHS, L) + push!(RHS, R) + push!(SuperMatch, Match) + + append!(SuperVarMap, vars) + push!(SuperSigmaMap, sumID) + append!(SuperSummandMap, summandIDs) + end + + # Combine all rules in parallel and apply + rule = Rule(oplus(LHS), oplus(RHS)) + m = ACSetTransformation(oplus(SuperMatch), deca_source, Var = SuperVarMap, Op2 = SuperOp2Map, Ξ£ = SuperSigmaMap, Summand = SuperSummandMap) + + rewrite_match(rule, m) +end + +#""" function process_average_rewrite(deca_source::SummationDecapode) +#Rewrites SummationDecapode, deca_source, by including averages +#of redundent operations. While this function only searches for op1s +#to match on, because of preprocessing, this indirectly includes op2 +#and summations in the final result. +#""" +function process_average_rewrite(deca_source::SummationDecapode) + targetVars = find_tgts_of_many_ops(deca_source) + + # If no rewrites available, then don't rewrite + if(length(targetVars) == 0) + return deca_source + end + + LHS = [] + RHS = [] + + SuperMatch = [] + SuperVarMap = Vector{Int}() + SuperOp1Map = Vector{Int}() + + varSerial = 0 + sumSerial = 0 + + # Create rewrite rules for multiple op1s leading into target + for varID in targetVars + targetOp1 = get_valid_op1s(deca_source, varID) + vars = vcat(deca_source[targetOp1, :src], varID) + + num_nodes_match = length(vars) + nary_of_rewrite = num_nodes_match - 1 + + result_index = num_nodes_match + sum_index = 2 * result_index + + variable_types = deca_source[vars, :type] + variable_var = deca_source[vars, :name] + variable_op1 = deca_source[targetOp1, :op1] + + Match = @acset SummationDecapode{Any, Any, Symbol} begin + Var = num_nodes_match + type = variable_types + name = variable_var + + Op1 = nary_of_rewrite + src = 1:nary_of_rewrite + tgt = fill(result_index, nary_of_rewrite) + op1 = variable_op1 + end + + I = @acset SummationDecapode{Any, Any, Symbol} begin + Var = num_nodes_match + type = variable_types + name = variable_var + end + + Sub = @acset SummationDecapode{Any, Any, Symbol} begin + Var = 2 * num_nodes_match + type = vcat(variable_types, variable_types) + name = vcat(variable_var, map(x -> Symbol("β€’β€’", varSerial + x), 1:nary_of_rewrite), [Symbol("β€’β€’sum", sumSerial)]) + Op1 = nary_of_rewrite + 1 + src = vcat(1:nary_of_rewrite, sum_index) + tgt = vcat(num_nodes_match+1:sum_index-1, [result_index]) + op1 = vcat(variable_op1, Symbol(:avg, nary_of_rewrite)) + Ξ£ = 1 + sum = [sum_index] + Summand = nary_of_rewrite + summand = num_nodes_match+1:sum_index-1 + summation = fill(1, nary_of_rewrite) + end + + varSerial += nary_of_rewrite + sumSerial += 1 + + L = ACSetTransformation(I, Match, Var = 1:num_nodes_match); + R = ACSetTransformation(I, Sub, Var = 1:num_nodes_match); + + push!(LHS, L) + push!(RHS, R) + push!(SuperMatch, Match) + + append!(SuperVarMap, vars) + append!(SuperOp1Map, targetOp1) + end + + # Combine all rewrite rules and apply at once + rule = Rule(oplus(LHS), oplus(RHS)) + + m = ACSetTransformation(oplus(SuperMatch), deca_source, Var = SuperVarMap, Op1 = SuperOp1Map) + rewrite_match(rule, m) +end + +""" function average_rewrite(deca_source::SummationDecapode) + +Compute each quantitity in the given Decapode by the average of all computation paths leading to that node. +""" +function average_rewrite(deca_source::SummationDecapode) + return process_average_rewrite(preprocess_average_rewrite(deca_source)) +end + +#""" function find_variable_mapping(deca_source, deca_tgt) +#Returns array of match on variables between from a Decapode +#source to a target, also returns if mapping is valid +#WARNING: This assumes that variable names are unique. +#If variable names are not unique or do not exist, +#corrsponding mapping value is set to 0. +#""" +function find_variable_mapping(deca_source, deca_tgt) + + mapping = [] + valid_matching = true + + for varID in parts(deca_source, :Var) + varName = deca_source[varID, :name] + matches = incident(deca_tgt, varName, :name) + if(length(matches) >= 2) + # println("Name for variable at index $varID named $varName is not unique. Setting to 0.") + valid_matching = false + push!(mapping, 0) + elseif(length(matches) == 0) + # println("Variable at index $varID named $varName does not exist in target. Setting to 0.") + valid_matching = false + push!(mapping, 0) + else + push!(mapping, only(matches)) + end + end + + return mapping, valid_matching +end diff --git a/src/visualization.jl b/src/visualization.jl new file mode 100644 index 0000000..b934a6d --- /dev/null +++ b/src/visualization.jl @@ -0,0 +1,65 @@ +import Catlab.Graphics.Graphviz +using Catlab.Graphics +using Catlab.Graphics.Graphviz +using Catlab.Graphs.PropertyGraphs +using Catlab.Graphs +using Catlab.Graphs.BasicGraphs + +#= reg_to_sub = Dict('0'=>'β‚€', '1'=>"₁", '2'=>'β‚‚', '3'=>'₃', '4'=>'β‚„', + '5'=>'β‚…', '6'=>'₆','7'=>'₇', '8'=>'β‚ˆ', '9'=>'₉', 'r'=>'β€’', 'l'=>'L', ) =# + + +typename(d, v) = begin + t = d[v, :type] + return t +end + +# function Catlab.Graphics.to_graphviz_property_graph(d::AbstractNamedDecapode; kw...) +# to_graphviz_property_graph(d::AbstractNamedDecapode; typename=spacename, directed = true, kw...) +# end + +# function Catlab.Graphics.to_graphviz_property_graph(d::SummationDecapode; kw...) +# to_graphviz_property_graph(d::SummationDecapode; typename=spacename, directed = true, kw...) +# end +function Catlab.Graphics.to_graphviz_property_graph(d::AbstractNamedDecapode; typename=typename, directed=true, kw...) + pg = PropertyGraph{Any}(;kw...) + vids = map(parts(d, :Var)) do v + add_vertex!(pg, label=varname(d,v)) + end + + # Add entry and exit vertices and wires + if(directed) + for state in infer_states(d) + tempv = add_vertex!(pg) + add_edge!(pg, tempv, vids[state]) + end + end + + map(parts(d, :Op1)) do op + s, t = d[op, :src], d[op, :tgt] + add_edge!(pg, vids[s],vids[t], label=Decapode_edge_label(d[op,:op1])) + end + + map(parts(d, :Op2)) do op + s, t = d[op, :proj1], d[op, :proj2] + r = d[op, :res] + v = add_vertex!(pg, label="$(typename(d, s))Γ—$(typename(d,t))", shape="rectangle") + + # If in directed mode, sources point into the projection field + # Else, everything points out + if(directed) + add_edge!(pg, vids[s], v, label="π₁", style="dashed") + add_edge!(pg, vids[t], v, label="Ο€β‚‚", style="dashed") + else + add_edge!(pg, v, vids[s], label="π₁", style="dashed") + add_edge!(pg, v, vids[t], label="Ο€β‚‚", style="dashed") + end + add_edge!(pg, v, vids[r], label=Decapode_edge_label(d[op, :op2])) + end + + return pg +end + +savevizsvg(g, fname::String) = open(fname, "w") do fp + run_graphviz(fp, to_graphviz(to_graphviz_property_graph(nsdp)), prog="neato", format="svg") +end diff --git a/test/Project.toml b/test/Project.toml deleted file mode 100644 index f267256..0000000 --- a/test/Project.toml +++ /dev/null @@ -1,3 +0,0 @@ -[deps] -DiagrammaticEquations = "b66562e1-fa90-4e8b-9505-c909188fab76" -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/test/core.jl b/test/core.jl index d9bf789..7f5983d 100644 --- a/test/core.jl +++ b/test/core.jl @@ -1 +1 @@ -@test hello("World") == "Hello, World!" +@test "Hello, World!" == "Hello, World!" diff --git a/test/migrate/collages.jl b/test/migrate/collages.jl new file mode 100644 index 0000000..90cbda8 --- /dev/null +++ b/test/migrate/collages.jl @@ -0,0 +1,74 @@ +using Test +using DiagrammaticEquations +using Catlab + +# TODO: Special names for morphisms that match a method of grabbing boundary +# simplices. + +# TODO: Initial conditions. +# - This will mean that we need to return both a masked Decapode, and a means of pluggin initial data in for physical quantities, replacing `constuct`. +# TODO: Temporal boundary conditions. +# TODO: General boundaries i.e. arbitrary functions of solutions. + +# TODO: Add test with empty boundary Decapode. + +# Test simple boundary masks. +DiffusionDynamics = @decapode begin + K::Form0 + βˆ‚β‚œ(K) == ∘(d,⋆,d,⋆)(K) +end +DiffusionBoundaries = @decapode begin + (Kb1, Kb2, Null)::Form0 +end + +DiffusionMorphism = @relation () begin + rb1_leftwall(C, Cb1) + rb2_rightwall(C, Cb2) + rb3(CΜ‡, Zero) +end + +DiffusionSymbols = Dict( + :C => :K, + :CΜ‡ => :KΜ‡, + :Cb1 => :Kb1, + :Cb2 => :Kb2, + :Zero => :Null) + +DiffusionCollage = DiagrammaticEquations.collate( + DiffusionDynamics, + DiffusionBoundaries, + DiffusionMorphism, + DiffusionSymbols) + +@test DiffusionCollage == @acset SummationDecapode{Any, Any, Symbol} begin + Var = 8 + TVar = 1 + Op1 = 2 + Op2 = 3 + src = [5, 1] + tgt = [2, 2] + proj1 = [3, 5, 7] + proj2 = [4, 6, 8] + res = [1, 3, 2] + incl = [2] + op1 = Any[:βˆ‚β‚œ, [:d, :⋆, :d, :⋆]] + op2 = [:rb1_leftwall, :rb2_rightwall, :rb3] + type = [:Form0, :infer, :Form0, :Form0, :Form0, :Form0, :infer, :Form0] + name = [:r1_K, :r3_KΜ‡, :r2_K, :Kb1, :K, :Kb2, :KΜ‡, :Null] +end + +# Note: Since the order does not matter in which rb1 and rb2 are applied, it +# seems informal to state that one goes before the other. +# It might be better to provide a semantics for incident edges a la: +#Diffusion = @Decapode begin +# C::Form0 +# βˆ‚β‚œ(C) == rb3(∘(d,⋆,d,⋆)(rb1(C))) +# βˆ‚β‚œ(C) == rb3(∘(d,⋆,d,⋆)(rb2(C))) +#end +# Such a technique would preserve the technical definition of "collage". + +# Test gensim on a collage. +c = Collage(DiffusionDynamics, DiffusionBoundaries, + DiffusionMorphism, DiffusionSymbols) + +# @test gensim(c) == gensim(DiffusionCollage) diff --git a/test/migrate/rewrite.jl b/test/migrate/rewrite.jl new file mode 100644 index 0000000..d42932f --- /dev/null +++ b/test/migrate/rewrite.jl @@ -0,0 +1,459 @@ +using Test + +using DiagrammaticEquations +import DiagrammaticEquations: average_rewrite + +using Catlab.ACSetInterface + + +#= draw(g; kw...) = to_graphviz(g; node_labels=true, edge_labels=true, kw...) +draw(f::ACSetTransformation; kw...) = + to_graphviz(f; node_labels=true, edge_labels=true, draw_codom=false, kw...) =# + +######################### + +# No valid rewrites, return original Decapode +DecaTest0 = quote + A::Form0{X} + B::Form1{X} + C::Form2{X} + + D::Form0{X} + E::Form1{X} + F::Form2{X} + + G::Form0{X} + H::Form0{X} + I::Form0{X} + J::Form0{X} + + B == a(A) + C == b(B) + + F == f(D, E) + + J == G + H + I +end + +Test0 = SummationDecapode(parse_decapode(DecaTest0)) +Test0Res = average_rewrite(Test0) +@test Test0 == Test0Res + +# Trivial rewrite test +# Make sure var, op names and forms are preserved +DecaTest1 = quote + D₁::Form0{X} + Dβ‚‚::Form1{X} + F::Form2{X} + + F == c₁(D₁) + F == cβ‚‚(Dβ‚‚) +end + +Test1 = SummationDecapode(parse_decapode(DecaTest1)) +Test1Res = average_rewrite(Test1) + +Test1Expected = @acset SummationDecapode{Any, Any, Symbol} begin + Var = 6 + type = [:Form0, :Form1, :Form2, :Form0, :Form1, :Form2] + name = [:D₁, :Dβ‚‚, :F, Symbol("β€’β€’1"), Symbol("β€’β€’2"), Symbol("β€’β€’sum0")] + + Op1 = 3 + src = [1, 2, 6] + tgt = [4, 5, 3] + op1 = [:c₁, :cβ‚‚, :avg2] + + Ξ£ = 1 + sum = [6] + + Summand = 2 + summand = [4, 5] + summation = [1, 1] +end + +@test Test1Res == Test1Expected + +# Test with multiple rewrites, op1 only +DecaTest2 = quote + C₁::Form0{X} + Cβ‚‚::Form0{X} + D₁::Form1{X} + Dβ‚‚::Form1{X} + F::Form1{X} + + D₁ == dβ‚€(C₁) + D₁ == dβ‚€(Cβ‚‚) + F == c₁(D₁) + F == cβ‚‚(Dβ‚‚) + +end + +Test2 = SummationDecapode(parse_decapode(DecaTest2)) +Test2Res = average_rewrite(Test2) + +Test2Expected = @acset SummationDecapode{Any, Any, Symbol} begin + Var = 11 + + type = [:Form0, :Form0, :Form1, :Form0, :Form0, + :Form1, :Form1, :Form1, :Form1, :Form1, :Form1] + + name = [:C₁, :Cβ‚‚, :D₁, Symbol("β€’β€’1"), Symbol("β€’β€’2"), Symbol("β€’β€’sum0"), + :Dβ‚‚, :F, Symbol("β€’β€’3"), Symbol("β€’β€’4"), Symbol("β€’β€’sum1")] + + Op1 = 6 + src = [1, 2, 6, 3, 7, 11] + tgt = [4, 5, 3, 9, 10, 8] + op1 = [:dβ‚€, :dβ‚€, :avg2, :c₁, :cβ‚‚, :avg2] + + Ξ£ = 2 + sum = [6, 11] + + Summand = 4 + summand = [4, 5, 9, 10] + summation = [1, 1, 2, 2] +end + +@test Test2Res == Test2Expected + +# Test to ensure rewrites work for op1, op2, and sums +DecaTest3 = quote + A::Form0{X} + B::Form0{X} + C::Form0{X} + D::Form0{X} + E::Form1{X} + F::Form2{X} + G::Form2{X} + + G == ∧(A, B) + G == k(C) + G == t(D) + G == F + E +end + +Test3 = SummationDecapode(parse_decapode(DecaTest3)) +Test3Res = average_rewrite(Test3) + +Test3Expected = @acset SummationDecapode{Any, Any, Symbol} begin + Var = 14 + + type = Any[:Form2, :Form2, :Form0, :Form0, :Form2, :Form2, + :Form2, :Form0, :Form0, :Form2, :Form0, :Form0, :Form2, :Form1] + + name = [:Temp_0, :Temp_1, :C, :D, :G, Symbol("β€’β€’1"), Symbol("β€’β€’2"), + Symbol("β€’β€’3"), Symbol("β€’β€’4"), Symbol("β€’β€’sum0"), :A, :B, :F, :E] + + Op1 = 5 + src = [1, 2, 3, 4, 10] + tgt = [6, 7, 8, 9, 5] + op1 = [:temp, :temp, :k, :t, :avg4] + + Op2 = 1 + proj1 = [11] + proj2 = [12] + res = [1] + op2 = [:∧] + + Ξ£ = 2 + sum = [10, 2] + + Summand = 6 + summand = [6, 7, 8, 9, 13, 14] + summation = [1, 1, 1, 1, 2, 2] +end + +@test Test3Res == Test3Expected + +# Test to ensure that ops from the same source are all preserved +DecaTest4 = quote + C::Form0{X} + D::Form1{X} + + D == k(C) + D == t(C) + D == p(C) +end + +Test4 = SummationDecapode(parse_decapode(DecaTest4)) +Test4Res = average_rewrite(Test4) + +Test4Expected = @acset SummationDecapode{Any, Any, Symbol} begin + Var = 6 + type = Any[:Form0, :Form1, :Form0, :Form0, :Form0, :Form1] + name = [:C, :D, Symbol("β€’β€’1"), Symbol("β€’β€’2"), Symbol("β€’β€’3"), Symbol("β€’β€’sum0")] + + Op1 = 4 + src = [1, 1, 1, 6] + tgt = [3, 4, 5, 2] + op1 = Any[:k, :t, :p, :avg3] + + Ξ£ = 1 + sum = [6] + + Summand = 3 + summand = [3, 4, 5] + summation = [1, 1, 1] +end + +@test Test4Res == Test4Expected + +# Test that larger nary rewrites function properly +DecaTest5 = quote + A::Form0{X} + B::Form0{X} + C::Form1{X} + D::Form2{X} + E::Form1{X} + F::Form0{X} + G::Form2{X} + + G == f(F) + G == e(E) + G == d(D) + G == c(C) + G == b(B) + G == a(A) +end + +Test5 = SummationDecapode(parse_decapode(DecaTest5)) +Test5Res = average_rewrite(Test5) + +Test5Expected = @acset SummationDecapode{Any, Any, Symbol} begin + Var = 14 + type = Any[:Form0, :Form1, :Form2, :Form1, :Form0, :Form0, + :Form2, :Form0, :Form1, :Form2, :Form1, :Form0, :Form0, :Form2] + name = [:F, :E, :D, :C, :B, :A, :G, Symbol("β€’β€’1"), Symbol("β€’β€’2"), Symbol("β€’β€’3"), Symbol("β€’β€’4"), Symbol("β€’β€’5"), Symbol("β€’β€’6"), Symbol("β€’β€’sum0")] + + Op1 = 7 + src = [1, 2, 3, 4, 5, 6, 14] + tgt = [8, 9, 10, 11, 12, 13, 7] + op1 = Any[:f, :e, :d, :c, :b, :a, :avg6] + + Ξ£ = 1 + sum = [14] + + Summand = 6 + summand = [8, 9, 10, 11, 12, 13] + summation = [1, 1, 1, 1, 1, 1] +end + +@test Test5Res == Test5Expected + +# Test multiple rewrites with op2 +DecaTest6 = quote + A::Form0{X} + B::Form1{X} + C::Form2{X} + D::Form0{X} + E::Form1{X} + F::Form2{X} + + F == k(A) + F == t(E) + E == p(B, C) + E == q(B, D) +end + +Test6 = SummationDecapode(parse_decapode(DecaTest6)) +Test6Res = average_rewrite(Test6) + +Test6Expected = @acset SummationDecapode{Any, Any, Symbol} begin + Var = 14 + type = Any[:Form1, :Form1, :Form1, :Form1, :Form1, :Form1, + :Form0, :Form2, :Form0, :Form1, :Form2, :Form1, :Form2, :Form0] + name = [:Temp_0, :Temp_1, :E, Symbol("β€’β€’1"), Symbol("β€’β€’2"), Symbol("β€’β€’sum0"), :A, :F, Symbol("β€’β€’3"), Symbol("β€’β€’4"), Symbol("β€’β€’sum1"), :B, :C, :D] + + Op1 = 6 + src = [1, 2, 6, 7, 3, 11] + tgt = [4, 5, 3, 9, 10, 8] + op1 = Any[:temp, :temp, :avg2, :k, :t, :avg2] + + Op2 = 2 + proj1 = [12, 12] + proj2 = [13, 14] + res = [1, 2] + op2 = Any[:p, :q] + + Ξ£ = 2 + sum = [6, 11] + + Summand = 4 + summand = [4, 5, 9, 10] + summation = [1, 1, 2, 2] +end + +@test Test6Res == Test6Expected + +# Test multiple rewrites with sums +DecaTest7 = quote + A::Form0{X} + B::Form1{X} + C::Form2{X} + D::Form1{X} + E::Form0{X} + F::Form2{X} + + F == A + B + F == C + D + E +end + +Test7 = SummationDecapode(parse_decapode(DecaTest7)) +Test7Res = average_rewrite(Test7) + +Test7Expected = @acset SummationDecapode{Any, Any, Symbol} begin + Var = 11 + type = Any[:Form2, :Form2, :Form2, :Form2, :Form2, :Form2, + :Form0, :Form1, :Form2, :Form1, :Form0] + name = [:Temp_0, :Temp_1, :F, Symbol("β€’β€’1"), Symbol("β€’β€’2"), Symbol("β€’β€’sum0"), :A, :B, :C, :D, :E] + + Op1 = 3 + src = [1, 2, 6] + tgt = [4, 5, 3] + op1 = Any[:temp, :temp, :avg2] + + Ξ£ = 3 + sum = [6, 1, 2] + + Summand = 7 + summand = [4, 5, 7, 8, 9, 10, 11] + summation = [1, 1, 2, 2, 3, 3, 3] +end + +@test Test7Res == Test7Expected + +# Test that rewrite ignores forbidden ops, like βˆ‚β‚œ +# TODO: This test might break if TVar behavior changes +DecaTest8 = quote + D₁::Form1{X} + Dβ‚‚::Form2{X} + F::Form0{X} + + βˆ‚β‚œ(D₁) == F + F == cβ‚‚(Dβ‚‚) +end + +Test8 = SummationDecapode(parse_decapode(DecaTest8)) +Test8Res = average_rewrite(Test8) + +Test8Expected = @acset SummationDecapode{Any, Any, Symbol} begin + Var = 3 + type = Any[:Form1, :Form2, :Form0] + name = [:D₁, :Dβ‚‚, :F] + + TVar = 1 + incl = [3] + + Op1 = 2 + src = [1, 2] + tgt = [3, 3] + op1 = Any[:βˆ‚β‚œ, :cβ‚‚] +end + +@test Test8Res == Test8Expected + +#=DecaTest9 = quote + A::Form0{X} + B::Form0{X} + C::Form0{X} + D::Form0{X} + E::Form2{X} + F::Form3{X} + HΜ‡::Form5{X} + H::Form5{X} + + HΜ‡ == k(B) + HΜ‡ == p(D) + βˆ‚β‚œ(H) == HΜ‡ +end + +Test9 = SummationDecapode(parse_decapode(DecaTest9)) +Test9Res = average_rewrite(Test9)=# + +# Test that rewrites preverse TVars, ignore βˆ‚β‚œ +DecaTest10 = quote + A::Form0{X} + B::Form0{X} + C::Form0{X} + D::Form0{X} + HΜ‡::Form2{X} + H::Form2{X} + + A == b(B) + A == d(D) + HΜ‡ == c(C) + βˆ‚β‚œ(H) == HΜ‡ +end + +Test10 = SummationDecapode(parse_decapode(DecaTest10)) +Test10Res = average_rewrite(Test10) + +Test10Expected = @acset SummationDecapode{Any, Any, Symbol} begin + Var = 9 + type = Any[:Form0, :Form0, :Form0, :Form0, :Form0, :Form0, + :Form0, :Form2, :Form2] + name = [:B, :D, :A, Symbol("β€’β€’1"), Symbol("β€’β€’2"), Symbol("β€’β€’sum0"), :C, :αΈ’, :H] + + TVar = 1 + incl = [8] + + Op1 = 5 + src = [1, 2, 6, 7, 9] + tgt = [4, 5, 3, 8, 8] + op1 = Any[:b, :d, :avg2, :c, :βˆ‚β‚œ] + + Ξ£ = 1 + sum = [6] + + Summand = 2 + summand = [4, 5] + summation = [1, 1] +end + +@test Test10Res == Test10Expected + +# Test for benchmarking large results, still gives correct output +function makePerfectBinaryDeca(h) + num_nodes = 2^h - 1 + num_interior = 2^(h-1) - 1 + BinaryTest = @acset SummationDecapode{Any, Any, Symbol} begin + Var = num_nodes + type = fill(:Form1, num_nodes) + name = map(x -> Symbol("β€’$x"), 1:num_nodes) + + Op1 = 2 * num_interior + src = vcat(map(x->2*x, 1:num_interior), map(x->2*x+1, 1:num_interior)) + tgt = vcat(1:num_interior, 1:num_interior) + op1 = map(x -> Symbol("op$x"), 1:(2 * num_interior)) + + end +end + +h = 5 +BinTest = makePerfectBinaryDeca(h) +BinTestRes = average_rewrite(BinTest) + +BinTestExpected = @acset SummationDecapode{Any, Any, Symbol} begin + Var = 76 + type = Any[:Form1, :Form1, :Form1, :Form1, :Form1, :Form1, + :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, + :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1, :Form1] + name = [Symbol("β€’2"), Symbol("β€’3"), Symbol("β€’1"), Symbol("β€’β€’1"), Symbol("β€’β€’2"), Symbol("β€’β€’sum0"), Symbol("β€’4"), Symbol("β€’5"), Symbol("β€’β€’3"), Symbol("β€’β€’4"), Symbol("β€’β€’sum1"), Symbol("β€’6"), Symbol("β€’7"), Symbol("β€’β€’5"), Symbol("β€’β€’6"), Symbol("β€’β€’sum2"), Symbol("β€’8"), Symbol("β€’9"), Symbol("β€’β€’7"), Symbol("β€’β€’8"), Symbol("β€’β€’sum3"), Symbol("β€’10"), Symbol("β€’11"), Symbol("β€’β€’9"), Symbol("β€’β€’10"), Symbol("β€’β€’sum4"), Symbol("β€’12"), Symbol("β€’13"), Symbol("β€’β€’11"), Symbol("β€’β€’12"), Symbol("β€’β€’sum5"), Symbol("β€’14"), Symbol("β€’15"), Symbol("β€’β€’13"), + Symbol("β€’β€’14"), Symbol("β€’β€’sum6"), Symbol("β€’16"), Symbol("β€’17"), Symbol("β€’β€’15"), Symbol("β€’β€’16"), Symbol("β€’β€’sum7"), Symbol("β€’18"), Symbol("β€’19"), Symbol("β€’β€’17"), Symbol("β€’β€’18"), Symbol("β€’β€’sum8"), Symbol("β€’20"), Symbol("β€’21"), Symbol("β€’β€’19"), Symbol("β€’β€’20"), Symbol("β€’β€’sum9"), Symbol("β€’22"), Symbol("β€’23"), Symbol("β€’β€’21"), Symbol("β€’β€’22"), Symbol("β€’β€’sum10"), Symbol("β€’24"), Symbol("β€’25"), Symbol("β€’β€’23"), Symbol("β€’β€’24"), Symbol("β€’β€’sum11"), Symbol("β€’26"), Symbol("β€’27"), Symbol("β€’β€’25"), Symbol("β€’β€’26"), Symbol("β€’β€’sum12"), Symbol("β€’28"), Symbol("β€’29"), Symbol("β€’β€’27"), Symbol("β€’β€’28"), Symbol("β€’β€’sum13"), Symbol("β€’30"), Symbol("β€’31"), Symbol("β€’β€’29"), Symbol("β€’β€’30"), Symbol("β€’β€’sum14")] + + Op1 = 45 + src = [1, 2, 6, 7, 8, 11, 12, 13, 16, 17, 18, 21, 22, 23, 26, 27, 28, 31, 32, 33, 36, 37, 38, 41, 42, 43, 46, 47, 48, + 51, 52, 53, 56, 57, 58, 61, 62, 63, 66, 67, 68, 71, 72, 73, 76] + tgt = [4, 5, 3, 9, 10, 1, 14, 15, 2, 19, 20, 7, 24, 25, 8, + 29, 30, 12, 34, 35, 13, 39, 40, 17, 44, 45, 18, 49, 50, 22, 54, 55, 23, 59, 60, 27, 64, 65, 28, 69, 70, 32, 74, 75, 33] + op1 = Any[:op1, :op16, :avg2, :op2, :op17, :avg2, :op3, :op18, :avg2, :op4, :op19, :avg2, :op5, :op20, :avg2, :op6, :op21, :avg2, :op7, :op22, :avg2, :op8, :op23, :avg2, :op9, :op24, :avg2, :op10, :op25, :avg2, :op11, :op26, :avg2, :op12, :op27, :avg2, :op13, :op28, :avg2, :op14, :op29, :avg2, + :op15, :op30, :avg2] + + Ξ£ = 15 + sum = [6, 11, 16, 21, 26, 31, 36, 41, 46, 51, 56, 61, 66, 71, 76] + + Summand = 30 + summand = [4, 5, 9, 10, 14, 15, 19, 20, 24, 25, 29, 30, 34, 35, 39, 40, 44, 45, 49, 50, 54, 55, 59, 60, 64, 65, 69, 70, 74, 75] + summation = [1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14, 15, 15] +end + +@test BinTestRes == BinTestExpected diff --git a/test/migrate/visualization.jl b/test/migrate/visualization.jl new file mode 100644 index 0000000..4d2fb6c --- /dev/null +++ b/test/migrate/visualization.jl @@ -0,0 +1,74 @@ +using Test +using DiagrammaticEquations +using Catlab.Graphics +using Catlab.Graphics.Graphviz +using Catlab.Graphs.PropertyGraphs + +DecaTest = quote + (A, B, C, D)::Form0{X} + + D == k(A + B) + p(C + B) + end + +Test1 = SummationDecapode(parse_decapode(DecaTest)) +t1 = to_graphviz(Test1) +@test Graphviz.filter_statements(t1, Graphviz.Edge, :label) == ["k", "p", "+", "+", "+"] +@test Graphviz.filter_statements(t1, Graphviz.Node, :label) == [ "A:Ξ©β‚€", "B:Ξ©β‚€", "C:Ξ©β‚€", "D:Ξ©β‚€", "β€’1:Ξ©β€’", "sum_1:Ξ©β€’", "β€’2:Ξ©β€’", "sum_2:Ξ©β€’", "", "", "Ξ£1", "Ξ£2", "Ξ£3" ] +@test Graphviz.filter_statements(t1, Graphviz.Node, :shape) == [ "none", "none", "circle", "circle", "circle"] +# Test that the default attributes are correct. +@test t1.graph_attrs == Dict(:rankdir => "TB") +@test t1.node_attrs == Dict(:height => "0.05", :margin => "0", :shape => "oval", :width => "0.05") + +t1_attributes = to_graphviz(Test1, edge_attrs=Dict(:color => "cornflowerblue"), node_attrs=Dict(:shape => "egg"), graph_attrs=Dict(:rankdir => "LR")) +# Test that the default attributes are overwritten only where specified. +@test t1_attributes.edge_attrs == Dict(:arrowsize => "0.5", :color => "cornflowerblue") +@test t1_attributes.node_attrs == Dict(:height => "0.05", :margin => "0", :shape => "egg", :width => "0.05") +@test t1_attributes.graph_attrs == Dict(:rankdir => "LR") +# Test that the per-edge and per-node attributes are not overwritten. +@test Graphviz.filter_statements(t1_attributes, Graphviz.Edge, :label) == ["k", "p", "+", "+", "+"] +@test Graphviz.filter_statements(t1_attributes, Graphviz.Node, :label) == [ "A:Ξ©β‚€", "B:Ξ©β‚€", "C:Ξ©β‚€", "D:Ξ©β‚€", "β€’1:Ξ©β€’", "sum_1:Ξ©β€’", "β€’2:Ξ©β€’", "sum_2:Ξ©β€’", "", "", "Ξ£1", "Ξ£2", "Ξ£3" ] +@test Graphviz.filter_statements(t1_attributes, Graphviz.Node, :shape) == [ "none", "none", "circle", "circle", "circle"] + +t1_no_default_changes = to_graphviz(Test1, node_attrs=Dict(:color => "red"), graph_attrs=Dict(:bgcolor => "fuchsia")) +# Test that the default attributes are not overwritten when not specified. +# (In this case, there should be no overwriting of default node shape, and graph rankdir.) +@test t1_no_default_changes.node_attrs == Dict(:color => "red", :height => "0.05", :margin => "0", :shape => "oval", :width => "0.05") +@test t1_no_default_changes.graph_attrs == Dict(:bgcolor => "fuchsia", :rankdir => "TB") + +DecaTest2 = quote + A::Form0{X} + B::Form0{X} + C::Form0{X} + D::Form0{X} + DΜ‡::Form0{X} + + DΜ‡ == k(A, B) + p(C, B) - 1 + βˆ‚β‚œ(D) == DΜ‡ +end + +Test2 = SummationDecapode(parse_decapode(DecaTest2)) +t2 = to_graphviz(Test2) +@test Graphviz.filter_statements(t2, Graphviz.Edge, :label) == ["βˆ‚β‚œ", "π₁", "Ο€β‚‚", "k", "π₁", "Ο€β‚‚", "p", "π₁", "Ο€β‚‚", "-", "+"] +@test Graphviz.filter_statements(t2, Graphviz.Node, :label) == ["A:Ξ©β‚€", "B:Ξ©β‚€", "C:Ξ©β‚€", "D:Ξ©β‚€", "Ḋ:Ξ©β‚€", "1:Ξ©L", "β€’1:Ξ©β€’", "β€’2:Ξ©β€’", "sum_1:Ξ©β€’", "", "", "Ξ©β‚€Γ—Ξ©β‚€", "Ξ©β‚€Γ—Ξ©β‚€", "Ξ©β€’Γ—Ξ©L", "Ξ£1"] +@test Graphviz.filter_statements(t2, Graphviz.Node, :shape) == ["none", "none", "rectangle", "rectangle", "rectangle", "circle"] + +t2_undirected = to_graphviz(Test2, directed = false) +@test Graphviz.filter_statements(t2_undirected, Graphviz.Edge, :label) == ["βˆ‚β‚œ", "π₁", "Ο€β‚‚", "k", "π₁", "Ο€β‚‚", "p", "π₁", "Ο€β‚‚", "-", "+"] +@test Graphviz.filter_statements(t2_undirected, Graphviz.Node, :label) == ["A:Ξ©β‚€", "B:Ξ©β‚€", "C:Ξ©β‚€", "D:Ξ©β‚€", "Ḋ:Ξ©β‚€", "1:Ξ©L", "β€’1:Ξ©β€’", "β€’2:Ξ©β€’", "sum_1:Ξ©β€’", "Ξ©β‚€Γ—Ξ©β‚€", "Ξ©β‚€Γ—Ξ©β‚€", "Ξ©β€’Γ—Ξ©L", "Ξ£1"] +@test Graphviz.filter_statements(t2_undirected, Graphviz.Node, :shape) == ["rectangle", "rectangle", "rectangle", "circle"] + +Test3 = SummationDecapode(parse_decapode(quote + A::Form0 + B::Form1 + C::Form2 + D::DualForm0 + E::DualForm1 + F::DualForm2 + G::Literal + H::Parameter + I::Constant + J::infer + end)) + +t3 = to_graphviz(Test3) +@test Graphviz.filter_statements(t3, Graphviz.Node, :label) == ["A:Ξ©β‚€", "B:Ω₁", "C:Ξ©β‚‚", "D:Ξ©Μƒβ‚€", "E:Ω̃₁", "F:Ξ©Μƒβ‚‚", "G:Ξ©L", "H:Ξ©P", "I:Ξ©C", "J:Ξ©β€’", "", ""] diff --git a/test/runtests.jl b/test/runtests.jl index 1348967..57bc81c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,7 +1,17 @@ using Test -using AlgebraicX - @testset "Core" begin include("core.jl") end + +@testset "Visualization" begin + include("migrate/visualization.jl") +end + +@testset "Average Rewriting" begin + include("migrate/rewrite.jl") +end + +@testset "Collages" begin + include("migrate/collages.jl") +end diff --git a/todo.md b/todo.md new file mode 100644 index 0000000..351cc88 --- /dev/null +++ b/todo.md @@ -0,0 +1,3 @@ +# Refactoring Notes +Decapodey things still in DEq: +* DerivOp is referenced in src/language