Building computational models via structural mathematics

Kris Brown

Topos Institute

(press s for speaker notes)

3/6/24

Outline

I. Future of software engineering

II. Mini examples of the future paradigm

III. Intuition for how structural mathematics works

Stages of development for software engineering

  1. We solve problems one at a time, as they come.

This starts feeling repetitive.

  1. Abstractions act as a solution to an entire class of problems.

This feels repetitive insofar as we feel the problem classes are related.

Common abstractions are things like functions / datatypes / scripts in our language.

  1. We migrate abstractions to allow them to address changes in problem specification.

Future paradigm: mathematically-principled + automatic abstraction reuse

Understand structure of the abstraction + implementation + the relationship between them.

Three examples of abstractions

Abstraction: databases (SQL)

CREATE SCHEMA COLORGRAPH;
CREATE TABLE VERT (ID PRIMARY KEY, COLOR VARCHAR);
CREATE TABLE EDGE (ID PRIMARY KEY, 
                   FOREIGN KEY TGT REFERENCES VERT(ID),
                   FOREIGN KEY SRC REFERENCES VERT(ID));


INSERT INTO VERT VALUES ('B')
INSERT INTO VERT VALUES ('R')
INSERT INTO VERT VALUES ('Y')
INSERT INTO EDGE VALUES (1,2)
INSERT INTO EDGE VALUES (2,3)
INSERT INTO EDGE VALUES (3,1)

Abstraction: databases (Julia)

struct ColorGraph
  edges::Set{Pair{Int, Int}}
  vertices::Set{Int}
  colors::Dict{Int, String}
end
@present SchColorGraph begin
  (V, E) :: Ob; (src, tgt) :: Hom(E, V)
  Color :: AttrType; color :: Attr(V, Color)
end
@acset_type ColorGraph(SchColorGraph){String}


function triangle!(graph=ColorGraph())
  a, b, c = [add_vertex!(graph, color)  
             for color in "BRY"]
  [add_edge!(graph, s, t) for 
    (s, t) in [[a,b],[b,c],[c,a]]]
  return graph
end

New problem expressed in terms of old problem.

Gluing: side-by-side comparison

"""Modify to allow it to overlap existing vertex IDs"""
function triangle!(graph, v1=nothing, v2=nothing, v3=nothing)
  vs = map([v1=>"B", v2=>"R", v3=>"Y"]) do (v, color)
    isnothing(v) ? add_vertex!(graph, color) : v
  end
  [add_edge!(graph, vs[i]...) for i in [1:2,2:3,[3,1]]]
  vs
end

"""Now expressible as an abstraction of `triangle!`"""
function big_tri!(graph)
  blue, red, _ = triangle!(graph)
  _, _, yellow = triangle!(graph, blue)
  triangle!(graph, nothing, red, yellow)
end
function replace!(graph, fk::Symbol, 
                  find::Int, repl::Int)
end # TODO

function big_tri!(graph)
  triangle!(graph)
  triangle!(graph)
  triangle!(graph)
  for fk in [:src, :tgt]
    replace!(graph, fk, 5, 2)
    replace!(graph, fk, 7, 1)
    replace!(graph, fk, 9, 5)
  end
  [rem_vertex!(graph, v) for v in [5,7,9]]
end

Gluing: side-by-side comparison

"""Modify to allow it to overlap existing vertex IDs"""
function triangle!(graph, v1=nothing, v2=nothing, v3=nothing)
  vs = map([v1=>"B", v2=>"R", v3=>"Y"]) do (v, color)
    isnothing(v) ? add_vertex!(graph, color) : v
  end
  [add_edge!(graph, vs[i]...) for i in [1:2,2:3,[3,1]]]
  vs
end

"""Now expressible as an abstraction of `triangle!`"""
function big_tri!(graph)
  blue, red, _ = triangle!(graph)
  _, _, yellow = triangle!(graph, blue)
  triangle!(graph, nothing, red, yellow)
end
function replace!(graph, fk::Symbol, 
                  find::Int, repl::Int)
end # TODO

function big_tri!(graph)
  triangle!(graph)
  triangle!(graph)
  triangle!(graph)
  for fk in [:src, :tgt]
    replace!(graph, fk, 5, 2)
    replace!(graph, fk, 7, 1)
    replace!(graph, fk, 9, 5)
  end
  [rem_vertex!(graph, v) for v in [5,7,9]]
end


AlgebraicJulia

# Building blocks
T1,T2,T3 = [triangle!() for _ in 1:3]
b,r,y = [add_vertex!(ColorGraph(), c) for c in "BRY"]

# Relation of building blocks to each other
pattern = @relation (;) where (t1, t2, t3) begin
  b(t1, t2); r(t2, t3); y(t3, t1)
end

# Composition
big_tri = glue(pattern; T1, T2, T3, b, r, y)

Gluing: side-by-side comparison

"""Modify to allow it to overlap existing vertex IDs"""
function triangle!(graph, v1=nothing, v2=nothing, v3=nothing)
  vs = map([v1=>"B", v2=>"R", v3=>"Y"]) do (v, color)
    isnothing(v) ? add_vertex!(graph, color) : v
  end
  [add_edge!(graph, vs[i]...) for i in [1:2,2:3,[3,1]]]
  vs
end

"""Now expressible as an abstraction of `triangle!`"""
function big_tri!(graph)
  blue, red, _ = triangle!(graph)
  _, _, yellow = triangle!(graph, blue)
  triangle!(graph, nothing, red, yellow)
end
function replace!(graph, fk::Symbol, 
                  find::Int, repl::Int)
end # TODO

function big_tri!(graph)
  triangle!(graph)
  triangle!(graph)
  triangle!(graph)
  for fk in [:src, :tgt]
    replace!(graph, fk, 5, 2)
    replace!(graph, fk, 7, 1)
    replace!(graph, fk, 9, 5)
  end
  [rem_vertex!(graph, v) for v in [5,7,9]]
end


AlgebraicJulia

# Building blocks
T1,T2,T3 = [triangle!() for _ in 1:3]
b,r,y = [add_vertex!(ColorGraph(), c) for c in "BRY"]

# Relation of building blocks to each other
pattern = @relation (;) where (t1, t2, t3) begin
  b(t1, t2); r(t2, t3); y(t3, t1)
end

# Composition
big_tri = glue(pattern; T1, T2, T3, b, r, y)

Abstraction: Petri nets

struct PetriNet
  S::Set{Symbol}
  T::Dict{Symbol, 
       Pair{Vector{Symbol},
            Vector{Symbol}
           }
     }
end
@present SchemaPetriNet(FreeSchema) begin 
  (S, T, I, O)::Ob
  is::Hom(I, S)
  os::Hom(O, S)
  it::Hom(I, T)
  ot::Hom(O, T)
end
@acset_type PetriNet(SchemaPetriNet)

sis = PetriNet(Set([:S, :I]), 
               Dict(:inf => ([:S,:I] => [:I,:I]),
                    :rec => ([:I]    => [:S])))

function ODE(p::PetriNet, rates, init, time) end
function stochastic(p::PetriNet, rates, init, time) end

New problem expressed in terms of old problem.

Multiplying: side-by-side comparison

prepend(pre) = s -> pre * "_" * s

US, EU, E, W = 
  prepend.(["US", "EU", "east", "west"])

function mult_EU(petri::PetriNet)
  res = PetriNet()
  for state in petri.S
    us = add_state!(res, US(state))
    eu = add_state!(res, EU(state))
    add_transition!(res, E(state), [us]=>[eu])
    add_transition!(res, W(state), [eu]=>[us])
  end
  for (T, (I, O)) in petri.T
    add_transition!(res, US(T), US.(I)=>US.(O))
    add_transition!(res, EU(T), EU.(I)=>EU.(O))
  end
  res
end
function multiply(r1::PetriNet, r2::PetriNet)
  res = PetriNet()
  for (s1, s2) in Iterators.product(r1.S, r2.S)
    add_state!(res, "$(s1)_$(s2)")
  end
  for rx1 in r1.T
    for s2 in r2.S
      add_transition!(res, rename_rxn(rx1, s2))
    end
  end
  for rx2 in r2.T
    for s1 in r1.S
      add_transition!(res, rename_rxn(rx2, s1))
    end
  end
  RxnNet(rs)
end

rename_rxn(rxn, species::Symbol) = # TODO

Multiplying: back to basics

Multiplying: back to basics

Multiplying: back to basics

Multiplying: side-by-side comparison

prepend(pre) = s -> pre * "_" * s

US, EU, E, W = 
  prepend.(["US", "EU", "east", "west"])

function mult_EU(petri::PetriNet)
  res = PetriNet()
  for state in petri.S
    us = add_state!(res, US(state))
    eu = add_state!(res, EU(state))
    add_transition!(res, E(state), [us]=>[eu])
    add_transition!(res, W(state), [eu]=>[us])
  end
  for (T, (I, O)) in petri.T
    add_transition!(res, US(T), US.(I)=>US.(O))
    add_transition!(res, EU(T), EU.(I)=>EU.(O))
  end
  res
end
function multiply(r1::PetriNet, r2::PetriNet)
  res = PetriNet()
  for (s1, s2) in Iterators.product(r1.S, r2.S)
    add_state!(res, "$(s1)_$(s2)")
  end
  for rx1 in r1.T
    for s2 in r2.S
      add_transition!(res, rename_rxn(rx1, s2))
    end
  end
  for rx2 in r2.T
    for s1 in r1.S
      add_transition!(res, rename_rxn(rx2, s1))
    end
  end
  RxnNet(rs)
end

rename_rxn(rxn, species::Symbol) = # TODO

AlgebraicJulia

P_base = @acset PetriNet begin 
  S=1; T=2; I=2; O=2; is=1; os=1; it=1:2; ot=1:2 
end

# Label P1 and P2 transitions as green or orange
h1 = homomorphism(P1, P_base; init=...)
h2 = homomorphism(P2, P_base; init=...)

# Composition of old problems
US_EU_SIS = multiply(h1, h2)

Multiplying: side-by-side comparison

prepend(pre) = s -> pre * "_" * s

US, EU, E, W = 
  prepend.(["US", "EU", "east", "west"])

function mult_EU(petri::PetriNet)
  res = PetriNet()
  for state in petri.S
    us = add_state!(res, US(state))
    eu = add_state!(res, EU(state))
    add_transition!(res, E(state), [us]=>[eu])
    add_transition!(res, W(state), [eu]=>[us])
  end
  for (T, (I, O)) in petri.T
    add_transition!(res, US(T), US.(I)=>US.(O))
    add_transition!(res, EU(T), EU.(I)=>EU.(O))
  end
  res
end
function multiply(r1::PetriNet, r2::PetriNet)
  res = PetriNet()
  for (s1, s2) in Iterators.product(r1.S, r2.S)
    add_state!(res, "$(s1)_$(s2)")
  end
  for rx1 in r1.T
    for s2 in r2.S
      add_transition!(res, rename_rxn(rx1, s2))
    end
  end
  for rx2 in r2.T
    for s1 in r1.S
      add_transition!(res, rename_rxn(rx2, s1))
    end
  end
  RxnNet(rs)
end

rename_rxn(rxn, species::Symbol) = # TODO

AlgebraicJulia

P_base = @acset PetriNet begin 
  S=1; T=2; I=2; O=2; is=1; os=1; it=1:2; ot=1:2 
end

# Label P1 and P2 transitions as green or orange
h1 = homomorphism(P1, P_base; init=...)
h2 = homomorphism(P2, P_base; init=...)

# Composition of old problems
US_EU_SIS = multiply(h1, h2)

Abstraction: Wiring diagrams

function goods_flow(steps::Int, initial::Vector, 
                    input::Function)
  output = []
  trade₁, trade₂ = initial
  for step in 1:steps
    trade₁, (trade₂, out) = [
      trader₁(input(step), trade₂),
      trader₂(trade₁)
    ]
    push!(output, out)
  end
  output
end
"""
A + B = C
±√(C) = (B, D) 

Find all integer solutions (A, D) up to `n`
"""
function solve_constraint(n::Int)
  filter(Iterators.product(0:n, 0:n)) do (A, D)
    B = -D
    C = A + B
    C == D^2
  end
end

Abstraction: Wiring diagrams

New problem expressed in terms of old problem.

Substituting: side-by-side comparison

function goods_flow(steps::Int, initial::Vector, 
                    input::Function)
  output = []
  trade₁, trade₂ = initial
  for step in 1:steps
    trade₁, (trade₂, out) = [
      trader₁(input(step), trade₂),
      trader₂(trade₁)
    ]
    push!(output, out)
  end
  output
end



\(\Rightarrow\)

function goods_flow(steps::Int, initial::Vector, 
                    input::Function)
  output = []
  trade₂, trade₃, trade₄ = initial
  for step in 1:steps
    trade₂, trade₃, (trade₄, out) = [ 
      trader₂(trade₃),
      trader₃(input(step), trade₂),
      trader₄(trade₃, trade₂),
    ]
    push!(output, out)
  end
  output
end

Substituting: side-by-side comparison

"""
A + B = C
±√(C) = (B, D) 

Find all integer solutions (A, D) up to `n`
"""
function solve_constraint(n::Int)
  filter(Iterators.product(0:n, 0:n)) do (A, D)
    B = -D
    C = A + B
    C == D^2
  end
end



\(\Rightarrow\)

"""
A + B = C
C * B = D
±√(D) = (B, E) 

Find all integer solutions (A, E) up to `n`
"""
function solve_constraint(n::Int)
  filter(Iterators.product(0:n, 0:n)) do (A, E)
    B = -E
    C = A + B
    D = C * B 
    D == E^2
  end
end

Substituting: side-by-side comparison

sys₁ = Trader(2, 1, (x,y)-> ...)
sys₂ = Trader(1, 2, (x,)-> ...)
sys₃ = Trader(2, 1, (x,y)-> ...)
sys₄ = Trader(2, 1, (x,y)-> ...)
sys₁ = Relation(2, 1, (xs,ys)-> xs[1] + xs[2] == ys[1])
sys₂ = Relation(1, 2, (xs,ys)-> sqrt(xs[1]) == ys[1] 
                                && ys[1]== -ys[2])
sys₃ = sys₁
sys₄ = Relation(2, 1, (xs,ys)-> xs[1] * xs[2] == ys[1])


AlgebraicJulia

wd₁₂ = WiringDiagram([:A], [:A])
b1 = add_box!(wd₁₂, Box(:op₁, [:A, :D], [:C]))
b2 = add_box!(wd₁₂, Box(:op₂, [:C], [:D, :A]))
add_wires!(wd₁₂, Pair[
  (input_id(wd₁₂), 1) => (b1, 2),
  (b1, 1)          => (b2, 1),
  (b2, 1)          => (b1, 1),
  (b2, 2)          => (output_id(wd₁₂), 1)
])


model = oapply(wd₁₂,[sys₁,sys₂])
wd′ = ocompose(wd₁₂, 1, wd₃₄)
model′= oapply(wd′,[sys₂,sys₃,sys₄])

Changing from one abstraction to another

New problem expressed in terms of old problem.

Changing from one abstraction to another

New problem expressed in terms of old problem.

Changing abstractions: side-by-side

"""Species and transitions are vertices.
   Inputs and outputs are edges."""
function petri_to_graph(p::PetriNet)
  grph = Graph()
  vs = Dict(s => add_vertex!(grph) for s in p.S)
  for (t, (i, o)) in pairs(p.T)
    t = add_vertex!(grph) 
    for eᵢ in i 
      add_edge!(grph, vs[eᵢ], t)
    end 
    for eₒ in o
      add_edge!(grph, t, vs[eₒ])
    end 
  end
  grph
end
"""
Vertices are species.
Edges are transitions with one input and
one output.
"""
function graph_to_petri(g::Graph)
  P = PetriNet()
  for i in G.vertices
    add_species(P, Symbol("v$i")) 
  end
  for (e, (s, t)) in enumerate(G.edges)
    add_transition(Symbol("e$e"),
                   [P.S[s]] => [P.S[t]])
  end
  P
end


AlgebraicJulia

F = FinFunctor(SchPetri, SchGraph; 
  init = (S => V, T => V, I => E, O => E))
my_graph = SigmaMigration(F, my_petri)
F = FinFunctor(SchPetri, SchGraph; 
  init = (S => V, T => E, I => E, O => E))
my_petri = DeltaMigration(F, my_graph)

Replacing code with data

Common theme: writing code vs declaring relationships between abstractions.

Problem Julia solution AlgebraicJulia solution
Different pieces of a model need to be glued together. Write a script which does the gluing or modifies how pieces are constructed. Declare how overlap relates to the building blocks. (colimits)
Different aspects of a model need to be combined / a distinction is needed. Write a script which creates copies of one aspect for every part of the other aspect. Declare how the different aspects interact with each other. (limits)
We want to integrate systems at different levels of granularity. Refactor the original code to incorporate the more detailed subsystem. Separate syntax/semantics. Declare how the part relates to the whole at syntax level. (operads)
We make a new assumption and want to migrate old knowledge into our new understanding. Write a script to convert old data into updated data. Declare how the new way of seeing the world (i.e. schema) is related to the old way. (data migration)

Why does it work?

Informal definition: a category is a bunch of things that are related to each other

Key intuition: category theory is concerned with universal properties.

  • These change something that we once thought of as a property of an object into a kind of relationship that object has towards related objects.

Example universal property: emptiness

Consider mathematical sets which are related to each other via functions.

Definition in terms of internal properties

The empty set is the unique set which has no elements in it.

But if we we look at how the empty set relates to all the other sets, we’ll eventually notice something about these relations.

Definition in terms of external relationships (universal properties)

The empty set is the unique set which has exactly one function into every other set.

Example universal property: emptiness

Consider colored graphs related to each other via vertex mappings which preserve color and edges.

Definition in terms of internal properties

The empty graph uniquely has no vertices nor edges in it.

But if we we look at how it relates to all the other graphs, we’ll eventually notice something characteristic.

Definition in terms of external relationships (universal properties)

The empty graph is the unique graph which has exactly one graph mapping into every other graph.

Universal properties and generalizable abstractions

Category theory enforces good conceptual hygeine - one isn’t allowed to depend on “implementation details” of the things which feature in its definitions.

This underlies the ability of models built in AlgebraicJulia to be extended and generalized without requiring messy code refactor.

Takeaways

CT is useful for the same reason interfaces are generally useful.

In particular, CT provides generalized1 notions of:

  • multiplication / multidimensionality
  • adding things side-by-side
  • gluing things along a common boundary
  • looking for a pattern
  • find-and-replace a pattern
  • parallel vs sequential processes
  • Mad Libs style filling in of wildcards
  • Zero and One
  • “Open” systems
  • Subsystems
  • Enforcing equations
  • Symmetry

These abstractions all fit very nicely with each other:

  • conceptually built out of basic ideas of limits, colimits, and morphisms.

We can use them to replace a large amount of our code with high level, conceptual data.

About the Topos Institute

Mission: to shape technology for public benefit by advancing sciences of connection and integration.

Three pillars of our work, from theory to practice to social impact:

  1. Collaborative modeling in science and engineering
  2. Collective intelligence, including theories of systems and interaction
  3. Research ethics


Decapodes.jl: multiphysics modeling

"""Define the multiphysics"""
Diffusion = @decapode DiffusionQuantities begin
  C::Form0{X}
  ϕ::Form1{X}
  ϕ == k(d₀{X}(C))   # Fick's first law
end
Advection = @decapode DiffusionQuantities begin
  C::Form0{X}
  (V, ϕ)::Form1{X}
  ϕ == ∧₀₁{X}(C,V)
end
Superposition = @decapode DiffusionQuantities begin
  (C, Ċ)::Form0{X}
  (ϕ, ϕ₁, ϕ₂)::Form1{X}
  ϕ == ϕ₁ + ϕ₂
== ⋆₀⁻¹{X}(dual_d₁{X}(⋆₁{X}(ϕ)))
  ∂ₜ{Form0{X}}(C) ==
end
compose_diff_adv = @relation (C, V) begin
  diffusion(C, ϕ₁)
  advection(C, ϕ₂, V)
  superposition(ϕ₁, ϕ₂, ϕ, C)
end
"""Geometry"""
mesh = loadmesh(Torus_30x10()) 
"""Assign semantics to operators"""
funcs = sym2func(mesh)
funcs[:k] = Dict(:operator => 0.05 * I(ne(mesh)), 
  :type => MatrixFunc())
funcs[:⋆₁] = Dict(:operator => (Val{1}, mesh, 
  hodge=DiagonalHodge()), :type => MatrixFunc());
funcs[:∧₀₁] = Dict(:operator => (r, c, v) -> r .= 
  (Tuple{0,1}, mesh, c, v), :type => InPlaceFunc())

Decapodes.jl: simulation

Resources

Hidden slide: this slide intentionally left blank

Hidden slide: Why Category Theory?

Focuses on relationships between things without talking about the things themselves.

Invented in the 1940’s to connect different branches of math.

A category consists of objects and morphisms (arrows).

  • We don’t need to know anything about the objects.
  • Compose \(A \rightarrow B\) and \(B \rightarrow C\) to get \(A \rightarrow C\).
  • Like a graph, but we care about paths, not edges.

CT studies certain shapes of combinations of arrows.

  • These can be local shapes, e.g. a span:    \(\huge \cdot \leftarrow \cdot \rightarrow \cdot\)

  • These can be global, e.g. an initial object: \(\huge \boxed{\cdot \rightarrow \cdot\rightarrow \cdot \rightarrow \dots}\)

Hidden slide: Applied Category Theory?

Compare to interfaces in computer science:

  • declare that some collection of things are related in a particular way without saying what they are.
interface Queue{A}

size(q:Queue) -> Int 
empty(q:Queue) -> Bool 
put(q:Queue, a:A) -> ()
get(q:Queue) -> A 

In some sense a category is just a particular interface.

interface Category{Ob,Arr}

dom(a:Arr) -> Ob 
codom(a:Arr) -> Ob 
compose(a:Arr, b::Arr) -> Arr 
id(o:Ob) -> Arr
  • Category of sets and functions
  • Category of sets and subsets
  • Category of \(\mathbb{Z}\) and \(\leq\)
  • Category of categories and functors
  • Category of chemical reaction networks
  • Category of chemical structures
  • Category of datasets
  • Category of datatypes and programs

CT is also the study of interfaces in general. It knows which are good ones.