Chemostat#

A chemostat can maintain a concentration of nutrients in a container by flowing in and flowing out nutrients each at a constant rate. Here we will set up a simulatable chemostat with a clonal and unevolving unicellular organism, and adjust the flow rate to observe the effect on the growth rate of the cells. More specifically, we will assume that a cell C replicates following metabolism of three units of the nutrient N, and that cells die memorylessly at a constant stochastic rate.

[2]:
from kappybara.system import System

system = System.from_ka(
    """
    %init: 10 C(n{0})  // Start with a few cells

    %obs: 'C' |C()|

    . -> N() @ 1  // Nutrient inflow
    N() -> . @ 0.1  // Nutrient outflow

    // Metabolism
    C(n{0}), N() -> C(n{1}), . @ 0.5
    C(n{1}), N() -> C(n{2}), . @ 0.5
    C(n{2}), N() -> C(n{3}), . @ 0.5

    C(n{3}), . -> C(n{0}), C(n{0}) @ 0.1 // Cell division
    C() -> . @ 0.01  // Cell death
    """
)

If we started with only, say, one cell, it would be reasonably likely that the population would die out before replicating sufficiently to virtually preclude extinction.

Let’s start the chemostat and observe the population size:

[3]:
while system.time < 10 ** 3:
    system.update()
system.monitor.plot();
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[3], line 2
      1 while system.time < 10 ** 3:
----> 2     system.update()
      3 system.monitor.plot();

File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/kappybara/system.py:475, in System.update(self)
    473 def update(self) -> None:
    474     """Perform one simulation step."""
--> 475     self.wait()
    476     if (rule := self.choose_rule()) is not None:
    477         self.apply_rule(rule)

File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/kappybara/system.py:439, in System.wait(self)
    433 """Advance simulation time according to exponential distribution.
    434
    435 Raises:
    436     RuntimeWarning: If system has no reactivity (infinite wait time).
    437 """
    438 try:
--> 439     self.time += random.expovariate(self.reactivity)
    440 except ZeroDivisionError:
    441     warnings.warn(
    442         "system has no reactivity: infinite wait time", RuntimeWarning
    443     )

File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/kappybara/system.py:430, in System.reactivity(self)
    423 @property
    424 def reactivity(self) -> float:
    425     """The total reactivity of the system.
    426
    427     Returns:
    428         Sum of all rule reactivities.
    429     """
--> 430     return sum(self.rule_reactivities)

File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/functools.py:998, in cached_property.__get__(self, instance, owner)
    996 val = cache.get(self.attrname, _NOT_FOUND)
    997 if val is _NOT_FOUND:
--> 998     val = self.func(instance)
    999     try:
   1000         cache[self.attrname] = val

File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/kappybara/system.py:421, in System.rule_reactivities(self)
    414 @cached_property
    415 def rule_reactivities(self) -> list[float]:
    416     """The reactivity of each rule in the system.
    417
    418     Returns:
    419         List of reactivities corresponding to system rules.
    420     """
--> 421     return [rule.reactivity(self) for rule in self.rules.values()]

File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/kappybara/rule.py:186, in KappaRule.reactivity(self, system)
    175 def reactivity(self, system: "System") -> float:
    176     """Calculate the total reactivity of this rule in the given system.
    177
    178     Args:
   (...)    183         for rule symmetry.
    184     """
    185     return (
--> 186         self.n_embeddings(system.mixture) // self.n_symmetries * self.rate(system)
    187     )

File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/functools.py:998, in cached_property.__get__(self, instance, owner)
    996 val = cache.get(self.attrname, _NOT_FOUND)
    997 if val is _NOT_FOUND:
--> 998     val = self.func(instance)
    999     try:
   1000         cache[self.attrname] = val

File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/kappybara/rule.py:218, in KappaRule.n_symmetries(self)
    215     r_site.agent = r
    216     r_site.partner = l_site
--> 218     l.interface["__temp__"] = l_site
    219     r.interface["__temp__"] = r_site
    221 pattern = Pattern(left_agents + right_agents)

AttributeError: 'NoneType' object has no attribute 'interface'

Now let’s increase inflow of the nutrient without changing the outflow rate and observe that the population size increases:

[4]:
system.add_rule(". -> N() @ 1")

while system.time < 5 * 10 ** 3:
    system.update()
system.monitor.plot();
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[4], line 4
      1 system.add_rule(". -> N() @ 1")
      3 while system.time < 5 * 10 ** 3:
----> 4     system.update()
      5 system.monitor.plot();

File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/kappybara/system.py:475, in System.update(self)
    473 def update(self) -> None:
    474     """Perform one simulation step."""
--> 475     self.wait()
    476     if (rule := self.choose_rule()) is not None:
    477         self.apply_rule(rule)

File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/kappybara/system.py:439, in System.wait(self)
    433 """Advance simulation time according to exponential distribution.
    434
    435 Raises:
    436     RuntimeWarning: If system has no reactivity (infinite wait time).
    437 """
    438 try:
--> 439     self.time += random.expovariate(self.reactivity)
    440 except ZeroDivisionError:
    441     warnings.warn(
    442         "system has no reactivity: infinite wait time", RuntimeWarning
    443     )

File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/kappybara/system.py:430, in System.reactivity(self)
    423 @property
    424 def reactivity(self) -> float:
    425     """The total reactivity of the system.
    426
    427     Returns:
    428         Sum of all rule reactivities.
    429     """
--> 430     return sum(self.rule_reactivities)

File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/functools.py:998, in cached_property.__get__(self, instance, owner)
    996 val = cache.get(self.attrname, _NOT_FOUND)
    997 if val is _NOT_FOUND:
--> 998     val = self.func(instance)
    999     try:
   1000         cache[self.attrname] = val

File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/kappybara/system.py:421, in System.rule_reactivities(self)
    414 @cached_property
    415 def rule_reactivities(self) -> list[float]:
    416     """The reactivity of each rule in the system.
    417
    418     Returns:
    419         List of reactivities corresponding to system rules.
    420     """
--> 421     return [rule.reactivity(self) for rule in self.rules.values()]

File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/kappybara/rule.py:186, in KappaRule.reactivity(self, system)
    175 def reactivity(self, system: "System") -> float:
    176     """Calculate the total reactivity of this rule in the given system.
    177
    178     Args:
   (...)    183         for rule symmetry.
    184     """
    185     return (
--> 186         self.n_embeddings(system.mixture) // self.n_symmetries * self.rate(system)
    187     )

File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/functools.py:998, in cached_property.__get__(self, instance, owner)
    996 val = cache.get(self.attrname, _NOT_FOUND)
    997 if val is _NOT_FOUND:
--> 998     val = self.func(instance)
    999     try:
   1000         cache[self.attrname] = val

File /opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/kappybara/rule.py:218, in KappaRule.n_symmetries(self)
    215     r_site.agent = r
    216     r_site.partner = l_site
--> 218     l.interface["__temp__"] = l_site
    219     r.interface["__temp__"] = r_site
    221 pattern = Pattern(left_agents + right_agents)

AttributeError: 'NoneType' object has no attribute 'interface'