Introduction
Undirected wiring diagrams (UWDs) are a categorical formalism for describing how open systems compose by sharing interfaces. In a UWD, each subsystem is a box with ports, and composition happens by wiring ports of different boxes to common junctions. Because the wires are undirected, junctions model shared variables rather than directed signal flow.
UWDs originate from applied category theory and are a core data structure in the AlgebraicJulia ecosystem. They are used to compose dynamical systems, agent-based models, Petri nets, stock-flow models, and more. The key idea is that you can specify the architecture of a composed system (which subsystems share which variables) separately from the dynamics of each subsystem.
For example, an SIR epidemiological model can be decomposed into two sub-processes—infection and recovery—that share populations through junctions:
-
Infection involves the susceptible (
s) and infected (i) populations. -
Recovery involves the infected (
i) and recovered (r) populations.
The junction i is shared between both boxes, expressing
that the same infected population participates in both processes.
The UWD schema
A UWD is defined as an ACSet (attributed C-set) over the schema
SchUWD. The schema has four object types and three
morphisms:
| Object | Role |
|---|---|
Box |
A subsystem / process |
Port |
An interface point on a box |
OuterPort |
An interface point exposed to the outside world |
Junction |
A shared variable that ports and outer ports connect to |
| Morphism | Signature | Meaning |
|---|---|---|
box |
Port → Box | Which box a port belongs to |
junction |
Port → Junction | Which junction a port is wired to |
outer_junction |
OuterPort → Junction | Which junction an outer port exposes |
Junctions also carry a name attribute
(name: Junction → Name) so that each shared variable has a
human-readable label.
SchUWD
#> <acsets::BasicSchema>
#> @ obs : chr [1:4] "Box" "Port" "OuterPort" "Junction"
#> @ homs :List of 3
#> .. $ :List of 3
#> .. ..$ name : chr "box"
#> .. ..$ dom : chr "Port"
#> .. ..$ codom: chr "Box"
#> .. $ :List of 3
#> .. ..$ name : chr "junction"
#> .. ..$ dom : chr "Port"
#> .. ..$ codom: chr "Junction"
#> .. $ :List of 3
#> .. ..$ name : chr "outer_junction"
#> .. ..$ dom : chr "OuterPort"
#> .. ..$ codom: chr "Junction"
#> @ attrtypes: chr "Name"
#> @ attrs :List of 1
#> .. $ :List of 3
#> .. ..$ name : chr "name"
#> .. ..$ dom : chr "Junction"
#> .. ..$ codom: chr "Name"An empty UWD is created with UWD():
Creating UWDs with relation()
The relation() function provides a DSL inspired by
relational algebra. Outer port junction names are passed as bare
(unquoted) arguments, and boxes are specified through a
.boxes list of box_spec() calls.
Step 1: Define box specifications
Each box_spec() takes a name and a set of junction names
that the box connects to:
Step 2: Build the UWD with relation()
Pass the outer port junction names as bare symbols, and the box
specifications via .boxes:
w_rel <- relation(s, i, r, .boxes = list(
box_spec("infection", "s", "i"),
box_spec("recovery", "i", "r")
))This creates a UWD with:
-
3 junctions named
s,i,r - 3 outer ports (one for each junction exposed externally)
- 2 boxes (infection and recovery)
- 4 ports (infection has 2 ports, recovery has 2 ports)
Creating UWDs with uwd()
The uwd() function provides a more concise syntax. The
first argument is a character vector of outer port junction names, and
subsequent named arguments define boxes as character vectors of junction
names:
This produces the same structure as the relation() call
above:
nparts(w, "Junction")
#> [1] 3
nparts(w, "OuterPort")
#> [1] 3
nparts(w, "Box")
#> [1] 2
nparts(w, "Port")
#> [1] 4The uwd() syntax is generally preferred for its brevity,
while relation() may be more readable when box
specifications are complex or built programmatically.
Inspecting UWDs
Counting parts
Use nparts() to count elements of each type:
Querying morphisms with subpart()
Use subpart() to follow morphisms and attributes. For
example, to find which box each port belongs to:
# Port 1 belongs to which box?
subpart(w, 1, "box")
#> [1] 1
# Port 3 belongs to which box?
subpart(w, 3, "box")
#> [1] 2To find which junction each port connects to:
Listing all junction names
Use parts() to get all part IDs, then retrieve their
names:
Visualizing UWDs
The uwd_to_dot() function generates a Graphviz DOT
string for visualizing a UWD. In the output:
- Circles represent junctions (labeled with the junction name)
- Rectangles represent boxes
- Diamonds represent outer ports (the external interface)
- Edges connect boxes to junctions (via ports) and outer ports to junctions
to_graphviz(w)If you have the DiagrammeR package installed, you can
render this interactively with to_graphviz(w).
Epidemiological example: SIR model
The classic SIR (Susceptible-Infected-Recovered) compartmental model is a natural example of system composition. The model consists of two processes:
-
Infection: susceptible individuals become infected
through contact with infected individuals. This process involves the
sandicompartments. -
Recovery: infected individuals recover. This
process involves the
iandrcompartments.
The junction i is shared between both processes,
expressing that the infected population is the interface between
infection and recovery.
sir <- uwd(
c("s", "i", "r"),
infection = c("s", "i"),
recovery = c("i", "r")
)
nparts(sir, "Box")
#> [1] 2
nparts(sir, "Port")
#> [1] 4
nparts(sir, "Junction")
#> [1] 3The DOT visualization shows the two boxes connected through their shared junction:
to_graphviz(sir)This architecture can be used downstream to compose ODE systems, Petri nets, or agent-based models for each process into a complete SIR model.
Predator-prey example: Lotka-Volterra
The Lotka-Volterra predator-prey model decomposes into three processes:
-
Growth: prey (
x) reproduce, depending only on the prey population. -
Predation: predators (
y) consume prey (x), involving both populations. -
Decline: predators (
y) die, depending only on the predator population.
Let’s verify the structure:
nparts(lv, "Box") # 3 boxes: growth, predation, decline
#> [1] 3
nparts(lv, "Junction") # 2 junctions: x, y
#> [1] 2
nparts(lv, "OuterPort") # 2 outer ports
#> [1] 2
nparts(lv, "Port") # 4 ports: growth(1) + predation(2) + decline(1)
#> [1] 4The key feature here is that the x junction is shared
between growth and predation, and the y junction is shared
between predation and decline. Predation is the process that couples the
two populations.
to_graphviz(lv)Multi-component systems
UWDs naturally express more complex architectures where multiple subsystems share variables in overlapping ways. Consider an SEIR (Susceptible-Exposed-Infected-Recovered) epidemiological model with three processes:
-
Exposure: susceptible individuals are exposed
through contact with infected individuals (involves
s,e,i). -
Progression: exposed individuals become infectious
(involves
e,i). -
Recovery: infected individuals recover (involves
i,r).
seir <- uwd(
c("s", "e", "i", "r"),
exposure = c("s", "e", "i"),
progression = c("e", "i"),
recovery = c("i", "r")
)
nparts(seir, "Box") # 3 boxes
#> [1] 3
nparts(seir, "Junction") # 4 junctions: s, e, i, r
#> [1] 4
nparts(seir, "OuterPort") # 4 outer ports
#> [1] 4
nparts(seir, "Port") # 7 ports: exposure(3) + progression(2) + recovery(2)
#> [1] 7The junction i appears in all three processes: as an
input to exposure (infected individuals drive new exposures), as the
output of progression, and as the input to recovery. This is the power
of UWDs—junctions naturally express many-to-many sharing of variables
across subsystems.
to_graphviz(seir)A larger example: coupled SIR with vital dynamics
Consider two SIR-like processes augmented with vital dynamics (birth and death). This model has five processes sharing four compartments:
coupled <- uwd(
c("s", "i", "r"),
birth = c("s"),
infection = c("s", "i"),
recovery = c("i", "r"),
waning = c("r", "s"),
death = c("s", "i", "r")
)
nparts(coupled, "Box") # 5 boxes
#> [1] 5
nparts(coupled, "Junction") # 3 junctions
#> [1] 3
nparts(coupled, "OuterPort") # 3 outer ports
#> [1] 3
nparts(coupled, "Port") # 9 ports
#> [1] 10Here the junction s participates in four processes
(birth, infection, waning, and death), while i participates
in three (infection, recovery, and death). The death box
connects to all three compartments, showing how UWDs easily represent
processes with broad interfaces.
to_graphviz(coupled)Summary
| Function | Purpose |
|---|---|
SchUWD |
The UWD schema (objects, homs, attrs) |
UWD() |
Create an empty UWD |
relation() |
DSL for building UWDs with box_spec()
|
box_spec() |
Define a box with its junction connections |
uwd() |
Concise syntax for building UWDs |
nparts() |
Count parts of a given type |
subpart() |
Query morphisms and attributes |
parts() |
List all part IDs of a given type |
uwd_to_dot() |
Generate Graphviz DOT visualization |
UWDs provide a compositional architecture language: they describe
how subsystems connect without prescribing what each
subsystem does. This separation of architecture from dynamics is the
foundation for compositional modeling in the AlgebraicJulia ecosystem,
and catlab brings this approach to R.