Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Roe refactor #62

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions roe/Project.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name = "DEC"
uuid = "e670b126-1168-4583-bd0f-29deacb39f6a"
authors = ["Owen Lynch <[email protected]>"]
version = "0.1.0"

[deps]
AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c"
AssociatedTests = "e00e7eca-deca-4415-9dc4-9b3efe792c16"
CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0"
Colors = "5ae59095-9a9b-59fe-a467-6f913c188581"
CombinatorialSpaces = "b1c52339-7909-45ad-8b6a-6e388f7c67f2"
ComponentArrays = "b0b7db55-cfe3-40fc-9ded-d10e2dbeff66"
Crayons = "a8cc5b0e-0ffa-5ad4-8c14-923d3ee1735f"
Decapodes = "679ab3ea-c928-4fe6-8d59-fd451142d391"
Dtries = "fb203528-72e9-47b6-b8ee-6a26b9f77273"
GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326"
MLStyle = "d8e11817-5142-5d16-987a-aa16d5891078"
Metatheory = "e9d8d322-4543-424a-9be4-0cc815abe26c"
Moshi = "2e0e35c7-a2e4-4343-998d-7ef72827ed2d"
OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d"
OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
Reexport = "189a3867-3050-52da-a836-e630ba90ab69"
Revise = "295af30f-e4ad-537b-8983-00126c2a3abe"
StructEquality = "6ec83bb0-ed9f-11e9-3b4c-2b04cb4e219c"
SymbolicUtils = "d1185830-fcd6-423d-90d6-eec64667417b"
TermInterface = "8ea1fca8-c5ef-4a55-8b96-4e9afe9c9a3c"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[compat]
AbstractTrees = "0.4.5"
AssociatedTests = "0.1.0"
CairoMakie = "0.12.5"
Colors = "0.12.11"
CombinatorialSpaces = "0.6.7"
Crayons = "4.1.1"
Decapodes = "0.5.5"
GeometryBasics = "0.4.11"
MLStyle = "0.4.17"
Moshi = "0.3.1"
OrderedCollections = "1.6.3"
OrdinaryDiffEq = "6.86.0"
Reexport = "1.2.2"
StructEquality = "2.1.0"
SymbolicUtils = "1.5.1"
Test = "1.11.0"
133 changes: 133 additions & 0 deletions roe/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# Roe

This is a refactor of the core of diagrammatic equations, attempting to achieve a "more Julionic" approach to the problem of typed computer algebra via direct use of multiple dispatch.

## Signature

The "signature" of the DEC is encoded in a module `ThDEC`, in the following way.

First, we make a type `Sort`, elements of which represent types in the discrete exterior calculus, for instance scalars, dual/primal forms of any degree, and vector fields.

Then, we make a Julia function for each sort in the DEC. We define these Julia functions to *act on sorts*. So for instance, for the wedge product, we write a function definition like

```julia
function ∧(s1::Sort, s2::Sort)
@match (s1, s2) begin
(Form(i, isdual), Scalar()) || (Scalar(), Form(i, isdual)) => Form(i, isdual)
(Form(i1, isdual), Form(i2, isdual)) =>
if i1 + i2 <= 2
Form(i1 + i2, isdual)
else
throw(SortError("Can only take a wedge product when the dimensions of the forms add to less than 2: tried to wedge product $i1 and $i2"))
end
_ => throw(SortError("Can only take a wedge product of two forms of the same duality"))
end
end
```

The advantage of encoding the signature in this way is twofold.

1. We can give high-quality, context-specific errors when types fail to match.
2. It doesn't depend on any external libraries (except for MLStyle for convenience); it is just Plain Old Julia.

## Using the signature to wrap symbolic algebra

We can then wrap various symbolic frameworks by including the sorts as type parameters/metadata. For instance, for Metatheory, we create a wrapper struct `Var{s::Sort}` which wraps a `Metatheory.Id` (note that `s::Sort` rather than `s<:Sort`) We then define our methods on this struct as

```julia
unop_dec = [:∂ₜ, :d, :★, :-, :♯, :♭]
for unop in unop_dec
@eval begin
@nospecialize
function $unop(v::Var{s}) where s
s′ = $unop(s)
Var{s′}(roe(v), addcall!(graph(v), $unop, (id(v),)))
end

export $unop
end
end
```

SymbolicUtils.jl is totally analogous:

```julia
unop_dec = [:∂ₜ, :d, :★, :♯, :♭, :-]
for unop in unop_dec
@eval begin
@nospecialize
function $unop(v::SymbolicUtils.BasicSymbolic{DECVar{s}}) where s
s′ = $unop(s)
SymbolicUtils.Term{DECVar{s′}}($unop, [v])
end

export $unop
end
end
```

SymbolicUtils gets confused when the type parameter to `BasicSymbolic` is not a type: we work around this by passing in `DECVar{s}` (name subject to change), which is a zero-field struct that wraps a `Sort` as a type parameter.

## Models and namespacing

Models are then plain old Julia functions that accept as their first argument a "roe." The point of the "Roe" is to record variables that are created and equations that are asserted by the function. It looks something like:

```julia
struct Roe{T}
vars::Dtry{T}
eqs::Vector{Tuple{T, T}}
end
```

The type parameter `T` could be instantiated with `BasicSymbolic{<:DECVar}` or `Var`, depending on whether we are working with SymbolicUtils or Metatheory.

So, for instance, the Klausmeier model might look like

```julia
function klausmeier(roe::Roe)
@vars roe n::DualForm0 w::DualForm0 dX::Form1 a::Constant{DualForm0} ν::Constant{DualForm0}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these be replaced by n::Form(0,Dual) or something? or does @vars handle legacy decapodes names and replace them?

@vars roe m::Number
# The equations for our model
@eq roe (∂ₜ(w) == a + w + (w * (n^2)) + ν * L(dX,w))
@eq roe (∂ₜ(n) == w * n^2 - m*n + Δ(n))
end
```

Namespacing is achieved by moving the roe into a namespace before passing it into submodels. So, for instance, to make a model with two Klausmeier submodels that share the same `m`, we could do:

```julia
function double_klausmeier(roe::Roe)
klausmeier(namespaced(roe, :k1))
klausmeier(namespaced(roe, :k2))

@eq roe (roe.k1.m == roe.k2.m)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

having them share the same dX and w would make sense because that would give you two species of plant sharing the same water supply. Although at thought point, you would want to break the models down into smaller pieces and reassemble it with only one water supply.

This is how you would it in current Decapodes. You have to break out any factor that gets superposition into its own models so that colimits the category of diagrams do the right thing.

# See Klausmeier Equation 2.a
Hydrodynamics = @decapode begin
  (w, consumption)::DualForm0
  dX::Form1
  (a,ν)::Constant
  ∂ₜ(w) == a - w - consumption + ν * L(dX, w)
end

# See Klausmeier Equation 2.b
Phytodynamics = @decapode begin
  (n,w)::DualForm0
  m::Constant
  consumption = w * n^2
  ∂ₜ(n) == consumption - m*n + Δ(n)
end

Superposition = @decapde begin
  total = part1 + part2
end

compose_klausmeier = @relation () begin
  phyto(N1, W, consumption1)
  phyto(N2, W, consumption2)
  superposition(consumption, consumption1, consumption2)
  hydro(consumption, W)
end

klausmeier_cospan = oapply(compose_klausmeier,
                           [Open(Phytodynamics, [:consumption, :w]), Open(Phytodynamics, [:consumption, :w])
                            Open(Hydrodynamics, [:total, :part1, :part2]),
                            Open(Hydrodynamics, [:consumption, :w])])
Klausmeier = apex(klausmeier_cospan)

I guess you could do it in Roe by replacing the definition of a term with a sum? That seems like it would cause you to flush the egraph and resaturate, because you removed an equation.

end
```

The implementation of `namespace` would be something like

```julia
function namespace(roe::Roe{T}, name::Symbol) where {T}
Roe(get(roe.vars, name, Dtry{T}()), roe.eqs)
end
```

Here, `get` either gets a pre-existing subnamespace at `name`, or creates a new subnamespace and inserts it at `name`. An alternative implementation would have a `namespace::Vec{Symbol}` field on `Roe` which is used to prefix every newly created variable.

An alternative to adding the equation `roe.k1.m == roe.k2.m` would be to have `klausmeier` take `m` as a parameter, which would look like

```julia
function klausmeier(roe::Roe, m)
@vars roe n::DualForm0 w::DualForm0 dX::Form1 a::Constant{DualForm0} ν::Constant{DualForm0}
# The equations for our model
@eq roe (∂ₜ(w) == a + w + (w * (n^2)) + ν * L(dX,w))
@eq roe (∂ₜ(n) == w * n^2 - m*n + Δ(n))
end

function double_klausemeier(roe::Roe)
@vars roe m::Number

klausmeier(namespaced(roe, :k1), m)
klausmeier(namespaced(roe, :k2), m)
end
```
22 changes: 22 additions & 0 deletions roe/docs/literate/egraphs.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# This lesson covers the internals of Metatheory.jl-style E-Graphs. Let's reuse the heat_equation model on a new roe.
roe = Roe(DEC.ThDEC.Sort);
function heat_equation(roe)
u = fresh!(roe, PrimalForm(0), :u)

∂ₜ(u) ≐ Δ(u)

([u], [])
end

# We apply the model to the roe and collect its state variables.
(state, _) = heat_equation(roe)

# Recall from the Introduction that an E-Graph is a bipartite graph of ENodes and EClasses. Let's look at the EClasses:
classes = roe.graph.classes
# The keys are Metatheory Id types which store an Id. The values are EClasses, which are implementations of equivalence classes. Nodes which share the same EClass are considered equivalent.



# The constants in Roe are a dictionary of hashes of functions and constants. Let's extract just the values again:
vals = collect(values(e.graph.constants))
# The `u` is ::RootVar{Form} and ∂ₜ, ★, d are all functions defined in ThDEC/signature.jl file.
74 changes: 74 additions & 0 deletions roe/docs/literate/heatequation.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Load AlgebraicJulia dependencies
using DEC
import DEC.ThDEC: Δ # conflicts with CombinatorialSpaces

# load other dependencies
using ComponentArrays
using CombinatorialSpaces
using GeometryBasics
using OrdinaryDiffEq
Point2D = Point2{Float64}
Point3D = Point3{Float64}
using CairoMakie

## Here we define the 1D heat equation model with one state variable and no parameters. That is, given an e-graph "roe," we define `u` to be a primal 0-form. The root variable carries a reference to the e-graph which it resides in. We then assert that the time derivative of the state is just its Laplacian. We return the state variable.
function heat_equation(roe)
u = fresh!(roe, PrimalForm(0), :u)

∂ₜ(u) ≐ Δ(u)

([u], [])
end

# Since this is a model in the DEC, we need to initialize the primal and dual meshes.
rect = triangulated_grid(100, 100, 1, 1, Point3D);
d_rect = EmbeddedDeltaDualComplex2D{Bool, Float64, Point3D}(rect);
subdivide_duals!(d_rect, Circumcenter());

# Now that we have a dual mesh, we can associate operators in our theory with precomputed matrices from Decapodes.jl.
op_lookup = ThDEC.precompute_matrices(d_rect, DiagonalHodge())

# Now we produce a "vector field" function which, given a model and operators in a theory, returns a function to be passed to the ODESolver. In stages, this function
#
# 1) extracts the Root Variables (state or parameter term) and runs the extractor along the e-graph,
# 2) extracts the derivative terms from the model into an SSA
# 3) yields a function accepting derivative terms, state terms, and parameter terms, whose body is both the lines, and derivatives.
vf = vfield(heat_equation, op_lookup)

# Let's initialize the
U = first.(d_rect[:point]);

# TODO component arrays
constants_and_parameters = ()

# We will run this for 500 timesteps.
t0 = 500.0

@info("Precompiling Solver")
prob = ODEProblem(vf, U, (0, t0), constants_and_parameters);
soln = solve(prob, Tsit5());

## 1-D HEAT EQUATION WITH DIFFUSIVITY

function heat_equation_with_constants(roe)
u = fresh!(roe, PrimalForm(0), :u)
k = fresh!(roe, Scalar(), :k)
ℓ = fresh!(roe, Scalar(), :ℓ)

∂ₜ(u) ≐ k * Δ(u)

([u], [k])
end

# we can reuse the mesh and operator lookup
vf = vfield(heat_equation_with_constants, operator_lookup)

# we can reuse the initial condition U but are specifying diffusivity constants.
constants_and_parameters = ComponentArray(k=0.25,);
t0 = 500

@info("Precompiling solver")
prob = ODEProblem(vf, U, (0, t0), constants_and_parameters);
soln = solve(prob, Tsit5());


52 changes: 52 additions & 0 deletions roe/docs/literate/tutorial.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# This tutorial is a slower-paced introduction into the design. Here, we will construct a simple exponential model.
using DEC
using Test
using Metatheory.EGraphs
using ComponentArrays
using GeometryBasics
using OrdinaryDiffEq
Point2D = Point2{Float64}
Point3D = Point3{Float64}

using CairoMakie

# We define our model of exponential growth. This model is a function which accepts a Roe and returns a tuple of State and Parameter variables. Let's break it down:
#
# 1. Function adds root variables (::RootVar) to the Roe. The root variables have no child nodes.
# 2. Our model makes claims about what terms equal one another. The "≐" operator is an infix of "equate!" which claims unites the ids of the left and right VecExprs.
# 3. The State and Parameter variables are returned. Each variable points to the same parent Roe.
#
#
# Each variable points to the same Roe.
function exp_growth(roe)
u = fresh!(roe, PrimalForm(0), :u)
k = fresh!(roe, Scalar(), :k)

∂ₜ(u) ≐ k * u

([u], [k])
end

# We now need to initialize the primal and dual meshes we'll need to compute with.
rect = triangulated_grid(100, 100, 1, 1, Point3D);
d_rect = EmbeddedDeltaDualComplex2D{Bool, Float64, Point3D}(rect);
subdivide_duals!(d_rect, Circumcenter());

# For the theory of the DEC, we will need to associate each operator to the precomputed matrix specific to our dual mesh.
operator_lookup = ThDEC.create_dynamic_model(d_rect, DiagonalHodge())

# We now need to convert our model to an ODEProblem. In our case, ``vfield`` produces
vf = vfield(exp_growth, operator_lookup)

U = first.(d_rect[:point]);

constants_and_parameters = ComponentArray(k=-0.5,)

t0 = 50.0

@info("Precompiling Solver")
prob = ODEProblem(vf, U, (0, t0), constants_and_parameters);
soln = solve(prob, Tsit5());

save_dynamics(soln, "decay.gif")

57 changes: 57 additions & 0 deletions roe/docs/make.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using Documenter
using Literate
using Distributed

using DEC

using CairoMakie

# 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/DEC.jl/blob/gh-pages/dev"
# config["repo_root_url"] = "https://github.com/AlgebraicJulia/Decapodes.jl/blob/main/docs"
# end

const literate_dir = joinpath(@__DIR__, "..", "examples")
const generated_dir = joinpath(@__DIR__, "src", "examples")

@info "Building literate files"
for (root, dirs, files) in walkdir(literate_dir)
out_dir = joinpath(generated_dir, relpath(root, literate_dir))
pmap(files) do file
f,l = splitext(file)
if l == ".jl" && !startswith(f, "_")
Literate.markdown(joinpath(root, file), out_dir;
config=config, documenter=true, credit=false)
Literate.notebook(joinpath(root, file), out_dir;
execute=true, documenter=true, credit=false)
end
end
end
@info "Completed literate"

pages = Any[]
push!(pages, "DEC.jl" => "index.md")
push!(pages, "Library Reference" => "api.md")

@info "Building Documenter.jl docs"
makedocs(
modules = [DEC],
format = Documenter.HTML(
assets = ["assets/analytics.js"],
),
remotes = nothing,
sitename = "DEC.jl",
doctest = false,
checkdocs = :none,
pages = pages)


@info "Deploying docs"
deploydocs(
target = "build",
repo = "github.com/AlgebraicJulia/DEC.jl.git",
branch = "gh-pages",
devbranch = "main"
)
Loading
Loading