The lac operon#

The E. coli lac operon controls the production of enzymes needed to metabolize lactose and is regulated in large part by lactose availability. This example model includes

  • LacI (I) repression of the lac operon via blocking of the operator (O),

  • lactose (L) transport by LacY (Y) permease,

  • conversion of lactose to allolactose (A) by LacZ β-galactosidase (Z),

  • and allolactose-mediated derepression of the operon.

[2]:
from kappybara.system import System

system = System.from_ka(
    """
    %init: 1 O(i[.])
    %init: 1 I(o[.])
    %init: 10 Y()
    %init: 10 Z(l[.])

    %obs: 'Extracellular lactose' |L(loc{out})| / 100
    %obs: 'LacY' |Y()|
    %obs: 'Free Lac operon' |O(i[.])|

    I(o[.]), O(i[.]) <-> I(o[1]), O(i[1]) @ 1, 0.1  // Repress the lac operon
    Y(), L(loc{out}) -> Y(), L(loc{in}) @ 0.01  // Transport lactose into the cell
    Z(l[.]), L(loc{in}) -> Z(l[.]), A(z[.], i[.]) @ 1  // Convert lactose into allolactose
    A(i[.]), I(o[.]) <-> A(i[1]), I(o[1]) @ 1, 0.1  // Deactivate the repressor by allolactose
    O(i[.]), ., . -> O(i[.]), Z(l[.]), Y() @ 1 // Express the lac operon
    L(loc{in}) -> E() @ 1  // Metabolize lactose

    // Degradation
    A() -> . @ 0.1
    Y() -> . @ 0.5
    Z() -> . @ 0.5
    """
)

We simulate the system first without lactose, then add extracellular lactose to observe the regulatory response.

[3]:
# Simulate a while with no extracellular lactose
while system.time < 300:
    system.update()

# Add extracellular lactose and continue simulating
system.mixture.instantiate("L(loc{out})", 1000)
while system.time < 800:
   system.update()
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[3], line 3
      1 # Simulate a while with no extracellular lactose
      2 while system.time < 300:
----> 3     system.update()
      5 # Add extracellular lactose and continue simulating
      6 system.mixture.instantiate("L(loc{out})", 1000)

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'
[4]:
system.monitor.plot();
../_images/examples_lac_operon_5_0.png

Without lactose, the operon remains mostly repressed. When extracellular lactose is added,

  • existing LacY transports some lactose into the cell,

  • intracellular lactose is converted to allolactose by LacZ,

  • allolactose inactivates the LacI repressor,

  • derepressing the operon and leading to increased LacY and LacZ production.

As lactose is metabolized the operon is again repressed.