# Composing open dynamical systems II: Undirected composition

$$

$$

In this series of posts on open dynamical systems, we explore various theories of composition for open dynamical systems (shown in the diagram below) and use these theories to build complex systems out of primitive ones. In the previous post, we explored directed theories of composition (shown in purple). In this post, we will focus on undirected theories of composition (shown in blue) and give examples of composing dynamical systems using the AlgebraicDynamics.jl package.

## Complex compositions

Careful bookkeeping is endemic to implementing a complicated composition pattern. Previously we applied pullback functorial data migration in order to construct complex composition patterns more easily and with less room for error. In this post, we give two more strategies, which both rely on the rich structure of the composition syntax: 1. **Hierarchical composition**, in which a primitive system can itself be a composition of systems. Hierarchical composition is extremely useful when we want to divide and conquer or when we want to build upon a composite system without starting from scratch. This strategy is only available in open composition syntaxes such as \mathsf{Sch}(\mathsf{UWD}), \mathsf{Sch}(\mathsf{DWD}), \mathsf{Sch}(\mathsf{OpenCPG}). 2. **Taking (co)limits of composition patterns.** This strategy is available in all of the composition syntaxes discussed so far.

We will demonstrate these two strategies as we construct a complex ecosystem with six species: rabbits, foxes, hawks, little fish, big fish, and sharks.

### Divide and conquer

First we will apply hierarchical composition so that we don’t have to simultaneously study all of the interactions between the six species. We will separately model the three sub-ecosystems — land, air, and water — which may themselves be compositions of even more primitive system. Once we have constructed these sub-systems, we will compose them according to the following pattern:

```
#| output: false
eco_pattern = @relation (rabbits, foxes, hawks, littlefish, bigfish, sharks) begin
land_eco(rabbits, foxes, hawks)
air_eco(hawks, littlefish)
ocean_eco(littlefish, bigfish, sharks)
end
```

```
#| echo: false
to_graphviz(eco_pattern, box_labels = :name, junction_labels = :variable, edge_attrs=Dict(:len => ".75"))
```

In other words, there aren’t two independent hawk populations for the land and air sub-ecosystems. So in the final model we identify the hawk population from the land ecosystem and the hawk population from the air ecosystem. Likewise, for little fish.

### Land

Let’s zoom-in on the land ecosystem. We have five primitive systems which share variables: 1. rabbit growth: \dot r(t) = \alpha_1 r(t) 2. rabbit/fox predation: \dot r(t) = -\beta_1 r(t) f(t),\; \dot f(t) = \gamma_1 r(t)f(t) 3. fox decline: \dot f(t) = -\delta_1 f(t) 4. rabbit/hawk predation: \dot r(t) = -\beta_2 r(t)h(t),\; \dot h(t) = \gamma_2 r(t)h(t) 5. hawk decline: \dot h(t) = -\delta_2 h(t)

Therefore, the desired composition pattern has five boxes and many ports and wires to keep track of. Instead of implementing this composition pattern by hand, we construct it as a pushout of two simpler composition patterns — one which represents the rabbit/fox interactions and one which represents the rabbit/hawk interactions.

For the rabbit/fox composition pattern note that there are not two independent rabbit populations — one that grows and one that gets eaten by foxes. Likewise, there are not two independent fox populations — one that declines and one that feasts on rabbits. To capture these interactions, we identify the two rabbit populations and identify the two fox populations via the following composition pattern.

```
#| output: false
rabbitfox_pattern = @relation (rabbits, foxes) begin
rabbit_growth(rabbits)
rabbit_fox_predation(rabbits,foxes)
fox_decline(foxes)
end
```

```
#| echo: false
to_graphviz(rabbitfox_pattern, box_labels = :name, junction_labels = :variable, edge_attrs=Dict(:len => ".75"))
```

The rabbit/hawk composition pattern is identical.

```
#| output: false
rabbithawk_pattern = @relation (rabbits, hawks) begin
rabbit_growth(rabbits)
rabbit_hawk_predation(rabbits,hawks)
hawk_decline(hawks)
end
```

Now we construct the complete composition pattern for the land ecosystem by gluing these two composition patterns along the box corresponding to rabbit growth.

```
#| output: false
using Catlab.CategoricalAlgebra
# Define the composition pattern for rabbit growth
rabbit_pattern = @relation (rabbits,) -> rabbit_growth(rabbits)
# Define transformations between the composition patterns
rabbitfox_transform = ACSetTransformation((Box=[1], Junction=[1], Port=[1], OuterPort=[1]), rabbit_pattern, rabbitfox_pattern)
rabbithawk_transform = ACSetTransformation((Box=[1], Junction=[1], Port=[1], OuterPort=[1]), rabbit_pattern, rabbithawk_pattern)
# Take the pushout
land_pattern = ob(pushout(rabbitfox_transform, rabbithawk_transform))
```

```
#| echo: false
to_graphviz(land_pattern, box_labels = :name, junction_labels = :variable, edge_attrs=Dict(:len => ".9"))
```

Phew! After seeing the complexity of this composition pattern, we are grateful that we used a pushout instead of constructing it by hand. Lastly, we define the primitive systems and compose.

```
#| output: false
dotr(u,p,t) = p.α₁*u
dotrf(u,p,t) = [-p.β₁*u[1]*u[2], p.γ₁*u[1]*u[2]]
dotf(u,p,t) = -p.δ₁*u
dotrh(u, p, t) = [-p.β₂*u[1]*u[2], p.γ₂*u[1]*u[2]]
doth(u, p, t) = -p.δ₂*u
# Define the primitive systems
rabbit_growth = ContinuousResourceSharer{Float64}(1, 1, dotr, [1])
rabbitfox_predation = ContinuousResourceSharer{Float64}(2, 2, dotrf, [1,2])
fox_decline = ContinuousResourceSharer{Float64}(1, 1, dotf, [1])
rabbithawk_predation= ContinuousResourceSharer{Float64}(2, 2, dotrh, [1,2])
hawk_decline = ContinuousResourceSharer{Float64}(1, 1, doth, [1])
# Compose
land_sys= oapply(land_pattern, [rabbit_growth, rabbitfox_predation, fox_decline, rabbithawk_predation, hawk_decline]);
```

The resource sharer `land_sys`

models the land ecosystem. Although it will play the role of a primitive system in our total ecosystem, we see that it is a composite itself. This structure — primitive systems themselves being composites — is the essence of hierarchical composition.

### Air

The air ecosystem is straightforwardly defined by the following resource sharer which models hawk/little fish predation. Here we don’t model the decay of hawks or the growth of little fish because these processes are already accounted for in the land and water ecosystems. The air ecosystem is a pure coupling term between the two systems.

### Water

In the previous post, we defined a ocean ecosystem as the directed composition of machines. In this aquatic foodchain, sharks eat big fish and big fish eat little fish. We can turn the machine representing this ocean ecosystem into a resource sharer as follows.

```
#| include: false
using AlgebraicDynamics.DWDDynam
dotfish(f, x, p, t) = [p.α₄*f[1] - p.β₄*x[1]*f[1]]
dotFISH(F, x, p, t) = [p.γ₄*x[1]*F[1] - p.δ₄*F[1] - p.β₅*x[2]*F[1]] # p[1] - prey, p[2] - predator
dotsharks(s, x, p, t) = [p.γ₅*s[1]*x[1]-p.δ₅*s[1]]
# Define the primitive systems
fish = ContinuousMachine{Float64}(1,1,1, dotfish, (f,p,t)->f)
FISH = ContinuousMachine{Float64}(2,1,1, dotFISH, (F,p,t)->F)
sharks = ContinuousMachine{Float64}(1,1,1, dotsharks, (s,p,t)->s)
# define the composition pattern
ocean_pattern = WiringDiagram([], [])
fish_box = add_box!(ocean_pattern, Box("little fish", [:pop], [:pop]))
Fish_box = add_box!(ocean_pattern, Box("Big Fish", [:pop, :pop], [:pop]))
shark_box = add_box!(ocean_pattern, Box("sharks", [:pop], [:pop]))
add_wires!(ocean_pattern, Pair[
(fish_box, 1) => (Fish_box, 1),
(shark_box, 1) => (Fish_box, 2),
(Fish_box, 1) => (fish_box, 1),
(Fish_box, 1) => (shark_box, 1)
])
# compose
ocean_sys = oapply(ocean_pattern, [fish, FISH, sharks]);
```

### Putting it all together

Now that we have constructed resource sharers for the three sub-ecosystems, we are ready to plug them into the established composition pattern, `eco_pattern`

.

```
# Compose
eco_system = oapply(eco_pattern, [land_sys, air_sys, water_sys])
# Plot and solve
u0 = [100.0, 50.0, 20.0, 100, 10, 2.0]
params = LVector(
α₁ = 0.3, β₁ = 0.015, γ₁ = 0.015, δ₁ = 0.7, β₂ = .01, γ₂ = .01, δ₂ = .5, # land params
γ₃ = 0.001, β₃ = 0.003, # air params
α₄ = 0.35, β₄ = 0.015, γ₄ = 0.015, δ₄ = 0.7, β₅ = 0.017, γ₅ = 0.017, δ₅ = 0.35 # water params
)
tspan = (0.0, 75.0)
prob = ODEProblem(eco_system, u0, tspan, params)
sol = solve(prob, Tsit5())
plot(sol, lw=2, label = ["rabbits" "foxes" "hawks" "little fish" "big fish" "sharks"])
```

A successful example of hierarchical composition!

To really see the benefits of hierarchical composition, let’s take a look at how we could have achieved this composition without the layered approach.

```
#| output: false
# Define the (flattened) composition pattern
flattened_pattern = ocompose(eco_pattern, 1, land_pattern)
#Compose
eco_sys2 = oapply(flattened_pattern, [rabbit_growth, rabbitfox_predation, fox_decline, rabbithawk_predation, hawk_decline, air_sys, water_sys]);
```

While this strategy produces the same composite system,^{3} it lacks the clarity of the hierarchical approach which we can see from the flattened composition pattern:

## What next?

You can find more composite dynamical systems (including a multi-city SIR model and a cellular automata) in the documentation for AlgebraicDynamics.jl. As this package develops, a large class of open projects is to implement black and grey boxing functors between operad algebras, such as: - A bridge from AlgebraicPetri to AlgebraicDynamics - Integrators (beyond Euler’s method), including ones that take advantage of hierarchical and compositional structure - An implementation of the claim “every machine is a resource sharer and every composition of machines is a composition of resource sharers”

## References

## Footnotes

Undirected wiring diagrams form an operad where operadic composition is given by a pushout, shown in (Spivak 2013). Here you can learn more about the operadic composition of undirected wiring diagrams as well as the

`@relation`

macro for specifying undirected wiring diagrams.↩︎This interpretation of composition of dynamical systems is mathemetized by the cospan algebra \mathsf{Dynam} defined in (Baez and Pollard 2017).↩︎

Functoriality of the cospan algebra \mathsf{Dynam} implies that the resource sharers

`eco_sys`

and`eco_sys2`

have identical dynamics even though the dynamics are described by different expressions.↩︎