Skip to content

Three-gene Boolean network

The three-gene Boolean network of Raman, updated synchronously.

  • Name: three_gene_boolean_network
  • Field: biology/systems_biology
  • Formalism: discrete map
  • References: Raman 2021

Factory

@register_model(
    field='biology/systems_biology',
    tags=('boolean network', 'gene regulation', 'discrete map', 'attractors'),
    references=(RAMAN,),
    defaults={'until': 10, 'dt': 1},
)
def three_gene_boolean_network(x: Sequence[int] = (1, 0, 0)) -> System:
    """Return the three-gene Boolean network of Raman, updated synchronously.

    The genes follow `g1* = g2 AND NOT g3`, `g2* = NOT g1`, and `g3* = NOT g3 OR g2`. Its eight states fall into two
    basins: the point attractor `011` and the cycle `001 -> 010 -> 111` of period three, which the state `100` enters.

    Args:
        x:
            The initial values of `g1`, `g2`, and `g3`.

    Returns:
        system:
            The model, with the values `g1.x`, `g2.x`, and `g3.x`.

    Examples:
        >>> from skcomplex.models.biology.systems_biology import three_gene_boolean_network
        >>> three_gene_boolean_network().interactions[2].table
        (1, 0, 1, 1)
    """
    active = [name for name, value in zip(THREE_GENE_RULES, x, strict=True) if value]
    return _rule_network(THREE_GENE_RULES, active)

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

g1.x: x_g1(t + 1) == -(x_g3(t) - 1)*x_g2(t)
g2.x: x_g2(t + 1) == -x_g1(t) + 1
g3.x: x_g3(t + 1) == (x_g2(t) - 1)*(x_g3(t) - 1) - (x_g3(t) - 1)*x_g2(t) + x_g2(t)*x_g3(t)