Boolean repressilator
The Boolean repressilator, where LacI represses TetR, TetR represses cI, and cI represses LacI.
- Name:
boolean_repressilator - Field:
biology/systems_biology - Formalism: discrete map
- References: Raman 2021; Elowitz and Leibler 2000
Factory
@register_model(
field='biology/systems_biology',
tags=('boolean network', 'repressilator', 'oscillator', 'discrete map', 'delay'),
references=(RAMAN, 'Elowitz and Leibler 2000'),
defaults={'until': 24, 'dt': 1},
)
def boolean_repressilator(x: Sequence[int] = (1, 0, 0), *, delay: int = 0) -> System:
"""Return the Boolean repressilator, where LacI represses TetR, TetR represses cI, and cI represses LacI.
The transfer functions are `TetR* = NOT LacI`, `cI* = NOT TetR`, and `LacI* = NOT cI`. From a state with one or two
genes on, the genes switch on and off in turn with period 6, and the states `000` and `111` alternate with period
2. A repression delayed by `delay` steps passes through that many relay nodes, `<repressor>_1` and so on, which
copy the repressor one step later and start at its initial value. The oscillation remains, with period
`6 (delay + 1)`.
Args:
x:
The initial values of LacI, TetR, and cI.
delay:
The number of steps by which every repression is delayed.
Returns:
system:
The model, with the values `LacI.x`, `TetR.x`, `cI.x`, and the relays `<repressor>_<k>.x`.
Raises:
ValidationError:
The delay is negative.
Examples:
>>> from skcomplex.models.biology.systems_biology import boolean_repressilator
>>> [node.label for node in boolean_repressilator(delay=1).elements]
['LacI', 'TetR', 'cI', 'LacI_1', 'TetR_1', 'cI_1']
"""
if delay < 0:
error_msg = f'The delay must be a nonnegative number of steps, not {delay}.'
raise ValidationError(error_msg)
genes = [BooleanNodeElement(name, x=int(value)) for name, value in zip(REPRESSILATOR_GENES, x, strict=True)]
relays = [BooleanNodeElement(f'{gene.label}_{step}', x=gene.x) for step in range(1, delay + 1) for gene in genes]
functions: list[BooleanFunctionInteraction] = []
for repressor, target in zip(REPRESSILATOR_GENES, (*REPRESSILATOR_GENES[1:], REPRESSILATOR_GENES[0]), strict=True):
chain = [repressor, *(f'{repressor}_{step}' for step in range(1, delay + 1))]
functions.extend(
BooleanFunctionInteraction(f'relay_{relay}', target=relay, inputs=[source], table=(0, 1))
for source, relay in itertools.pairwise(chain)
)
functions.append(
BooleanFunctionInteraction(f'repression_{target}', target=target, inputs=[chain[-1]], table=(1, 0)),
)
return System(elements=[*genes, *relays], interactions=functions)
Components
class BooleanNodeElement(Element):
"""Node of a Boolean network, a gene or protein that is on or off.
Args:
label:
The label of the node.
visual:
How the element is drawn, or `None` for `default_visual_`.
x:
The initial value, 0 or 1.
Examples:
>>> from skcomplex.models.biology.systems_biology import BooleanNodeElement
>>> BooleanNodeElement('Sic1', x=1).x
1
"""
def __init__(
self,
label: str,
visual: VisualSpec | None = None,
*,
x: Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))] = 0,
) -> None:
super().__init__(label, visual=visual)
self.x = x
class BooleanFunctionInteraction(MapInteraction):
"""Synchronous update of a node by a truth table of any number of input nodes.
A table of `2^k` entries reads the `k` inputs as the bits of a binary number, from the most significant, and gives
the next value of the target. The update is the sum, over the patterns whose entry is 1, of the product that is 1
exactly on that pattern.
Args:
label:
The label of the interaction.
target:
The path of the updated node.
inputs:
The paths of the input nodes, in the order of the bits of the table.
table:
The next value of the target for each of the `2^k` input patterns.
Raises:
ValidationError:
The table does not have one entry per pattern of the inputs.
Examples:
>>> from skcomplex.models.biology.systems_biology import BooleanFunctionInteraction
>>> BooleanFunctionInteraction('and', target='c', inputs=['a', 'b'], table=(0, 0, 0, 1)).table
(0, 0, 0, 1)
"""
def __init__(
self,
label: str,
*,
target: Annotated[str, RoleSpec()],
inputs: Annotated[list[str], RoleSpec(variadic=True)],
table: tuple[int, ...] = (0,),
) -> None:
super().__init__(label, target=target, state='x', function=self._get_next)
self.inputs = inputs
self.table = table
def _get_pattern(self, inputs: Sequence[Expression], index: int) -> Expression:
"""Return the product that is 1 when the inputs, read as bits from the most significant, spell an index."""
bits = [(index >> (len(inputs) - 1 - position)) & 1 for position in range(len(inputs))]
return prod((value if bit else 1 - value for value, bit in zip(inputs, bits, strict=True)), start=SR(1))
def _get_next(self, s: SimpleNamespace) -> Expression:
"""Return the next value of the target by the truth table."""
inputs = [item.x for item in s.inputs]
terms = [self._get_pattern(inputs, index) for index, entry in enumerate(self.table) if entry]
return sum(terms, start=SR(0))
def _check(self) -> None:
"""Raise an error for a table without one entry per pattern of the inputs.
Raises:
ValidationError:
The table does not have one entry per pattern of the inputs.
"""
super()._check()
if len(self.table) != 2 ** len(self.inputs):
error_msg = (
f'BooleanFunction {self.label} reads {len(self.inputs)} inputs, which take {2 ** len(self.inputs)} '
f'patterns, but its table has {len(self.table)} entries.'
)
raise ValidationError(error_msg)
Equations
LacI.x: x_LacI(t + 1) == -x_cI(t) + 1
TetR.x: x_TetR(t + 1) == -x_LacI(t) + 1
cI.x: x_cI(t + 1) == -x_TetR(t) + 1
