Tutorial#

Kappybara works in terms of patterns, mixtures, and rules, culminating in systems. Let’s see how the API works at each level. (This tutorial is a work in progress and will in the future also describe such core functionality as expressions and ComponentMixtures.)

Patterns#

As in Kappa, patterns (subsets of a mixture on which rules can operate) can be broken down into components (a connected set of agents). Agents are typed objects with named sites, each of which has an internal state and possibly a binding partner – the site of another agent. All of these classes of objects can be constructed from Kappa strings using the from_kappa classmethod.

[2]:
from kappybara.pattern import Agent, Pattern, Component

# Parse agents A and B from Kappa strings
a = Agent.from_kappa("A(x[.])")
b = Agent.from_kappa("B(x[.])")
print("Agent A:", a.kappa_str)
print("Agent B:", b.kappa_str)

# Create an AB complex
complex = Component.from_kappa("A(x[1]), B(x[1])")
# Or equivalently:
complex = Pattern.from_kappa("A(x[1]), B(x[1])").components[0]
print("Complex", complex.kappa_str)
Agent A: A(x[.])
Agent B: B(x[.])
Complex A(x[1]), B(x[1])

Each agent in a mixture is essentially a node in a graph. Kappybara implements functions such as the ones below, identifying neighbors of an agent and checking whether a component embeds in another, or is in other words isomorphic to a subset of another.

[3]:
root = next(iter(complex))
print("Root agent type:", root.type)
print("Neighbors of root:", [neighbor.type for neighbor in root.neighbors])

self_embeddings = list(complex.embeddings(complex))
print("Embeddings of complex in itself:", len(self_embeddings))
Root agent type: A
Neighbors of root: ['B']
Embeddings of complex in itself: 1

Mixtures#

A mixture, like components and patterns, is a collection of agents, but one which facilitates the application of rules by efficiently updating embeddings according to changes in the mixture. Mixtures can be initialized and adjusted programatically, or again initialized from Kappa strings.

[4]:
from kappybara.mixture import Mixture

mixture = Mixture()
mixture.instantiate("A(x[.])", n_copies=3)
mixture.instantiate("B(x[.])", n_copies=2)
mixture.instantiate("A(x[1]), B(x[1])", n_copies=2)

print(f"Mixture as a Kappa string:\n{mixture.kappa_str}\n")

# Track a component pattern to query embeddings efficiently
mixture.track_component(complex)
print("#AB embeddings (cached):", len(mixture.embeddings(complex)))
Mixture as a Kappa string:
%init: 3 A(x[.])
%init: 2 B(x[1]), A(x[1])
%init: 2 B(x[.])

#AB embeddings (cached): 2

Rules#

Rules transform agents matched by their left-hand side into those specified by the right-hand side. Specifically, rate(system) evaluates the stochastic rate (possibly using variables), n_embeddings(mixture) counts applicable embeddings, and select(mixture) samples a specific embedding and returns a MixtureUpdate, which a mixture can then take to efficiently apply the corresponding update.

Let’s add a rule to bind A and B into AB and see how the mixture applies it.

[5]:
from kappybara.rule import KappaRule

print("AB count before one binding event:", len(mixture.embeddings(complex)))

bind = KappaRule.from_kappa("A(x[.]), B(x[.]) -> A(x[1]), B(x[1]) @ 1")

# Track the components on the left-hand side of the rule
for component in bind.left.components:
    mixture.track_component(component)

update = bind.select(mixture)
mixture.apply_update(update)

mixture.track_component(complex)
print("AB count after one binding event:", len(mixture.embeddings(complex)))
AB count before one binding event: 2
AB count after one binding event: 3

Systems#

A system bundles a mixture with rules and observables and is used for simulation. Start with the reversible binding system in the Examples gallery to see how the API works at this highest level.