Skip to content

Even and S-gap subshifts

The shift on the S-gap subshift, the binary sequences whose runs of zeros between ones lie in S.

  • Name: gap_subshift
  • Field: computer_science/automata
  • Formalism: lattice dynamics
  • References: Ceccherini-Silberstein and Coornaert 2023

Select this system

from skcomplex.systems import create_simulation, create_system, describe

system = create_system('gap_subshift')
entry = describe('gap_subshift')
trajectory = create_simulation('gap_subshift').prepare().run().trajectory_

Factory

@register_system(
    field='computer_science/automata',
    tags=('subshift', 'S-gap', 'even shift', 'renewal', 'sofic'),
    references=('Ceccherini-Silberstein and Coornaert 2023',),
    defaults={'until': 100, 'dt': 1},
    observables=_density_observables,
)
def gap_subshift(
    size: int = 120,
    *,
    gaps: Sequence[int] | None = None,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return the shift on the S-gap subshift, the binary sequences whose runs of zeros between ones lie in `S`.

    With `S` the even numbers it is the even subshift, strictly sofic and not of finite type
    (Ceccherini-Silberstein and Coornaert 2023, Exercises 1.85 and 1.92). The cells are a window of a configuration
    drawn from the measure of maximal entropy, whose gaps are independent with the probability `λ^-(h + 1)` of the gap
    `h`, where `λ` solves `sum_(h in S) λ^-(h + 1) = 1` and `log λ` is the entropy. Every step shifts the window one
    cell to the left, and the cell entering on the right is a one with the probability that the gap since the last one
    ends there. A gap longer than the window counts as the window. Gaps are taken below ten times the size.

    Args:
        size:
            The number of cells of the window.

        gaps:
            The allowed numbers of zeros between consecutive ones, the set `S`, or `None` for the even numbers.

        random_state:
            The seed or generator of the initial window.

    Returns:
        system:
            The system, with the cells `cells.alive`.

    Raises:
        ParameterError:
            No allowed gap lies below ten times the size.

    Examples:
        >>> from skcomplex.systems.computer_science.automata import gap_subshift
        >>> cells = gap_subshift(40).elements[0].values['alive']
        >>> runs = [len(run) for run in ''.join(str(int(cell)) for cell in cells).strip('0').split('1') if run]
        >>> all(run % 2 == 0 for run in runs)
        True
    """
    _, probabilities, hazards = _gap_law(_gap_lengths(size, gaps))
    cells = _renewal_cells(size, probabilities, random_state)
    neighborhood = LatticeNeighborhood(tuple((offset,) for offset in range(1 - size, 2)))
    return System(
        elements=[
            LatticeElement(
                'cells',
                element=BinaryCellElement,
                space=LatticeSpace(size, boundary='fixed', fill=OUTSIDE),
                values={'alive': cells},
            ),
        ],
        interactions=[
            LocalRuleInteraction(
                'shift',
                target='cells',
                state='alive',
                rule=functools.partial(_gap_shift, hazards=hazards),
                neighborhood=neighborhood,
                stochastic=True,
            ),
        ],
    )

Components

Elements:

  • BinaryCellElement, from Element

Interactions:

  • LocalRuleInteraction
class BinaryCellElement(Element):
    """Cell of an automaton whose state `alive` is 0 or 1, changed by the local rule of an interaction.

    Args:
        label:
            The label of the cell or the lattice group.

        visual:
            How the element is drawn, or `None` for `default_visual_`.

        alive:
            The initial value, 0 or 1.

    Examples:
        >>> from skcomplex.systems.computer_science.automata import BinaryCellElement
        >>> BinaryCellElement('cell', alive=1).alive
        1
    """

    def __init__(
        self,
        label: str,
        visual: VisualSpec | None = None,
        *,
        alive: Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))] = 0,
    ) -> None:
        super().__init__(label, visual=visual)
        self.alive = alive

class LocalRuleInteraction(Interaction):
    """Local rule of a cellular automaton, which sets the next value of every cell from its ordered neighbourhood.

    The rule is a table of the next value of every configuration of the neighbourhood, `TableSpec`, a rule of the sum
    of the neighbourhood, `TotalisticSpec`, a rule of the cell and the sum of its neighbours, `OuterTotalisticSpec`, an
    object whose `table()` returns a `TableSpec`, or a function. A function receives one array per neighbour of every
    read state, in neighbourhood order, with one value per cell, the uniform array `random` when the rule is stochastic,
    and the parameters of the bound group it names, and returns the next values of the states, one array per state. A
    state of several components per cell, such as a truncated power series of `PowerSeriesSpec`, reaches a function as
    one row of components per cell. A `StateTranslationNeighborhood` reads a group space at the product of every site
    and the group element that the first read state holds there.

    Args:
        label:
            The label of the interaction, unique among the parts of its system.

        target:
            The path of the group on the cells of a lattice, the nodes of a network, or a Cayley graph.

        state:
            The name of the state the rule sets, or a tuple of the names of the states it sets together.

        rule:
            The table, the totalistic rule, or the function.

        neighborhood:
            The ordered neighbourhood of every cell, such as `VonNeumannNeighborhood(center=True)`.

        reads:
            The names of the states whose neighbourhoods a function reads, or `None` for the states it sets.

        schedule:
            `'synchronous'`, `'random-sequential'`, `'random-site'`, `'checkerboard'`, or `'colored'`.

        stochastic:
            Whether a function receives the uniform random array `random`.

    Raises:
        ValidationError:
            The state or the read states are not names, or the neighbourhood is not a neighbourhood.

    Examples:
        >>> from skcomplex.core.components.interactions import LocalRuleInteraction
        >>> from skcomplex.core.components.spaces import VonNeumannNeighborhood
        >>> from skcomplex.core.specs import TotalisticSpec
        >>> parity = TotalisticSpec({1: 1, 3: 1})
        >>> around = VonNeumannNeighborhood(n_dim=1, center=True)
        >>> rule = LocalRuleInteraction('parity', target='cells', state='cell', rule=parity, neighborhood=around)
        >>> rule.schedule
        'synchronous'
    """

    law_classes: ClassVar[tuple[type[BaseLaw], ...]] = (LocalRuleLaw,)

    def __init__(
        self,
        label: str,
        *,
        target: Annotated[str, RoleSpec(elements=(ElementKind.ANY,), states=('state', 'reads'))],
        state: str | tuple[str, ...],
        rule: object,
        neighborhood: BaseNeighborhood,
        reads: Sequence[str] | None = None,
        schedule: str = 'synchronous',
        stochastic: bool = False,
    ) -> None:
        super().__init__(label)
        self.target = target
        self.state = state
        self.rule = rule
        self.neighborhood = neighborhood
        self.reads = reads
        self.schedule = schedule
        self.stochastic = stochastic

    def _get_names(self) -> tuple[str, ...]:
        """Return the names of the states the rule sets."""
        return (self.state,) if isinstance(self.state, str) else tuple(self.state)

    def _check(self) -> None:
        """Raise an error for states that are not names, or a neighbourhood that is not one.

        Raises:
            ValidationError:
                The state or the read states are not names, or the neighbourhood is not a neighbourhood.
        """
        super()._check()
        names = [*self._get_names(), *(self.reads or ())]
        if not names or not all(isinstance(name, str) and name.isidentifier() for name in names):
            error_msg = f'The local rule {self.label!r} sets {self.state!r} and reads {self.reads!r}. Give state names.'
            raise ValidationError(error_msg)
        if not isinstance(self.neighborhood, BaseNeighborhood):
            error_msg = f'The neighbourhood of the local rule {self.label!r} is {self.neighborhood!r}. Give one.'
            raise ValidationError(error_msg)

    def _get_laws(self, namespace: SimpleNamespace) -> list[BaseLaw]:
        """Return the local rule law of the states of the target.

        Args:
            namespace:
                The namespace of the symbols the laws can use, with `namespace.target`.

        Returns:
            laws:
                The local rule law.
        """
        targets = tuple(getattr(self._get_member(namespace, 'target'), name) for name in self._get_names())
        reads = (
            None
            if self.reads is None
            else tuple(getattr(self._get_member(namespace, 'target'), name) for name in self.reads)
        )
        return [
            LocalRuleLaw(
                targets[0] if isinstance(self.state, str) else targets,
                self.neighborhood,
                self.rule,
                reads=reads,
                schedule=self.schedule,
                stochastic=self.stochastic,
                label=self.label,
            ),
        ]